Repository: conan-io/conan Branch: develop2 Commit: 202f42c2b26a Files: 1135 Total size: 7.3 MB Directory structure: gitextract_tvb7y0th/ ├── .ci/ │ ├── __init__.py │ ├── bump_dev_version.py │ └── docker/ │ └── conan-tests ├── .editorconfig ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── actions/ │ │ └── test-coverage/ │ │ └── action.yml │ └── workflows/ │ ├── build-binaries.yml │ ├── linux-tests.yml │ ├── main.yml │ ├── osx-tests.yml │ └── win-tests.yml ├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── codecov.yml ├── conan/ │ ├── __init__.py │ ├── api/ │ │ ├── __init__.py │ │ ├── conan_api.py │ │ ├── input.py │ │ ├── model/ │ │ │ ├── __init__.py │ │ │ ├── list.py │ │ │ ├── refs.py │ │ │ └── remote.py │ │ ├── output.py │ │ └── subapi/ │ │ ├── __init__.py │ │ ├── audit.py │ │ ├── cache.py │ │ ├── command.py │ │ ├── config.py │ │ ├── download.py │ │ ├── export.py │ │ ├── graph.py │ │ ├── install.py │ │ ├── list.py │ │ ├── local.py │ │ ├── lockfile.py │ │ ├── new.py │ │ ├── profiles.py │ │ ├── remotes.py │ │ ├── remove.py │ │ ├── report.py │ │ ├── upload.py │ │ └── workspace.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── args.py │ │ ├── cli.py │ │ ├── command.py │ │ ├── commands/ │ │ │ ├── __init__.py │ │ │ ├── audit.py │ │ │ ├── build.py │ │ │ ├── cache.py │ │ │ ├── config.py │ │ │ ├── create.py │ │ │ ├── download.py │ │ │ ├── editable.py │ │ │ ├── export.py │ │ │ ├── export_pkg.py │ │ │ ├── graph.py │ │ │ ├── inspect.py │ │ │ ├── install.py │ │ │ ├── list.py │ │ │ ├── lock.py │ │ │ ├── new.py │ │ │ ├── pkglist.py │ │ │ ├── profile.py │ │ │ ├── remote.py │ │ │ ├── remove.py │ │ │ ├── report.py │ │ │ ├── require.py │ │ │ ├── run.py │ │ │ ├── search.py │ │ │ ├── source.py │ │ │ ├── test.py │ │ │ ├── upload.py │ │ │ ├── version.py │ │ │ └── workspace.py │ │ ├── exit_codes.py │ │ ├── formatters/ │ │ │ ├── __init__.py │ │ │ ├── audit/ │ │ │ │ ├── __init__.py │ │ │ │ └── vulnerabilities.py │ │ │ ├── graph/ │ │ │ │ ├── __init__.py │ │ │ │ ├── build_order_html.py │ │ │ │ ├── graph.py │ │ │ │ ├── graph_info_text.py │ │ │ │ ├── info_graph_dot.py │ │ │ │ └── info_graph_html.py │ │ │ ├── list/ │ │ │ │ ├── __init__.py │ │ │ │ ├── list.py │ │ │ │ └── search_table_html.py │ │ │ └── report/ │ │ │ ├── __init__.py │ │ │ ├── diff.py │ │ │ └── diff_html.py │ │ └── printers/ │ │ ├── __init__.py │ │ └── graph.py │ ├── cps/ │ │ ├── __init__.py │ │ └── cps.py │ ├── errors.py │ ├── internal/ │ │ ├── __init__.py │ │ ├── api/ │ │ │ ├── __init__.py │ │ │ ├── audit/ │ │ │ │ ├── __init__.py │ │ │ │ └── providers.py │ │ │ ├── config/ │ │ │ │ ├── __init__.py │ │ │ │ └── config_installer.py │ │ │ ├── detect/ │ │ │ │ ├── __init__.py │ │ │ │ ├── detect_api.py │ │ │ │ └── detect_vs.py │ │ │ ├── export.py │ │ │ ├── install/ │ │ │ │ ├── __init__.py │ │ │ │ └── generators.py │ │ │ ├── list/ │ │ │ │ ├── __init__.py │ │ │ │ └── query_parse.py │ │ │ ├── local/ │ │ │ │ ├── __init__.py │ │ │ │ └── editable.py │ │ │ ├── migrations.py │ │ │ ├── new/ │ │ │ │ ├── __init__.py │ │ │ │ ├── alias_new.py │ │ │ │ ├── autoools_exe.py │ │ │ │ ├── autotools_lib.py │ │ │ │ ├── basic.py │ │ │ │ ├── bazel_7_exe.py │ │ │ │ ├── bazel_7_lib.py │ │ │ │ ├── bazel_exe.py │ │ │ │ ├── bazel_lib.py │ │ │ │ ├── cmake_exe.py │ │ │ │ ├── cmake_lib.py │ │ │ │ ├── header_lib.py │ │ │ │ ├── local_recipes_index.py │ │ │ │ ├── meson_exe.py │ │ │ │ ├── meson_lib.py │ │ │ │ ├── msbuild_exe.py │ │ │ │ ├── msbuild_lib.py │ │ │ │ ├── premake_exe.py │ │ │ │ ├── premake_lib.py │ │ │ │ ├── qbs_lib.py │ │ │ │ └── workspace.py │ │ │ ├── profile/ │ │ │ │ ├── __init__.py │ │ │ │ ├── detect.py │ │ │ │ └── profile_loader.py │ │ │ ├── remotes/ │ │ │ │ ├── __init__.py │ │ │ │ ├── encrypt.py │ │ │ │ └── localdb.py │ │ │ ├── upload.py │ │ │ └── uploader.py │ │ ├── cache/ │ │ │ ├── __init__.py │ │ │ ├── cache.py │ │ │ ├── conan_reference_layout.py │ │ │ ├── db/ │ │ │ │ ├── __init__.py │ │ │ │ ├── cache_database.py │ │ │ │ ├── packages_table.py │ │ │ │ ├── recipes_table.py │ │ │ │ └── table.py │ │ │ ├── home_paths.py │ │ │ └── integrity_check.py │ │ ├── conan_app.py │ │ ├── default_settings.py │ │ ├── deploy.py │ │ ├── errors.py │ │ ├── graph/ │ │ │ ├── __init__.py │ │ │ ├── build_mode.py │ │ │ ├── compatibility.py │ │ │ ├── compute_pid.py │ │ │ ├── graph.py │ │ │ ├── graph_binaries.py │ │ │ ├── graph_builder.py │ │ │ ├── graph_error.py │ │ │ ├── install_graph.py │ │ │ ├── installer.py │ │ │ ├── profile_node_definer.py │ │ │ ├── provides.py │ │ │ ├── proxy.py │ │ │ ├── python_requires.py │ │ │ └── range_resolver.py │ │ ├── hook_manager.py │ │ ├── internal_tools.py │ │ ├── loader.py │ │ ├── methods.py │ │ ├── model/ │ │ │ ├── __init__.py │ │ │ ├── conan_file.py │ │ │ ├── conanconfig.py │ │ │ ├── conanfile_interface.py │ │ │ ├── conf.py │ │ │ ├── cpp_info.py │ │ │ ├── dependencies.py │ │ │ ├── info.py │ │ │ ├── layout.py │ │ │ ├── lockfile.py │ │ │ ├── manifest.py │ │ │ ├── options.py │ │ │ ├── pkg_type.py │ │ │ ├── profile.py │ │ │ ├── recipe_ref.py │ │ │ ├── requires.py │ │ │ ├── settings.py │ │ │ ├── version.py │ │ │ ├── version_range.py │ │ │ └── workspace.py │ │ ├── paths.py │ │ ├── rest/ │ │ │ ├── __init__.py │ │ │ ├── auth_manager.py │ │ │ ├── caching_file_downloader.py │ │ │ ├── client_routes.py │ │ │ ├── conan_requester.py │ │ │ ├── download_cache.py │ │ │ ├── file_downloader.py │ │ │ ├── file_uploader.py │ │ │ ├── pkg_sign.py │ │ │ ├── remote_credentials.py │ │ │ ├── remote_manager.py │ │ │ ├── rest_client.py │ │ │ ├── rest_client_local_recipe_index.py │ │ │ ├── rest_client_v2.py │ │ │ └── rest_routes.py │ │ ├── runner/ │ │ │ ├── __init__.py │ │ │ ├── docker.py │ │ │ ├── output.py │ │ │ ├── ssh.py │ │ │ └── wsl.py │ │ ├── source.py │ │ ├── subsystems.py │ │ └── util/ │ │ ├── __init__.py │ │ ├── config_parser.py │ │ ├── dates.py │ │ ├── files.py │ │ └── runners.py │ ├── test/ │ │ ├── __init__.py │ │ ├── assets/ │ │ │ ├── __init__.py │ │ │ ├── autotools.py │ │ │ ├── cmake.py │ │ │ ├── genconanfile.py │ │ │ ├── premake.py │ │ │ ├── sources.py │ │ │ └── visual_project_files.py │ │ └── utils/ │ │ ├── __init__.py │ │ ├── artifactory.py │ │ ├── env.py │ │ ├── file_server.py │ │ ├── mocks.py │ │ ├── profiles.py │ │ ├── scm.py │ │ ├── server_launcher.py │ │ ├── test_files.py │ │ └── tools.py │ └── tools/ │ ├── __init__.py │ ├── android/ │ │ ├── __init__.py │ │ └── utils.py │ ├── apple/ │ │ ├── __init__.py │ │ ├── apple.py │ │ ├── xcodebuild.py │ │ ├── xcodedeps.py │ │ └── xcodetoolchain.py │ ├── build/ │ │ ├── __init__.py │ │ ├── compiler.py │ │ ├── cppstd.py │ │ ├── cpu.py │ │ ├── cross_building.py │ │ ├── cstd.py │ │ ├── flags.py │ │ └── stdcpp_library.py │ ├── cmake/ │ │ ├── __init__.py │ │ ├── cmake.py │ │ ├── cmakeconfigdeps/ │ │ │ ├── __init__.py │ │ │ ├── cmakeconfigdeps.py │ │ │ ├── config.py │ │ │ ├── config_version.py │ │ │ ├── target_configuration.py │ │ │ └── targets.py │ │ ├── cmakedeps/ │ │ │ ├── __init__.py │ │ │ ├── cmakedeps.py │ │ │ └── templates/ │ │ │ ├── __init__.py │ │ │ ├── config.py │ │ │ ├── config_version.py │ │ │ ├── macros.py │ │ │ ├── target_configuration.py │ │ │ ├── target_data.py │ │ │ └── targets.py │ │ ├── layout.py │ │ ├── presets.py │ │ ├── toolchain/ │ │ │ ├── __init__.py │ │ │ ├── blocks.py │ │ │ └── toolchain.py │ │ └── utils.py │ ├── cps/ │ │ ├── __init__.py │ │ └── cps_deps.py │ ├── env/ │ │ ├── __init__.py │ │ ├── environment.py │ │ ├── virtualbuildenv.py │ │ └── virtualrunenv.py │ ├── files/ │ │ ├── __init__.py │ │ ├── conandata.py │ │ ├── copy_pattern.py │ │ ├── files.py │ │ ├── patches.py │ │ └── symlinks/ │ │ ├── __init__.py │ │ └── symlinks.py │ ├── gnu/ │ │ ├── __init__.py │ │ ├── autotools.py │ │ ├── autotoolsdeps.py │ │ ├── autotoolstoolchain.py │ │ ├── get_gnu_triplet.py │ │ ├── gnudeps_flags.py │ │ ├── gnutoolchain.py │ │ ├── makedeps.py │ │ ├── pkgconfig.py │ │ └── pkgconfigdeps.py │ ├── google/ │ │ ├── __init__.py │ │ ├── bazel.py │ │ ├── bazeldeps.py │ │ ├── layout.py │ │ └── toolchain.py │ ├── intel/ │ │ ├── __init__.py │ │ └── intel_cc.py │ ├── layout/ │ │ └── __init__.py │ ├── meson/ │ │ ├── __init__.py │ │ ├── helpers.py │ │ ├── meson.py │ │ └── toolchain.py │ ├── microsoft/ │ │ ├── __init__.py │ │ ├── layout.py │ │ ├── msbuild.py │ │ ├── msbuilddeps.py │ │ ├── nmakedeps.py │ │ ├── nmaketoolchain.py │ │ ├── subsystems.py │ │ ├── toolchain.py │ │ └── visual.py │ ├── premake/ │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── premake.py │ │ ├── premakedeps.py │ │ └── toolchain.py │ ├── qbs/ │ │ ├── __init__.py │ │ ├── common.py │ │ ├── qbs.py │ │ ├── qbsdeps.py │ │ └── qbsprofile.py │ ├── ros/ │ │ ├── __init__.py │ │ └── rosenv.py │ ├── sbom/ │ │ ├── __init__.py │ │ ├── cyclonedx.py │ │ └── spdx_licenses.py │ ├── scm/ │ │ ├── __init__.py │ │ └── git.py │ ├── scons/ │ │ ├── __init__.py │ │ └── sconsdeps.py │ └── system/ │ ├── __init__.py │ ├── package_manager.py │ └── python_manager.py ├── conans/ │ ├── __init__.py │ ├── conan.py │ ├── conan_server.py │ ├── migrations.py │ ├── requirements.txt │ ├── requirements_dev.txt │ ├── requirements_runner.txt │ ├── requirements_server.txt │ └── server/ │ ├── __init__.py │ ├── conf/ │ │ ├── __init__.py │ │ └── default_server_conf.py │ ├── crypto/ │ │ ├── __init__.py │ │ └── jwt/ │ │ ├── __init__.py │ │ └── jwt_credentials_manager.py │ ├── launcher.py │ ├── migrate.py │ ├── migrations.py │ ├── plugin_loader.py │ ├── rest/ │ │ ├── __init__.py │ │ ├── api_v2.py │ │ ├── bottle_plugins/ │ │ │ ├── __init__.py │ │ │ ├── authorization_header.py │ │ │ ├── http_basic_authentication.py │ │ │ ├── jwt_authentication.py │ │ │ └── return_handler.py │ │ ├── bottle_routes.py │ │ ├── controller/ │ │ │ ├── __init__.py │ │ │ └── v2/ │ │ │ ├── __init__.py │ │ │ ├── conan.py │ │ │ ├── delete.py │ │ │ ├── ping.py │ │ │ ├── revisions.py │ │ │ ├── search.py │ │ │ └── users.py │ │ └── server.py │ ├── revision_list.py │ ├── server_launcher.py │ ├── service/ │ │ ├── __init__.py │ │ ├── authorize.py │ │ ├── mime.py │ │ ├── user_service.py │ │ └── v2/ │ │ ├── __init__.py │ │ ├── search.py │ │ └── service_v2.py │ ├── store/ │ │ ├── __init__.py │ │ ├── disk_adapter.py │ │ └── server_store.py │ └── utils/ │ ├── __init__.py │ └── files.py ├── contributors.txt ├── pyinstaller.py ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── setup.py ├── setup_server.py └── test/ ├── .gitignore ├── README.md ├── __init__.py ├── conftest.py ├── functional/ │ ├── __init__.py │ ├── command/ │ │ ├── __init__.py │ │ ├── dockerfiles/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile_args │ │ │ ├── Dockerfile_ninja │ │ │ ├── Dockerfile_profile_detect │ │ │ └── Dockerfile_test │ │ ├── export_test.py │ │ ├── profile_test.py │ │ ├── report_test.py │ │ ├── runner_test.py │ │ ├── test_build.py │ │ ├── test_config_install.py │ │ ├── test_config_install_pkg.py │ │ ├── test_custom_symlink_home.py │ │ ├── test_install_deploy.py │ │ └── test_new.py │ ├── conftest.py │ ├── layout/ │ │ ├── __init__.py │ │ ├── test_build_system_layout_helpers.py │ │ ├── test_editable_cmake.py │ │ ├── test_editable_cmake_components.py │ │ ├── test_editable_msbuild.py │ │ ├── test_exports_sources.py │ │ ├── test_in_cache.py │ │ ├── test_in_subfolder.py │ │ ├── test_local_commands.py │ │ └── test_source_folder.py │ ├── revisions_test.py │ ├── sbom/ │ │ ├── __init__.py │ │ └── test_cyclonedx.py │ ├── subsystems_build_test.py │ ├── test_local_recipes_index.py │ ├── test_profile_detect_api.py │ ├── test_third_party_patch_flow.py │ ├── toolchains/ │ │ ├── __init__.py │ │ ├── android/ │ │ │ ├── __init__.py │ │ │ └── test_using_cmake.py │ │ ├── apple/ │ │ │ ├── __init__.py │ │ │ ├── test_xcodebuild.py │ │ │ ├── test_xcodebuild_targets.py │ │ │ ├── test_xcodedeps_build_configs.py │ │ │ ├── test_xcodedeps_components.py │ │ │ └── test_xcodetoolchain.py │ │ ├── autotools/ │ │ │ ├── __init__.py │ │ │ └── test_universal_binaries.py │ │ ├── cmake/ │ │ │ ├── __init__.py │ │ │ ├── cmakeconfigdeps/ │ │ │ │ ├── __init__.py │ │ │ │ ├── test_cmakeconfigdeps_aliases.py │ │ │ │ ├── test_cmakeconfigdeps_frameworks.py │ │ │ │ ├── test_cmakeconfigdeps_new.py │ │ │ │ ├── test_cmakeconfigdeps_new_cpp_linkage.py │ │ │ │ ├── test_cmakeconfigdeps_new_paths.py │ │ │ │ └── test_cmakeconfigdeps_sources.py │ │ │ ├── cmakedeps/ │ │ │ │ ├── __init__.py │ │ │ │ ├── test_apple_frameworks.py │ │ │ │ ├── test_build_context_protobuf.py │ │ │ │ ├── test_build_context_transitive_build.py │ │ │ │ ├── test_cmakedeps.py │ │ │ │ ├── test_cmakedeps_aggregator.py │ │ │ │ ├── test_cmakedeps_aliases.py │ │ │ │ ├── test_cmakedeps_and_linker_flags.py │ │ │ │ ├── test_cmakedeps_build_modules.py │ │ │ │ ├── test_cmakedeps_components.py │ │ │ │ ├── test_cmakedeps_components_names.py │ │ │ │ ├── test_cmakedeps_custom_configs.py │ │ │ │ ├── test_cmakedeps_find_module_and_config.py │ │ │ │ ├── test_cmakedeps_transitivity.py │ │ │ │ ├── test_cmakedeps_versions.py │ │ │ │ ├── test_conditional_build_type.py │ │ │ │ ├── test_link_order.py │ │ │ │ └── test_weird_library_names.py │ │ │ ├── test_cmake.py │ │ │ ├── test_cmake_and_no_soname_flag.py │ │ │ ├── test_cmake_extra_variables.py │ │ │ ├── test_cmake_find_none.py │ │ │ ├── test_cmake_multi.py │ │ │ ├── test_cmake_toolchain.py │ │ │ ├── test_cmake_toolchain_m1.py │ │ │ ├── test_cmake_toolchain_presets.py │ │ │ ├── test_cmake_toolchain_win_clang.py │ │ │ ├── test_cmake_toolchain_xcode_flags.py │ │ │ ├── test_cmake_transitive_rpath.py │ │ │ ├── test_cmaketoolchain_paths.py │ │ │ ├── test_cps.py │ │ │ ├── test_ninja.py │ │ │ ├── test_presets_inherit.py │ │ │ ├── test_shared_cmake.py │ │ │ ├── test_transitive_build_scripts.py │ │ │ ├── test_universal_binaries.py │ │ │ └── test_v2_cmake_template.py │ │ ├── emscripten/ │ │ │ ├── __init__.py │ │ │ └── test_emcc.py │ │ ├── env/ │ │ │ ├── __init__.py │ │ │ ├── test_complete.py │ │ │ └── test_virtualenv_powershell.py │ │ ├── gnu/ │ │ │ ├── __init__.py │ │ │ ├── autotools/ │ │ │ │ ├── __init__.py │ │ │ │ ├── test_android.py │ │ │ │ ├── test_apple_toolchain.py │ │ │ │ ├── test_basic.py │ │ │ │ ├── test_crossbuild_triplet.py │ │ │ │ ├── test_ios.py │ │ │ │ └── test_win_bash.py │ │ │ ├── test_gnutoolchain_android.py │ │ │ ├── test_gnutoolchain_apple.py │ │ │ ├── test_makedeps.py │ │ │ ├── test_pkg_config.py │ │ │ ├── test_pkgconfigdeps.py │ │ │ ├── test_pkgconfigdeps_autotools.py │ │ │ ├── test_universal_binaries.py │ │ │ └── test_v2_autotools_template.py │ │ ├── google/ │ │ │ ├── __init__.py │ │ │ ├── test_bazel.py │ │ │ └── test_bazeltoolchain_cross_compilation.py │ │ ├── intel/ │ │ │ ├── __init__.py │ │ │ ├── test_intel_cc.py │ │ │ └── test_using_msbuild.py │ │ ├── ios/ │ │ │ ├── __init__.py │ │ │ ├── _utils.py │ │ │ └── test_using_cmake.py │ │ ├── meson/ │ │ │ ├── __init__.py │ │ │ ├── _base.py │ │ │ ├── test_backend.py │ │ │ ├── test_cross_compilation.py │ │ │ ├── test_install.py │ │ │ ├── test_meson.py │ │ │ ├── test_meson_and_gnu_deps_flags.py │ │ │ ├── test_meson_and_objc.py │ │ │ ├── test_meson_native_attribute.py │ │ │ ├── test_meson_transitive_rpath_sysroot.py │ │ │ ├── test_pkg_config_reuse.py │ │ │ ├── test_preprocessor_definitions.py │ │ │ ├── test_subproject.py │ │ │ ├── test_test.py │ │ │ └── test_v2_meson_template.py │ │ ├── microsoft/ │ │ │ ├── __init__.py │ │ │ ├── test_msbuild.py │ │ │ ├── test_msbuilddeps.py │ │ │ ├── test_msbuilddeps_components.py │ │ │ ├── test_msbuilddeps_traits.py │ │ │ ├── test_msbuildtoolchain.py │ │ │ ├── test_v2_msbuild_template.py │ │ │ └── test_vcvars.py │ │ ├── qbs/ │ │ │ ├── __init__.py │ │ │ ├── test_qbs.py │ │ │ ├── test_qbsdeps.py │ │ │ ├── test_qbsprofile.py │ │ │ └── test_qbsprofile_gen.py │ │ ├── scons/ │ │ │ ├── __init__.py │ │ │ └── test_sconsdeps.py │ │ ├── test_basic.py │ │ ├── test_nmake_toolchain.py │ │ └── test_premake.py │ ├── tools/ │ │ ├── __init__.py │ │ ├── scm/ │ │ │ ├── __init__.py │ │ │ ├── test_git.py │ │ │ ├── test_git_get_commit.py │ │ │ └── test_version.py │ │ ├── system/ │ │ │ ├── __init__.py │ │ │ ├── package_manager_test.py │ │ │ └── python_manager_test.py │ │ ├── test_apple_tools.py │ │ └── test_files.py │ ├── tools_versions_test.py │ ├── util/ │ │ ├── __init__.py │ │ ├── test_cmd_args_to_string.py │ │ └── tools_test.py │ ├── utils.py │ └── workspace/ │ ├── __init__.py │ └── test_workspace.py ├── integration/ │ ├── __init__.py │ ├── build_requires/ │ │ ├── __init__.py │ │ ├── build_requires_test.py │ │ ├── profile_build_requires_test.py │ │ ├── test_build_requires_source_method.py │ │ ├── test_install_test_build_require.py │ │ ├── test_relocatable_toolchain.py │ │ └── test_toolchain_packages.py │ ├── cache/ │ │ ├── __init__.py │ │ ├── backup_sources_test.py │ │ ├── cache2_update_test.py │ │ ├── download_cache_test.py │ │ ├── rmdir_fail_test.py │ │ ├── storage_path_test.py │ │ ├── test_home_special_char.py │ │ ├── test_package_revisions.py │ │ └── test_same_pref_removal.py │ ├── command/ │ │ ├── __init__.py │ │ ├── alias_test.py │ │ ├── cache/ │ │ │ ├── __init__.py │ │ │ ├── test_cache_clean.py │ │ │ ├── test_cache_integrity.py │ │ │ ├── test_cache_path.py │ │ │ ├── test_cache_save_restore.py │ │ │ └── test_cache_sign.py │ │ ├── config_test.py │ │ ├── create_test.py │ │ ├── custom_commands_test.py │ │ ├── download/ │ │ │ ├── __init__.py │ │ │ ├── download_parallel_test.py │ │ │ ├── download_selected_packages_test.py │ │ │ ├── download_test.py │ │ │ └── test_download_patterns.py │ │ ├── export/ │ │ │ ├── __init__.py │ │ │ ├── export_dirty_test.py │ │ │ ├── export_path_test.py │ │ │ ├── export_sources_test.py │ │ │ ├── export_test.py │ │ │ ├── exports_method_test.py │ │ │ └── test_export.py │ │ ├── export_pkg_test.py │ │ ├── help_test.py │ │ ├── info/ │ │ │ ├── __init__.py │ │ │ ├── info_options_test.py │ │ │ ├── info_test.py │ │ │ ├── test_graph_info_graphical.py │ │ │ ├── test_info_build_order.py │ │ │ └── test_info_folders.py │ │ ├── install/ │ │ │ ├── __init__.py │ │ │ ├── install_cascade_test.py │ │ │ ├── install_missing_dep_test.py │ │ │ ├── install_parallel_test.py │ │ │ ├── install_test.py │ │ │ ├── install_update_test.py │ │ │ ├── test_graph_build_mode.py │ │ │ └── test_install_transitive.py │ │ ├── list/ │ │ │ ├── __init__.py │ │ │ ├── list_test.py │ │ │ ├── search_test.py │ │ │ ├── test_combined_pkglist_flows.py │ │ │ └── test_list_lru.py │ │ ├── remote/ │ │ │ ├── __init__.py │ │ │ ├── remote_test.py │ │ │ ├── remote_verify_ssl_test.py │ │ │ └── test_remote_users.py │ │ ├── remove_empty_dirs_test.py │ │ ├── remove_test.py │ │ ├── require/ │ │ │ ├── __init__.py │ │ │ └── test_command_require.py │ │ ├── source_test.py │ │ ├── test_audit.py │ │ ├── test_build.py │ │ ├── test_forced_download_source.py │ │ ├── test_graph_find_binaries.py │ │ ├── test_inspect.py │ │ ├── test_new.py │ │ ├── test_outdated.py │ │ ├── test_output.py │ │ ├── test_package_test.py │ │ ├── test_profile.py │ │ ├── test_run.py │ │ ├── test_version.py │ │ └── upload/ │ │ ├── __init__.py │ │ ├── test_upload_bundle.py │ │ ├── test_upload_parallel.py │ │ ├── test_upload_patterns.py │ │ ├── upload_complete_test.py │ │ ├── upload_compression_test.py │ │ └── upload_test.py │ ├── conan_api/ │ │ ├── __init__.py │ │ ├── exit_with_code_test.py │ │ ├── list_test.py │ │ ├── test_cli.py │ │ ├── test_local_api.py │ │ └── test_profile_api.py │ ├── conan_v2/ │ │ ├── __init__.py │ │ └── test_legacy_cpp_info.py │ ├── conanfile/ │ │ ├── __init__.py │ │ ├── conan_data_test.py │ │ ├── conanfile_errors_test.py │ │ ├── conanfile_helpers_test.py │ │ ├── files/ │ │ │ ├── .gitattributes │ │ │ ├── conanfile_utf16be_with_bom.txt │ │ │ ├── conanfile_utf16le_with_bom.txt │ │ │ ├── conanfile_utf8.txt │ │ │ └── conanfile_utf8_with_bom.txt │ │ ├── folders_access_test.py │ │ ├── generators_list_test.py │ │ ├── init_test.py │ │ ├── invalid_configuration_test.py │ │ ├── load_requires_file_test.py │ │ ├── no_copy_source_test.py │ │ ├── required_conan_version_test.py │ │ ├── runner_test.py │ │ ├── same_userchannel_test.py │ │ ├── set_name_version_test.py │ │ ├── test_attributes_scope.py │ │ ├── test_conanfile_txt_encodings.py │ │ ├── test_conanfile_txt_test_requires.py │ │ ├── test_cpp_info_serialize.py │ │ ├── test_deploy_method.py │ │ ├── test_deprecated.py │ │ ├── test_exception_printing.py │ │ ├── test_finalize_method.py │ │ ├── test_print_in_conanfile.py │ │ └── test_version_str.py │ ├── configuration/ │ │ ├── __init__.py │ │ ├── client_certs_test.py │ │ ├── conf/ │ │ │ ├── __init__.py │ │ │ ├── test_auth_source_plugin.py │ │ │ ├── test_conf.py │ │ │ ├── test_conf_copy.py │ │ │ ├── test_conf_from_br.py │ │ │ ├── test_conf_package_id.py │ │ │ └── test_conf_profile.py │ │ ├── custom_setting_test_package_test.py │ │ ├── default_profile_test.py │ │ ├── invalid_settings_test.py │ │ ├── profile_test.py │ │ ├── proxies_conf_test.py │ │ ├── requester_test.py │ │ ├── required_version_test.py │ │ ├── test_auth_remote_plugin.py │ │ ├── test_custom_symlinked_home.py │ │ ├── test_profile_jinja.py │ │ ├── test_profile_plugin.py │ │ └── test_profile_priority.py │ ├── cps/ │ │ ├── __init__.py │ │ └── test_cps.py │ ├── cross_building/ │ │ ├── __init__.py │ │ ├── build_requires_from_profile_test.py │ │ ├── test_cross_build_options.py │ │ └── test_package_test.py │ ├── editable/ │ │ ├── __init__.py │ │ ├── editable_add_test.py │ │ ├── editable_remove_test.py │ │ ├── forbidden_commands_test.py │ │ ├── test_editable_envvars.py │ │ ├── test_editable_import.py │ │ ├── test_editable_layout.py │ │ ├── test_editable_ranges.py │ │ ├── test_editables_layout.py │ │ └── transitive_editable_test.py │ ├── environment/ │ │ ├── __init__.py │ │ ├── test_buildenv_profile.py │ │ ├── test_env.py │ │ └── test_runenv_profile.py │ ├── extensions/ │ │ ├── __init__.py │ │ ├── hooks/ │ │ │ ├── __init__.py │ │ │ ├── hook_test.py │ │ │ ├── test_post_export.py │ │ │ └── test_post_package.py │ │ ├── test_cppstd_compat.py │ │ ├── test_plugin_cmd_wrapper.py │ │ ├── test_profile_plugin.py │ │ └── test_profile_plugin_runtime.py │ ├── generators/ │ │ ├── __init__.py │ │ ├── generators_test.py │ │ ├── order_libs_test.py │ │ ├── test_custom_global_generators.py │ │ └── test_generators_from_br.py │ ├── graph/ │ │ ├── __init__.py │ │ ├── conflict_diamond_test.py │ │ ├── core/ │ │ │ ├── __init__.py │ │ │ ├── graph_manager_base.py │ │ │ ├── graph_manager_test.py │ │ │ ├── test_alias.py │ │ │ ├── test_auto_package_type.py │ │ │ ├── test_build_require_invalid.py │ │ │ ├── test_build_requires.py │ │ │ ├── test_options.py │ │ │ ├── test_provides.py │ │ │ └── test_version_ranges.py │ │ ├── require_override_test.py │ │ ├── test_dependencies_visit.py │ │ ├── test_divergent_cppstd_build_host.py │ │ ├── test_platform_requires.py │ │ ├── test_pure_runtime_dep.py │ │ ├── test_remote_resolution.py │ │ ├── test_repackaging.py │ │ ├── test_replace_requires.py │ │ ├── test_require_same_pkg_versions.py │ │ ├── test_skip_binaries.py │ │ ├── test_skip_build.py │ │ ├── test_subgraph_reports.py │ │ ├── test_system_tools.py │ │ ├── test_test_requires.py │ │ ├── test_validate_build.py │ │ ├── ux/ │ │ │ ├── __init__.py │ │ │ └── loop_detection_test.py │ │ └── version_ranges/ │ │ ├── __init__.py │ │ ├── test_version_range_conf.py │ │ ├── version_range_override_test.py │ │ ├── version_ranges_cached_test.py │ │ └── version_ranges_diamond_test.py │ ├── layout/ │ │ ├── __init__.py │ │ ├── devflow_test.py │ │ ├── export_folder_variable_test.py │ │ ├── test_cmake_build_folder.py │ │ ├── test_layout_generate.py │ │ ├── test_layout_paths.py │ │ └── test_legacy_cpp_info_and_layout.py │ ├── lockfile/ │ │ ├── __init__.py │ │ ├── test_ci.py │ │ ├── test_ci_overrides.py │ │ ├── test_ci_revisions.py │ │ ├── test_compatibility.py │ │ ├── test_graph_overrides.py │ │ ├── test_lock_alias.py │ │ ├── test_lock_build_requires.py │ │ ├── test_lock_merge.py │ │ ├── test_lock_packages.py │ │ ├── test_lock_pyrequires.py │ │ ├── test_lock_pyrequires_revisions.py │ │ ├── test_lock_requires.py │ │ ├── test_lock_requires_revisions.py │ │ ├── test_options.py │ │ └── test_user_overrides.py │ ├── metadata/ │ │ ├── __init__.py │ │ ├── test_metadata_collect.py │ │ ├── test_metadata_commands.py │ │ ├── test_metadata_deploy.py │ │ ├── test_metadata_logs.py │ │ └── test_metadata_test_package.py │ ├── options/ │ │ ├── __init__.py │ │ ├── options_test.py │ │ ├── test_configure_options.py │ │ ├── test_options_build_requires.py │ │ └── test_package_config_test.py │ ├── package_id/ │ │ ├── __init__.py │ │ ├── build_id_test.py │ │ ├── compatible_test.py │ │ ├── package_id_and_confs_test.py │ │ ├── package_id_modes_test.py │ │ ├── package_id_requires_modes_test.py │ │ ├── package_id_test.py │ │ ├── python_requires_package_id_test.py │ │ ├── test_cache_compatibles.py │ │ ├── test_config_package_id.py │ │ ├── test_default_package_id.py │ │ ├── test_package_id_test_requires.py │ │ ├── test_valid_package_id_values.py │ │ ├── test_validate.py │ │ ├── transitive_header_only_test.py │ │ └── transitive_options_affect_id_test.py │ ├── py_requires/ │ │ ├── __init__.py │ │ └── python_requires_test.py │ ├── remote/ │ │ ├── __init__.py │ │ ├── auth_bearer_test.py │ │ ├── auth_test.py │ │ ├── broken_download_test.py │ │ ├── download_retries_test.py │ │ ├── download_test.py │ │ ├── multi_remote_checks_test.py │ │ ├── multi_remote_test.py │ │ ├── requester_test.py │ │ ├── rest_api_test.py │ │ ├── retry_test.py │ │ ├── selected_remotes_test.py │ │ ├── server_error_test.py │ │ ├── test_conaninfo_parsing.py │ │ ├── test_local_recipes_index.py │ │ ├── test_offline.py │ │ ├── test_remote_file_credentials.py │ │ ├── test_remote_recipes_only.py │ │ └── test_request_headers.py │ ├── sbom/ │ │ ├── __init__.py │ │ └── test_cyclonedx.py │ ├── settings/ │ │ ├── __init__.py │ │ ├── built_type_setting_test.py │ │ ├── per_package_settings_test.py │ │ ├── remove_subsetting_test.py │ │ ├── settings_override_test.py │ │ ├── test_disable_settings_assignment.py │ │ ├── test_non_defining_settings.py │ │ ├── test_settings_possible_values.py │ │ └── test_settings_user.py │ ├── symlinks/ │ │ ├── __init__.py │ │ └── symlinks_test.py │ ├── sysroot_test.py │ ├── system_reqs_test.py │ ├── test_components.py │ ├── test_components_error.py │ ├── test_compressions.py │ ├── test_db_error.py │ ├── test_migrations.py │ ├── test_package_python_files.py │ ├── test_package_vendor.py │ ├── test_pkg_signing.py │ ├── test_recipe_policies.py │ ├── test_source_download_password.py │ ├── test_timestamp_error.py │ ├── tgz_macos_dot_files_test.py │ ├── toolchains/ │ │ ├── __init__.py │ │ ├── apple/ │ │ │ ├── __init__.py │ │ │ ├── test_xcodedeps.py │ │ │ └── test_xcodetoolchain.py │ │ ├── cmake/ │ │ │ ├── __init__.py │ │ │ ├── cmakeconfigdeps/ │ │ │ │ ├── __init__.py │ │ │ │ ├── test_cmakeconfigdeps.py │ │ │ │ └── test_cmakeconfigdeps_frameworks.py │ │ │ ├── cmakedeps/ │ │ │ │ ├── __init__.py │ │ │ │ ├── test_cmakedeps.py │ │ │ │ └── test_cmakedeps_find_module_and_config.py │ │ │ ├── test_cmake.py │ │ │ ├── test_cmaketoolchain.py │ │ │ └── test_cmaketoolchain_blocks.py │ │ ├── env/ │ │ │ ├── __init__.py │ │ │ ├── test_buildenv.py │ │ │ ├── test_environment.py │ │ │ ├── test_virtualenv_default_apply.py │ │ │ ├── test_virtualenv_object_access.py │ │ │ └── test_virtualenv_winbash.py │ │ ├── gnu/ │ │ │ ├── __init__.py │ │ │ ├── test_autotools.py │ │ │ ├── test_autotoolsdeps.py │ │ │ ├── test_autotoolstoolchain.py │ │ │ ├── test_basic_layout.py │ │ │ ├── test_gnutoolchain.py │ │ │ ├── test_makedeps.py │ │ │ └── test_pkgconfigdeps.py │ │ ├── google/ │ │ │ ├── __init__.py │ │ │ ├── test_bazeldeps.py │ │ │ └── test_bazeltoolchain.py │ │ ├── intel/ │ │ │ ├── __init__.py │ │ │ └── test_intel_cc.py │ │ ├── meson/ │ │ │ ├── __init__.py │ │ │ └── test_mesontoolchain.py │ │ ├── microsoft/ │ │ │ ├── __init__.py │ │ │ ├── test_msbuild_toolchain.py │ │ │ ├── test_msbuilddeps.py │ │ │ ├── test_msbuildtoolchain.py │ │ │ ├── test_nmakedeps.py │ │ │ ├── test_nmaketoolchain.py │ │ │ ├── test_vs_layout.py │ │ │ └── vcvars_test.py │ │ ├── premake/ │ │ │ ├── __init__.py │ │ │ ├── test_premake.py │ │ │ ├── test_premakedeps.py │ │ │ └── test_premaketoolchain.py │ │ ├── qbs/ │ │ │ ├── __init__.py │ │ │ ├── test_qbsdeps.py │ │ │ └── test_qbsprofile.py │ │ ├── scons/ │ │ │ ├── __init__.py │ │ │ └── test_scondeps.py │ │ ├── test_raise_on_universal_binaries.py │ │ └── test_toolchain_namespaces.py │ ├── tools/ │ │ ├── __init__.py │ │ ├── conan_version_test.py │ │ ├── cppstd_minimum_version_test.py │ │ ├── cpu_count_test.py │ │ ├── file_tools_test.py │ │ ├── fix_symlinks_test.py │ │ ├── ros/ │ │ │ ├── __init__.py │ │ │ └── test_rosenv.py │ │ └── system/ │ │ ├── __init__.py │ │ └── package_manager_test.py │ └── workspace/ │ ├── __init__.py │ └── test_workspace.py ├── performance/ │ ├── .gitignore │ ├── __init__.py │ ├── test_compatibility_performance.py │ ├── test_db_performance.py │ └── test_large_graph.py └── unittests/ ├── __init__.py ├── cli/ │ ├── __init__.py │ ├── common_test.py │ └── test_cli_ref_matching.py ├── client/ │ ├── __init__.py │ ├── build/ │ │ ├── __init__.py │ │ ├── c_std_flags_test.py │ │ ├── compiler_flags_test.py │ │ └── cpp_std_flags_test.py │ ├── command/ │ │ ├── __init__.py │ │ └── parse_arguments_test.py │ ├── conan_output_test.py │ ├── conanfile_loader_test.py │ ├── conf/ │ │ ├── __init__.py │ │ ├── config_installer/ │ │ │ ├── __init__.py │ │ │ └── test_install_folder.py │ │ └── detect/ │ │ ├── __init__.py │ │ └── test_gcc_compiler.py │ ├── file_copier/ │ │ ├── __init__.py │ │ └── test_report_copied_files.py │ ├── graph/ │ │ ├── __init__.py │ │ ├── build_mode_test.py │ │ └── deps_graph_test.py │ ├── migrations/ │ │ ├── __init__.py │ │ └── test_migrator.py │ ├── optimize_conanfile_load_test.py │ ├── pkg_sign_test.py │ ├── profile_loader/ │ │ ├── __init__.py │ │ ├── compiler_cppstd_test.py │ │ └── profile_loader_test.py │ ├── remote_manager_test.py │ ├── rest/ │ │ ├── __init__.py │ │ ├── downloader_test.py │ │ ├── requester_test.py │ │ ├── response_test.py │ │ ├── rest_client_v2/ │ │ │ ├── __init__.py │ │ │ └── rest_client_v2_test.py │ │ └── uploader_test.py │ ├── source/ │ │ └── __init__.py │ ├── tools/ │ │ ├── __init__.py │ │ ├── cppstd_required_test.py │ │ ├── files/ │ │ │ ├── __init__.py │ │ │ └── rename_test.py │ │ └── test_env.py │ ├── userio_test.py │ └── util/ │ ├── __init__.py │ └── time_test.py ├── model/ │ ├── __init__.py │ ├── build_info/ │ │ ├── __init__.py │ │ ├── components_test.py │ │ ├── generic_properties_test.py │ │ ├── new_build_info_test.py │ │ └── test_deduce_locations.py │ ├── conanfile_test.py │ ├── info_test.py │ ├── manifest_test.py │ ├── options_test.py │ ├── other_settings_test.py │ ├── profile_test.py │ ├── settings_test.py │ ├── test_conf.py │ ├── test_list.py │ ├── test_package_reference.py │ ├── test_recipe_reference.py │ ├── version/ │ │ ├── __init__.py │ │ ├── test_version_bump.py │ │ ├── test_version_comparison.py │ │ ├── test_version_parse.py │ │ ├── test_version_range.py │ │ └── test_version_range_intersection.py │ └── versionrepr_test.py ├── paths/ │ ├── __init__.py │ └── user_home_test.py ├── search/ │ ├── __init__.py │ ├── cache_db_search_test.py │ └── search_query_parse_test.py ├── server/ │ ├── __init__.py │ ├── authenticator_plugin_test.py │ ├── conan_server_config_parser_test.py │ ├── conf_test.py │ ├── crypto/ │ │ ├── __init__.py │ │ └── jwt_test.py │ ├── revision_list_test.py │ ├── service/ │ │ ├── __init__.py │ │ ├── authorizer_test.py │ │ └── service_test.py │ └── test_utils.py ├── source/ │ ├── __init__.py │ └── merge_directories_test.py ├── tools/ │ ├── __init__.py │ ├── android/ │ │ ├── __init__.py │ │ └── test_android_tools.py │ ├── apple/ │ │ ├── __init__.py │ │ ├── test_apple_tools.py │ │ └── test_xcodebuild.py │ ├── build/ │ │ ├── __init__.py │ │ ├── test_can_run.py │ │ ├── test_compiler.py │ │ ├── test_cppstd.py │ │ ├── test_cross_building.py │ │ ├── test_cstd.py │ │ └── test_stdcpp_library.py │ ├── cmake/ │ │ ├── __init__.py │ │ ├── test_cmake_cmd_line_args.py │ │ ├── test_cmake_install.py │ │ ├── test_cmake_presets_definitions.py │ │ ├── test_cmake_test.py │ │ └── test_cmaketoolchain.py │ ├── env/ │ │ ├── __init__.py │ │ ├── test_env.py │ │ └── test_env_files.py │ ├── files/ │ │ ├── __init__.py │ │ ├── checksums_test.py │ │ ├── collect_lib_test.py │ │ ├── test_chmod.py │ │ ├── test_downloads.py │ │ ├── test_file_read_and_write.py │ │ ├── test_patches.py │ │ ├── test_rename.py │ │ ├── test_rm.py │ │ ├── test_symlinks.py │ │ ├── test_tool_copy.py │ │ ├── test_toolchain.py │ │ └── test_zipping.py │ ├── files_patch_test.py │ ├── gnu/ │ │ ├── __init__.py │ │ ├── autotools_test.py │ │ ├── autotools_toolchain_test.py │ │ ├── autotoolschain_test.py │ │ ├── gnudepsflags_test.py │ │ ├── test_gnutoolchain.py │ │ └── test_triplets.py │ ├── google/ │ │ ├── __init__.py │ │ └── test_bazel.py │ ├── intel/ │ │ ├── __init__.py │ │ └── test_intel_cc.py │ ├── meson/ │ │ ├── __init__.py │ │ └── test_meson.py │ ├── microsoft/ │ │ ├── __init__.py │ │ ├── conantoolchain.props │ │ ├── conantoolchain_release_x64.props │ │ ├── test_check_min_vs.py │ │ ├── test_msbuild.py │ │ ├── test_msvs_toolset.py │ │ └── test_subsystem.py │ └── system/ │ ├── __init__.py │ └── python_manager_test.py └── util/ ├── __init__.py ├── apple_test.py ├── conanfile_tools_test.py ├── detect_libc_test.py ├── detect_test.py ├── detected_architecture_test.py ├── file_hashes_test.py ├── files/ │ ├── __init__.py │ ├── strip_root_extract_test.py │ ├── tar_extract_test.py │ ├── test_copy_compat.py │ ├── test_dirty.py │ └── test_remove.py ├── files_extract_wildcard_test.py ├── local_db_test.py ├── output_test.py ├── test_encrypt.py ├── tools_test.py ├── unix_path_test.py ├── xz_test.py └── zip_permissions_test.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ci/__init__.py ================================================ ================================================ FILE: .ci/bump_dev_version.py ================================================ import os import time def replace_in_file(file_path, search, replace): with open(file_path, "r") as handle: content = handle.read() if search not in content: raise Exception("Incorrect development version in conan/__init__.py") content = content.replace(search, replace) content = content.encode("utf-8") with open(file_path, "wb") as handle: handle.write(content) def bump_dev(): vfile = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../conan/__init__.py") snapshot = "%s" % int(time.time()) replace_in_file(vfile, "-dev'", "-dev%s'" % snapshot) if __name__ == "__main__": bump_dev() ================================================ FILE: .ci/docker/conan-tests ================================================ FROM ubuntu:24.04 LABEL maintainer="Conan.io " ENV DEBIAN_FRONTEND=noninteractive ENV PY37=3.7.9 \ PY38=3.8.6 \ PY39=3.9.2 \ PY310=3.10.16 \ PY312=3.12.3 \ PY313=3.13.0 \ PY314=3.14.0 \ CMAKE_3_15=/usr/share/cmake-3.15.7/bin/cmake \ CMAKE_3_16=/usr/share/cmake-3.16.9/bin/cmake \ CMAKE_3_17=/usr/share/cmake-3.17.5/bin/cmake \ CMAKE_3_19=/usr/share/cmake-3.19.7/bin/cmake \ CMAKE_3_23=/usr/share/cmake-3.23.5/bin/cmake \ CMAKE_3_27=/usr/share/cmake-3.27.9/bin/cmake \ CMAKE_4_2=/usr/share/cmake-4.2.1/bin/cmake \ GCC_9=/usr/bin/gcc-9 \ GXX_9=/usr/bin/g++-9 \ GCC_11=/usr/bin/gcc-11 \ GXX_11=/usr/bin/g++-11 \ CLANG_14=/usr/bin/clang-14 \ CLANGXX_14=/usr/bin/clang++-14 \ BAZEL_6=6.5.0 \ BAZEL_7=7.6.2 \ BAZEL_8=8.4.2 \ EMSDK=4.0.22 RUN apt-get update && \ apt-get install -y --no-install-recommends \ software-properties-common \ build-essential \ libtool \ automake \ autoconf \ pkg-config \ gettext \ git \ curl \ make \ libssl-dev \ zlib1g-dev \ libbz2-dev \ libreadline-dev \ libsqlite3-dev \ wget \ llvm \ libncurses5-dev \ libncursesw5-dev \ xz-utils \ tk-dev \ libffi-dev \ liblzma-dev \ libzstd-dev \ python3-openssl \ ca-certificates \ sudo \ tar \ linux-libc-dev \ subversion \ subversion-tools \ ninja-build \ gcc-9 \ g++-9 \ gcc-11 \ g++-11 \ clang-14 \ clang++-14 \ gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf \ unzip \ apt-transport-https \ gnupg-agent \ gcc-9-multilib \ g++-9-multilib \ gcc-11-multilib \ g++-11-multilib \ scons && \ # fix: asm/errno.h: No such file or directory ln -s /usr/include/asm-generic/ /usr/include/asm && \ add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ apt-get update && \ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && \ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu jammy stable" && \ apt-get update && \ apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io && \ rm -rf /var/lib/apt/lists/* RUN useradd -m -s /bin/bash conan && \ echo 'conan ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers ENV HOME /home/conan ENV PYENV_ROOT $HOME/.pyenv ENV PATH $PYENV_ROOT/bin:$PYENV_ROOT/shims:/usr/bin:/bin:$PATH RUN curl https://pyenv.run | bash && \ pyenv install $PY37 && \ pyenv install $PY38 && \ pyenv install $PY39 && \ pyenv install $PY310 && \ pyenv install $PY312 && \ pyenv install $PY314 && \ pyenv global $PY39 && \ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ python get-pip.py && \ rm get-pip.py RUN chown -R conan:conan $HOME USER root RUN wget https://github.com/Kitware/CMake/releases/download/v3.15.7/cmake-3.15.7-Linux-x86_64.tar.gz && \ tar -xvzf cmake-3.15.7-Linux-x86_64.tar.gz && mv cmake-3.15.7-Linux-x86_64 /usr/share/cmake-3.15.7 && \ wget https://github.com/Kitware/CMake/releases/download/v3.16.9/cmake-3.16.9-Linux-x86_64.tar.gz && \ tar -xvzf cmake-3.16.9-Linux-x86_64.tar.gz && mv cmake-3.16.9-Linux-x86_64 /usr/share/cmake-3.16.9 && \ wget https://github.com/Kitware/CMake/releases/download/v3.17.5/cmake-3.17.5-Linux-x86_64.tar.gz && \ tar -xvzf cmake-3.17.5-Linux-x86_64.tar.gz && mv cmake-3.17.5-Linux-x86_64 /usr/share/cmake-3.17.5 && \ wget https://github.com/Kitware/CMake/releases/download/v3.19.7/cmake-3.19.7-Linux-x86_64.tar.gz && \ tar -xvzf cmake-3.19.7-Linux-x86_64.tar.gz && mv cmake-3.19.7-Linux-x86_64 /usr/share/cmake-3.19.7 && \ wget https://github.com/Kitware/CMake/releases/download/v3.23.5/cmake-3.23.5-Linux-x86_64.tar.gz && \ tar -xvzf cmake-3.23.5-Linux-x86_64.tar.gz && mv cmake-3.23.5-linux-x86_64/ /usr/share/cmake-3.23.5 && \ wget https://github.com/Kitware/CMake/releases/download/v3.27.9/cmake-3.27.9-Linux-x86_64.tar.gz && \ tar -xvzf cmake-3.27.9-Linux-x86_64.tar.gz && mv cmake-3.27.9-linux-x86_64/ /usr/share/cmake-3.27.9 && \ wget https://cmake.org/files/v4.2/cmake-4.2.1-linux-x86_64.tar.gz && \ tar -xvzf cmake-4.2.1-linux-x86_64.tar.gz && mv cmake-4.2.1-linux-x86_64/ /usr/share/cmake-4.2.1 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_15 10 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_16 20 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_17 30 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_19 40 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_23 50 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_27 60 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_4_2 70 && \ # set CMake 3.15 as default update-alternatives --set cmake $CMAKE_3_15 RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 10 && \ update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 30 && \ update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 10 && \ update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 30 && \ update-alternatives --install /usr/bin/clang clang /usr/bin/clang-14 10 && \ update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-14 10 && \ update-alternatives --set gcc /usr/bin/gcc-9 && \ update-alternatives --set g++ /usr/bin/g++-9 && \ update-alternatives --set clang /usr/bin/clang-14 && \ update-alternatives --set clang++ /usr/bin/clang++-14 RUN mkdir -p /usr/share/bazel-$BAZEL_6/bin && \ wget https://github.com/bazelbuild/bazel/releases/download/${BAZEL_6}/bazel-${BAZEL_6}-linux-x86_64 && \ chmod +x bazel-${BAZEL_6}-linux-x86_64 && \ mv bazel-${BAZEL_6}-linux-x86_64 /usr/share/bazel-$BAZEL_6/bin/bazel && \ mkdir -p /usr/share/bazel-$BAZEL_7/bin && \ wget https://github.com/bazelbuild/bazel/releases/download/${BAZEL_7}/bazel-${BAZEL_7}-linux-x86_64 && \ chmod +x bazel-${BAZEL_7}-linux-x86_64 && \ mv bazel-${BAZEL_7}-linux-x86_64 /usr/share/bazel-$BAZEL_7/bin/bazel && \ mkdir -p /usr/share/bazel-$BAZEL_8/bin && \ wget https://github.com/bazelbuild/bazel/releases/download/${BAZEL_8}/bazel-${BAZEL_8}-linux-x86_64 && \ chmod +x bazel-${BAZEL_8}-linux-x86_64 && \ mv bazel-${BAZEL_8}-linux-x86_64 /usr/share/bazel-$BAZEL_8/bin/bazel RUN wget https://github.com/premake/premake-core/releases/download/v5.0.0-beta4/premake-5.0.0-beta4-linux.tar.gz && \ tar -xvzf premake-5.0.0-beta4-linux.tar.gz && chmod +x premake5 && mkdir /usr/share/premake && \ mv premake5 /usr/share/premake RUN cd /tmp && \ mkdir qbs && \ cd qbs && \ curl -L https://download.qt.io/official_releases/qbs/2.6.0/qbs-linux-x86_64-2.6.0.tar.gz > qbs-linux-x86_64-2.6.0.tar.gz && \ tar -xzf qbs-linux-x86_64-2.6.0.tar.gz && \ mv qbs-linux-x86_64-2.6.0 /usr/share/qbs && \ rm qbs-linux-x86_64-2.6.0.tar.gz RUN cd /tmp && \ wget https://github.com/emscripten-core/emsdk/archive/refs/tags/${EMSDK}.tar.gz && \ tar xzf ${EMSDK}.tar.gz --directory /usr/share && \ cd /usr/share/emsdk-${EMSDK} && \ pyenv local 3.12 && \ ./emsdk update && \ ./emsdk install latest && \ ./emsdk activate latest --permanent && \ . /usr/share/emsdk-${EMSDK}/emsdk_env.sh && \ embuilder build MINIMAL && \ embuilder build MINIMAL --wasm64 # echo "EMSDK_QUIET=1 . /usr/share/emsdk-${EMSDK}/emsdk_env.sh" >> /etc/bash.bashrc # Manually add the emsdk binaries to the PATH and set emcc cache directory to a writable location ENV PATH="/usr/share/emsdk-$EMSDK:/usr/share/emsdk-$EMSDK/upstream/emscripten:/usr/share/emsdk-$EMSDK/node/22.16.0_64bit/bin:$PATH" \ EM_CACHE=$HOME/.emscripten_cache RUN echo "export QT_NO_GLIB=1" >> /etc/profile.d/qt.sh USER conan WORKDIR $HOME CMD ["/bin/bash"] ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org/ root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 # Docstrings and comments use max_line_length = 79 [*.py] max_line_length = 101 ================================================ FILE: .github/CONTRIBUTING.md ================================================ Contributing to Conan ===================== The following summarizes the process for contributing to the Conan project. Community --------- Conan is an Open Source MIT licensed project. Conan is developed by the Conan maintainers and a great community of contributors. Dev-flow & Pull Requests ------------------------ Conan follows the ["GitFlow"](https://datasift.github.io/gitflow/IntroducingGitFlow.html) branching model. Issues are triaged and categorized mainly by type (feature, bug...) complexity (high, medium...) and priority (high, medium...) using GitHub labels. Then they are moved to a stage called "queue" when they are considered to be ready for implementation. To contribute follow the next steps: 1. Comment in the corresponding issue that you want to contribute the feature/fix proposed. If there is no open issue, we strongly suggest to open one to gather feedback. 2. Check that the issue has been staged to "queue" or ask @conan-io/barbarians to do it. This helps in terms of validation and discussion of possible implementation of the feature/fix. 3. Fork the [Conan main repository](https://github.com/conan-io/conan) and create a `feature/xxx` branch from the `develop2` branch and develop your fix/feature as discussed in previous step. 4. Try to keep your branch updated with the `develop2` branch to avoid conflicts. 5. Run the ``test/unittest`` and ``test/integration`` test suite locally, as described in [Conan tests guidelines section](https://github.com/conan-io/conan/blob/develop2/test/README.md). If you are doing changes to a build system integration, locate the respective folder for that integration in ``test/functional`` and run the tests of that folder. It is not expected that contributors have to run the full test suite locally, as it requires too many external tools. 6. Open a pull request, and select `develop2` as the base branch. Never open a pull request to ``release/xxx`` branches, unless the branch is to be part of the next 2.X.Y patch. In that case, the PR should be targeted to ``release/2.X``. 7. Add the text (besides other comments): "fixes #IssueNumber" in the body of the PR, referring to the issue of step 1. 8. Submit a PR to the Conan documentation about the changes done providing examples if needed. The ``conan-io`` organization maintainers will review and help with the coding of tests. Finally it will be assigned to milestone. Each milestone corresponds to a Conan release, a branch `release/xxx` will be opened from the `develop2` branch. When the branch is ready, a tag corresponding to the version will be pushed and it will be merged into `develop2` branch. The release plan is done monthly but dates are not fixed, the ``conan-io`` organization maintainers reserve the right of altering the issues of each milestone as well as the release dates. Issues ------ If you think you found a bug in Conan open an issue indicating the following: - Explain the Conan version, Operating System, compiler and any other tool that could be related to the issue. - Explain, as detailed as possible, how to reproduce the issue. Use git repos to contain code/recipes to reproduce issues. - Include the expected behavior as well as what actually happened. - Provide output captures (as text). - Feel free to attach a screenshot or video illustrating the issue if you think it will be helpful. For any suggestion, feature request or question open an issue indicating the following: - Questions and support requests are always welcome. - Use the [question] or [suggestion] tags in the title. - Try to explain the motivation, what are you trying to do, what is the pain it tries to solve. - What do you expect from Conan. We use the following tags to control the status of the issues: - **triaging**: Issue is being considered or under discussion. Maintainers are trying to understand the use case or gathering the necessary information. - **queue**: Issue has been categorized and is ready to be done in a following release (not necessarily in the next one). - **in-progress**: A milestone has previously been assigned to the issue and is now under development. - **review**: Issue has a PR associated that solves it (remember to use the GitHub keywords "closes #IssueNumber", "fixes #IssueNumber"... in the description of the PR). - **closed via PR**: A PR with the fix or new feature has been merged to `develop2` and the issue will be fixed in the next monthly release. Code of conduct --------------- Try to be polite, Conan maintainers and contributors are really willing to help and we enjoy it. Please keep in mind these points: - There are limited resources/time and not all issues/pull requests can be considered as well as we would like. - ``conan-io`` maintainers can tag/close/modify any opened issue and it should not be interpreted as a rude or disrespectful action. It **always** responds to organizational purposes. A closed issue can be perfectly reopened or further commented. - It is very hard to keep the project in good health in terms of technical debt, usability, serviceability, etc. If the proposed feature is not clearly stated, enough information is not provided or it is not sure that would be a good feature for the future development of the project, it won't be accepted. The community plays a very important role during this discussion so it strongly encouraged to explain the value of a feature for the community, the needed documentation and its use cases. - Backwards compatibility and not breaking users' packages is very important and it won't be done unless there are very good reasons. - You should not get bothered if you feel unattended, Conan is an Open Source project, not a commercial product. Try to explain what you really need and we will try to help you. - The Conan maintainers won't tolerate harassment or abuse. If a user is not following the code of conduct: - We will contact the user to remind the rules. - If the misconduct continues, they will be banned from the repository for one week. - After that, any other code of conduct transgression will carry the permanent ban from all the conan-io organization repositories. Code style ---------- - In general, follow [pep8](https://www.python.org/dev/peps/pep-0008/) - Limit all lines to a maximum of 101 characters (`Right margin` setting in PyCharm) - Specify imports in the following order: system, `blank line`, 3rd-party, `blank line`, own; all sorted alphabetically: ``` import os import platform import shutil from tqdm import tqdm from conan.tools.cmake import CMakeToolchain from conan.tools.files import save, load ``` - Write tests, if possible ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug Report description: Report a bug, something does not work as it's supposed to title: '[bug]' body: - type: textarea id: description attributes: label: Describe the bug description: Include the bug description and environment details placeholder: | Environment details: OS, compiler, Conan version, Conan profiles, etc. Description: xxxx validations: required: false - type: textarea id: steps attributes: label: How to reproduce it description: It would be great to know how to reproduce it locally placeholder: | Commands to reproduce it, remote repository to use it locally, etc. Every detail is more than welcome! validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature Request description: Request a new feature or suggest a change title: '[feature] SHORT DESCRIPTION' body: - type: markdown attributes: value: | Thanks for taking the time to submit a request. **Please don't forget to update the issue title.** - type: textarea id: suggestion attributes: label: What is your suggestion? description: Please be as specific as possible! placeholder: Hi! I would like for conan to be able to ... validations: required: true - type: checkboxes id: contributing attributes: label: Have you read the CONTRIBUTING guide? description: It can be found in [CONTRIBUTING.md](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md) options: - label: I've read the CONTRIBUTING guide validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/question.yml ================================================ name: Question description: If something needs clarification title: '[question] SHORT DESCRIPTION' body: - type: markdown attributes: value: | Thanks for taking the time to fill your question. **Please don't forget to update the issue title.** - type: textarea id: question attributes: label: What is your question? description: Please be as specific as possible! placeholder: Hi! I have a question regarding ... validations: required: true - type: checkboxes id: contributing attributes: label: Have you read the CONTRIBUTING guide? description: It can be found in [CONTRIBUTING.md](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md) options: - label: I've read the CONTRIBUTING guide validations: required: true ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Changelog: (Feature | Fix | Bugfix): Describe here your pull request Docs: https://github.com/conan-io/docs/pull/XXXX - [ ] Refer to the issue that supports this Pull Request. - [ ] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request. - [ ] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop2/.github/CONTRIBUTING.md). - [ ] I've followed the PEP8 style guides for Python code. - [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. ================================================ FILE: .github/actions/test-coverage/action.yml ================================================ description: 'Run tests enabling coverage in certain conditions and upload coverage artifacts for later process' inputs: python-version: description: 'Python version in which the tests was ran' required: true test-type: description: 'Test suite name' required: true duration: description: 'Show N slowest test durations (N=0 for all)' required: true default: '10' tests: description: 'Tests folder and options to run' required: false workers: description: 'Number of workers to run tests' default: auto runs: using: 'composite' steps: - name: Run tests with coverage if: ${{ inputs.tests }} shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} run: | pytest ${{ inputs.tests }} --durations=${{ inputs.duration }} -n ${{ inputs.workers }} ${{ github.ref == 'refs/heads/develop2' && '--cov=conan --cov=conans --cov=test --cov-report=' || '' }} - name: Rename coverage file if: github.ref == 'refs/heads/develop2' shell: bash run: mv .coverage .coverage.${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.test-type }} - name: Upload coverage artifact if: github.ref == 'refs/heads/develop2' uses: actions/upload-artifact@v4 with: name: .coverage.${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.test-type }} path: .coverage.${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.test-type }} include-hidden-files: true ================================================ FILE: .github/workflows/build-binaries.yml ================================================ name: Build Conan Binaries run-name: Build Conan Binaries - v${{ inputs.conan_version }} - ${{ inputs.target_sha }} - ${{ inputs.request_id }} on: workflow_dispatch: inputs: conan_version: { description: Conan version to package, required: true, type: string } target_sha: { description: Git SHA to package, required: true, type: string } request_id: { description: ID to identify run, required: true, type: string } permissions: contents: read jobs: package: name: Package for ${{ matrix.platform }}/${{ matrix.arch }} strategy: fail-fast: false matrix: include: - { platform: Linux, arch: x86_64, runner: ubuntu-24.04 } - { platform: Windows, arch: x86_64, runner: windows-2022 } - { platform: Windows, arch: i686, runner: windows-2022 } - { platform: Windows, arch: arm64, runner: windows-11-arm } - { platform: Macos, arch: arm64, runner: macos-14 } - { platform: Macos, arch: x86_64, runner: macos-14 } - { platform: Linux, arch: arm64, runner: ubuntu-24.04-arm } runs-on: ${{ matrix.runner }} steps: - name: Show inputs shell: bash run: | echo "conan_version=${{ inputs.conan_version }}" echo "target_sha=${{ inputs.target_sha }}" echo "request_id=${{ inputs.request_id }}" - name: Generate Read-Only App Token id: generate_token uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.GH_APP_RELEASE_ID }} private-key: ${{ secrets.GH_APP_RELEASE_PRIVATE_KEY }} permission-contents: read owner: conan-io repositories: | conan release-tools - name: Checkout release-tools uses: actions/checkout@v4 with: repository: conan-io/release-tools token: ${{ steps.generate_token.outputs.token }} - name: Build packages shell: bash env: PLATFORM: ${{ matrix.platform }} ARCH: ${{ matrix.arch }} run: | ./jenkins/build_packages.sh "${{ inputs.conan_version }}" "${{ inputs.target_sha }}" - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: conan-${{ inputs.conan_version }}-${{ inputs.target_sha }}-${{ matrix.platform }}-${{ matrix.arch }}-${{ inputs.request_id }} path: dist/* ================================================ FILE: .github/workflows/linux-tests.yml ================================================ name: Linux tests on: workflow_call: inputs: python-versions: required: true type: string run-slow-tests: required: false default: 'false' type: string jobs: build_container: runs-on: ubuntu-latest name: Build Linux container outputs: image_tag: ${{ steps.dockerfile_hash.outputs.tag }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Calculate Dockerfile checksum id: dockerfile_hash run: | DOCKERFILE_HASH=$(find ./.ci/docker/conan-tests -type f -exec sha256sum {} \; | sha256sum | cut -d' ' -f1) echo "tag=$DOCKERFILE_HASH" >> $GITHUB_OUTPUT - name: Check if image exists id: check_image run: | if docker manifest inspect ghcr.io/${{ github.repository_owner }}/conan-tests:${{ steps.dockerfile_hash.outputs.tag }} > /dev/null 2>&1; then echo "status=exists" >> $GITHUB_OUTPUT else echo "status=no-image" >> $GITHUB_OUTPUT fi - name: Build and push image if not exists if: steps.check_image.outputs.status == 'no-image' run: | docker build -t ghcr.io/${{ github.repository_owner }}/conan-tests:${{ steps.dockerfile_hash.outputs.tag }} -f ./.ci/docker/conan-tests . docker push ghcr.io/${{ github.repository_owner }}/conan-tests:${{ steps.dockerfile_hash.outputs.tag }} linux_tests: needs: build_container runs-on: ubuntu-latest container: image: ghcr.io/${{ github.repository_owner }}/conan-tests:${{ needs.build_container.outputs.image_tag }} options: --user conan strategy: matrix: python-version: ${{ fromJson(inputs.python-versions) }} test-type: [unittests, integration, functional] include: - test-type: unittests test-name: Unit - test-type: integration test-name: Integration - test-type: functional test-name: Functional name: ${{ matrix.test-name }} Tests (${{ matrix.python-version }}) steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} run: | pyenv global ${{ matrix.python-version }} python --version - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }} - name: Install dependencies run: | pip install --upgrade pip pip install -r conans/requirements.txt pip install -r conans/requirements_dev.txt pip install -r conans/requirements_server.txt pip install meson - name: Run tests uses: ./.github/actions/test-coverage with: python-version: ${{ matrix.python-version }} test-type: ${{ matrix.test-type }} tests: ${{ matrix.test-type == 'functional' && (inputs.run-slow-tests == 'true' && 'test/functional' || 'test/functional -m "not slow"') || format('test/{0}', matrix.test-type) }} linux_runner_tests: needs: build_container runs-on: ubuntu-latest strategy: matrix: # Use modern versions due to docker incompatibility with python <3.8 python-version: ${{ github.event_name != 'pull_request' && fromJson('["3.13", "3.9"]') || fromJson('["3.10"]') }} name: Runner Tests (${{ matrix.python-version }}) steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements_runner*.txt') }} - name: Install dependencies run: | pip install --upgrade pip pip install -r conans/requirements.txt pip install -r conans/requirements_dev.txt pip install -r conans/requirements_server.txt pip install -r conans/requirements_runner.txt - name: Run runner tests uses: ./.github/actions/test-coverage with: python-version: ${{ matrix.python-version }} test-type: runners tests: '-m docker_runner -rs' workers: 1 ================================================ FILE: .github/workflows/main.yml ================================================ name: Main Workflow on: push: branches: - develop2 - release/* - '*' pull_request: branches: - '*' - 'release/*' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: ensure_latest_tag_merged: runs-on: ubuntu-latest name: Ensure latest release is merged in develop2 branch steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - shell: bash run: | git fetch --tags --prune latest_tag=$(git tag -l --sort=-v:refname | head -n1) echo "Checking that branch 'develop2' contains the latest release tag: $latest_tag" git merge-base --is-ancestor "$latest_tag" origin/develop2 set_python_versions: runs-on: ubuntu-latest outputs: python_versions_linux_windows: ${{ steps.set_versions.outputs.python_versions_linux_windows }} python_versions_macos: ${{ steps.set_versions.outputs.python_versions_macos }} run_slow_tests: ${{ steps.set_versions.outputs.run_slow_tests }} name: Determine Python versions steps: - name: Fetch current PR body and check for slow-tests marker if: github.event_name == 'pull_request' id: fetch_pr run: | BODY=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \ | jq -r '.body // ""') if echo "$BODY" | grep -Fq '$runslowtests'; then echo "run_slow_tests=true" >> $GITHUB_OUTPUT else echo "run_slow_tests=false" >> $GITHUB_OUTPUT fi - name: Determine Python versions id: set_versions run: | if [[ "${{ github.ref }}" == "refs/heads/develop2" || "${{ github.ref }}" == refs/heads/release/* ]]; then echo "python_versions_linux_windows=['3.14', '3.7']" >> $GITHUB_OUTPUT echo "python_versions_macos=['3.14', '3.8']" >> $GITHUB_OUTPUT else echo "python_versions_linux_windows=['3.10']" >> $GITHUB_OUTPUT echo "python_versions_macos=['3.10']" >> $GITHUB_OUTPUT fi # Run slow tests on develop2, or when PR description contains $runslowtests (checked in fetch_pr step) if [[ "${{ github.ref }}" == "refs/heads/develop2" ]]; then echo "run_slow_tests=true" >> $GITHUB_OUTPUT elif [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "run_slow_tests=${{ steps.fetch_pr.outputs.run_slow_tests }}" >> $GITHUB_OUTPUT else echo "run_slow_tests=false" >> $GITHUB_OUTPUT fi linux_suite: needs: [ensure_latest_tag_merged, set_python_versions] uses: ./.github/workflows/linux-tests.yml name: Linux test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_linux_windows }} run-slow-tests: ${{ needs.set_python_versions.outputs.run_slow_tests }} osx_suite: needs: [ensure_latest_tag_merged, set_python_versions] uses: ./.github/workflows/osx-tests.yml name: OSX test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_macos }} run-slow-tests: ${{ needs.set_python_versions.outputs.run_slow_tests }} windows_suite: needs: [ensure_latest_tag_merged, set_python_versions] uses: ./.github/workflows/win-tests.yml name: Windows test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_linux_windows }} run-slow-tests: ${{ needs.set_python_versions.outputs.run_slow_tests }} code_coverage: runs-on: ubuntu-latest name: Code coverage if: github.ref == 'refs/heads/develop2' # Only measure code coverage on main branch needs: [linux_suite, osx_suite, windows_suite] steps: - name: Checkout code uses: actions/checkout@v4 - name: Download coverage artifacts uses: actions/download-artifact@v4 with: merge-multiple: true - name: Merge coverage reports run: | pip install coverage coverage combine coverage report coverage xml - name: Code coverage uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - uses: geekyeggo/delete-artifact@v5 with: name: | .coverage.* deploy_to_pypi_test: runs-on: ubuntu-latest name: Deploy to TestPyPI if: github.ref == 'refs/heads/develop2' needs: [linux_suite, osx_suite, windows_suite] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies run: | pip install --upgrade pip pip install twine - name: Bump Dev Version run: | python .ci/bump_dev_version.py - name: Build Package run: | python setup.py sdist - name: Upload to TestPyPI env: TWINE_USERNAME: ${{ secrets.TEST_PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} run: | python -m twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/* - name: Deploy conan-server to TestPyPI env: TWINE_USERNAME: ${{ secrets.TEST_PYPI_SERVER_USERNAME }} TWINE_PASSWORD: ${{ secrets.TEST_PYPI_SERVER_PASSWORD }} run: | rm -rf dist/ mv setup_server.py setup.py python setup.py sdist python -m twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/* ================================================ FILE: .github/workflows/osx-tests.yml ================================================ name: OSX Tests on: workflow_call: inputs: python-versions: required: true type: string run-slow-tests: required: false default: 'false' type: string jobs: osx_setup: runs-on: macos-15 name: Setup and Cache Dependencies steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install Python requirements run: | pip install --upgrade pip pip install -r conans/requirements.txt pip install -r conans/requirements_server.txt pip install -r conans/requirements_dev.txt pip install meson - name: Uninstall default CMake run: brew uninstall --formula cmake || true - name: Cache Homebrew packages id: cache-brew uses: actions/cache@v4 with: path: ~/Library/Caches/Homebrew key: ${{ runner.os }}-brew - name: Install homebrew dependencies run: | brew install xcodegen make libtool zlib autoconf automake ninja emscripten - name: Cache CMake and Bazel installations id: cache-tools uses: actions/cache@v4 with: path: | ~/Applications/CMake/3.15.7 ~/Applications/CMake/3.19.7 ~/Applications/CMake/3.23.5 ~/Applications/CMake/3.27.9 ~/Applications/CMake/4.2.1 ~/Applications/bazel/6.5.0 ~/Applications/bazel/7.6.2 ~/Applications/bazel/8.4.2 key: ${{ runner.os }}-conan-tools-cache - name: Build CMake old versions not available for ARM if: steps.cache-tools.outputs.cache-hit != 'true' run: | set -e CMAKE_BUILD_VERSIONS=("3.15.7") for version in "${CMAKE_BUILD_VERSIONS[@]}"; do echo "Compiling CMake version ${version} from source for ARM..." wget -q --no-check-certificate https://cmake.org/files/v${version%.*}/cmake-${version}.tar.gz tar -xzf cmake-${version}.tar.gz cd cmake-${version} mkdir build && cd build ../bootstrap --prefix=${HOME}/Applications/CMake/${version} -- -DCMAKE_USE_OPENSSL=ON make -j$(sysctl -n hw.ncpu) make install ${HOME}/Applications/CMake/${version}/bin/cmake --version cd ../../ rm -rf cmake-${version} cmake-${version}.tar.gz done - name: Install universal CMake versions if: steps.cache-tools.outputs.cache-hit != 'true' run: | set -e CMAKE_PRECOMP_VERSIONS=("3.19.7" "3.23.5" "3.27.9" "4.2.1") for version in "${CMAKE_PRECOMP_VERSIONS[@]}"; do echo "Downloading and installing precompiled universal CMake version ${version}..." wget -q --no-check-certificate https://cmake.org/files/v${version%.*}/cmake-${version}-macos-universal.tar.gz tar -xzf cmake-${version}-macos-universal.tar.gz \ --exclude=CMake.app/Contents/bin/cmake-gui \ --exclude=CMake.app/Contents/doc/cmake \ --exclude=CMake.app/Contents/share/cmake-${version%.*}/Help \ --exclude=CMake.app/Contents/share/vim mkdir -p ${HOME}/Applications/CMake/${version} cp -fR cmake-${version}-macos-universal/CMake.app/Contents/* ${HOME}/Applications/CMake/${version} ${HOME}/Applications/CMake/${version}/bin/cmake --version rm -rf cmake-${version}-macos-universal rm cmake-${version}-macos-universal.tar.gz done - name: Install Bazel versions if: steps.cache-tools.outputs.cache-hit != 'true' run: | set -e for version in 6.5.0 7.6.2 8.4.2; do mkdir -p ${HOME}/Applications/bazel/${version} wget -q -O ${HOME}/Applications/bazel/${version}/bazel https://github.com/bazelbuild/bazel/releases/download/${version}/bazel-${version}-darwin-arm64 chmod +x ${HOME}/Applications/bazel/${version}/bazel done osx_tests: needs: osx_setup runs-on: macos-15 strategy: fail-fast: true matrix: python-version: ${{ fromJson(inputs.python-versions) }} test-type: [unittests, integration, functional] include: - test-type: unittests test-name: Unit - test-type: integration test-name: Integration - test-type: functional test-name: Functional name: ${{ matrix.test-name }} Tests (${{ matrix.python-version }}) steps: - name: Checkout code uses: actions/checkout@v4 - name: Restore tools cache uses: actions/cache@v4 with: path: | ~/Applications/CMake/3.15.7 ~/Applications/CMake/3.19.7 ~/Applications/CMake/3.23.5 ~/Applications/CMake/3.27.9 ~/Applications/CMake/4.2.1 ~/Applications/bazel/6.5.0 ~/Applications/bazel/7.6.2 ~/Applications/bazel/8.4.2 key: ${{ runner.os }}-conan-tools-cache - name: Select Xcode 16.4 run: | sudo xcode-select -s /Applications/Xcode_16.4.app xcodebuild -version xcrun --sdk macosx --show-sdk-version clang --version # Install system dependencies BEFORE setting up the matrix Python. # This prevents Emscripten (which requires Python 3.10+) from crashing # when trying to use Python 3.8 from the matrix. - name: Install homebrew dependencies run: | brew update brew install xcodegen make libtool zlib autoconf automake ninja emscripten export PATH=${HOME}/Applications/CMake/3.15.7/bin:$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin emcc --version cmake --version bazel --version - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Python Dependencies run: | pip install --upgrade pip pip install -r conans/requirements.txt pip install -r conans/requirements_server.txt pip install -r conans/requirements_dev.txt pip install meson - name: Run tests uses: ./.github/actions/test-coverage with: python-version: ${{ matrix.python-version }} test-type: ${{ matrix.test-type }} tests: ${{ matrix.test-type == 'functional' && (inputs.run-slow-tests == 'true' && 'test/functional' || 'test/functional -m "not slow"') || format('test/{0}', matrix.test-type) }} ================================================ FILE: .github/workflows/win-tests.yml ================================================ name: Windows Tests on: workflow_call: inputs: python-versions: required: true type: string run-slow-tests: required: false default: 'false' type: string jobs: unit_integration_tests: runs-on: windows-2022 strategy: matrix: python-version: ${{ fromJson(inputs.python-versions) }} name: Unit & Integration Tests (${{ matrix.python-version }}) steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Visual Studio Build Tools run: | Invoke-WebRequest -Uri "https://aka.ms/vs/15/release/vs_buildtools.exe" -OutFile "vs_buildtools15.exe" Start-Process -FilePath ".\vs_buildtools15.exe" -ArgumentList ` "--quiet", "--wait", "--norestart", "--nocache", ` "--add", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", ` "--add", "Microsoft.Component.MSBuild" -WindowStyle Hidden -Wait - name: Determine pip cache directory id: pip-cache-dir shell: pwsh run: echo "PIP_CACHE_DIR=$(pip cache dir)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Cache pip packages uses: actions/cache@v4 with: path: ${{ env.PIP_CACHE_DIR }} key: pip-packages-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }} restore-keys: | pip-packages-${{ runner.os }}-${{ matrix.python-version }}- - name: Install Python requirements run: | pip install --upgrade pip pip install -r conans/requirements.txt pip install -r conans/requirements_dev.txt pip install -r conans/requirements_server.txt git config --global core.autocrlf false - name: Run Unit & Integration Tests uses: ./.github/actions/test-coverage with: python-version: ${{ matrix.python-version }} test-type: unit-integration tests: test/unittests test/integration functional_tests: runs-on: windows-2022 strategy: matrix: python-version: ${{ fromJson(inputs.python-versions) }} name: Functional Tests (${{ matrix.python-version }}) steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install MSVC v14.38 Toolset run: | Start-Process -Wait "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe" -ArgumentList {` modify ` --quiet ` --installPath "C:\Program Files\Microsoft Visual Studio\2022\Enterprise" ` --add ` Microsoft.VisualStudio.Component.VC.14.38.17.8.x86.x64 ` } - name: Verify MSVC v14.38 toolset installation run: dir "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC" - name: Install Visual Studio Build Tools run: | Invoke-WebRequest -Uri "https://aka.ms/vs/15/release/vs_buildtools.exe" -OutFile "vs_buildtools15.exe" Start-Process -FilePath ".\vs_buildtools15.exe" -ArgumentList ` "--quiet", "--wait", "--norestart", "--nocache", ` "--add", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", ` "--add", "Microsoft.VisualStudio.Component.Windows81SDK", ` "--add", "Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core", ` "--add", "Microsoft.Component.MSBuild", ` "--add", "Microsoft.VisualStudio.Component.VC.140" -WindowStyle Hidden -Wait - name: Determine pip cache directory id: pip-cache-dir shell: pwsh run: echo "PIP_CACHE_DIR=$(pip cache dir)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Cache pip packages uses: actions/cache@v4 with: path: ${{ env.PIP_CACHE_DIR }} key: pip-packages-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }} restore-keys: | pip-packages-${{ runner.os }}-${{ matrix.python-version }}- - name: Install Python requirements run: | pip install --upgrade pip pip install -r conans/requirements.txt pip install -r conans/requirements_server.txt pip install -r conans/requirements_dev.txt pip install meson - name: "Set choco cache" run: choco config set cacheLocation C:\choco-cache - uses: actions/cache@v4 with: path: C:\choco-cache key: choco-cache - name: Install Chocolatey packages run: | choco install pkgconfiglite --version 0.28 choco install ninja --version 1.10.2 choco install mingw # mirrors.kernel.org/sourceware/cygwin returns 404; use alternative mirror from this list: https://cygwin.com/mirrors.html choco install cygwin --execution-timeout=300 --package-parameters="'/Site:https://mirror.clarkson.edu/cygwin/'" choco install cyg-get cyg-get automake gcc-g++ make binutils --verbose - uses: msys2/setup-msys2@v2 id: msys2-setup with: update: true # It's important that the default environment that is used is MSYS # we check this default in a test msystem: MSYS install: >- mingw-w64-x86_64-toolchain mingw-w64-i686-toolchain mingw-w64-ucrt-x86_64-toolchain mingw-w64-clang-x86_64-toolchain base-devel gcc autoconf-wrapper automake libtool - name: Cache CMake and Bazel installations id: cache-tools uses: actions/cache@v4 with: path: | C:\tools\cmake\3.15.7 C:\tools\cmake\3.19.7 C:\tools\cmake\3.23.5 C:\tools\cmake\3.27.9 C:\tools\cmake\4.2.1 C:\tools\bazel\6.5.0 C:\tools\bazel\7.6.2 C:\tools\bazel\8.4.2 key: ${{ runner.os }}-conan-tools-cache - name: Install CMake versions if: steps.cache-tools.outputs.cache-hit != 'true' run: | $CMAKE_BUILD_VERSIONS = "3.15.7", "3.19.7", "3.23.5", "3.27.9", "4.2.1" foreach ($version in $CMAKE_BUILD_VERSIONS) { Write-Host "Downloading CMake version $version for Windows..." $destination = "C:\tools\cmake\$version" if (-not (Test-Path $destination)) { New-Item -Path $destination -ItemType Directory } $major_minor_version = ($version -split '\.')[0..1] -join '.' if ( $major_minor_version -eq "3.15" -or $major_minor_version -eq "3.19" ) { $arch = "win64-x64" } else { $arch = "windows-x86_64" } $url = "https://cmake.org/files/v$major_minor_version/cmake-$version-$arch.zip" $zipFile = "cmake-$version-windows-x86_64.zip" Invoke-WebRequest -Uri $url -OutFile $zipFile Expand-Archive -Path $zipFile -DestinationPath $destination -Force Remove-Item $zipFile } - name: Install Bazel versions if: steps.cache-tools.outputs.cache-hit != 'true' run: | $BAZEL_BUILD_VERSIONS = "6.5.0", "7.6.2", "8.4.2" foreach ($version in $BAZEL_BUILD_VERSIONS) { Write-Host "Downloading Bazel version $version for Windows..." $destination = "C:\tools\bazel\$version" if (-not (Test-Path $destination)) { New-Item -Path $destination -ItemType Directory } $major_minor_version = ($version -split '\.')[0..1] -join '.' $url = "https://github.com/bazelbuild/bazel/releases/download/$version/bazel-$version-windows-x86_64.zip" $zipFile = "bazel-$version-windows-x86_64.zip" Invoke-WebRequest -Uri $url -OutFile $zipFile Expand-Archive -Path $zipFile -DestinationPath $destination -Force Remove-Item $zipFile } - name: Prepare environment for functional tests run: | git config --global core.autocrlf false $pathsToRemove = @() $pathsToRemove += "C:\mingw64\bin" # To avoid that CMake finds gcc there $pathsToRemove += "C:\Strawberry\c\bin" $pathsToRemove += "C:\Program Files\CMake\bin" # Remove the default CMake version $pathsToRemove += "C:\Program Files\Git\usr\bin" # To avoid using uname and other tools from there foreach ($dir in $pathsToRemove) { $newPath = ($env:PATH -split ";") -ne $dir -join ";" [System.Environment]::SetEnvironmentVariable('PATH', $newPath) Write-Host "$dir removed from PATH. Current PATH: $env:PATH" } # Check GCC is not in Path $gccPath = Get-Command gcc.exe -ErrorAction SilentlyContinue if ($null -ne $gccPath) { Write-Host "GCC found in PATH at: $($gccPath.Path)" } else { Write-Host "GCC not found in PATH." } $shortGuid = [System.Guid]::NewGuid().ToString().Substring(0, 4) $randomFolder = [System.IO.Path]::Combine("D:\\", "tmp_tests", $shortGuid) New-Item -ItemType Directory -Force -Path $randomFolder $env:CONAN_TEST_FOLDER = $randomFolder $env:Path = "C:\tools\cmake\3.15.7\cmake-3.15.7-win64-x64\bin;" + $env:Path $msys2Path = '${{ steps.msys2-setup.outputs.msys2-location }}' [System.Environment]::SetEnvironmentVariable('MSYS2_PATH', $msys2Path, [System.EnvironmentVariableTarget]::Process) Write-Host "Added MSYS2_PATH environment variable: $msys2Path" # Export variables so they persist in the next steps echo "PATH=$env:Path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append echo "CONAN_TEST_FOLDER=$env:CONAN_TEST_FOLDER" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append echo "MSYS2_PATH=$msys2Path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Run Functional Tests uses: ./.github/actions/test-coverage with: python-version: ${{ matrix.python-version }} test-type: functional tests: ${{ inputs.run-slow-tests == 'true' && 'test/functional' || 'test/functional -m "not slow"' }} ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ venv/ .venv/ *.egg-info/ .installed.cfg *.egg pip-wheel-metadata/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # OS generated files # ###################### .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes Icon? ehthumbs.db Thumbs.db .pydev* .project # IDEs # ######## .metadata .idea .history/ conan.conf *default_package_folder #Eclipse folder .settings #VScode folder .vscode #Generated certificate file cacert.pem #linux backup and vim files *~ .*.sw? Session.vim #Pyinstaller generated binaries /pyinstaller # Run tests in docker in current dir .bash_history .conan_server/ .sudo_as_admin_successful .noseids # add excluded !conans/client/build !conan/tools/build !test/unittests/client/build !test/unittests/tools/build ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2019 JFrog LTD Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE.md ================================================ FILE: README.md ================================================ JFrog | Conan 2.0 Logo # Conan Decentralized, open-source (MIT), C/C++ package manager. - Homepage: https://conan.io/ - Github: https://github.com/conan-io/conan - Docs: https://docs.conan.io - Slack: https://cpplang.slack.com (#conan channel. Please, click [here](https://cppalliance.org/slack/#cpp-slack) to get an invitation) - Twitter: https://twitter.com/conan_io - Blog: https://blog.conan.io - Security reports: https://jfrog.com/trust/report-vulnerability Conan is a package manager for C and C++ developers: - It is fully decentralized. Users can host their packages on their servers, privately. Integrates with Artifactory and Bintray. - Portable. Works across all platforms, including Linux, OSX, Windows (with native and first-class support, WSL, MinGW), Solaris, FreeBSD, embedded and cross-compiling, docker, WSL - Manage binaries. It can create, upload and download binaries for any configuration and platform, even cross-compiling, saving lots of time in development and continuous integration. The binary compatibility can be configured and customized. Manage all your artifacts in the same way on all platforms. - Integrates with any build system, including any proprietary and custom one. Provides tested support for major build systems (CMake, MSBuild, Makefiles, Meson, etc). - Extensible: Its Python-based recipes, together with extension points allow for great power and flexibility. - Large and active community, especially in GitHub (https://github.com/conan-io/conan) and Slack (https://cppalliance.org/slack/ #conan channel). This community also creates and maintains packages in ConanCenter and Bincrafters repositories in Bintray. - Stable. Used in production by many companies, since 1.0 there is a commitment not to break package recipes and documented behavior. This is the **developer/maintainer** documentation. For user documentation, go to https://docs.conan.io ## Setup You can run Conan from source in Windows, MacOS, and Linux: - **Install pip following** [pip docs](https://pip.pypa.io/en/stable/installation/). - **Clone Conan repository:** ```bash $ git clone https://github.com/conan-io/conan.git conan-io ``` > **Note**: repository directory name matters, some directories are known to be problematic to run tests (e.g. `conan`). `conan-io` directory name was tested and guaranteed to be working. - **Install in editable mode** ```bash $ cd conan-io && sudo pip install -e . ``` If you are in Windows, using ``sudo`` is not required. Some Linux distros won't allow you to put Python packages in editable mode in the root Python installation, and creating a virtual environment ``venv`` first, is mandatory. - **You are ready, try to run Conan:** ```bash $ conan --help Consumer commands install Installs the requirements specified in a recipe (conanfile.py or conanfile.txt). ... Conan commands. Type "conan -h" for help ``` ## Contributing to the project Feedback and contribution are always welcome in this project. Please read our [contributing guide](https://github.com/conan-io/conan/blob/develop2/.github/CONTRIBUTING.md). Also, if you plan to contribute, please add some testing for your changes. You can read the [Conan tests guidelines section](https://github.com/conan-io/conan/blob/develop2/test/README.md) for some advice on how to write tests for Conan. ### Running the tests **Install Python requirements** ```bash $ python -m pip install -r conans/requirements.txt $ python -m pip install -r conans/requirements_server.txt $ python -m pip install -r conans/requirements_dev.txt ``` If you are not on Windows and you are not using a Python virtual environment, you will need to run these commands using `sudo`. Before you can run the tests, you need to set a few environment variables first. ```bash $ export PYTHONPATH=$PYTHONPATH:$(pwd) ``` On Windows it would be (while being in the Conan root directory): ```bash $ set PYTHONPATH=. ``` Conan test suite defines and configures some required tools (CMake, Ninja, etc) in the ``conftest.py`` and allows to define a custom ``conftest_user.py``. Some specific versions, like cmake>=3.15 are necessary. You can run the tests like this: ```bash $ python -m pytest . ``` A few minutes later it should print ``OK``: ```bash ............................................................................................ ---------------------------------------------------------------------- Ran 146 tests in 50.993s OK ``` To run specific tests, you can specify the test name too, something like: ```bash $ python -m pytest test/functional/command/export_test.py::TestRevisionModeSCM::test_revision_mode_scm -s ``` The `-s` argument can be useful to see some output that otherwise is captured by *pytest*. Also, you can run tests against an instance of Artifactory. Those tests should add the attribute `artifactory_ready`. ```bash $ python -m pytest . -m artifactory_ready ``` Some environment variables have to be defined to run them. For example, for an Artifactory instance that is running on the localhost with default user and password configured, the variables could take the values: ```bash $ export CONAN_TEST_WITH_ARTIFACTORY=1 $ export ARTIFACTORY_DEFAULT_URL=http://localhost:8081/artifactory $ export ARTIFACTORY_DEFAULT_USER=admin $ export ARTIFACTORY_DEFAULT_PASSWORD=password ``` `ARTIFACTORY_DEFAULT_URL` is the base URL for the Artifactory repo, not one for a specific repository. Running the tests with a real Artifactory instance will create repos on the fly so please use a separate server for testing purposes. ## License [MIT LICENSE](LICENSE.md) ================================================ FILE: codecov.yml ================================================ comment: false # Disable codecov PR comments -> leave only the checks coverage: status: project: default: # https://docs.codecov.com/docs/commit-status#informational # Avoids failing on coverage decrease informational: true patch: default: informational: true ignore: - "test/performance" ================================================ FILE: conan/__init__.py ================================================ from conan.internal.model.conan_file import ConanFile from conan.internal.model.workspace import Workspace from conan.internal.model.version import Version __version__ = '2.27.0-dev' conan_version = Version(__version__) ================================================ FILE: conan/api/__init__.py ================================================ ================================================ FILE: conan/api/conan_api.py ================================================ import os import sys from conan.api.output import init_colorama from conan.api.subapi.audit import AuditAPI from conan.api.subapi.cache import CacheAPI from conan.api.subapi.command import CommandAPI from conan.api.subapi.local import LocalAPI from conan.api.subapi.lockfile import LockfileAPI from conan.api.subapi.report import ReportAPI from conan.api.subapi.workspace import WorkspaceAPI from conan.api.subapi.config import ConfigAPI from conan.api.subapi.download import DownloadAPI from conan.api.subapi.export import ExportAPI from conan.api.subapi.install import InstallAPI from conan.api.subapi.graph import GraphAPI from conan.api.subapi.new import NewAPI from conan.api.subapi.profiles import ProfilesAPI from conan.api.subapi.list import ListAPI from conan.api.subapi.remotes import RemotesAPI from conan.api.subapi.remove import RemoveAPI from conan.api.subapi.upload import UploadAPI from conan.errors import ConanException from conan.internal.api.remotes.localdb import LocalDB from conan.internal.cache.cache import PkgCache from conan.internal.cache.home_paths import HomePaths from conan.internal.hook_manager import HookManager from conan.internal.model.conf import load_global_conf, ConfDefinition, CORE_CONF_PATTERN from conan.internal.model.settings import load_settings_yml from conan.internal.paths import get_conan_user_home from conan.internal.api.migrations import ClientMigrator from conan.internal.model.version_range import validate_conan_version from conan.internal.rest.auth_manager import ConanApiAuthManager from conan.internal.rest.conan_requester import ConanRequester from conan.internal.rest.remote_manager import RemoteManager class ConanAPI: """ This is the main object to interact with the Conan API. It provides all the subapis to work with recipes, packages, remotes, etc., which are exposed as attributes of this class, and should not be created directly. """ def __init__(self, cache_folder=None): """ :param cache_folder: Conan cache/home folder. It will have less priority than the ``"home_folder"`` defined in a Workspace. """ version = sys.version_info if version.major == 2 or version.minor < 7: raise ConanException("Conan needs Python >= 3.7") if cache_folder is not None and not os.path.isabs(cache_folder): raise ConanException("cache_folder has to be an absolute path") init_colorama(sys.stderr) # Deprecated, but still used internally, prefer home_folder self.cache_folder = cache_folder or get_conan_user_home() self._home_folder = self.cache_folder self._api_helpers = self._ApiHelpers(self) self.migrate() #: Used to interact with the local Conan configuration self.config: ConfigAPI = ConfigAPI(self, self._api_helpers) #: Used to interact with remotes self.remotes: RemotesAPI = RemotesAPI(self, self._api_helpers) self.command = CommandAPI(self) #: Used to get latest refs and list refs of recipes and packages self.list: ListAPI = ListAPI(self, self._api_helpers) self.profiles = ProfilesAPI(self, self._api_helpers) #: Used to install binaries, sources, deploy packages and more self.install: InstallAPI = InstallAPI(self, self._api_helpers) self.graph = GraphAPI(self, self._api_helpers) #: Used to export recipes and pre-compiled package binaries to the Conan cache self.export: ExportAPI = ExportAPI(self, self._api_helpers) self.remove = RemoveAPI(self, self._api_helpers) self.new = NewAPI(self) #: Used to upload recipes and packages to remotes self.upload: UploadAPI = UploadAPI(self, self._api_helpers) #: Used to download recipes and packages from remotes self.download: DownloadAPI = DownloadAPI(self, self._api_helpers) #: Used to interact wit the packages storage cache self.cache: CacheAPI = CacheAPI(self, self._api_helpers) #: Used to read and manage lockfile files self.lockfile: LockfileAPI = LockfileAPI(self) #: Local flow helpers for developer "source", "build", "editable" commands self.local: LocalAPI = LocalAPI(self, self._api_helpers) #: Used to check vulnerabilities of dependencies self.audit: AuditAPI = AuditAPI(self) #: Used to manage workspaces self.workspace: WorkspaceAPI = WorkspaceAPI(self) self.report: ReportAPI = ReportAPI(self, self._api_helpers) @property def home_folder(self) -> str: """ Where the Conan user home is located. Read only. Can be modified by the ``CONAN_HOME`` environment variable or by the ``.conanrc`` file in the current directory or any parent directory when Conan is called. """ return self._home_folder def reinit(self): """ Reinitialize the Conan API. This is useful when the configuration changes. """ self._api_helpers.reinit() self.local.reinit() def migrate(self): # Migration system # TODO: A prettier refactoring of migrators would be nice from conan import conan_version migrator = ClientMigrator(self._home_folder, conan_version) migrator.migrate() class _ApiHelpers: # This is an internal implementation detail of Conan, DO NOT USE def __init__(self, conan_api): self._conan_api = conan_api self._cli_core_confs = None self._init_global_conf() # TODO: Make uniform lazy vs non lazy collaborators self.hook_manager = HookManager(HomePaths(self._conan_api.home_folder).hooks_path) # Wraps an http_requester to inject proxies, certs, etc self._requester = ConanRequester(self.global_conf, self._conan_api.home_folder) self.cache = PkgCache(self._conan_api.home_folder, self.global_conf) self._settings_yml = None self._remote_manager = None def set_core_confs(self, core_confs): confs = ConfDefinition() for c in core_confs: if not CORE_CONF_PATTERN.match(c): raise ConanException(f"Only core. values are allowed in --core-conf. Got {c}") confs.loads("\n".join(core_confs)) confs.validate() self._cli_core_confs = confs # Last but not least, apply the new configuration # This will in turn call ApiHelpers.reinit() as the very first thing self._conan_api.reinit() def _init_global_conf(self): self.global_conf = load_global_conf(self._conan_api.home_folder) if self._cli_core_confs: self.global_conf.update_conf_definition(self._cli_core_confs) required_range_new = self.global_conf.get("core:required_conan_version") if required_range_new: validate_conan_version(required_range_new) def reinit(self): self._init_global_conf() self.hook_manager.reinit() self._requester = ConanRequester(self.global_conf, self._conan_api.home_folder) self._settings_yml = None self.cache = PkgCache(self._conan_api.home_folder, self.global_conf) self._remote_manager = None @property def settings_yml(self): if self._settings_yml is None: self._settings_yml = load_settings_yml(self._conan_api.home_folder) return self._settings_yml @property def remote_manager(self): if self._remote_manager is None: home_folder = self._conan_api.home_folder localdb = LocalDB(home_folder) requester = self._conan_api._api_helpers.requester # noqa auth_manager = ConanApiAuthManager(requester, self._conan_api.home_folder, localdb, self.global_conf) self._remote_manager = RemoteManager(self.cache, auth_manager, home_folder) return self._remote_manager @property def requester(self): return self._requester ================================================ FILE: conan/api/input.py ================================================ import getpass import sys from conan.errors import ConanException class UserInput: """Class to interact with the user, used to show messages and ask for information""" def __init__(self, non_interactive): """ Params: ins: input stream out: ConanOutput, should have "write" method """ self._ins = sys.stdin # FIXME: circular include, move "color_enabled" function to better location from conan.api.output import ConanOutput self._out = ConanOutput() self._interactive = not non_interactive def _raise_if_non_interactive(self): if not self._interactive: raise ConanException("Conan interactive mode disabled") def raw_input(self): self._raise_if_non_interactive() return input() def request_login(self, remote_name, username=None): """Request user to input their name and password :param remote_name: :param username If username is specified it only request password""" self._raise_if_non_interactive() if not username: self._out.login_msg(f"Remote '{remote_name}' username: ") username = self.get_username() self._out.login_msg("Please enter a password for " f"user '{username}' on remote '{remote_name}': ") try: pwd = self.get_password() except ConanException: raise except Exception as e: raise ConanException('Cancelled pass %s' % e) return username, pwd def get_username(self): """Overridable for testing purpose""" return self.raw_input() @staticmethod def get_password(): """Overridable for testing purpose""" try: return getpass.getpass("") except BaseException: # For KeyboardInterrupt too raise ConanException("Interrupted interactive password input") def request_string(self, msg, default_value=None): """Request user to input a msg :param default_value: :param msg Name of the msg """ self._raise_if_non_interactive() if default_value: self._out.write('%s (%s): ' % (msg, default_value)) else: self._out.write('%s: ' % msg) s = self._ins.readline().replace("\n", "") if default_value is not None and s == '': return default_value return s def request_boolean(self, msg, default_option=None): """Request user to input a boolean""" ret = None while ret is None: if default_option: s = self.request_string("%s (YES/no)" % msg) elif default_option is False: s = self.request_string("%s (NO/yes)" % msg) else: s = self.request_string("%s (yes/no)" % msg) if default_option is not None and s == '': return default_option if s.lower() in ['yes', 'y']: ret = True elif s.lower() in ['no', 'n']: ret = False else: self._out.error(f"{s} is not a valid answer") return ret ================================================ FILE: conan/api/model/__init__.py ================================================ from conan.api.model.remote import Remote, LOCAL_RECIPES_INDEX from conan.api.model.refs import RecipeReference, PkgReference from conan.api.model.list import PackagesList, MultiPackagesList, ListPattern ================================================ FILE: conan/api/model/list.py ================================================ import copy import fnmatch import json import os from json import JSONDecodeError from typing import Iterable, Tuple, Dict from conan.api.model import RecipeReference, PkgReference from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.errors import NotFoundException from conan.internal.model.version_range import VersionRange from conan.internal.graph.graph import RECIPE_EDITABLE, RECIPE_CONSUMER, RECIPE_PLATFORM, \ RECIPE_VIRTUAL, BINARY_SKIP, BINARY_MISSING, BINARY_INVALID from conan.internal.util.files import load class MultiPackagesList: """ A collection of PackagesList by remote name.""" def __init__(self): self.lists = {} def __getitem__(self, name): try: return self.lists[name] except KeyError: raise ConanException(f"'{name}' doesn't exist in package list") def add(self, name, pkg_list): self.lists[name] = pkg_list def add_error(self, remote_name, error): self.lists[remote_name] = {"error": error} def serialize(self): """ Serialize object to a dictionary.""" return {k: v.serialize() if isinstance(v, PackagesList) else v for k, v in self.lists.items()} def merge(self, other): for k, v in other.lists.items(): self.lists.setdefault(k, PackagesList()).merge(v) @staticmethod def load(file): """ Create an instance of the class from a serialized JSON file path pointed by ``file``.""" try: content = json.loads(load(file)) except JSONDecodeError as e: raise ConanException(f"Package list file invalid JSON: {file}\n{e}") except Exception as e: raise ConanException(f"Package list file missing or broken: {file}\n{e}") # Check if input json is not a graph file if "graph" in content: base_path = os.path.basename(file) raise ConanException( 'Expected a package list file but found a graph file. You can create a "package list" JSON file by running:\n\n' f"\tconan list --graph {base_path} --format=json > pkglist.json\n\n" "More Info at 'https://docs.conan.io/2/examples/commands/pkglists.html" ) result = {} for remote, pkglist in content.items(): if "error" in pkglist: result[remote] = pkglist else: result[remote] = PackagesList.deserialize(pkglist) pkglist = MultiPackagesList() pkglist.lists = result return pkglist @staticmethod def load_graph(graphfile, graph_recipes=None, graph_binaries=None, context=None): """ Create an instance of the class from a graph file path, which is the json format returned by a few commands like ``conan graph info`` or ``conan create/install.`` :parameter str graphfile: Path to the graph file :parameter list[str] graph_recipes: List for kinds of recipes to return. For example ``"cache"`` will return only recipes in the local cache, ``"download"`` will return only recipes that have been downloaded, and passing ``"*"`` will return all recipes. :parameter list[str] graph_binaries: List for kinds of binaries to return. For example ``"cache"`` will return only binaries in the local cache, ``"download"`` will return only binaries that have been downloaded, ``"build"`` will return only binaries that are built, ``"missing"`` will return only binaries that are missing, ``"invalid"`` will return only binaries that are invalid, and passing ``"*"`` will return all binaries. :parameter str context: Context to filter the graph, can be ``"host"``, ``"build"``, ``"host-only"`` or ``"build-only"`` """ if not os.path.isfile(graphfile): raise ConanException(f"Graph file not found: {graphfile}") try: graph = json.loads(load(graphfile)) # Check if input json is a graph file if "graph" not in graph: raise ConanException( 'Expected a graph file but found an unexpected JSON file format. You can create a "graph" JSON file by running:\n\n' f"\tconan [ graph-info | create | export-pkg | install | test ] --format=json > graph.json\n\n" "More Info at 'https://docs.conan.io/2/reference/commands/formatters/graph_info_json_formatter.html" ) mpkglist = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries, context=context) return mpkglist except JSONDecodeError as e: raise ConanException(f"Graph file invalid JSON: {graphfile}\n{e}") except KeyError as e: raise ConanException(f'Graph file {graphfile} is missing the required "{e}" key in its contents.\n' "Note that the graph file should not be filtered " "if you expect to use it with the list command.") except ConanException as e: raise e except Exception as e: raise ConanException(f"Graph file broken: {graphfile}\n{e}") @staticmethod def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None): base_context = context.split("-")[0] if context else None pkglist = MultiPackagesList() cache_list = PackagesList() if graph_recipes is None and graph_binaries is None: recipes = ["*"] binaries = ["*"] else: recipes = [r.lower() for r in graph_recipes or []] binaries = [b.lower() for b in graph_binaries or []] pkglist.lists["Local Cache"] = cache_list for node in graph["graph"]["nodes"].values(): if base_context and node['context'] != base_context: continue # We need to add the python_requires too python_requires = node.get("python_requires") if python_requires is not None: for pyref, pyreq in python_requires.items(): pyrecipe = pyreq["recipe"] if pyrecipe == RECIPE_EDITABLE: continue pyref = RecipeReference.loads(pyref) if any(r == "*" or r == pyrecipe for r in recipes): cache_list.add_ref(pyref) pyremote = pyreq["remote"] if pyremote: remote_list = pkglist.lists.setdefault(pyremote, PackagesList()) remote_list.add_ref(pyref) recipe = node["recipe"] if recipe in (RECIPE_EDITABLE, RECIPE_CONSUMER, RECIPE_VIRTUAL, RECIPE_PLATFORM): continue ref = node["ref"] ref = RecipeReference.loads(ref) ref.timestamp = node["rrev_timestamp"] recipe = recipe.lower() if any(r == "*" or r == recipe for r in recipes): cache_list.add_ref(ref) remote = node["remote"] if remote: remote_list = pkglist.lists.setdefault(remote, PackagesList()) remote_list.add_ref(ref) pref = PkgReference(ref, node["package_id"], node["prev"], node["prev_timestamp"]) binary_remote = node["binary_remote"] if binary_remote: remote_list = pkglist.lists.setdefault(binary_remote, PackagesList()) remote_list.add_ref(ref) # Binary listed forces recipe listed remote_list.add_pref(pref) binary = node["binary"] if binary in (BINARY_SKIP, BINARY_INVALID, BINARY_MISSING): continue binary = binary.lower() if any(b == "*" or b == binary for b in binaries): cache_list.add_ref(ref) # Binary listed forces recipe listed cache_list.add_pref(pref, node["info"]) # Now filter possible exclusive contexts once that we know how they are distributed MultiPackagesList._filter_exclusive_context(pkglist, graph, context) return pkglist @staticmethod def _filter_exclusive_context(mpkglist, graph, context): if context not in ("host-only", "build-only"): return pref_contexts = {} for node in graph["graph"]["nodes"].values(): if (node["recipe"] not in (RECIPE_CONSUMER, RECIPE_VIRTUAL) and node["ref"] and node["package_id"]): pref = node["ref"] + ":" + node["package_id"] pref_contexts.setdefault(pref, set()).add(node['context']) opposite_context = "build" if context == "host-only" else "host" for remote, pkglist in mpkglist.lists.items(): for pref, contexts in pref_contexts.items(): if opposite_context in contexts: pref = PkgReference.loads(pref) if pkglist.has_rref(pref.ref): rev_dict = pkglist.recipe_dict(pref.ref) rev_dict.get("packages", {}).pop(pref.package_id, None) if len(rev_dict.get("packages", {})) == 0: pkglist._data.pop(str(pref.ref), None) class PackagesList: """ A collection of recipes, revisions and packages.""" def __init__(self): self._data = {} def __bool__(self): """ Whether the package list contains any recipe""" return bool(self._data) def merge(self, other): assert isinstance(other, PackagesList) def recursive_dict_update(d, u): # TODO: repeated from conandata.py for k, v in u.items(): if isinstance(v, dict): d[k] = recursive_dict_update(d.get(k, {}), v) else: d[k] = v return d recursive_dict_update(self._data, other._data) def split(self): """ Returns a list of PackageList, split one per reference. This can be useful to parallelize things like upload, parallelizing per-reference """ result = [] for r, content in self._data.items(): subpkglist = PackagesList() subpkglist._data[r] = content result.append(subpkglist) return result def only_recipes(self) -> None: """ Filter out all the packages and package revisions, keep only the recipes and recipe revisions in self._data. """ for ref, ref_dict in self._data.items(): for rrev_dict in ref_dict.get("revisions", {}).values(): rrev_dict.pop("packages", None) def add_refs(self, refs): ConanOutput().warning("PackagesLists.add_refs() non-public, non-documented method will be " "removed, use .add_ref() instead", warn_tag="deprecated") # RREVS alreday come in ASCENDING order, so upload does older revisions first for ref in refs: self.add_ref(ref) def add_ref(self, ref: RecipeReference) -> None: """ Adds a new RecipeReference to a package list """ ref_dict = self._data.setdefault(str(ref), {}) if ref.revision: revs_dict = ref_dict.setdefault("revisions", {}) rev_dict = revs_dict.setdefault(ref.revision, {}) if ref.timestamp: rev_dict["timestamp"] = ref.timestamp def add_prefs(self, rrev, prefs): ConanOutput().warning("PackageLists.add_prefs() non-public, non-documented method will be " "removed, use .add_pref() instead", warn_tag="deprecated") # Prevs already come in ASCENDING order, so upload does older revisions first for p in prefs: self.add_pref(p) def add_pref(self, pref: PkgReference, pkg_info: dict = None) -> None: """ Add a PkgReference to an already existing RecipeReference inside a package list """ # Prevs already come in ASCENDING order, so upload does older revisions first rev_dict = self.recipe_dict(pref.ref) packages_dict = rev_dict.setdefault("packages", {}) package_dict = packages_dict.setdefault(pref.package_id, {}) if pref.revision: prevs_dict = package_dict.setdefault("revisions", {}) prev_dict = prevs_dict.setdefault(pref.revision, {}) if pref.timestamp: prev_dict["timestamp"] = pref.timestamp if pkg_info is not None: package_dict["info"] = pkg_info def add_configurations(self, confs): ConanOutput().warning("PackageLists.add_configurations() non-public, non-documented method " "will be removed, use .add_pref() instead", warn_tag="deprecated") for pref, conf in confs.items(): rev_dict = self.recipe_dict(pref.ref) try: rev_dict["packages"][pref.package_id]["info"] = conf except KeyError: # If package_id does not exist, do nothing, only add to existing prefs pass def refs(self): ConanOutput().warning("PackageLists.refs() non-public, non-documented method will be " "removed, use .items() instead", warn_tag="deprecated") result = {} for ref, ref_dict in self._data.items(): for rrev, rrev_dict in ref_dict.get("revisions", {}).items(): t = rrev_dict.get("timestamp") recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this if t is not None: recipe.timestamp = t result[recipe] = rrev_dict return result def items(self) -> Iterable[Tuple[RecipeReference, Dict[PkgReference, Dict]]]: """ Iterate the contents of the package list. The first dictionary is the information directly belonging to the recipe-revision. The second dictionary contains PkgReference as keys, and a dictionary with the values belonging to that specific package reference (settings, options, etc.). """ for ref, ref_dict in self._data.items(): for rrev, rrev_dict in ref_dict.get("revisions", {}).items(): recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this t = rrev_dict.get("timestamp") if t is not None: recipe.timestamp = t packages = {} for package_id, pkg_info in rrev_dict.get("packages", {}).items(): prevs = pkg_info.get("revisions", {}) for prev, prev_info in prevs.items(): t = prev_info.get("timestamp") pref = PkgReference(recipe, package_id, prev, t) packages[pref] = prev_info yield recipe, packages def recipe_dict(self, ref: RecipeReference): """ Gives read/write access to the dictionary containing a specific RecipeReference information. """ return self._data[str(ref)]["revisions"][ref.revision] def has_rref(self, ref: RecipeReference) -> bool: # Checks if the PackagesList contains the given RecipeReference. return str(ref) in self._data and ref.revision in self._data[str(ref)].get("revisions", {}) def package_dict(self, pref: PkgReference): """ Gives read/write access to the dictionary containing a specific PkgReference information """ ref_dict = self.recipe_dict(pref.ref) return ref_dict["packages"][pref.package_id]["revisions"][pref.revision] @staticmethod def prefs(ref, recipe_bundle): ConanOutput().warning("PackageLists.prefs() non-public, non-documented method will be " "removed, use .items() instead", warn_tag="deprecated") result = {} for package_id, pkg_bundle in recipe_bundle.get("packages", {}).items(): prevs = pkg_bundle.get("revisions", {}) for prev, prev_bundle in prevs.items(): t = prev_bundle.get("timestamp") pref = PkgReference(ref, package_id, prev, t) result[pref] = prev_bundle return result def serialize(self): """ Serialize the instance to a dictionary.""" return copy.deepcopy(self._data) @staticmethod def deserialize(data): """ Loads the data from a serialized dictionary.""" result = PackagesList() result._data = copy.deepcopy(data) return result class ListPattern: """ Object holding a pattern that matches recipes, revisions and packages.""" def __init__(self, expression, rrev="latest", package_id=None, prev="latest", only_recipe=False): """ :param expression: The pattern to match, e.g. ``"name/*:*"`` :param rrev: The recipe revision to match, defaults to ``"latest"``, can also be ``"!latest"`` or ``"~latest"`` to match all but the latest revision, a pattern like ``"1234*"`` to match a specific revision, or a specific revision like ``"1234"``. :param package_id: The package ID to match, defaults to ``None``, which matches all package IDs. :param prev: The package revision to match, defaults to ``"latest"``, can also be ``"!latest"`` or ``"~latest"`` to match all but the latest revision, a pattern like ``"1234*"`` to match a specific revision, or a specific revision like ``"1234"``. :param only_recipe: If ``True``, only the recipe part of the expression is parsed, ignoring ``package_id`` and ``prev``. This is useful for commands that only operate on recipes, like ``conan search``. """ def split(s, c, default=None): if not s: return None, default tokens = s.split(c, 1) if len(tokens) == 2: return tokens[0], tokens[1] or default return tokens[0], default recipe, package = split(expression, ":") self.raw = expression self.ref, rrev = split(recipe, "#", rrev) ref, user_channel = split(self.ref, "@") self.name, self.version = split(ref, "/") self.user, self.channel = split(user_channel, "/") self.rrev, _ = split(rrev, "%") self.package_id, prev = split(package, "#", prev) self.prev, _ = split(prev, "%") if only_recipe: if self.package_id: raise ConanException("Do not specify 'package_id' with 'only-recipe'") else: self.package_id = self.package_id or package_id @staticmethod def _only_latest(rev): return rev in ["!latest", "~latest"] @property def search_ref(self): vrange = self._version_range if vrange: return str(RecipeReference(self.name, "*", self.user, self.channel)) if "*" in self.ref or not self.version or (self.package_id is None and self.rrev is None): return self.ref @property def _version_range(self): if self.version and self.version.startswith("[") and self.version.endswith("]"): return VersionRange(self.version[1:-1]) def filter_versions(self, refs, resolve_prereleases=None): vrange = self._version_range if vrange: refs = [r for r in refs if vrange.contains(r.version, resolve_prereleases)] return refs @property def is_latest_rrev(self): return self.rrev == "latest" @property def is_latest_prev(self): return self.prev == "latest" def check_refs(self, refs): if not refs and self.ref and "*" not in self.ref: raise NotFoundException(f"Recipe '{self.ref}' not found") def filter_rrevs(self, rrevs): if self._only_latest(self.rrev): return rrevs[1:] rrevs = [r for r in rrevs if fnmatch.fnmatch(r.revision, self.rrev)] if not rrevs: refs_str = f'{self.ref}#{self.rrev}' if "*" not in refs_str: raise ConanException(f"Recipe revision '{refs_str}' not found") return rrevs def filter_prefs(self, prefs): prefs = [p for p in prefs if fnmatch.fnmatch(p.package_id, self.package_id)] if not prefs: refs_str = f'{self.ref}#{self.rrev}:{self.package_id}' if "*" not in refs_str: raise ConanException(f"Package ID '{self.raw}' not found") return prefs def filter_prevs(self, prevs): if self._only_latest(self.prev): return prevs[1:] prevs = [p for p in prevs if fnmatch.fnmatch(p.revision, self.prev)] if not prevs: refs_str = f'{self.ref}#{self.rrev}:{self.package_id}#{self.prev}' if "*" not in refs_str: raise ConanException(f"Package revision '{self.raw}' not found") return prevs ================================================ FILE: conan/api/model/refs.py ================================================ import fnmatch import re from functools import total_ordering from conan.errors import ConanException from conan.internal.model.version import Version from conan.internal.util.dates import timestamp_to_str @total_ordering class RecipeReference: """ An exact (no version-range, no alias) reference of a recipe, it represents a reference of the form ``name/version[@user/channel][#revision][%timestamp]``. Should be enough to locate a recipe in the cache or in a server, and validation will be external to this class, at specific points (export, api, etc). """ def __init__(self, name=None, version=None, user=None, channel=None, revision=None, timestamp=None): """ The attributes should be regarded as immutable, and should not be modified by the user.""" #: Name of the reference self.name: str = name if version is not None and not isinstance(version, Version): version = Version(version) #: Version of the reference self.version: Version = version # This MUST be a version if we want to be able to order #: User of the reference, if any self.user = user #: Channel of the reference, if any self.channel = channel #: Revision of the reference, if any self.revision = revision #: Timestamp of the reference, if any self.timestamp = timestamp def copy(self): # Used for creating copy in lockfile-overrides mechanism return RecipeReference(self.name, self.version, self.user, self.channel, self.revision, self.timestamp) def __repr__(self): """ long repr like pkg/0.1@user/channel#rrev%timestamp """ result = self.repr_notime() if self.timestamp is not None: result += "%{}".format(self.timestamp) return result def repr_notime(self): result = self.__str__() if self.revision is not None: result += "#{}".format(self.revision) return result def repr_humantime(self): result = self.repr_notime() assert self.timestamp result += " ({})".format(timestamp_to_str(self.timestamp)) return result def __str__(self): """ shorter representation, excluding the revision and timestamp """ if self.name is None: return "" result = "/".join([self.name, str(self.version)]) if self.user: result += "@{}".format(self.user) if self.channel: assert self.user result += "/{}".format(self.channel) return result def __lt__(self, ref): # The timestamp goes before the revision for ordering revisions chronologically # In theory this is enough for sorting # When no timestamp is given, it will always have lower priority, to avoid comparison # errors float <> None return (self.name, self.version, self.user or "", self.channel or "", self.timestamp or 0, self.revision or "") \ < (ref.name, ref.version, ref.user or "", ref.channel or "", ref.timestamp or 0, ref.revision or "") def __eq__(self, ref): # Timestamp doesn't affect equality. # This is necessary for building an ordered list of UNIQUE recipe_references for Lockfile if ref is None: return False # If one revision is not defined, they are equal if self.revision is not None and ref.revision is not None: return (self.name, self.version, self.user, self.channel, self.revision) == \ (ref.name, ref.version, ref.user, ref.channel, ref.revision) return (self.name, self.version, self.user, self.channel) == \ (ref.name, ref.version, ref.user, ref.channel) def __hash__(self): # This is necessary for building an ordered list of UNIQUE recipe_references for Lockfile return hash((self.name, self.version, self.user, self.channel)) @staticmethod def loads(rref): """ Instantiates an object from a string, in the form: ``name/version[@user/channel][#revision][%timestamp]``""" try: # timestamp tokens = rref.rsplit("%", 1) text = tokens[0] timestamp = float(tokens[1]) if len(tokens) == 2 else None # revision tokens = text.split("#", 1) ref = tokens[0] revision = tokens[1] if len(tokens) == 2 else None # name, version always here tokens = ref.split("@", 1) name, version = tokens[0].split("/", 1) assert name and version # user and channel if len(tokens) == 2 and tokens[1]: tokens = tokens[1].split("/", 1) user = tokens[0] if tokens[0] else None channel = tokens[1] if len(tokens) == 2 else None else: user = channel = None return RecipeReference(name, version, user, channel, revision, timestamp) except Exception: from conan.errors import ConanException raise ConanException( f"{rref} is not a valid recipe reference, provide a reference" f" in the form name/version[@user/channel]") def validate_ref(self, allow_uppercase=False): """ Check that the reference is valid, and raise a ``ConanException`` if not. """ from conan.api.output import ConanOutput self_str = str(self) if self_str != self_str.lower(): if not allow_uppercase: raise ConanException(f"Conan packages names '{self_str}' must be all lowercase") else: ConanOutput().warning(f"Package name '{self_str}' has uppercase, and has been " "allowed by temporary config. This will break in later 2.X") if len(self_str) > 200: raise ConanException(f"Package reference too long >200 {self_str}") if ":" in repr(self): raise ConanException(f"Invalid recipe reference '{repr(self)}' is a package reference") if not allow_uppercase: validation_pattern = re.compile(r"^[a-z0-9_][a-z0-9_+.-]{1,100}\Z") else: validation_pattern = re.compile(r"^[a-zA-Z0-9_][a-zA-Z0-9_+.-]{1,100}\Z") if validation_pattern.match(self.name) is None: raise ConanException(f"Invalid package name '{self.name}'") if validation_pattern.match(str(self.version)) is None: raise ConanException(f"Invalid package version '{self.version}'") if self.user and validation_pattern.match(self.user) is None: raise ConanException(f"Invalid package user '{self.user}'") if self.channel and validation_pattern.match(self.channel) is None: raise ConanException(f"Invalid package channel '{self.channel}'") # Warn if they use .+ in the name/user/channel, as it can be problematic for generators pattern = re.compile(r'[.+]') if pattern.search(self.name): ConanOutput().warning(f"Name containing special chars is discouraged '{self.name}'") if self.user and pattern.search(self.user): ConanOutput().warning(f"User containing special chars is discouraged '{self.user}'") if self.channel and pattern.search(self.channel): ConanOutput().warning(f"Channel containing special chars is discouraged " f"'{self.channel}'") def matches(self, pattern, is_consumer): """ fnmatches the reference against the provided pattern. :parameter str pattern: the pattern to match against, it can contain wildcards, and can start with ``!`` or ``~`` to negate the match. A special value of ``&`` will return a match only of ``is_consumer`` is ``True`` :parameter bool is_consumer: if ``True``, the pattern ``&`` will match this reference. """ negate = False if pattern.startswith("!") or pattern.startswith("~"): pattern = pattern[1:] negate = True no_user_channel = False if pattern.endswith("@"): # it means we want to match only without user/channel pattern = pattern[:-1] no_user_channel = True elif "@#" in pattern: pattern = pattern.replace("@#", "#") no_user_channel = True condition = ((pattern == "&" and is_consumer) or fnmatch.fnmatchcase(str(self), pattern) or fnmatch.fnmatchcase(self.repr_notime(), pattern)) if no_user_channel: condition = condition and not self.user and not self.channel if negate: return not condition return condition def partial_match(self, pattern): # Finds if pattern matches any of partial sums of tokens of conan reference tokens = [self.name, "/", str(self.version)] if self.user: tokens += ["@", self.user] if self.channel: tokens += ["/", self.channel] if self.revision: tokens += ["#", self.revision] partial = "" for token in tokens: partial += token if pattern.match(partial): return True class PkgReference: def __init__(self, ref=None, package_id=None, revision=None, timestamp=None): self.ref = ref self.package_id = package_id self.revision = revision self.timestamp = timestamp # float, Unix seconds UTC def __repr__(self): """ long repr like pkg/0.1@user/channel#rrev%timestamp """ if self.ref is None: return "" result = repr(self.ref) if self.package_id: result += ":{}".format(self.package_id) if self.revision is not None: result += "#{}".format(self.revision) if self.timestamp is not None: result += "%{}".format(self.timestamp) return result def repr_notime(self): if self.ref is None: return "" result = self.ref.repr_notime() if self.package_id: result += ":{}".format(self.package_id) if self.revision is not None: result += "#{}".format(self.revision) return result def repr_humantime(self): result = self.repr_notime() assert self.timestamp result += " ({})".format(timestamp_to_str(self.timestamp)) return result def __str__(self): """ shorter representation, excluding the revision and timestamp """ if self.ref is None: return "" result = str(self.ref) if self.package_id: result += ":{}".format(self.package_id) return result def __lt__(self, ref): # The timestamp goes before the revision for ordering revisions chronologically raise Exception("WHO IS COMPARING PACKAGE REFERENCES?") # return (self.name, self.version, self.user, self.channel, self.timestamp, self.revision) \ # < (ref.name, ref.version, ref.user, ref.channel, ref._timestamp, ref.revision) def __eq__(self, other): # TODO: In case of equality, should it use the revision and timestamp? # Used: # at "graph_binaries" to check: cache_latest_prev != pref # at "installer" to check: if pkg_layout.reference != pref (probably just optimization?) # at "revisions_test" return self.ref == other.ref and self.package_id == other.package_id and \ self.revision == other.revision def __hash__(self): # Used in dicts of PkgReferences as keys like the cached nodes in the graph binaries return hash((self.ref, self.package_id, self.revision)) @staticmethod def loads(pkg_ref): # TODO: change this default to validate only on end points try: tokens = pkg_ref.split(":", 1) assert len(tokens) == 2 ref, pkg_id = tokens ref = RecipeReference.loads(ref) # timestamp tokens = pkg_id.rsplit("%", 1) text = tokens[0] timestamp = float(tokens[1]) if len(tokens) == 2 else None # revision tokens = text.split("#", 1) package_id = tokens[0] revision = tokens[1] if len(tokens) == 2 else None return PkgReference(ref, package_id, revision, timestamp) except Exception: raise ConanException( f"{pkg_ref} is not a valid package reference, provide a reference" f" in the form name/version[@user/channel:package_id]") ================================================ FILE: conan/api/model/remote.py ================================================ LOCAL_RECIPES_INDEX = "local-recipes-index" class Remote: """ The ``Remote`` class represents a remote registry of packages. """ def __init__(self, name, url, verify_ssl=True, disabled=False, allowed_packages=None, remote_type=None, recipes_only=False): """ A Remote object can be constructed to be passed as an argument to RemotesAPI methods. When possible, it is better to use Remote objects returned by the API, but for the ``RemotesAPI.add()`` method, for which a new constructed object is necessary. It is recommended to use named arguments like ``Remote(..., verify_ssl=False)`` in the constructor. :param name: The name of the remote. :param url: The URL of the remote repository (or local folder for "local-recipes-index"). :param verify_ssl: Enable SSL Certificate validation. :param disabled: Disable the remote repository. :param allowed_packages: List of patterns of allowed packages from this remote :param remote_type: Type of the remote repository, use "local-recipes-index" or ``None`` :param recipes_only: If True, binaries form this remote will be ignored and never used """ self.name = name # Read only, is the key self.url = url self.verify_ssl = verify_ssl self.disabled = disabled self.allowed_packages = allowed_packages self.recipes_only = recipes_only self.remote_type = remote_type self._caching = {} def __eq__(self, other): if other is None: return False return (self.name == other.name and self.url == other.url and self.verify_ssl == other.verify_ssl and self.disabled == other.disabled) def __str__(self): allowed_msg = "" if self.allowed_packages: allowed_msg = ", Allowed packages: {}".format(", ".join(self.allowed_packages)) if self.recipes_only: allowed_msg += ", Recipes only" if self.remote_type == LOCAL_RECIPES_INDEX: return "{}: {} [{}, Enabled: {}{}]".format(self.name, self.url, LOCAL_RECIPES_INDEX, not self.disabled, allowed_msg) return "{}: {} [Verify SSL: {}, Enabled: {}{}]".format(self.name, self.url, self.verify_ssl, not self.disabled, allowed_msg) def __repr__(self): return str(self) def invalidate_cache(self): """ If external operations might have modified the remote since it was instantiated, this method can be called to invalidate the cache. Note that this is done automatically when the remote is used in any operation by Conan, such as uploading packages, so this method is not usually needed when only interacting with the Conan API""" self._caching = {} ================================================ FILE: conan/api/output.py ================================================ import fnmatch import os import sys import time from threading import Lock import colorama from colorama import Fore, Style from conan.errors import ConanException LEVEL_QUIET = 80 # -q LEVEL_ERROR = 70 # Errors LEVEL_WARNING = 60 # Warnings LEVEL_NOTICE = 50 # Important messages to attract user attention. LEVEL_STATUS = 40 # Default - The main interesting messages that users might be interested in. LEVEL_VERBOSE = 30 # -v Detailed informational messages. LEVEL_DEBUG = 20 # -vv Closely related to internal implementation details LEVEL_TRACE = 10 # -vvv Fine-grained messages with very low-level implementation details class Color: """ Wrapper around colorama colors that are undefined in importing """ RED = Fore.RED # @UndefinedVariable WHITE = Fore.WHITE # @UndefinedVariable CYAN = Fore.CYAN # @UndefinedVariable GREEN = Fore.GREEN # @UndefinedVariable MAGENTA = Fore.MAGENTA # @UndefinedVariable BLUE = Fore.BLUE # @UndefinedVariable YELLOW = Fore.YELLOW # @UndefinedVariable BLACK = Fore.BLACK # @UndefinedVariable BRIGHT_RED = Style.BRIGHT + Fore.RED # @UndefinedVariable BRIGHT_BLUE = Style.BRIGHT + Fore.BLUE # @UndefinedVariable BRIGHT_YELLOW = Style.BRIGHT + Fore.YELLOW # @UndefinedVariable BRIGHT_GREEN = Style.BRIGHT + Fore.GREEN # @UndefinedVariable BRIGHT_CYAN = Style.BRIGHT + Fore.CYAN # @UndefinedVariable BRIGHT_WHITE = Style.BRIGHT + Fore.WHITE # @UndefinedVariable BRIGHT_MAGENTA = Style.BRIGHT + Fore.MAGENTA # @UndefinedVariable if os.environ.get("CONAN_COLOR_DARK"): Color.WHITE = Fore.BLACK Color.CYAN = Fore.BLUE Color.YELLOW = Fore.MAGENTA Color.BRIGHT_WHITE = Fore.BLACK Color.BRIGHT_CYAN = Fore.BLUE Color.BRIGHT_YELLOW = Fore.MAGENTA Color.BRIGHT_GREEN = Fore.GREEN def init_colorama(stream): import colorama if _color_enabled(stream): if os.getenv("CLICOLOR_FORCE", "0") != "0": # Otherwise it is not really forced if colorama doesn't feel it colorama.init(strip=False, convert=False) else: colorama.init() def _color_enabled(stream): """ NO_COLOR: No colors https://no-color.org/ Command-line software which adds ANSI color to its output by default should check for the presence of a NO_COLOR environment variable that, when present (**regardless of its value**), prevents the addition of ANSI color. CLICOLOR_FORCE: Force color https://bixense.com/clicolors/ """ if os.getenv("CLICOLOR_FORCE", "0") != "0": # CLICOLOR_FORCE != 0, ANSI colors should be enabled no matter what. return True if os.getenv("NO_COLOR") is not None: return False return hasattr(stream, "isatty") and stream.isatty() class ConanOutput: """ A singleton class to handle output messages in Conan. Recipes should only access this class through the ``self.output`` attribute of the recipe, but custom commands or tools can instantiate it directly, where doing so for each message is a valid practice. It provides methods to write messages at different levels of verbosity, such as debug, info, warning, and error. The output level can be controlled by the user through command-line options or environment variables. The output methods return the instance itself, so different methods can be chained together. """ # Singleton _conan_output_level = LEVEL_STATUS _silent_warn_tags = [] _warnings_as_errors = [] lock = Lock() def __init__(self, scope: str = ""): """ Initialize the ConanOutput instance. :parameter scope: A string that represents the scope of the output. This is usually the reference of the recipe being executed, like ``pkg/1.0@user/channel`` and is prefixed to the output messages. If not provided, it defaults to an empty string. """ self.stream = sys.stderr self._scope = scope # FIXME: This is needed because in testing we are redirecting the sys.stderr to a buffer # stream to capture it, so colorama is not there to strip the color bytes self._color = _color_enabled(self.stream) @classmethod def define_silence_warnings(cls, warnings): cls._silent_warn_tags = warnings @classmethod def set_warnings_as_errors(cls, value): cls._warnings_as_errors = value @classmethod def get_output_level(cls): return cls._conan_output_level @classmethod def set_output_level(cls, level): cls._conan_output_level = level @classmethod def valid_log_levels(cls): return {"quiet": LEVEL_QUIET, # -vquiet 80 "error": LEVEL_ERROR, # -verror 70 "warning": LEVEL_WARNING, # -vwaring 60 "notice": LEVEL_NOTICE, # -vnotice 50 "status": LEVEL_STATUS, # -vstatus 40 None: LEVEL_VERBOSE, # -v 30 "verbose": LEVEL_VERBOSE, # -vverbose 30 "debug": LEVEL_DEBUG, # -vdebug 20 "v": LEVEL_DEBUG, # -vv 20 "trace": LEVEL_TRACE, # -vtrace 10 "vv": LEVEL_TRACE # -vvv 10 } @classmethod def define_log_level(cls, v): env_level = os.getenv("CONAN_LOG_LEVEL") v = env_level or v levels = cls.valid_log_levels() try: level = levels[v] except KeyError: msg = " defined in CONAN_LOG_LEVEL environment variable" if env_level else "" vals = "quiet, error, warning, notice, status, verbose, debug(v), trace(vv)" raise ConanException(f"Invalid argument '-v{v}'{msg}.\nAllowed values: {vals}") else: cls.set_output_level(level) @classmethod def level_allowed(cls, level): return cls._conan_output_level <= level @property def color(self): return self._color @property def scope(self): return self._scope @scope.setter def scope(self, out_scope): self._scope = out_scope @property def is_terminal(self): return hasattr(self.stream, "isatty") and self.stream.isatty() def writeln(self, data, fg=None, bg=None): return self.write(data, fg, bg, newline=True) def write(self, data, fg=None, bg=None, newline=False): if self._conan_output_level > LEVEL_NOTICE: return self if self._color and (fg or bg): data = "%s%s%s%s" % (fg or '', bg or '', data, Style.RESET_ALL) if newline: data = "%s\n" % data with self.lock: self.stream.write(data) self.stream.flush() return self def box(self, msg: str): """ Draw a box around the message, useful for important messages""" color = Color.BRIGHT_GREEN self.writeln("\n**************************************************", fg=color) self.writeln(f'*{msg: ^48}*', fg=color) self.writeln(f"**************************************************\n", fg=color) return self def login_msg(self, msg, newline=False): # unconditional to the error level, this has to show always self._write_message(msg, newline=newline) return self def _write_message(self, msg, fg=None, bg=None, newline=True): if isinstance(msg, dict): # For traces we can receive a dict already, we try to transform then into more natural # text msg = ", ".join([f"{k}: {v}" for k, v in msg.items()]) msg = "=> {}".format(msg) # msg = json.dumps(msg, sort_keys=True, default=json_encoder) if self._scope: if self._color: ret = f"{fg or ''}{bg or ''}{self._scope}: {msg}{Style.RESET_ALL}" else: ret = f"{self._scope}: {msg}" else: if self._color: ret = f"{fg or ''}{bg or ''}{msg}{Style.RESET_ALL}" else: ret = msg if newline: ret = f"{ret}\n" with self.lock: self.stream.write(ret) self.stream.flush() def trace(self, msg: str): """ This is the most extreme level of detail. Trace messages log every little step the system takes, including function entries and exits, variable changes, and other very specific events. This message won't be printed unless the user has set the log level to trace (e.g., using the ``-vvv`` option in the command line). It’s used when full visibility of everything happening in the system is required, but should be used carefully due to the large amount of information it can generate.""" if self._conan_output_level <= LEVEL_TRACE: self._write_message(msg, fg=Color.BLUE) return self def debug(self, msg: str, fg: str = Color.MAGENTA, bg: str = None): """ With a high level of detail, it is mainly used for debugging code. This message won't be printed unless the user has set the log level to debug (e.g., using the ``-vv`` option in the command line). These messages provide useful information for developers, such as variable values or execution flow details, to trace errors or analyze the program's behavior.""" if self._conan_output_level <= LEVEL_DEBUG: self._write_message(msg, fg=fg, bg=bg) return self def verbose(self, msg: str, fg: str = None, bg: str = None): """ Displays additional and detailed information that, while not critical, can be useful for better understanding how the system is working. This message won't be printed unless the user has set the log level to verbose (e.g., using the ``-v`` option in the command line). It’s appropriate for gaining more context without overloading the logs with excessive detail. Useful when more clarity is needed than a simple info.""" if self._conan_output_level <= LEVEL_VERBOSE: self._write_message(msg, fg=fg, bg=bg) return self def status(self, msg: str, fg: str = None, bg: str = None, newline: bool = True): """ Provides general information about the system or ongoing operations. Info messages are basic and used to inform about common events, like the start or completion of processes, without implying specific problems or achievements.""" if self._conan_output_level <= LEVEL_STATUS: self._write_message(msg, fg=fg, bg=bg, newline=newline) return self info = status def title(self, msg: str): """ Draws a title around the message, useful for important messages""" if self._conan_output_level <= LEVEL_NOTICE: self._write_message("\n======== {} ========".format(msg), fg=Color.BRIGHT_MAGENTA) return self def subtitle(self, msg: str): """ Draws a subtitle around the message, useful for important messages""" if self._conan_output_level <= LEVEL_NOTICE: self._write_message("\n-------- {} --------".format(msg), fg=Color.BRIGHT_MAGENTA) return self def highlight(self, msg: str): """ Marks or emphasizes important events or processes that need to stand out but don’t necessarily indicate success or error. These messages draw attention to key points that may be relevant for the user or administrator.""" if self._conan_output_level <= LEVEL_NOTICE: self._write_message(msg, fg=Color.BRIGHT_MAGENTA) return self def success(self, msg: str): """ Shows that an operation has been completed successfully. This type of message is useful to confirm that key processes or tasks have finished correctly, which is essential for good application monitoring.""" if self._conan_output_level <= LEVEL_NOTICE: self._write_message(msg, fg=Color.BRIGHT_GREEN) return self @staticmethod def _warn_tag_matches(warn_tag, patterns): lookup_tag = warn_tag or "unknown" return any(fnmatch.fnmatch(lookup_tag, pattern) for pattern in patterns) def warning(self, msg: str, warn_tag: str = None): """ Highlights a potential issue that, while not stopping the system, could cause problems in the future or under certain conditions. Warnings signal abnormal situations that should be reviewed but don’t necessarily cause an immediate halt in operations. Notice that if the tag matches the pattern in the ``core:warnings_as_errors`` configuration, and is not skipped, this will be upgraded to an error, and raise an exception when the output is printed, so that the error does not pass unnoticed.""" _treat_as_error = self._warn_tag_matches(warn_tag, self._warnings_as_errors) if (self._conan_output_level <= LEVEL_WARNING or (_treat_as_error and self._conan_output_level <= LEVEL_ERROR)): if self._warn_tag_matches(warn_tag, self._silent_warn_tags): return self warn_tag_msg = "" if warn_tag is None else f"{warn_tag}: " output = f"{warn_tag_msg}{msg}" if _treat_as_error: self.error(output) else: self._write_message(f"WARN: {output}", Color.YELLOW) return self def error(self, msg: str, error_type: str = None): """ Indicates that a serious issue has occurred that prevents the system or application from continuing to function correctly. Typically, this represents a failure in the normal flow of execution, such as a service crash or a critical exception. Notice that if the user has set the ``core:warnings_as_errors`` configuration, this will raise an exception when the output is printed, so that the error does not pass unnoticed.""" if self._warnings_as_errors and error_type != "exception": raise ConanException(msg) if self._conan_output_level <= LEVEL_ERROR: self._write_message("ERROR: {}".format(msg), Color.RED) return self def flush(self): self.stream.flush() def cli_out_write(data, fg=None, bg=None, endline="\n", indentation=0): """ Output to be used by formatters to dump information to stdout """ if (fg or bg) and _color_enabled(sys.stdout): # need color data = f"{' ' * indentation}{fg or ''}{bg or ''}{data}{Style.RESET_ALL}{endline}" sys.stdout.write(data) else: data = f"{' ' * indentation}{data}{endline}" if sys.stdout.isatty(): # https://github.com/conan-io/conan/issues/17245 avoid colorama crash and overhead # skip deinit/reinit if stdout is not a TTY to preserve redirected output to file colorama.deinit() sys.stdout.write(data) colorama.reinit() else: sys.stdout.write(data) class TimedOutput: def __init__(self, interval, out=None, msg_format=None): self._interval = interval self._msg_format = msg_format self._t = time.time() self._out = out or ConanOutput() def info(self, msg, *args, **kwargs): t = time.time() if t - self._t > self._interval: self._t = t if self._msg_format: msg = self._msg_format(msg, *args, **kwargs) self._out.info(msg) ================================================ FILE: conan/api/subapi/__init__.py ================================================ ================================================ FILE: conan/api/subapi/audit.py ================================================ import binascii import json import os import base64 from conan.internal.api.audit.providers import ConanCenterProvider, PrivateProvider from conan.errors import ConanException from conan.internal.api.remotes.encrypt import encode, decode from conan.internal.model.recipe_ref import RecipeReference from conan.internal.util.files import save, load CONAN_CENTER_AUDIT_PROVIDER_NAME = "conancenter" CYPHER_KEY = "private" class AuditAPI: """ This class provides the functionality to scan references for vulnerabilities. """ def __init__(self, conan_api): self._conan_api = conan_api self._home_folder = conan_api.home_folder self._providers_path = os.path.join(self._home_folder, "audit_providers.json") self._provider_cls = { "conan-center-proxy": ConanCenterProvider, "private": PrivateProvider, } @staticmethod def scan(deps_graph, provider, context=None): """ Scan a given recipe for vulnerabilities in its dependencies. """ refs = sorted(set(RecipeReference.loads(f"{node.ref.name}/{node.ref.version}") for node in deps_graph.nodes[1:] if context is None or node.context == context), key=lambda ref: ref.name) return provider.get_cves(refs) @staticmethod def list(references, provider): """ List the vulnerabilities of the given reference. """ refs = [RecipeReference.loads(ref) for ref in references] for ref in refs: ref.validate_ref() return provider.get_cves(refs) def get_provider(self, provider_name): """ Get the provider by name. """ # TODO: More work remains to be done here, hardcoded for now for testing providers = _load_providers(self._providers_path) if provider_name not in providers: add_arguments = ( "--url=https://audit.conan.io/ --type=conan-center-proxy" if provider_name == CONAN_CENTER_AUDIT_PROVIDER_NAME else "--url= --type=" ) register_message = ( f"If you don't have a valid token, register at: https://audit.conan.io/register." if provider_name == CONAN_CENTER_AUDIT_PROVIDER_NAME else "" ) raise ConanException( f"Provider '{provider_name}' not found. Please specify a valid provider name or add " f"it using: 'conan audit provider add {provider_name} {add_arguments} " f"--token='\n{register_message}" ) provider_data = providers[provider_name] safe_provider_name = provider_name.replace("-", "_") env_token = os.getenv(f"CONAN_AUDIT_PROVIDER_TOKEN_{safe_provider_name.upper()}") if env_token: # Always override the token with the environment variable provider_data["token"] = env_token elif "token" in provider_data: try: enc_token = base64.standard_b64decode(provider_data["token"]).decode() provider_data["token"] = decode(enc_token, CYPHER_KEY) except binascii.Error: raise ConanException(f"Invalid token format for provider '{provider_name}'. " f"The token might be corrupt.") provider_cls = self._provider_cls.get(provider_data["type"]) return provider_cls(self._conan_api, provider_name, provider_data) def list_providers(self): """ Get all available providers. """ providers = _load_providers(self._providers_path) result = [] for name, provider_data in providers.items(): provider_cls = self._provider_cls.get(provider_data["type"]) result.append(provider_cls(self._conan_api, name, provider_data)) return result def add_provider(self, name, url, provider_type): """ Add a provider. """ providers = _load_providers(self._providers_path) if name in providers: raise ConanException(f"Provider '{name}' already exists") if provider_type not in self._provider_cls: raise ConanException(f"Provider type '{provider_type}' not found") providers[name] = { "name": name, "url": url, "type": provider_type } _save_providers(self._providers_path, providers) def remove_provider(self, provider_name): """ Remove a provider. """ providers = _load_providers(self._providers_path) if provider_name not in providers: raise ConanException(f"Provider '{provider_name}' not found") del providers[provider_name] _save_providers(self._providers_path, providers) def auth_provider(self, provider, token): """ Authenticate a provider. """ if not provider: raise ConanException("Provider not found") providers = _load_providers(self._providers_path) assert provider.name in providers encode_token = encode(token, CYPHER_KEY).encode() providers[provider.name]["token"] = base64.standard_b64encode(encode_token).decode() setattr(provider, "token", token) _save_providers(self._providers_path, providers) def _load_providers(providers_path): if not os.path.exists(providers_path): default_providers = { CONAN_CENTER_AUDIT_PROVIDER_NAME: { "url": "https://audit.conan.io/", "type": "conan-center-proxy" } } save(providers_path, json.dumps(default_providers, indent=4)) return json.loads(load(providers_path)) def _save_providers(providers_path, providers): save(providers_path, json.dumps(providers, indent=4)) # Make readable & writeable only by current user os.chmod(providers_path, 0o600) ================================================ FILE: conan/api/subapi/cache.py ================================================ import json import os import shutil import tarfile import tempfile from conan.api.model import PackagesList from conan.api.output import ConanOutput from conan.internal.api.uploader import compress_files, get_compress_level from conan.internal.cache.cache import PkgCache from conan.internal.cache.conan_reference_layout import (EXPORT_SRC_FOLDER, EXPORT_FOLDER, SRC_FOLDER, METADATA, DOWNLOAD_EXPORT_FOLDER) from conan.internal.cache.home_paths import HomePaths from conan.internal.cache.integrity_check import IntegrityChecker from conan.internal.paths import COMPRESSIONS from conan.internal.rest.download_cache import DownloadCache from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.api.uploader import PackagePreparator from conan.internal.conan_app import ConanApp from conan.internal.rest.pkg_sign import PkgSignaturesPlugin from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import rmdir, mkdir, remove, save class CacheAPI: """ This CacheAPI is used to interact with the packages storage cache Note that the Conan packages cache is exclusively **read-only** for user code. Only Conan can write or modify the folders and files in the Conan cache. In general, when a method returns a folder, it is mostly for debugging purposes and read-only access, but never to modify the contents of the cache. """ def __init__(self, conan_api, api_helpers): self._conan_api = conan_api self._api_helpers = api_helpers def export_path(self, ref: RecipeReference): """Returns the path of the recipe conanfile and exported files in the Conan cache This folder is exclusively for **read-only** access, typically for debugging purposes, it is completely forbidden to modify any of its contents. :param ref: RecipeReference. If it includes recipe revision, that exact revision will be returned, if it doesn't include recipe revision, it will return the latest revision one. :return: path to the folder, as a string :raises: ConanExcepcion if the folder doesn't exist """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) ref = _resolve_latest_ref(cache, ref) ref_layout = cache.recipe_layout(ref) return _check_folder_existence(ref, "export", ref_layout.export()) def recipe_metadata_path(self, ref: RecipeReference): """Returns the path of the recipe metadata files in the Conan cache Exceptionally, adding or modifying the files within this folder is allowed, as the metadata files are not taken into account into the computation of the recipe hash (recipe revision). :param ref: RecipeReference. If it includes recipe revision, that exact revision will be returned, if it doesn't include recipe revision, it will return the latest revision one. :return: path to the folder, as a string :raises: ConanExcepcion if the folder doesn't exist """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) ref = _resolve_latest_ref(cache, ref) ref_layout = cache.recipe_layout(ref) return _check_folder_existence(ref, "metadata", ref_layout.metadata()) def export_source_path(self, ref: RecipeReference): """Returns the path of the exported sources in the Conan cache Note that the exported sources only exist in the cache when the package has been created locally or built from source. This folder is exclusively for **read-only** access, typically for debugging purposes, it is completely forbidden to modify any of its contents. :param ref: RecipeReference. If it includes recipe revision, that exact revision will be returned, if it doesn't include recipe revision, it will return the latest revision one. :return: path to the folder, as a string :raises: ConanExcepcion if the folder doesn't exist """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) ref = _resolve_latest_ref(cache, ref) ref_layout = cache.recipe_layout(ref) return _check_folder_existence(ref, "export_sources", ref_layout.export_sources()) def source_path(self, ref: RecipeReference): """Returns the path of the temporary source folder in the Conan cache Note that the source folder only exist in the cache when the package has been created locally or built from source. This folder is exclusively for **read-only** access, typically for debugging purposes, it is completely forbidden to modify any of its contents. :param ref: RecipeReference. If it includes recipe revision, that exact revision will be returned, if it doesn't include recipe revision, it will return the latest revision one. :return: path to the folder, as a string :raises: ConanExcepcion if the folder doesn't exist """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) ref = _resolve_latest_ref(cache, ref) ref_layout = cache.recipe_layout(ref) return _check_folder_existence(ref, "source", ref_layout.source()) def build_path(self, pref: PkgReference): """Returns the path of the temporary build folder in the Conan cache Note that the build folder only exist in the cache when the package has been created locally or built from source. This folder is exclusively for **read-only** access, typically for debugging purposes, it is completely forbidden to modify any of its contents. :param pref: PkgReference. If it includes recipe revision, that exact revision will be returned, if it doesn't include recipe revision, it will return the latest revision one. Exactly same behavior for the package revision. :return: path to the folder, as a string :raises: ConanExcepcion if the folder doesn't exist """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) pref = _resolve_latest_pref(cache, pref) ref_layout = cache.pkg_layout(pref) return _check_folder_existence(pref, "build", ref_layout.build()) def package_metadata_path(self, pref: PkgReference): """Returns the path of the package metadata folder in the Conan cache Exceptionally, adding or modifying the files within this folder is allowed, as the metadata files are not taken into account into the computation of the package hash (package revision). :param pref: PkgReference. If it includes recipe revision, that exact revision will be returned, if it doesn't include recipe revision, it will return the latest revision one. Exactly same behavior for the package revision. :return: path to the folder, as a string :raises: ConanExcepcion if the folder doesn't exist """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) pref = _resolve_latest_pref(cache, pref) ref_layout = cache.pkg_layout(pref) return _check_folder_existence(pref, "metadata", ref_layout.metadata()) def package_path(self, pref: PkgReference): """Returns the path of the package folder in the Conan cache This folder is exclusively for **read-only** access, typically for debugging purposes, it is completely forbidden to modify any of its contents. :param pref: PkgReference. If it includes recipe revision, that exact revision will be returned, if it doesn't include recipe revision, it will return the latest revision one. Exactly same behavior for the package revision. :return: path to the folder, as a string :raises: ConanExcepcion if the folder doesn't exist """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) pref = _resolve_latest_pref(cache, pref) ref_layout = cache.pkg_layout(pref) if os.path.exists(ref_layout.finalize()): return ref_layout.finalize() return _check_folder_existence(pref, "package", ref_layout.package()) def check_integrity(self, package_list, return_pkg_list=False): """ Check if the recipes and packages are corrupted :param package_list: PackagesList to check :param return_pkg_list: If True, return a PackagesList with corrupted artifacts :return: PackagesList with corrupted artifacts if return_pkg_list is True :raises: ConanExcepcion if there are corrupted artifacts and return_pkg_list is False """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) checker = IntegrityChecker(cache) corrupted_pkg_list = checker.check(package_list) if return_pkg_list: return corrupted_pkg_list if corrupted_pkg_list: raise ConanException("There are corrupted artifacts, check the error logs") def sign(self, package_list): """Sign packages with the package signing plugin""" cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) pkg_signer = PkgSignaturesPlugin(cache, self._conan_api.home_folder) if not pkg_signer.is_sign_configured: raise ConanException( "The sign() function in the package sign plugin is not defined. For more " "information on how to configure the plugin, please read the documentation at " "https://docs.conan.io/2/reference/extensions/package_signing.html.") app = ConanApp(self._conan_api) preparator = PackagePreparator(app, self._api_helpers.cache, self._api_helpers.remote_manager, self._api_helpers.global_conf) # Some packages can have missing sources/exports_sources enabled_remotes = self._conan_api.remotes.list() preparator.prepare(package_list, enabled_remotes, None, force=True) for rref, packages in package_list.items(): recipe_bundle = package_list.recipe_dict(rref) rref_folder = cache.recipe_layout(rref).download_export() try: pkg_signer.sign_pkg(rref, recipe_bundle.get("files", {}), rref_folder) except Exception as e: recipe_bundle["pkgsign_error"] = str(e) for pref in packages: pkg_bundle = package_list.package_dict(pref) if pkg_bundle: pref_folder = cache.pkg_layout(pref).download_package() try: pkg_signer.sign_pkg(pref, pkg_bundle.get("files", {}), pref_folder) except Exception as e: pkg_bundle["pkgsign_error"] = str(e) return package_list def verify(self, package_list): """Verify packages with the package signing plugin""" cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) pkg_signer = PkgSignaturesPlugin(cache, self._conan_api.home_folder) if not pkg_signer.is_verify_configured: raise ConanException( "The verify() function in the package sign plugin is not defined. For more " "information on how to configure the plugin, please read the documentation at " "https://docs.conan.io/2/reference/extensions/package_signing.html.") for rref, packages in package_list.items(): recipe_bundle = package_list.recipe_dict(rref) layout = cache.recipe_layout(rref) rref_folder = layout.download_export() files = {file: os.path.join(rref_folder, file) for file in sorted(os.listdir(rref_folder)) if not file.startswith(METADATA)} recipe_bundle["files"] = files try: pkg_signer.verify(rref, rref_folder, layout.metadata(), files) except Exception as e: recipe_bundle["pkgsign_error"] = str(e) for pref in packages: pkg_bundle = package_list.package_dict(pref) if pkg_bundle: layout = cache.pkg_layout(pref) pref_folder = layout.download_package() files = {file: os.path.join(pref_folder, file) for file in sorted(os.listdir(pref_folder)) if not file.startswith(METADATA)} pkg_bundle["files"] = files try: pkg_signer.verify(pref, pref_folder, layout.metadata(), files) except Exception as e: pkg_bundle["pkgsign_error"] = str(e) return package_list def clean(self, package_list, source=True, build=True, download=True, temp=True, backup_sources=False) -> None: """ Remove non critical folders from the cache, like source, build and download (.tgz store) folders. :param package_list: the package lists that should be cleaned :param source: boolean, remove the "source" folder if True :param build: boolean, remove the "build" folder if True :param download: boolean, remove the "download (.tgz)" folder if True :param temp: boolean, remove the temporary folders :param backup_sources: boolean, remove the "source" folder if True :return: """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) if temp: rmdir(cache.temp_folder) # Clean those build folders that didn't succeed to create a package and wont be in DB builds_folder = cache.builds_folder if os.path.isdir(builds_folder): ConanOutput().verbose(f"Cleaning temporary folders") for subdir in os.listdir(builds_folder): folder = os.path.join(builds_folder, subdir) manifest = os.path.join(folder, "p", "conanmanifest.txt") info = os.path.join(folder, "p", "conaninfo.txt") if not os.path.exists(manifest) or not os.path.exists(info): rmdir(folder) if backup_sources: backup_files = self._conan_api.cache.get_backup_sources(package_list, exclude=False, only_upload=False) ConanOutput().verbose(f"Cleaning {len(backup_files)} backup sources") for f in backup_files: remove(f) for ref, packages in package_list.items(): ConanOutput(ref.repr_notime()).verbose("Cleaning recipe cache contents") ref_layout = cache.recipe_layout(ref) if source: rmdir(ref_layout.source()) if download: rmdir(ref_layout.download_export()) for pref in packages: ConanOutput(pref).verbose("Cleaning package cache contents") pref_layout = cache.pkg_layout(pref) if build: rmdir(pref_layout.build()) # It is important to remove the "build_id" identifier if build-folder is removed cache.remove_build_id(pref) if download: rmdir(pref_layout.download_package()) def save(self, package_list: PackagesList, path, no_source=False) -> None: """Create a compressed archive with recipes and packages from the Conan cache that can be later restored in another cache. Do not manipulate the contents of the resulting archive, as it also contains metadata, and modifying the contents would be equivalent to modify the Conan package cache, which is forbidden. :param package_list: PackagesList containing the recipes and packages to add to the compressed archive :param path: The archive file to generate. Based on the extension of the file, different compression formats can be used (.tgz, .txz and .tzst, the latter only for Python>=3.14). :param no_source: If True, the source folders in the cache will not be added to the archive. :return: """ global_conf = self._api_helpers.global_conf cache = PkgCache(self._conan_api.cache_folder, global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache out = ConanOutput() mkdir(os.path.dirname(path)) tgz_name = os.path.basename(path) compressformat = next((e for e in COMPRESSIONS if tgz_name.endswith(e)), None) if not compressformat: raise ConanException(f"Unsupported compression format for {tgz_name}") compresslevel = get_compress_level(compressformat, global_conf) tar_files: dict[str, str] = {} # {path_in_tar: abs_path} for ref, packages in package_list.items(): ref_layout = cache.recipe_layout(ref) recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder) recipe_folder = recipe_folder.replace("\\", "/") # make win paths portable ref_bundle = package_list.recipe_dict(ref) ref_bundle["recipe_folder"] = recipe_folder out.info(f"Saving {ref}: {recipe_folder}") # Package only selected folders, not DOWNLOAD one for f in (EXPORT_FOLDER, EXPORT_SRC_FOLDER, SRC_FOLDER): if f == SRC_FOLDER and no_source: continue cachepath = os.path.join(cache_folder, recipe_folder, f) if os.path.exists(cachepath): tar_files[f"{recipe_folder}/{f}"] = cachepath cachepath = os.path.join(cache_folder, recipe_folder, DOWNLOAD_EXPORT_FOLDER, METADATA) if os.path.exists(cachepath): tar_files[f"{recipe_folder}/{DOWNLOAD_EXPORT_FOLDER}/{METADATA}"] = cachepath for pref in packages: pref_layout = cache.pkg_layout(pref) pkg_folder = pref_layout.package() folder = os.path.relpath(pkg_folder, cache_folder) folder = folder.replace("\\", "/") # make win paths portable pkg_dict = package_list.package_dict(pref) pkg_dict["package_folder"] = folder out.info(f"Saving {pref}: {folder}") tar_files[folder] = os.path.join(cache_folder, folder) if os.path.exists(pref_layout.metadata()): metadata_folder = os.path.relpath(pref_layout.metadata(), cache_folder) metadata_folder = metadata_folder.replace("\\", "/") # make paths portable pkg_dict["metadata_folder"] = metadata_folder out.info(f"Saving {pref} metadata: {metadata_folder}") tar_files[metadata_folder] = os.path.join(cache_folder, metadata_folder) # Create a temporary file in order to reuse compress_files functionality serialized = json.dumps(package_list.serialize(), indent=2) pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json") save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path compress_files(tar_files, tgz_name, os.path.dirname(path), compresslevel, recursive=True) remove(pkglist_path) ConanOutput().success(f"Created cache save file: {path}") def restore(self, path) -> PackagesList: """Restore a compressed archive with recipes and packages previously saved from another Conan cache into the currently active Conan cache. :param path: The archive file to restore. Based on the extension of the file, different compression formats can be used (.tgz, .txz and .tzst, the latter only for Python>=3.14). :return: a PackageLists with the recipes and packages that have been restored to the cache """ if not os.path.isfile(path): raise ConanException(f"Restore archive doesn't exist in {path}") cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache with open(path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) fileobj = the_tar.extractfile("pkglist.json") pkglist = fileobj.read() the_tar.extraction_filter = (lambda member, _: member) # fully_trusted (Py 3.14) the_tar.extractall(path=cache_folder) the_tar.close() # After unzipping the files, we need to update the DB that references these files out = ConanOutput() package_list = PackagesList.deserialize(json.loads(pkglist)) for ref, packages in package_list.items(): ref_bundle = package_list.recipe_dict(ref) ref.timestamp = revision_timestamp_now() ref_bundle["timestamp"] = ref.timestamp try: recipe_layout = cache.recipe_layout(ref) except ConanException: recipe_layout = cache.create_ref_layout(ref) # new DB folder entry recipe_folder = ref_bundle["recipe_folder"] rel_path = os.path.relpath(recipe_layout.base_folder, cache_folder) rel_path = rel_path.replace("\\", "/") # In the case of recipes, they are always "in place", so just checking it assert rel_path == recipe_folder, f"{rel_path}!={recipe_folder}" out.info(f"Restore: {ref} in {recipe_folder}") for pref in packages: pref.timestamp = revision_timestamp_now() pref_bundle = package_list.package_dict(pref) pref_bundle["timestamp"] = pref.timestamp try: pkg_layout = cache.pkg_layout(pref) except ConanException: pkg_layout = cache.create_pkg_layout(pref) # DB Folder entry # FIXME: This is not taking into account the existence of previous package unzipped_pkg_folder = pref_bundle["package_folder"] out.info(f"Restore: {pref} in {unzipped_pkg_folder}") # If the DB folder entry is different to the disk unzipped one, we need to move it # This happens for built (not downloaded) packages in the source "conan cache save" db_pkg_folder = os.path.relpath(pkg_layout.package(), cache_folder) db_pkg_folder = db_pkg_folder.replace("\\", "/") if db_pkg_folder != unzipped_pkg_folder: # If a previous package exists, like a previous restore, then remove it if os.path.exists(pkg_layout.package()): shutil.rmtree(pkg_layout.package()) shutil.move(os.path.join(cache_folder, unzipped_pkg_folder), pkg_layout.package()) pref_bundle["package_folder"] = db_pkg_folder unzipped_metadata_folder = pref_bundle.get("metadata_folder") if unzipped_metadata_folder: # FIXME: Restore metadata is not incremental, but destructive out.info(f"Restore: {pref} metadata in {unzipped_metadata_folder}") db_metadata_folder = os.path.relpath(pkg_layout.metadata(), cache_folder) db_metadata_folder = db_metadata_folder.replace("\\", "/") if db_metadata_folder != unzipped_metadata_folder: # We need to put the package in the final location in the cache if os.path.exists(pkg_layout.metadata()): shutil.rmtree(pkg_layout.metadata()) shutil.move(os.path.join(cache_folder, unzipped_metadata_folder), pkg_layout.metadata()) pref_bundle["metadata_folder"] = db_metadata_folder return package_list def get_backup_sources(self, package_list=None, exclude=True, only_upload=True): """Get list of backup source files currently present in the cache, either all of them if no argument, or filtered by those belonging to the references in the package_list :param package_list: a PackagesList object to filter backup files from (The files should have been downloaded form any of the references in the package_list) :param exclude: if True, exclude the sources that come from URLs present the core.sources:exclude_urls global conf :param only_upload: if True, only return the files for packages that are set to be uploaded :return: A list of files that need to be uploaded """ config = self._api_helpers.global_conf download_cache_path = config.get("core.sources:download_cache") download_cache_path = download_cache_path or HomePaths( self._conan_api.cache_folder).default_sources_backup_folder excluded_urls = config.get("core.sources:exclude_urls", check_type=list, default=[]) if exclude else [] download_cache = DownloadCache(download_cache_path) return download_cache.get_backup_sources_files(excluded_urls, package_list, only_upload) def path_to_ref(self, path): # This method is explicitly not publicly documented, as mostly a command helper for # debugging, it shouldn't be used in any real API usage cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) result = cache.path_to_ref(path) if result is None: base, folder = os.path.split(path) result = cache.path_to_ref(base) return result def _resolve_latest_ref(cache, ref): if ref.revision is None or ref.revision == "latest": ref.revision = None result = cache.get_latest_recipe_revision(ref) if result is None: raise ConanException(f"'{ref}' not found in cache") ref = result return ref def _resolve_latest_pref(cache, pref): pref.ref = _resolve_latest_ref(cache, pref.ref) if pref.revision is None or pref.revision == "latest": pref.revision = None result = cache.get_latest_package_revision(pref) if result is None: raise ConanException(f"'{pref.repr_notime()}' not found in cache") pref = result return pref def _check_folder_existence(ref, folder_name, folder_path): if not os.path.exists(folder_path): raise ConanException(f"'{folder_name}' folder does not exist for the reference {ref}") return folder_path ================================================ FILE: conan/api/subapi/command.py ================================================ import os import shlex from conan.api.output import ConanOutput from conan.errors import ConanException class CommandAPI: def __init__(self, conan_api): self._conan_api = conan_api self.cli = None def run(self, cmd): if isinstance(cmd, str): cmd = shlex.split(cmd) if isinstance(cmd, list): current_cmd = cmd[0] args = cmd[1:] else: raise ConanException("Input of conan_api.command.run() should be a list or a string") commands = getattr(self.cli, "_commands") # to no make it public to users of Cli class try: command = commands[current_cmd] except KeyError: raise ConanException(f"Command {current_cmd} does not exist") # Conan has some global state in the ConanOutput class that # get redefined when running a command and leak to the calling scope # if running from a custom command. # Store the old one and restore it after the command execution as a workaround. _conan_output_level = ConanOutput._conan_output_level # noqa _silent_warn_tags = ConanOutput._silent_warn_tags # noqa _warnings_as_errors = ConanOutput._warnings_as_errors # noqa try: result = command.run_cli(self._conan_api, args) finally: ConanOutput._conan_output_level = _conan_output_level ConanOutput._silent_warn_tags = _silent_warn_tags ConanOutput._warnings_as_errors = _warnings_as_errors return result @staticmethod def get_runner(profile_host): if profile_host.runner and not os.environ.get("CONAN_RUNNER_ENVIRONMENT"): from conan.internal.runner.docker import DockerRunner from conan.internal.runner.ssh import SSHRunner from conan.internal.runner.wsl import WSLRunner try: runner_type = profile_host.runner['type'].lower() except KeyError: raise ConanException(f"Invalid runner configuration. 'type' must be defined") runner_instances_map = { 'docker': DockerRunner, # 'ssh': SSHRunner, # 'wsl': WSLRunner, } try: runner_instance = runner_instances_map[runner_type] except KeyError: raise ConanException(f"Invalid runner type '{runner_type}'. " f"Allowed values: {', '.join(runner_instances_map.keys())}") return runner_instance ================================================ FILE: conan/api/subapi/config.py ================================================ import os from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan.internal.conan_app import ConanApp from conan.internal.graph.graph import CONTEXT_HOST, RECIPE_VIRTUAL, Node from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.internal.graph.profile_node_definer import consumer_definer from conan.errors import ConanException from conan.internal.model.conanconfig import loadconanconfig, saveconanconfig, loadconanconfig_yml from conan.internal.model.conf import BUILT_IN_CONFS from conan.internal.model.pkg_type import PackageType from conan.api.model import RecipeReference, Remote from conan.internal.util.files import rmdir, remove class ConfigAPI: """ This API provides methods to manage the Conan configuration in the Conan home folder. It allows installing configurations from various sources, retrieving global configuration values, and listing available configurations. It also provides methods to clean the Conan home folder, resetting it to a clean state. """ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers def home(self): """ return the current Conan home folder containing the configuration files like remotes, settings, profiles, and the packages cache. It is provided for debugging purposes. Recall that it is not allowed to write, modify or remove packages in the packages cache, and that to automate tasks that uses packages from the cache Conan provides mechanisms like deployers or custom commands. """ return self._conan_api.cache_folder def install(self, path_or_url: str, verify_ssl, config_type=None, args=None, source_folder=None, target_folder=None) -> None: """ install Conan configuration from a git repo, from a zip file in an http server or a local folder Calling this method will cause a reinitilization of the full ConanAPI, with possible invalidation of cached information, and references to objects from the ConanAPI might become dangling or outdated. :param path_or_url: path or url to install. It can be a http://.../somefile.zip, a git repository URL, or a local folder :param verify_ssl: Argument passed to python-requests library for SSL verification :param config_type: type of configuration to install: "git", "dir", "file", "url" :param args: additional arguments to pass to git repositories cloning :param source_folder: If specified, install files from that folder of the origin only :param target_folder: If the files are to be installed in a specific folder in the Conan home. For example, if it is desired to install only profiles from a configuration and using source_folder="profiles", it might be expected to use target_folder="profiles" to keep the correct profile files location in the local home. """ from conan.internal.api.config.config_installer import configuration_install cache_folder = self._conan_api.cache_folder requester = self._helpers.requester configuration_install(cache_folder, requester, path_or_url, verify_ssl, config_type=config_type, args=args, source_folder=source_folder, target_folder=target_folder) self._conan_api.reinit() def install_package(self, require, lockfile=None, force=False, remotes=None, profile=None): """ install Conan configuration from a Conan package Calling this method will cause a reinitilization of the full ConanAPI, with possible invalidation of cached information, and references to objects from the ConanAPI might become dangling or outdated. :param require: The package requirement to be installed. It can contain version range expressions. If the revision is not specified, as a recipe ``requires``, it will also resolve to the latest recipe-revision :param lockfile: Lockfile to be used to constrain and lock the versions and recipe-revisions from the input requirements, to the exact versions and revisions specified in the lockfile :param force: If the package has already been installed, nothing will be done unless force is True :param remotes: Remotes to look for the configuration package :param profile: If specified, use that profile to resolve for profile-specific different configurations, like depending on different settings. :return: list of RecipeReferences of the installed configuration packages """ ConanOutput().warning("The 'conan config install-pkg' is experimental", warn_tag="experimental") require = RecipeReference.loads(require) required_pkgs = self.fetch_packages([require], lockfile, remotes, profile) installed_refs = self._install_pkgs(required_pkgs, force) self._conan_api.reinit() return installed_refs @staticmethod def load_conanconfig(path, remotes): # Internal, do not document yet. if os.path.isdir(path): path = os.path.join(path, "conanconfig.yml") requested_requires, urls = loadconanconfig_yml(path) if urls: new_remotes = [Remote(f"config_install_url{'_' + str(i)}", url=url) for i, url in enumerate(urls)] remotes = remotes or [] remotes += new_remotes return requested_requires, remotes def install_conanconfig(self, path, lockfile=None, force=False, remotes=None, profile=None): """ install Conan configuration from a Conan "conanconfig.yml" file Calling this method will cause a reinitilization of the full ConanAPI, with possible invalidation of cached information, and references to objects from the ConanAPI might become dangling or outdated. :param path: Path to the conanconfig.yml file containing the configuration packages requirement definitions :param lockfile: Lockfile to be used to constrain and lock the versions and recipe-revisions from the input requirements, to the exact versions and revisions specified in the lockfile :param force: If the package has already been installed, nothing will be done unless force is True :param remotes: Remotes to look for the configuration package :param profile: If specified, use that profile to resolve for profile-specific different configurations, like depending on different settings. :return: list of RecipeReferences of the installed configuration packages """ ConanOutput().warning("The 'conan config install-pkg' is experimental", warn_tag="experimental") requested_requires, remotes = self.load_conanconfig(path, remotes) required_pkgs = self.fetch_packages(requested_requires, lockfile, remotes, profile) installed_refs = self._install_pkgs(required_pkgs, force) self._conan_api.reinit() return installed_refs def _install_pkgs(self, required_pkgs, force): out = ConanOutput() out.title("Configuration packages to install") config_version_file = HomePaths(self._conan_api.home_folder).config_version_path if not os.path.exists(config_version_file): config_versions = [] else: ConanOutput().info(f"Reading existing config-versions file: {config_version_file}") config_versions = loadconanconfig(config_version_file) config_versions_dict = {r.name: r for r in config_versions} if len(config_versions_dict) < len(config_versions): raise ConanException("There are multiple requirements for the same package " f"with different versions: {config_version_file}") new_config = config_versions_dict.copy() for required_pkg in required_pkgs: new_config.pop(required_pkg.ref.name, None) # To ensure new order new_config[required_pkg.ref.name] = required_pkg.ref final_config_refs = [r for r in new_config.values()] prev_refs = "\n\t".join(repr(r) for r in config_versions) out.info(f"Previously installed configuration packages:\n\t{prev_refs}") new_refs = "\n\t".join(r.repr_notime() for r in final_config_refs) out.info(f"New configuration packages to install:\n\t{new_refs}") if list(config_versions_dict) == list(new_config)[:len(config_versions_dict)]: # There is no conflict in order, can be done safely if final_config_refs == config_versions: if force: out.warning("The requested configurations are identical to the already " "installed ones, but forcing re-installation because --force") to_install = required_pkgs else: out.info("The requested configurations are identical to the already " "installed ones, skipping re-installation") to_install = [] else: out.info("Installing new or updating configuration packages") to_install = required_pkgs else: # Change in order of existing configuration if force: out.warning("Installing these configuration packages will break the " "existing order, with possible side effects. " "Forcing the installation because --force was defined", warn_tag="risk") to_install = required_pkgs else: msg = ("Installing these configuration packages will break the " "existing order, with possible side effects, like breaking 'package_ids'.\n" "If you still want to enforce this configuration you can:\n" " Use 'conan config clean' first to fully reset your configuration.\n" " Or use 'conan config install-pkg --force' to force installation.") raise ConanException(msg) out.title("Installing configuration from packages") # install things and update the Conan cache "config_versions.json" file from conan.internal.api.config.config_installer import configuration_install cache_folder = self._conan_api.cache_folder requester = self._helpers.requester for pkg in to_install: out.info(f"Installing configuration from {pkg.ref}") configuration_install(cache_folder, requester, uri=pkg.conanfile.package_folder, verify_ssl=False, config_type="dir", ignore=["conaninfo.txt", "conanmanifest.txt"]) saveconanconfig(config_version_file, final_config_refs) return final_config_refs def fetch_packages(self, requires, lockfile=None, remotes=None, profile=None): """ get and download configuration packages into the Conan cache, without installing such configuration in the current Conan home. This shouldn't be necessary for regular Conan configuration, and used at the moment exclusively for the "conan lock upgrade-config" experimental command. """ conan_api = self._conan_api remotes = conan_api.remotes.list() if remotes is None else remotes profile_host = profile_build = profile or conan_api.profiles.get_profile([]) app = ConanApp(self._conan_api) cache = self._helpers.cache ConanOutput().title("Fetching requested configuration packages") result = [] for ref in requires: # Computation of a very simple graph that requires "ref" # Need to convert input requires to RecipeReference conanfile = app.loader.load_virtual(requires=[ref]) consumer_definer(conanfile, profile_host, profile_build) root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, recipe=RECIPE_VIRTUAL) root_node.is_conf = True update = ["*"] builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, cache, remotes, update, update, self._helpers.global_conf) deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) # Basic checks of the package: correct package_type and no-dependencies deps_graph.report_graph_error() pkg = deps_graph.root.edges[0].dst ConanOutput().info(f"Configuration from package: {pkg}") if pkg.conanfile.package_type is not PackageType.CONF: raise ConanException(f'{pkg.conanfile} is not of package_type="configuration"') if pkg.edges: raise ConanException(f"Configuration package {pkg.ref} cannot have dependencies") # The computation of the "package_id" and the download of the package is done as usual # By default we allow all remotes, and build_mode=None, always updating conan_api.graph.analyze_binaries(deps_graph, None, remotes, update=update, lockfile=lockfile) conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) result.append(pkg) return result def get(self, name, default=None, check_type=None): """ get the value of a global.conf item :param name: configuration value to return :param default: default value to return if the configuration doesn't contain a value :param check_type: check if value is of type check_type, only if the value is defined """ return self._helpers.global_conf.get(name, default=default, check_type=check_type) def show(self, pattern) -> dict: """ get the values of global.conf for those configurations that matches the pattern that have an actual user definition. Values with no user definitions will be skipped from the returned value, defaults for those confs won't be shown. :param pattern: pattern to match against :return: dict of configuration values """ return self._helpers.global_conf.show(pattern) @staticmethod def conf_list() -> dict: """ list all the available built-in configurations :return: A sorted dictionary with all possible built-in configurations """ return BUILT_IN_CONFS.copy() def clean(self) -> None: """ reset the Conan home folder to a clean state, removing all the user custom configuration, custom files, and resetting modified files """ contents = os.listdir(self.home()) packages_folder = (self._helpers.global_conf.get("core.cache:storage_path") or os.path.join(self.home(), "p")) for content in contents: content_path = os.path.join(self.home(), content) if content_path == packages_folder: continue ConanOutput().debug(f"Removing {content_path}") if os.path.isdir(content_path): rmdir(content_path) else: remove(content_path) self._conan_api.reinit() # CHECK: This also generates a remotes.json that is not there after a conan profile show? self._conan_api.migrate() @property def settings_yml(self): """ Get the contents of the settings.yml and user_settings.yml files, which define the possible values for settings. Note that this is different from the settings present in a conanfile, which represent the actual values for a specific package, while this property represents the possible values for each setting. This is intended to be a **read-only** value, do not try to attempt to modify, inject or remove settings with this attribute. :returns: A read-only object representing the settings scheme, with a ``possible_values()`` method that returns a dictionary with the possible values for each setting, and a ``fields`` property that returns an ordered list with the fields of each setting. Note that it's possible to access nested settings using attribute access, such as ``settings_yml.compiler.possible_values()``. """ class SettingsYmlInterface: def __init__(self, settings): self._settings = settings def possible_values(self): """ returns a dict with the possible values for each setting """ return self._settings.possible_values() @property def fields(self): """ returns a dict with the fields of each setting """ return self._settings.fields def __getattr__(self, item): return SettingsYmlInterface(getattr(self._settings, item)) def __str__(self): return str(self._settings) return SettingsYmlInterface(self._helpers.settings_yml) ================================================ FILE: conan/api/subapi/download.py ================================================ import time from multiprocessing.pool import ThreadPool from typing import Optional, List from conan.api.model import Remote, PackagesList from conan.api.output import ConanOutput from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference class DownloadAPI: """ This API is used to download recipes and packages from a remote server.""" def __init__(self, conan_api, api_helpers): self._conan_api = conan_api self._api_helpers = api_helpers def recipe(self, ref: RecipeReference, remote: Remote, metadata: Optional[List[str]] = None): """Download the recipe specified in the ref from the remote. If the recipe is already in the cache it will be skipped, but the specified metadata will be downloaded.""" output = ConanOutput() assert ref.revision, f"Reference '{ref}' must have revision" try: recipe_layout = self._api_helpers.cache.recipe_layout(ref) # raises if not found except ConanException: pass else: output.info(f"Skip recipe {ref.repr_notime()} download, already in cache") if metadata: self._api_helpers.remote_manager.get_recipe_metadata(recipe_layout, ref, remote, metadata) return False output.info(f"Downloading recipe '{ref.repr_notime()}'") if ref.timestamp is None: # we didnt obtain the timestamp before (in general it should be) # Respect the timestamp of the server, the ``get_recipe()`` doesn't do it internally # Best would be that ``get_recipe()`` returns the timestamp in the same call server_ref = self._api_helpers.remote_manager.get_recipe_revision(ref, remote) assert server_ref == ref ref.timestamp = server_ref.timestamp recipe_layout = self._api_helpers.remote_manager.get_recipe(ref, remote, metadata) # Download the sources too, don't be lazy output.info(f"Downloading '{str(ref)}' sources") self._api_helpers.remote_manager.get_recipe_sources(ref, recipe_layout, remote) return True def package(self, pref: PkgReference, remote: Remote, metadata: Optional[List[str]] = None): """Download the package specified in the pref from the remote. The recipe for this package binary must already exist in the cache. If the package is already in the cache it will be skipped, but the specified metadata will be downloaded.""" output = ConanOutput() try: self._api_helpers.cache.recipe_layout(pref.ref) # raises if not found except ConanException: raise ConanException("The recipe of the specified package " "doesn't exist, download it first") skip_download = self._api_helpers.cache.exists_prev(pref) if skip_download: output.info(f"Skip package {pref.repr_notime()} download, already in cache") if metadata: self._api_helpers.remote_manager.get_package_metadata(pref, remote, metadata) return False if pref.timestamp is None: # we didn't obtain the timestamp before (in general it should be) # Respect the timestamp of the server server_pref = self._api_helpers.remote_manager.get_package_revision(pref, remote) assert server_pref == pref pref.timestamp = server_pref.timestamp output.info(f"Downloading package '{pref.repr_notime()}'") self._api_helpers.remote_manager.get_package(pref, remote, metadata) return True def download_full(self, package_list: PackagesList, remote: Remote, metadata: Optional[List[str]] = None): """Download the recipes and packages specified in the ``package_list`` from the remote, parallelized based on ``core.download:parallel``""" def _download_pkglist(pkglist): for ref, packages in pkglist.items(): self.recipe(ref, remote, metadata) ref_dict = pkglist.recipe_dict(ref) ref_dict.pop("files", None) ref_dict.pop("upload-urls", None) for pref in packages: self.package(pref, remote, metadata) pkg_dict = pkglist.package_dict(pref) pkg_dict.pop("files", None) pkg_dict.pop("upload-urls", None) t = time.time() parallel = self._api_helpers.global_conf.get("core.download:parallel", default=1, check_type=int) thread_pool = ThreadPool(parallel) if parallel > 1 else None if not thread_pool or len(package_list._data) <= 1: # FIXME: Iteration when multiple rrevs _download_pkglist(package_list) else: ConanOutput().subtitle(f"Downloading with {parallel} parallel threads") thread_pool.map(_download_pkglist, package_list.split()) if thread_pool: thread_pool.close() thread_pool.join() elapsed = time.time() - t ConanOutput().success(f"Download completed in {int(elapsed)}s\n") ================================================ FILE: conan/api/subapi/export.py ================================================ import os from typing import List, Tuple from conan import ConanFile from conan.api.output import ConanOutput from conan.cli.printers.graph import print_graph_basic from conan.internal.cache.cache import PkgCache from conan.internal.conan_app import ConanApp from conan.internal.api.export import cmd_export from conan.internal.methods import run_package_method from conan.internal.graph.graph import BINARY_BUILD, RECIPE_INCACHE from conan.api.model import PkgReference, Remote, RecipeReference from conan.internal.util.files import mkdir class ExportAPI: """ This API provides methods to export artifacts, both recipes and pre-compiled package binaries from user folders to the Conan cache, as Conan recipes and Conan package binaries """ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers def export(self, path, name: str = None, version: str = None, user: str = None, channel: str = None, lockfile=None, remotes: List[Remote] = None) -> Tuple[RecipeReference, ConanFile]: """ Exports a ``conanfile.py`` recipe, together with its associated files to the Conan cache. A "recipe-revision" will be computed and assigned. :param path: Path to the conanfile to be exported :param name: Optional package name. Typically not necessary as it is defined by the recipe attribute or dynamically with the ``set_name()`` method. If it is defined in recipe and as an argument, but they don't match, an error will be raised. :param version: Optional version. It can be defined in the recipe with the version attribute or dynamically with the 'set_version()' method. If it is defined in recipe and as an argument, but they don't match, an error will be raised. :param user: Optional user. Can be defined by recipe attribute. If it is defined in recipe and as an argument, but they don't match, an error will be raised. :param channel: Optional channel. Can be defined by recipe attribute. If it is defined in recipe and as an argument, but they don't match, an error will be raised. :param lockfile: Optional, only relevant if the recipe has 'python-requires' to be locked :param remotes: Optional, only relevant to resolve 'python-requires' in remotes :return: A tuple of the exported RecipeReference and a ConanFile object """ ConanOutput().title("Exporting recipe to the cache") app = ConanApp(self._conan_api) hook_manager = self._helpers.hook_manager return cmd_export(app.loader,self._helpers.cache, hook_manager, self._helpers.global_conf, path, name, version, user, channel, graph_lock=lockfile, remotes=remotes) def export_pkg_graph(self, path, ref: RecipeReference, profile_host, profile_build, remotes: List[Remote], lockfile=None, is_build_require=False, skip_binaries=False, output_folder=None): """Computes a dependency graph for a given configuration, for an already existing (previously exported) recipe in the Conan cache. This method computes the full dependency graph, using the profiles, lockfile and remotes information as any other install/graph/create command. This is necessary in order to compute the "package_id" of the binary being exported into the Conan cache. The resulting dependency graph can be passed to ``export_pkg()`` method :param path: Path to the conanfile.py in the user folder :param ref: full RecipeReference, including recipe-revision :param profile_host: Profile for the host context :param profile_build: Profile for the build context :param lockfile: Optional lockfile :param remotes: List of Remotes :param is_build_require: In case a package intended to be used as a tool-requires :param skip_binaries: :param output_folder: The folder containing output files, like potential environment scripts :return: A Graph object that can be passed to ``export_pkg()`` method """ assert ref.revision, "ref argument must have recipe-revision defined" conan_api = self._conan_api deps_graph = conan_api.graph.load_graph_consumer(path, ref.name, str(ref.version), ref.user, ref.channel, profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, update=None, is_build_require=is_build_require) print_graph_basic(deps_graph) deps_graph.report_graph_error() conan_api.graph.analyze_binaries(deps_graph, build_mode=[ref.name], lockfile=lockfile, remotes=remotes) deps_graph.report_graph_error() root_node = deps_graph.root root_node.ref = ref # Make sure the root node revision is well defined if not skip_binaries: # unless the user explicitly opts-out with --skip-binaries, it is necessary to install # binaries, in case there are build_requires necessary like tool-requires=cmake # and package() method doing ``cmake.install()`` # for most cases, deps will be in cache already because of a previous "conan install" # but if it is not the case, the binaries from remotes will be downloaded conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) source_folder = os.path.dirname(path) conan_api.install.install_consumer(deps_graph=deps_graph, source_folder=source_folder, output_folder=output_folder) return deps_graph def export_pkg(self, graph, output_folder=None) -> None: """Executes the ``package()`` method of the exported recipe in order to copy the artifacts from user folder to the Conan cache package folder :param graph: A Graph object :param output_folder: Optional folder where generated files like environment scripts of dependencies have been installed """ cache = PkgCache(self._conan_api.cache_folder, self._helpers.global_conf) hook_manager = self._helpers.hook_manager # The graph has to be loaded with build_mode=[ref.name], so that node is not tried # to be downloaded from remotes # passing here the create_reference=ref argument is useful so the recipe is in "develop", # because the "package()" method is in develop=True already pkg_node = graph.root ref = pkg_node.ref source_folder = os.path.dirname(pkg_node.path) out = ConanOutput(scope=pkg_node.conanfile.display_name) out.info("Exporting binary from user folder to Conan cache") conanfile = pkg_node.conanfile package_id = pkg_node.package_id assert package_id is not None out.info("Packaging to %s" % package_id) pref = PkgReference(ref, package_id) pkg_layout = cache.create_build_pkg_layout(pref) conanfile.folders.set_base_folders(source_folder, output_folder) dest_package_folder = pkg_layout.package() conanfile.folders.set_base_package(dest_package_folder) mkdir(pkg_layout.metadata()) conanfile.folders.set_base_pkg_metadata(pkg_layout.metadata()) with pkg_layout.set_dirty_context_manager(): prev = run_package_method(conanfile, package_id, hook_manager, ref) pref = PkgReference(pref.ref, pref.package_id, prev) pkg_layout.reference = pref cache.assign_prev(pkg_layout) pkg_node.prev = prev pkg_node.pref_timestamp = pref.timestamp # assigned by assign_prev pkg_node.recipe = RECIPE_INCACHE pkg_node.binary = BINARY_BUILD # Make sure folder is updated final_folder = pkg_layout.package() conanfile.folders.set_base_package(final_folder) out.info(f"Package folder {final_folder}") out.success("Exported package binary") ================================================ FILE: conan/api/subapi/graph.py ================================================ from conan.api.output import ConanOutput from conan.internal.conan_app import ConanApp from conan.internal.model.recipe_ref import ref_matches from conan.internal.graph.graph import Node, RECIPE_CONSUMER, CONTEXT_HOST, RECIPE_VIRTUAL, \ CONTEXT_BUILD, BINARY_MISSING, DepsGraph from conan.internal.graph.graph_binaries import GraphBinariesAnalyzer from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.internal.graph.install_graph import InstallGraph, ProfileArgs from conan.internal.graph.profile_node_definer import initialize_conanfile_profile, consumer_definer from conan.errors import ConanException from conan.api.model import RecipeReference class GraphAPI: def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers def _load_root_consumer_conanfile(self, path, profile_host, profile_build, name=None, version=None, user=None, channel=None, update=None, remotes=None, lockfile=None, is_build_require=False): app = ConanApp(self._conan_api) if path.endswith(".py"): conanfile = app.loader.load_consumer(path, name=name, version=version, user=user, channel=channel, graph_lock=lockfile, remotes=remotes, update=update) ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) context = CONTEXT_BUILD if is_build_require else CONTEXT_HOST # Here, it is always the "host" context because it is the base, not the current node one initialize_conanfile_profile(conanfile, profile_build, profile_host, CONTEXT_HOST, is_build_require, ref) if ref.name: profile_host.options.scope(ref) root_node = Node(ref, conanfile, context=context, recipe=RECIPE_CONSUMER, path=path) root_node.should_build = True # It is a consumer, this is something we are building else: conanfile = app.loader.load_conanfile_txt(path) consumer_definer(conanfile, profile_host, profile_build) root_node = Node(None, conanfile, context=CONTEXT_HOST, recipe=RECIPE_CONSUMER, path=path) return root_node def load_root_test_conanfile(self, path, tested_reference, profile_host, profile_build, update=None, remotes=None, lockfile=None, tested_python_requires=None): """ Create and initialize a root node from a test_package/conanfile.py consumer :param tested_python_requires: the reference of the ``python_require`` to be tested :param lockfile: Might be good to lock python-requires, build-requires :param path: The full path to the test_package/conanfile.py being used :param tested_reference: The full RecipeReference of the tested package :param profile_host: :param profile_build: :param update: :param remotes: :return: a graph Node, recipe=RECIPE_CONSUMER """ app = ConanApp(self._conan_api) # necessary for correct resolution and update of remote python_requires loader = app.loader profile_host.options.scope(tested_reference) # do not try apply lock_python_requires for test_package/conanfile.py consumer conanfile = loader.load_consumer(path, user=tested_reference.user, channel=tested_reference.channel, graph_lock=lockfile, remotes=remotes, tested_python_requires=tested_python_requires, update=update) initialize_conanfile_profile(conanfile, profile_build, profile_host, CONTEXT_HOST, False) conanfile.display_name = "%s (test package)" % str(tested_reference) conanfile.output.scope = conanfile.display_name conanfile.tested_reference_str = repr(tested_reference) ref = RecipeReference(conanfile.name, conanfile.version, tested_reference.user, tested_reference.channel) root_node = Node(ref, conanfile, recipe=RECIPE_CONSUMER, context=CONTEXT_HOST, path=path) return root_node def _load_root_virtual_conanfile(self, profile_host, profile_build, requires, tool_requires, lockfile, remotes, update, check_updates=False, python_requires=None): if not python_requires and not requires and not tool_requires: raise ConanException("Provide requires or tool_requires") app = ConanApp(self._conan_api) conanfile = app.loader.load_virtual(requires=requires, tool_requires=tool_requires, python_requires=python_requires, graph_lock=lockfile, remotes=remotes, update=update, check_updates=check_updates) consumer_definer(conanfile, profile_host, profile_build) root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, recipe=RECIPE_VIRTUAL) return root_node @staticmethod def _scope_options(profile, requires, tool_requires): """ Command line helper to scope options when ``command -o myoption=myvalue`` is used, that needs to be converted to "-o pkg:myoption=myvalue". The "pkg" value will be computed from the given requires/tool_requires This is legacy, as options should always be scoped now """ if requires and len(requires) == 1 and not tool_requires: ref = requires[0] if str(ref.version).startswith("["): ref = ref.copy() ref.version = "*" profile.options.scope(ref) def load_graph_requires(self, requires, tool_requires, profile_host, profile_build, lockfile, remotes, update, check_updates=False, python_requires=None): requires = [RecipeReference.loads(r) if isinstance(r, str) else r for r in requires] \ if requires else None tool_requires = [RecipeReference.loads(r) if isinstance(r, str) else r for r in tool_requires] if tool_requires else None self._scope_options(profile_host, requires=requires, tool_requires=tool_requires) root_node = self._load_root_virtual_conanfile(requires=requires, tool_requires=tool_requires, profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, update=update, python_requires=python_requires) if not requires and not tool_requires and python_requires is not None: # This only happens at `conan create` for python-requires, the graph is not needed # in fact, it can cause errors, if tool-requires injected dep_graph = DepsGraph() dep_graph.add_node(root_node) return dep_graph # check_updates = args.check_updates if "check_updates" in args else False deps_graph = self.load_graph(root_node, profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, update=update, check_update=check_updates) return deps_graph def load_graph_consumer(self, path, name, version, user, channel, profile_host, profile_build, lockfile, remotes, update, check_updates=False, is_build_require=False): root_node = self._load_root_consumer_conanfile(path, profile_host, profile_build, name=name, version=version, user=user, channel=channel, lockfile=lockfile, remotes=remotes, update=update, is_build_require=is_build_require) deps_graph = self.load_graph(root_node, profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, update=update, check_update=check_updates) return deps_graph def load_graph(self, root_node, profile_host, profile_build, lockfile=None, remotes=None, update=None, check_update=False): """ Compute the dependency graph, starting from a root package, evaluation the graph with the provided configuration in profile_build, and profile_host. The resulting graph is a graph of recipes, but packages are not computed yet (package_ids) will be empty in the result. The result might have errors, like version or configuration conflicts, but it is still possible to inspect it. Only trying to install such graph will fail :param root_node: the starting point, an already initialized Node structure, as returned by the "load_root_node" api :param profile_host: The host profile :param profile_build: The build profile :param lockfile: A valid lockfile (None by default, means no locked) :param remotes: list of remotes we want to check :param update: (False by default), if Conan should look for newer versions or revisions for already existing recipes in the Conan cache :param check_update: For "graph info" command, check if there are recipe updates """ ConanOutput().title("Computing dependency graph") app = ConanApp(self._conan_api) assert profile_host is not None assert profile_build is not None remotes = remotes or [] cache = self._conan_api._api_helpers.cache # noqa builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, cache, remotes, update, check_update, self._conan_api._api_helpers.global_conf) deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) return deps_graph def analyze_binaries(self, graph, build_mode=None, remotes=None, update=None, lockfile=None, build_modes_test=None, tested_graph=None): """ Given a dependency graph, will compute the package_ids of all recipes in the graph, and evaluate if they should be built from sources, downloaded from a remote server, of if the packages are already in the local Conan cache :param lockfile: :param graph: a Conan dependency graph, as returned by "load_graph()" :param build_mode: TODO: Discuss if this should be a BuildMode object or list of arguments :param remotes: list of remotes :param update: (``False`` by default), if Conan should look for newer versions or revisions for already existing recipes in the Conan cache. It also accepts an array of reference patterns to limit the update to those references if any of the items match. Eg. ``False``, ``None`` or ``[]`` *means no update*, ``True`` or ``["*"]`` *means update all*, and ``["pkgA/*", "pkgB/1.0@user/channel"]`` *means to update only specific packages*. :param build_modes_test: the --build-test argument :param tested_graph: In case of a "test_package", the graph being tested """ ConanOutput().title("Computing necessary packages") binaries_analyzer = GraphBinariesAnalyzer(self._helpers.cache, self._helpers.remote_manager, self._conan_api.home_folder, self._helpers.global_conf, self._helpers.hook_manager) binaries_analyzer.evaluate_graph(graph, build_mode, lockfile, remotes, update, build_modes_test, tested_graph) @staticmethod def find_first_missing_binary(graph, missing=None): """ (Experimental) Given a dependency graph, will return the first node with a missing binary package """ for node in graph.ordered_iterate(): if ((not missing and node.binary == BINARY_MISSING) # First missing binary or specified or (missing and ref_matches(node.ref, missing, is_consumer=None))): return node.ref, node.conanfile.info raise ConanException("There is no missing binary") @staticmethod def build_order(deps_graph, order_by="recipe", reduce=False, profile_args=None): install_graph = InstallGraph(deps_graph, order_by=order_by, profile_args=ProfileArgs.from_args(profile_args)) if reduce: if order_by is None: raise ConanException("--reduce needs --order-by argument defined") install_graph.reduce() return install_graph @staticmethod def build_order_merge(files, reduce=False): result = InstallGraph.load(files[0]) if result.reduced: raise ConanException(f"Reduced build-order file cannot be merged: {files[0]}") for f in files[1:]: install_graph = InstallGraph.load(f) if install_graph.reduced: raise ConanException(f"Reduced build-order file cannot be merged: {f}") result.merge(install_graph) if reduce: result.reduce() return result ================================================ FILE: conan/api/subapi/install.py ================================================ import os from typing import List from conan.api.model import Remote from conan.internal.api.install.generators import write_generators from conan.internal.conan_app import ConanBasicApp from conan.internal.deploy import do_deploys from conan.internal.graph.install_graph import InstallGraph from conan.internal.graph.installer import BinaryInstaller from conan.errors import ConanInvalidConfiguration, ConanException class InstallAPI: """ This is the InstallAPI. It provides methods to install binaries, sources, prepare the consumer folder with generators and deploy, etc., all of them based on an already resolved dependency graph. """ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers def install_binaries(self, deps_graph, remotes: List[Remote] = None, return_install_error=False): """ Install binaries of a dependency graph. This is the equivalent to the ``conan install`` command, but working with an already resolved dependency graph, usually obtained from the corresponding ``GraphAPI`` methods. It will download the available packages from the given remotes, and then build the ones that were marked for build from source. System requirements will be installed as well, taking into account the ``tools.system.package_manager:mode`` conf to determine whether to install, check or skip them. :param deps_graph: Dependency graph to install packages for :param remotes: List of remotes to fetch packages from if necessary. :param return_install_error: If ``True``, do not raise an exception, but return it """ app = ConanBasicApp(self._conan_api) installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, self._helpers.hook_manager) install_graph = InstallGraph(deps_graph) install_graph.raise_errors() install_order = install_graph.install_order() installer.install_system_requires(deps_graph, install_order=install_order) try: # To be able to capture the output, report or save graph.json, then raise later installer.install(deps_graph, remotes, install_order=install_order) except ConanException as e: # If true, allows to return the exception, so progress can be reported like the # already built binaries to upload them if not return_install_error: raise return e def install_system_requires(self, graph, only_info=False): """ Install only the system requirements of a dependency graph. This is a subset of ``install_binaries`` which only deals with system requirements of an already resolved dependency graph, usually obtained from the corresponding ``GraphAPI`` methods. The ``tools.system.package_manager:mode`` conf will be taken into account to determine whether to install, check or skip system requirements. :param graph: Dependency graph to install system requirements for :param only_info: If ``True``, only reporting and checking of whether the system requirements are installed is performed. """ app = ConanBasicApp(self._conan_api) installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, self._helpers.hook_manager) installer.install_system_requires(graph, only_info) def install_sources(self, graph, remotes: List[Remote]): """ Download sources in the given dependency graph. If the ``tools.build:download_source`` conf is ``True``, sources will be downloaded for every package in the graph, otherwise only the packages marked for build from source will have their sources downloaded. ``tools.build:download_source=True`` is useful when users want to inspect the source code of all dependencies, even the ones that are not built from source. After this method, the ``conanfile.source_folder`` on each node of the dependency graph for which the sources have been downloaded will be set to the folder where sources have been downloaded. :param remotes: List of remotes where the ``exports_sources`` of the packages might be located :param graph: Dependency graph to download sources from """ app = ConanBasicApp(self._conan_api) installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, self._helpers.hook_manager) installer.install_sources(graph, remotes) def install_consumer(self, deps_graph, generators: List[str] = None, source_folder=None, output_folder=None, deploy=False, deploy_package: List[str] = None, deploy_folder=None, envs_generation=None): """ Prepare the folder of the root consumer of a dependency graph after installation of the dependencies. This ensures that the requested generators are created in the consumer folder, and also handles deployment if requested. :param deps_graph: Dependency graph whose root is the consumer we want to prepare :param generators: List of generators to be used in addition to the ones defined in the root conanfile, if any :param source_folder: Source folder of the consumer :param output_folder: Output folder of the consumer :param deploy: Deployer or list of deployers to be used for deployment :param deploy_package: Only deploy the packages matching these patterns (``None`` or empty for all) :param deploy_folder: Folder where to deploy, by default the build folder :param envs_generation: Anything other than ``None`` will activate the generation of virtual environment files for the root conanfile """ root_node = deps_graph.root conanfile = root_node.conanfile if conanfile.info is not None and conanfile.info.invalid: binary, reason = "Invalid", conanfile.info.invalid msg = "{}: Invalid ID: {}: {}".format(conanfile, binary, reason) raise ConanInvalidConfiguration(msg) if conanfile.info is not None and conanfile.info.cant_build and root_node.should_build: binary, reason = "Cannot build for this configuration", conanfile.info.cant_build msg = "{}: {}: {}".format(conanfile, binary, reason) raise ConanInvalidConfiguration(msg) conanfile.folders.set_base_folders(source_folder, output_folder) # The previous .set_base_folders has already decided between the source_folder and output if deploy or deploy_package: # Issue related: https://github.com/conan-io/conan/issues/16543 base_folder = os.path.abspath(deploy_folder) if deploy_folder \ else conanfile.folders.base_build do_deploys(self._conan_api.home_folder, deps_graph, deploy, deploy_package, base_folder) final_generators = [] # Don't use set for uniqueness because order matters for gen in conanfile.generators: if gen not in final_generators: final_generators.append(gen) for gen in (generators or []): if gen not in final_generators: final_generators.append(gen) conanfile.generators = final_generators hook_manager = self._helpers.hook_manager write_generators(conanfile, hook_manager, self._conan_api.home_folder, envs_generation=envs_generation) def deploy(self, graph, deployer: List[str], deploy_package: List[str]=None, deploy_folder=None) -> None: """ Run the given deployer in the dependency graph. No checks are performed in the graph, it is assumed to be already resolved and in a valid state to be deployed from. :param graph: The dependency graph to deploy :param deployer: List of deployers to be used :param deploy_package: Only deploy the packages matching these patterns (``None`` or empty for all) :param deploy_folder: Folder where to deploy, by default the build folder """ return do_deploys(self._conan_api.home_folder, graph, deployer, deploy_package=deploy_package, deploy_folder=deploy_folder) ================================================ FILE: conan/api/subapi/list.py ================================================ import os from collections import OrderedDict from typing import Dict from conan.api.model import PackagesList, MultiPackagesList, ListPattern, Remote from conan.api.output import ConanOutput, TimedOutput from conan.internal.api.list.query_parse import filter_package_configs from conan.internal.model.recipe_ref import ref_matches from conan.internal.paths import CONANINFO from conan.internal.errors import NotFoundException from conan.errors import ConanException from conan.internal.model.info import load_binary_info from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.dates import timestamp_now from conan.internal.util.files import load def _timelimit(expression): """ convert an expression like "2d" (2 days) or "3h" (3 hours) to a timestamp in the past with respect to current time """ time_value = expression[:-1] try: time_value = int(time_value) except TypeError: raise ConanException(f"Time value '{time_value}' must be an integer") time_units = expression[-1] units = {"y": 365 * 24 * 60 * 60, "M": 30 * 24 * 60 * 60, "w": 7 * 24 * 60 * 60, "d": 24 * 60 * 60, "h": 60 * 60, "m": 60, "s": 1} try: lru_value = time_value * units[time_units] except KeyError: raise ConanException(f"Unrecognized time unit: '{time_units}'. Use: {list(units)}") limit = timestamp_now() - lru_value return limit class ListAPI: """ Get references from the recipes and packages in the cache or a remote """ def __init__(self, conan_api, api_helpers): self._conan_api = conan_api self._api_helpers = api_helpers def latest_recipe_revision(self, ref: RecipeReference, remote: Remote = None): """ For a given recipe reference, return the latest revision of the recipe in the remote, or in the local cache if no remote is specified, or ``None`` if the recipe does not exist.""" assert ref.revision is None, "latest_recipe_revision: ref already have a revision" if remote: ret = self._api_helpers.remote_manager.get_latest_recipe_revision(ref, remote=remote) else: ret = self._api_helpers.cache.get_latest_recipe_revision(ref) return ret def recipe_revisions(self, ref: RecipeReference, remote: Remote = None): """ For a given recipe reference, return all the revisions of the recipe in the remote, or in the local cache if no remote is specified""" assert ref.revision is None, "recipe_revisions: ref already have a revision" if remote: results = self._api_helpers.remote_manager.get_recipe_revisions(ref, remote=remote) else: results = self._api_helpers.cache.get_recipe_revisions(ref) return results def latest_package_revision(self, pref: PkgReference, remote=None): # TODO: This returns None if the given package_id is not existing. It should probably # raise NotFound, but to keep aligned with the above ``latest_recipe_revision`` which # is used as an "exists" check too in other places, lets respect the None return assert pref.revision is None, "latest_package_revision: ref already have a revision" assert pref.package_id is not None, "package_id must be defined" if remote: ret = self._api_helpers.remote_manager.get_latest_package_revision(pref, remote=remote) else: ret = self._api_helpers.cache.get_latest_package_revision(pref) return ret def package_revisions(self, pref: PkgReference, remote=None): assert pref.ref.revision is not None, "package_revisions requires a recipe revision, " \ "check latest first if needed" if remote: results = self._api_helpers.remote_manager.get_package_revisions(pref, remote=remote) else: results = self._api_helpers.cache.get_package_revisions(pref) return results def _packages_configurations(self, ref: RecipeReference, remote=None) -> Dict[PkgReference, dict]: assert ref.revision is not None and ref.revision != "latest", \ "packages: ref should have a revision. Check latest if needed." if not remote: prefs = self._api_helpers.cache.get_package_references(ref) packages = _get_cache_packages_binary_info(self._api_helpers.cache, prefs) else: packages = self._api_helpers.remote_manager.search_packages(remote, ref) return packages @staticmethod def _filter_packages_configurations(pkg_configurations, query): """ :param pkg_configurations: Dict[PkgReference, PkgConfiguration] :param query: str like "os=Windows AND (arch=x86 OR compiler=gcc)" :return: Dict[PkgReference, PkgConfiguration] """ try: if "!" in query: raise ConanException("'!' character is not allowed") if "~" in query: raise ConanException("'~' character is not allowed") if " not " in query or query.startswith("not "): raise ConanException("'not' operator is not allowed") return filter_package_configs(pkg_configurations, query) except Exception as exc: raise ConanException("Invalid package query: %s. %s" % (query, exc)) @staticmethod def _filter_packages_profile(packages, profile, ref): result = {} profile_settings = profile.processed_settings.serialize() # Options are those for dependencies, like *:shared=True profile_options = profile.options._deps_package_options for pref, data in packages.items(): settings = data.get("settings", {}) settings_match = options_match = True for k, v in settings.items(): # Only the defined settings that don't match value = profile_settings.get(k) if value is not None and value != v: settings_match = False break options = data.get("options", {}) for k, v in options.items(): for pattern, pattern_options in profile_options.items(): # Accept &: as referring to the current package being listed, # even if it's not technically a "consumer" if ref_matches(ref, pattern, True): value = pattern_options.get_safe(k) if value is not None and value != v: options_match = False break if settings_match and options_match: result[pref] = data return result def select(self, pattern: ListPattern, package_query=None, remote: Remote = None, lru=None, profile=None) -> PackagesList: """For a given pattern, return a list of recipes and packages matching the provided filters. :parameter ListPattern pattern: Search criteria :parameter str package_query: When returning packages, expression of the form ``"os=Windows AND (arch=x86 OR compiler=gcc)"`` to filter packages by. If ``None``, all packages will be returned if requested. :parameter Remote remote: Remote to search in, if ``None``, it will search in the local cache. :parameter str lru: If set, it will filter the results to only include packages/binaries that have been used in the last 'lru' time. It can be a string like ``"2d"`` (2 days) or ``"3h"`` (3 hours). :parameter Profile profile: Profile to filter the packages by settings and options. """ # TODO: Implement better error forwarding for "list" command that captures Exceptions if package_query and pattern.package_id and "*" not in pattern.package_id: raise ConanException("Cannot specify '-p' package queries, " "if 'package_id' is not a pattern") if remote and lru: raise ConanException("'--lru' cannot be used in remotes, only in cache") select_bundle = PackagesList() # Avoid doing a ``search`` of recipes if it is an exact ref and it will be used later search_ref = pattern.search_ref cache = self._api_helpers.cache limit_time = _timelimit(lru) if lru else None out = ConanOutput() remote_name = "local cache" if not remote else remote.name if search_ref: refs = _search_recipes(self._api_helpers, search_ref, remote=remote) global_conf = self._api_helpers.global_conf # noqa resolve_prereleases = global_conf.get("core.version_ranges:resolve_prereleases") refs = pattern.filter_versions(refs, resolve_prereleases) pattern.check_refs(refs) out.info(f"Found {len(refs)} pkg/version recipes matching {search_ref} in {remote_name}") else: refs = [RecipeReference(pattern.name, pattern.version, pattern.user, pattern.channel)] # Show only the recipe references if pattern.package_id is None and pattern.rrev is None: for r in refs: select_bundle.add_ref(r) return select_bundle def msg_format(msg, item, total): return msg + f" ({total.index(item)}/{len(total)})" trefs = TimedOutput(5, msg_format=msg_format) for r in refs: # Older versions first trefs.info(f"Listing revisions of {r} in {remote_name}", r, refs) if pattern.is_latest_rrev or pattern.rrev is None: rrev = self.latest_recipe_revision(r, remote) if rrev is None: raise NotFoundException(f"Recipe '{r}' not found") rrevs = [rrev] else: rrevs = self.recipe_revisions(r, remote) rrevs = pattern.filter_rrevs(rrevs) rrevs = list(reversed(rrevs)) # Order older revisions first if lru and pattern.package_id is None: # Filter LRUs rrevs = [r for r in rrevs if cache.get_recipe_lru(r) < limit_time] for rr in rrevs: select_bundle.add_ref(rr) if pattern.package_id is None: # Stop if not displaying binaries continue trrevs = TimedOutput(5, msg_format=msg_format) for rrev in rrevs: trrevs.info(f"Listing binaries of {rrev.repr_notime()} in {remote_name}", rrev, rrevs) prefs = [] if "*" not in pattern.package_id and pattern.prev is not None: prefs.append(PkgReference(rrev, package_id=pattern.package_id)) packages = {} else: packages = self._packages_configurations(rrev, remote) if package_query is not None: packages = self._filter_packages_configurations(packages, package_query) if profile is not None: packages = self._filter_packages_profile(packages, profile, rrev) prefs = packages.keys() prefs = pattern.filter_prefs(prefs) packages = {pref: conf for pref, conf in packages.items() if pref in prefs} if pattern.prev is not None: new_prefs = [] for pref in prefs: # Maybe the package_configurations returned timestamp if pattern.is_latest_prev or pattern.prev is None: prev = self.latest_package_revision(pref, remote) if prev is None: raise NotFoundException(f"Binary package not found: '{pref}") new_prefs.append(prev) else: prevs = self.package_revisions(pref, remote) prevs = pattern.filter_prevs(prevs) prevs = list(reversed(prevs)) # Older revisions first new_prefs.extend(prevs) prefs = new_prefs if lru: # Filter LRUs prefs = [r for r in prefs if cache.get_package_lru(r) < limit_time] # Packages dict has been listed, even if empty select_bundle.recipe_dict(rrev)["packages"] = {} for p in prefs: # the "packages" dict is not using the package-revision pkg_info = packages.get(PkgReference(p.ref, p.package_id)) select_bundle.add_pref(p, pkg_info) return select_bundle def explain_missing_binaries(self, ref, conaninfo, remotes): """ (Experimental) Explain why a binary is missing in the cache """ ConanOutput().info(f"Missing binary: {ref}") ConanOutput().info(f"With conaninfo.txt (package_id):\n{conaninfo.dumps()}") conaninfo = load_binary_info(conaninfo.dumps()) # Collect all configurations candidates = [] ConanOutput().info(f"Finding binaries in the cache") pkg_configurations = self._packages_configurations(ref) candidates.extend(_BinaryDistance(pref, data, conaninfo) for pref, data in pkg_configurations.items()) for remote in remotes: try: ConanOutput().info(f"Finding binaries in remote {remote.name}") pkg_configurations = self._packages_configurations(ref, remote=remote) except Exception as e: ConanOutput().error(f"Error in remote '{remote.name}': {e}") else: candidates.extend(_BinaryDistance(pref, data, conaninfo, remote) for pref, data in pkg_configurations.items()) candidates.sort() pkglist = PackagesList() pkglist.add_ref(ref) # Return the closest matches, stop adding when distance is increased candidate_distance = None for candidate in candidates: if candidate_distance and candidate.distance != candidate_distance: break candidate_distance = candidate.distance pref = candidate.pref pkglist.add_pref(pref, candidate.binary_config) # Add the diff data rev_dict = pkglist.recipe_dict(ref) rev_dict["packages"][pref.package_id]["diff"] = candidate.serialize() remote = candidate.remote.name if candidate.remote else "Local Cache" rev_dict["packages"][pref.package_id]["remote"] = remote return pkglist def find_remotes(self, package_list, remotes): """ (Experimental) Find the remotes where the current package lists can be found """ result = MultiPackagesList() remote_manager = self._api_helpers.remote_manager for r in remotes: result_pkg_list = PackagesList() for ref, ref_contents in package_list.serialize().items(): ref = RecipeReference.loads(ref) try: remote_rrevs = remote_manager.get_recipe_revisions(ref, remote=r) except NotFoundException: continue revisions = ref_contents.get("revisions") if revisions is None: # This is a package list just with name/version if remote_rrevs: result_pkg_list.add_ref(ref) continue for revision, rev_content in revisions.items(): ref.revision = revision # We look for the value of revision in server, to return timestamp too found = next((r for r in remote_rrevs if r == ref), None) if not found: continue result_pkg_list.add_ref(found) packages = rev_content.get("packages") if packages is None: continue for pkgid, pkgcontent in packages.items(): pref = PkgReference(ref, pkgid) try: remote_prefs = remote_manager.get_package_revisions(pref, remote=r) except NotFoundException: continue pkg_revisions = pkgcontent.get("revisions") if pkg_revisions is None: # This is a package_id, no prevs if remote_prefs: result_pkg_list.add_pref(pref, pkgcontent.get("info")) continue for pkg_revision, prev_content in pkg_revisions.items(): pref.revision = pkg_revision # We look for the value of revision in server, to return timestamp too pfound = next((r for r in remote_prefs if r == pref), None) if not pfound: continue result_pkg_list.add_pref(pfound, pkgcontent.get("info")) if result_pkg_list: result.add(r.name, result_pkg_list) return result def outdated(self, deps_graph, remotes): # DO NOT USE YET # Data structure to store info per library dependencies = deps_graph.nodes[1:] dict_nodes = {} # When there are no dependencies command should stop if len(dependencies) == 0: return dict_nodes ConanOutput().title("Checking remotes") for node in dependencies: dict_nodes.setdefault(node.name, {"cache_refs": set(), "version_ranges": [], "latest_remote": None})["cache_refs"].add(node.ref) for version_range in deps_graph.resolved_ranges.keys(): dict_nodes[version_range.name]["version_ranges"].append(version_range) # find in remotes for node_name, node_info in dict_nodes.items(): ref_pattern = ListPattern(node_name, rrev=None, prev=None) for remote in remotes: try: remote_ref_list = self.select(ref_pattern, package_query=None, remote=remote) except NotFoundException: continue if not remote_ref_list: continue str_latest_ref = list(remote_ref_list.serialize().keys())[-1] recipe_ref = RecipeReference.loads(str_latest_ref) if (node_info["latest_remote"] is None or node_info["latest_remote"]["ref"] < recipe_ref): node_info["latest_remote"] = {"ref": recipe_ref, "remote": remote.name} # Filter nodes with no outdated versions filtered_nodes = {} for node_name, node in dict_nodes.items(): if node['latest_remote'] is not None and sorted(list(node['cache_refs']))[0] < \ node['latest_remote']['ref']: filtered_nodes[node_name] = node return filtered_nodes class _BinaryDistance: def __init__(self, pref, binary, expected, remote=None): self.remote = remote self.pref = pref self.binary_config = binary # Settings, special handling for os/arch binary_settings = binary.get("settings", {}) expected_settings = expected.get("settings", {}) platform = {k: v for k, v in binary_settings.items() if k in ("os", "arch")} expected_platform = {k: v for k, v in expected_settings.items() if k in ("os", "arch")} self.platform_diff = self._calculate_diff(platform, expected_platform) binary_settings = {k: v for k, v in binary_settings.items() if k not in ("os", "arch")} expected_settings = {k: v for k, v in expected_settings.items() if k not in ("os", "arch")} self.settings_diff = self._calculate_diff(binary_settings, expected_settings) self.settings_target_diff = self._calculate_diff(binary, expected, "settings_target") self.options_diff = self._calculate_diff(binary, expected, "options") self.deps_diff = self._requirement_diff(binary, expected, "requires") self.build_requires_diff = self._requirement_diff(binary, expected, "build_requires") self.python_requires_diff = self._requirement_diff(binary, expected, "python_requires") self.confs_diff = self._calculate_diff(binary, expected, "conf") @staticmethod def _requirement_diff(binary_requires, expected_requires, item): binary_requires = binary_requires.get(item, {}) expected_requires = expected_requires.get(item, {}) output = {} binary_requires = [RecipeReference.loads(r) for r in binary_requires] expected_requires = [RecipeReference.loads(r) for r in expected_requires] binary_requires = {r.name: r for r in binary_requires} for r in expected_requires: existing = binary_requires.get(r.name) if not existing or r != existing: output.setdefault("expected", []).append(repr(r)) output.setdefault("existing", []).append(repr(existing)) expected_requires = {r.name: r for r in expected_requires} for r in binary_requires.values(): existing = expected_requires.get(r.name) if not existing or r != existing: if repr(existing) not in output.get("expected", ()): output.setdefault("expected", []).append(repr(existing)) if repr(r) not in output.get("existing", ()): output.setdefault("existing", []).append(repr(r)) return output @staticmethod def _calculate_diff(binary_confs, expected_confs, item=None): if item is not None: binary_confs = binary_confs.get(item, {}) expected_confs = expected_confs.get(item, {}) output = {} for k, v in expected_confs.items(): value = binary_confs.get(k) if value != v: output.setdefault("expected", []).append(f"{k}={v}") output.setdefault("existing", []).append(f"{k}={value}") for k, v in binary_confs.items(): value = expected_confs.get(k) if value != v: if f"{k}={value}" not in output.get("expected", ()): output.setdefault("expected", []).append(f"{k}={value}") if f"{k}={v}" not in output.get("existing", ()): output.setdefault("existing", []).append(f"{k}={v}") return output def __lt__(self, other): return self.distance < other.distance def explanation(self): if self.platform_diff: return "This binary belongs to another OS or Architecture, highly incompatible." if self.settings_diff: return "This binary was built with different settings." if self.settings_target_diff: return "This binary was built with different settings_target." if self.options_diff: return "This binary was built with the same settings, but different options" if self.deps_diff: return "This binary has same settings and options, but different dependencies" if self.build_requires_diff: return "This binary has same settings, options and dependencies, but different build_requires" if self.python_requires_diff: return "This binary has same settings, options and dependencies, but different python_requires" if self.confs_diff: return "This binary has same settings, options and dependencies, but different confs" return "This binary is an exact match for the defined inputs" @property def distance(self): return (len(self.platform_diff.get("expected", [])), len(self.settings_diff.get("expected", [])), len(self.settings_target_diff.get("expected", [])), len(self.options_diff.get("expected", [])), len(self.deps_diff.get("expected", [])), len(self.build_requires_diff.get("expected", [])), len(self.python_requires_diff.get("expected", [])), len(self.confs_diff.get("expected", []))) def serialize(self): return {"platform": self.platform_diff, "settings": self.settings_diff, "settings_target": self.settings_target_diff, "options": self.options_diff, "dependencies": self.deps_diff, "build_requires": self.build_requires_diff, "python_requires": self.python_requires_diff, "confs": self.confs_diff, "explanation": self.explanation()} def _get_cache_packages_binary_info(cache, prefs) -> Dict[PkgReference, dict]: """ param package_layout: Layout for the given reference """ result = OrderedDict() for pref in prefs: latest_prev = cache.get_latest_package_revision(pref) pkg_layout = cache.pkg_layout(latest_prev) # Read conaninfo info_path = os.path.join(pkg_layout.package(), CONANINFO) if not os.path.exists(info_path): ConanOutput().error(f"Corrupted package '{pkg_layout.reference}' " f"without conaninfo.txt in: {info_path}") info = {} else: conan_info_content = load(info_path) info = load_binary_info(conan_info_content) pref = pkg_layout.reference # The key shoudln't have the latest package revision, we are asking for package configs pref.revision = None result[pkg_layout.reference] = info return result def _search_recipes(api_helpers, query: str, remote=None): only_none_user_channel = False if query and query.endswith("@"): only_none_user_channel = True query = query[:-1] if remote: refs = api_helpers.remote_manager.search_recipes(remote, query) else: refs = api_helpers.cache.search_recipes(query) ret = [] for r in refs: if not only_none_user_channel or (r.user is None and r.channel is None): ret.append(r) return sorted(ret) ================================================ FILE: conan/api/subapi/local.py ================================================ import os from typing import List from conan.cli import make_abs_path from conan.internal.conan_app import ConanApp from conan.internal.api.local.editable import EditablePackages from conan.internal.methods import run_build_method, run_source_method from conan.internal.graph.graph import CONTEXT_HOST from conan.internal.graph.profile_node_definer import initialize_conanfile_profile from conan.internal.errors import conanfile_exception_formatter from conan.errors import ConanException from conan.api.model import RecipeReference, Remote from conan.internal.util.files import chdir class LocalAPI: """ This ``LocalAPI`` contains several helpers related to the local development flow, i.e., locally calling ``source()`` or ``build()`` methods, or adding and removing editable packages """ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers self.editable_packages = EditablePackages(conan_api.home_folder) @staticmethod def get_conanfile_path(path, cwd, py): """ Obtain the full path to a conanfile file, either .txt or .py, from the current working directory. If both ``conanfile.py`` and a ``conanfile.txt`` are present, it will raise an error. :param path: Relative path to look for the file. Can be a folder or a file. :param cwd: The current working directory. :param py: If True, a conanfile.py must exist, a .txt is not valid in this case """ path = make_abs_path(path, cwd) if os.path.isdir(path): # Can be a folder path_py = os.path.join(path, "conanfile.py") if py: path = path_py else: path_txt = os.path.join(path, "conanfile.txt") if os.path.isfile(path_py) and os.path.isfile(path_txt): raise ConanException("Ambiguous command, both conanfile.py and " "conanfile.txt exist") path = path_py if os.path.isfile(path_py) else path_txt if not os.path.isfile(path): # Must exist raise ConanException("Conanfile not found at %s" % path) if py and not path.endswith(".py"): raise ConanException("A conanfile.py is needed, " + path + " is not acceptable") return path def editable_add(self, path, name=None, version=None, user=None, channel=None, cwd=None, output_folder=None, remotes: List[Remote] = None) -> RecipeReference: """ Add the conanfile in the given path as an editable package Note that for automation over editables it might be recommended to use the ``WorkspacesAPI`` instead of this API. :param path: Relative path to look for it. Can be a folder or a file. :param name: The name of the package. If not defined, it is taken from conanfile :param version: The version of the package. If not defined, it is taken from conanfile :param user: The user of the package. If not defined, it is taken from conanfile :param channel: The channel of the package. If not defined, it is taken from conanfile :param cwd: The current working directory :param output_folder: The output folder. If not defined, the recipe layout will be used. :param remotes: The remotes to resolve possible ``python-requires`` for this recipe if needed. :return: RecipeReference of the added package """ path = self.get_conanfile_path(path, cwd, py=True) app = ConanApp(self._conan_api) conanfile = app.loader.load_named(path, name, version, user, channel, remotes=remotes) if conanfile.name is None or conanfile.version is None: raise ConanException("Editable package recipe should declare its name and version") ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) # Retrieve conanfile.py from target_path target_path = self.get_conanfile_path(path=path, cwd=cwd, py=True) output_folder = make_abs_path(output_folder) if output_folder else None # Check the conanfile is there, and name/version matches self.editable_packages.add(ref, target_path, output_folder=output_folder) return ref def editable_remove(self, path=None, requires=None, cwd=None): """ Remove an editable package from the given path Note that for automation over editables it might be recommended to use the ``WorkspacesAPI`` instead of this API. :param path: Relative path to look for it. Can be a folder or a file. :param requires: Remove these requirements from editables (instead of by path) :param cwd: The current working directory :return: RecipeReference of the added package """ if path: path = make_abs_path(path, cwd) path = os.path.join(path, "conanfile.py") return self.editable_packages.remove(path, requires) def editable_list(self): return self.editable_packages.edited_refs def source(self, path, name=None, version=None, user=None, channel=None, remotes: List[Remote] = None): """ Calls the ``source()`` method of the current (user folder) ``conanfile.py`` This method does not require computing a dependency graph, because the ``source()`` method is assumed to be invariant with respect to settings, options and dependencies. :param path: Relative path to look for the conanfile. Can be a folder or a file. :param name: The name of the package. If not defined, it is taken from conanfile :param version: The version of the package. If not defined, it is taken from conanfile :param user: The user of the package. If not defined, it is taken from conanfile :param channel: The channel of the package. If not defined, it is taken from conanfile :param remotes: The remotes to resolve possible ``python-requires`` for this recipe if needed. """ app = ConanApp(self._conan_api) conanfile = app.loader.load_consumer(path, name=name, version=version, user=user, channel=channel, graph_lock=None, remotes=remotes) # This profile is empty, but with the conf from global.conf profile = self._conan_api.profiles.get_profile([]) initialize_conanfile_profile(conanfile, profile, profile, CONTEXT_HOST, False) # This is important, otherwise the ``conan source`` doesn't define layout and fails if hasattr(conanfile, "layout"): with conanfile_exception_formatter(conanfile, "layout"): conanfile.layout() folder = conanfile.recipe_folder if conanfile.folders.root is None else \ os.path.normpath(os.path.join(conanfile.recipe_folder, conanfile.folders.root)) conanfile.folders.set_base_source(folder) conanfile.folders.set_base_export_sources(folder) conanfile.folders.set_base_recipe_metadata(os.path.join(folder, "metadata")) # The generators are needed for the "conan source" local case with tool-requires conanfile.folders.set_base_generators(folder) conanfile.folders.set_base_build(None) conanfile.folders.set_base_package(None) hook_manager = self._helpers.hook_manager run_source_method(conanfile, hook_manager) def build(self, conanfile) -> None: """ Calls the ``build()`` method of the current (user folder) ``conanfile.py`` This method does require computing a dependency graph, because the ``build()`` method needs all dependencies and transitive dependencies. Then, the ``conanfile`` argument must be the one obtaind from a full dependency graph install operation, including both the graph comptutation and the binary installation. :param conanfile: ``Conanfile`` object representing the "root" node in the dependency graph, corresponding to a ``conanfile.py`` in the user folder, containing the ``build()`` method to be called. This ``conanfile`` object must have all of its dependencies computed and installed in the current Conan package cache to work. """ hook_manager = self._helpers.hook_manager conanfile.folders.set_base_package(conanfile.folders.base_build) conanfile.folders.set_base_pkg_metadata(os.path.join(conanfile.build_folder, "metadata")) run_build_method(conanfile, hook_manager) @staticmethod def test(conanfile) -> None: """ Calls the ``test()`` method of the current (user folder) ``test_package/conanfile.py`` This method does require computing a dependency graph, because the ``test()`` method needs all dependencies and transitive dependencies. Then, the ``conanfile`` argument must be the one obtaind from a full dependency graph install operation, including both the graph comptutation and the binary installation. Typically called after a ``build()`` one. :param conanfile: ``Conanfile`` object representing the "root" node in the dependency graph, corresponding to a conanfile.py in the user "test_package" folder, containing the ``test()`` method to be called. This ``conanfile`` object must have all of its dependencies computed and installed in the current Conan package cache to work. """ with conanfile_exception_formatter(conanfile, "test"): with chdir(conanfile.build_folder): conanfile.test() def inspect(self, conanfile_path, remotes, lockfile, name=None, version=None, user=None, channel=None): app = ConanApp(self._conan_api) conanfile = app.loader.load_named(conanfile_path, name=name, version=version, user=user, channel=channel, remotes=remotes, graph_lock=lockfile) return conanfile def reinit(self): self.editable_packages = EditablePackages(self._conan_api.home_folder) ================================================ FILE: conan/api/subapi/lockfile.py ================================================ import os from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.internal.graph.graph import Overrides from conan.errors import ConanException from conan.internal.model.lockfile import Lockfile, LOCKFILE class LockfileAPI: def __init__(self, conan_api): self._conan_api = conan_api @staticmethod def get_lockfile(lockfile=None, conanfile_path=None, cwd=None, partial=False, overrides=None) -> Lockfile: """ obtain a lockfile, following this logic: If lockfile is explicitly defined, it would be either absolute or relative to cwd and the lockfile file must exist. If lockfile="" (empty string) the default "conan.lock" lockfile will not be automatically used even if it is present. If lockfile is not defined, it will still look for a default conan.lock: - if conanfile_path is defined, it will be besides it - if conanfile_path is not defined, the default conan.lock should be in cwd - if the default conan.lock cannot be found, it is not an error :param partial: If the obtained lockfile will allow partial resolving :param cwd: the current working dir, if None, os.getcwd() will be used :param conanfile_path: The full path to the conanfile, if existing :param lockfile: the name of the lockfile file :param overrides: Dictionary of overrides {overriden: [new_ref1, new_ref2]} """ if lockfile == "": # Allow a way with ``--lockfile=""`` to optout automatic usage of conan.lock return cwd = cwd or os.getcwd() if lockfile is None: # Look for a default "conan.lock" # if path is defined, take it as reference base_path = os.path.dirname(conanfile_path) if conanfile_path else cwd lockfile_path = make_abs_path(LOCKFILE, base_path) if not os.path.isfile(lockfile_path): if overrides: raise ConanException("Cannot define overrides without a lockfile") return else: # explicit lockfile given lockfile_path = make_abs_path(lockfile, cwd) if not os.path.isfile(lockfile_path): raise ConanException("Lockfile doesn't exist: {}".format(lockfile_path)) graph_lock = Lockfile.load(lockfile_path) graph_lock.partial = partial if overrides: graph_lock._overrides = Overrides.deserialize(overrides) ConanOutput().info("Using lockfile: '{}'".format(lockfile_path)) return graph_lock def update_lockfile_export(self, lockfile, conanfile, ref, is_build_require=False): # The package_type is not fully processed at export is_python_require = conanfile.package_type == "python-require" is_require = not is_python_require and not is_build_require if hasattr(conanfile, "python_requires"): python_requires = conanfile.python_requires.all_refs() else: python_requires = [] python_requires = python_requires + ([ref] if is_python_require else []) new_lock = self.add_lockfile(lockfile, requires=[ref] if is_require else None, python_requires=python_requires, build_requires=[ref] if is_build_require else None) if lockfile is None: # If there was no lockfile, it is a partial one to lock export new_lock.partial = True return new_lock @staticmethod def update_lockfile(lockfile, graph, lock_packages=False, clean=False): if lockfile is None or clean: lockfile = Lockfile(graph, lock_packages) else: lockfile.update_lock(graph, lock_packages) return lockfile @staticmethod def merge_lockfiles(lockfiles): result = Lockfile() for lockfile in lockfiles: lockfile = make_abs_path(lockfile) graph_lock = Lockfile.load(lockfile) result.merge(graph_lock) return result @staticmethod def add_lockfile(lockfile=None, requires=None, build_requires=None, python_requires=None, config_requires=None): if lockfile is None: lockfile = Lockfile() # create a new lockfile lockfile.add(requires=requires, build_requires=build_requires, python_requires=python_requires, config_requires=config_requires) return lockfile @staticmethod def remove_lockfile(lockfile, requires=None, build_requires=None, python_requires=None, config_requires=None): lockfile.remove(requires=requires, build_requires=build_requires, python_requires=python_requires, config_requires=config_requires) return lockfile @staticmethod def save_lockfile(lockfile, lockfile_out, path=None): if lockfile_out is not None: lockfile_out = make_abs_path(lockfile_out, path) lockfile.save(lockfile_out) ConanOutput().info(f"Generated lockfile: {lockfile_out}") ================================================ FILE: conan/api/subapi/new.py ================================================ import fnmatch import os import shutil from jinja2 import Template, StrictUndefined, UndefinedError, Environment, meta from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.util.files import load, save from conan import __version__ class NewAPI: _NOT_TEMPLATES = "not_templates" # Filename containing filenames of files not to be rendered def __init__(self, conan_api): self._conan_api = conan_api def save_template(self, template, defines=None, output_folder=None, force=False): """ Save the 'template' files in the output_folder, replacing the template variables with the 'defines' :param template: The name of the template to use :param defines: A list with the 'k=v' variables to replace in the template :param output_folder: The folder where the template files will be saved, cwd if None :param force: If True, overwrite the files if they already exist, otherwise raise an error """ # Manually parsing the remainder definitions = {} for u in defines or []: try: k, v = u.split("=", 1) except ValueError: raise ConanException(f"Template definitions must be 'key=value', received '{u}'") k = k.replace("-", "") # Remove possible "--name=value" # For variables that only show up once, no need for list to keep compatible behaviour if k in definitions: if isinstance(definitions[k], list): definitions[k].append(v) else: definitions[k] = [definitions[k], v] else: definitions[k] = v files = self.get_template(template) # First priority: user folder is_builtin = False if not files: # then, try the templates in the Conan home files = self.get_home_template(template) if files: template_files, non_template_files = files else: template_files = self.get_builtin_template(template) non_template_files = {} is_builtin = True if not template_files and not non_template_files: raise ConanException(f"Template doesn't exist or not a folder: {template}") if is_builtin and template == "workspace": # hardcoded for the workspace special case definitions["name"] = "liba" template_files = self.render(template_files, definitions) # Saving the resulting files output = ConanOutput() output_folder = output_folder or os.getcwd() # Making sure they don't overwrite existing files for f, v in sorted(template_files.items()): path = os.path.join(output_folder, f) if os.path.exists(path) and not force: raise ConanException(f"File '{f}' already exists, and --force not defined, aborting") save(path, v) output.success("File saved: %s" % f) # copy non-templates for f, v in sorted(non_template_files.items()): path = os.path.join(output_folder, f) if os.path.exists(path) and not force: raise ConanException(f"File '{f}' already exists, and --force not defined, aborting") shutil.copy2(v, path) output.success("File saved: %s" % f) @staticmethod def get_builtin_template(template_name): from conan.internal.api.new.basic import basic_file, basic_default_file from conan.internal.api.new.alias_new import alias_file from conan.internal.api.new.cmake_exe import cmake_exe_files from conan.internal.api.new.cmake_lib import cmake_lib_files from conan.internal.api.new.header_lib import header_only_lib_files from conan.internal.api.new.meson_lib import meson_lib_files from conan.internal.api.new.meson_exe import meson_exe_files from conan.internal.api.new.msbuild_lib import msbuild_lib_files from conan.internal.api.new.msbuild_exe import msbuild_exe_files from conan.internal.api.new.bazel_lib import bazel_lib_files from conan.internal.api.new.bazel_exe import bazel_exe_files from conan.internal.api.new.bazel_7_lib import bazel_lib_files_7 from conan.internal.api.new.bazel_7_exe import bazel_exe_files_7 from conan.internal.api.new.autotools_lib import autotools_lib_files from conan.internal.api.new.autoools_exe import autotools_exe_files from conan.internal.api.new.premake_lib import premake_lib_files from conan.internal.api.new.premake_exe import premake_exe_files from conan.internal.api.new.local_recipes_index import local_recipes_index_files from conan.internal.api.new.qbs_lib import qbs_lib_files from conan.internal.api.new.workspace import workspace_files if not template_name: return basic_default_file new_templates = {"basic": basic_file, "cmake_lib": cmake_lib_files, "cmake_exe": cmake_exe_files, "header_lib": header_only_lib_files, "meson_lib": meson_lib_files, "meson_exe": meson_exe_files, "msbuild_lib": msbuild_lib_files, "msbuild_exe": msbuild_exe_files, # TODO: Rename xxx_7 to xxx when dropped Bazel 6.x compatibility "bazel_lib": bazel_lib_files, "bazel_exe": bazel_exe_files, "bazel_7_lib": bazel_lib_files_7, "bazel_7_exe": bazel_exe_files_7, "autotools_lib": autotools_lib_files, "autotools_exe": autotools_exe_files, "premake_lib": premake_lib_files, "premake_exe": premake_exe_files, "alias": alias_file, "local_recipes_index": local_recipes_index_files, "qbs_lib": qbs_lib_files, "workspace": workspace_files} template_files = new_templates.get(template_name) return template_files def get_template(self, template_folder): """ Load a template from a user absolute folder """ if os.path.isdir(template_folder): return self._read_files(template_folder) def get_home_template(self, template_name): """ Load a template from the Conan home templates/command/new folder """ folder_template = os.path.join(self._conan_api.home_folder, "templates", "command/new", template_name) if os.path.isdir(folder_template): return self._read_files(folder_template) def _read_files(self, template_folder): template_files, non_template_files = {}, {} excluded = os.path.join(template_folder, self._NOT_TEMPLATES) if os.path.exists(excluded): excluded = load(excluded) excluded = [] if not excluded else [s.strip() for s in excluded.splitlines() if s.strip()] else: excluded = [] for d, _, fs in os.walk(template_folder): for f in fs: if f == self._NOT_TEMPLATES: continue rel_d = os.path.relpath(d, template_folder) if d != template_folder else "" rel_f = os.path.join(rel_d, f) path = os.path.join(d, f) if not any(fnmatch.fnmatch(rel_f, exclude) for exclude in excluded): template_files[rel_f] = load(path) else: non_template_files[rel_f] = path return template_files, non_template_files @staticmethod def render(template_files, definitions): result = {} name = definitions.get("name", "mypkg") if isinstance(name, list): raise ConanException(f"name argument can't be multiple: {name}") if name != name.lower(): raise ConanException(f"name argument must be lowercase: {name}") definitions["conan_version"] = __version__ def ensure_list(key): value = definitions.get(key) # Convert to list, and forget about it if value: definitions[key] = [value] if isinstance(value, str) else value ensure_list("requires") ensure_list("tool_requires") def as_package_name(n): return n.replace("-", "_").replace("+", "_") def as_name(ref): ref = as_package_name(ref) if '/' in ref: ref = ref[0:ref.index('/')] return ref definitions["package_name"] = as_package_name(name).replace(".", "_") definitions["as_name"] = as_name definitions["names"] = lambda x: ", ".join(r.split("/", 1)[0] for r in x) if "name" not in definitions: definitions["name"] = "mypkg" if "version" not in definitions: definitions["version"] = "0.1" version = definitions.get("version") if isinstance(version, list): raise ConanException(f"version argument can't be multiple: {version}") try: for k, v in template_files.items(): k = Template(k, keep_trailing_newline=True, undefined=StrictUndefined).render( **definitions) v = Template(v, keep_trailing_newline=True, undefined=StrictUndefined).render( **definitions) if v: result[k] = v except UndefinedError: template_vars = [] for templ_str in template_files.values(): env = Environment() ast = env.parse(templ_str) template_vars.extend(meta.find_undeclared_variables(ast)) injected_vars = {"conan_version", "package_name", "as_name"} optional_vars = {"requires", "tool_requires", "output_root_dir"} template_vars = list(set(template_vars) - injected_vars - optional_vars) template_vars.sort() raise ConanException("Missing definitions for the template. " "Required definitions are: {}" .format(", ".join("'{}'".format(var) for var in template_vars))) return result ================================================ FILE: conan/api/subapi/profiles.py ================================================ import os from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file from conan.internal.api.profile.profile_loader import ProfileLoader from conan.internal.errors import scoped_traceback from conan.errors import ConanException from conan.internal.model.profile import Profile DEFAULT_PROFILE_NAME = "default" class ProfilesAPI: def __init__(self, conan_api, api_helpers): self._conan_api = conan_api self._api_helpers = api_helpers self._home_paths = HomePaths(conan_api.home_folder) def get_default_host(self): """ :return: the path to the default "host" profile, either in the cache or as defined by the user in configuration """ default_profile = os.environ.get("CONAN_DEFAULT_PROFILE") if default_profile is None: global_conf = self._api_helpers.global_conf default_profile = global_conf.get("core:default_profile", default=DEFAULT_PROFILE_NAME) default_profile = os.path.join(self._home_paths.profiles_path, default_profile) if not os.path.exists(default_profile): msg = ("The default host profile '{}' doesn't exist.\n" "You need to create a default profile (type 'conan profile detect' command)\n" "or specify your own profile with '--profile:host='") # TODO: Add detailed instructions when cli is improved raise ConanException(msg.format(default_profile)) return default_profile def get_default_build(self): """ :return: the path to the default "build" profile, either in the cache or as defined by the user in configuration """ default_profile = os.environ.get("CONAN_DEFAULT_BUILD_PROFILE") if default_profile is None: global_conf = self._api_helpers.global_conf default_profile = global_conf.get("core:default_build_profile", default=DEFAULT_PROFILE_NAME) default_profile = os.path.join(self._home_paths.profiles_path, default_profile) if not os.path.exists(default_profile): msg = ("The default build profile '{}' doesn't exist.\n" "You need to create a default profile (type 'conan profile detect' command)\n" "or specify your own profile with '--profile:build='") # TODO: Add detailed instructions when cli is improved raise ConanException(msg.format(default_profile)) return default_profile def get_profiles_from_args(self, args): build_profiles = args.profile_build or [self.get_default_build()] host_profiles = args.profile_host or [self.get_default_host()] global_conf = self._api_helpers.global_conf global_conf.validate() # TODO: Remove this from here cache_settings = self._api_helpers.settings_yml profile_plugin = self._load_profile_plugin() cwd = os.getcwd() profile_build = self._get_profile(build_profiles, args.settings_build, args.options_build, args.conf_build, cwd, cache_settings, profile_plugin, global_conf, context="build") profile_host = self._get_profile(host_profiles, args.settings_host, args.options_host, args.conf_host, cwd, cache_settings, profile_plugin, global_conf, context="host") return profile_host, profile_build def get_profile(self, profiles, settings=None, options=None, conf=None, cwd=None, context=None): """ Computes a Profile as the result of aggregating all the user arguments, first it loads the "profiles", composing them in order (last profile has priority), and finally adding the individual settings, options (priority over the profiles) """ assert isinstance(profiles, list), "Please provide a list of profiles" global_conf = self._api_helpers.global_conf global_conf.validate() # TODO: Remove this from here cache_settings = self._api_helpers.settings_yml profile_plugin = self._load_profile_plugin() profile = self._get_profile(profiles, settings, options, conf, cwd, cache_settings, profile_plugin, global_conf, context=context) return profile def _get_profile(self, profiles, settings, options, conf, cwd, cache_settings, profile_plugin, global_conf, context): loader = ProfileLoader(self._conan_api.cache_folder) profile = loader.from_cli_args(profiles, settings, options, conf, cwd, context) if profile_plugin is not None: try: profile_plugin(profile) except Exception as e: msg = f"Error while processing 'profile.py' plugin" msg = scoped_traceback(msg, e, scope="/extensions/plugins") raise ConanException(msg) profile.process_settings(cache_settings) profile.conf.validate() # Apply the new_config to the profiles the global one, so recipes get it too profile.conf.rebase_conf_definition(global_conf) for k, v in sorted(profile.options._package_options.items()): ConanOutput().warning("Unscoped option definition is ambiguous.\n" f"Use '&:{k}={v}' to refer to the current package.\n" f"Use '*:{k}={v}' or other pattern if the intent was to apply to " f"dependencies", warn_tag="legacy") if profile.conf.get("tools.graph:skip_test", check_type=bool): ConanOutput().warning("Usage of 'tools.graph:skip_test'", warn_tag="experimental") if not profile.conf.get("tools.build:skip_test", check_type=bool): ConanOutput().warning("tools.graph:skip_test set, but tools.build:skip_test is not, " "probably you need to define it too") return profile def get_path(self, profile, cwd=None, exists=True): """ :return: the resolved path of the given profile name, that could be in the cache, or local, depending on the "cwd" """ cwd = cwd or os.getcwd() profiles_folder = self._home_paths.profiles_path profile_path = ProfileLoader.get_profile_path(profiles_folder, profile, cwd, exists=exists) return profile_path def list(self): """ List all the profiles file sin the cache :return: an alphabetically ordered list of profile files in the default cache location """ # List is to be extended (directories should not have a trailing slash) paths_to_ignore = ['.DS_Store'] profiles = [] profiles_path = self._home_paths.profiles_path if os.path.exists(profiles_path): for current_directory, _, files in os.walk(profiles_path, followlinks=True): files = filter(lambda file: os.path.relpath( os.path.join(current_directory, file), profiles_path) not in paths_to_ignore, files) for filename in files: rel_path = os.path.relpath(os.path.join(current_directory, filename), profiles_path) profiles.append(rel_path) profiles.sort() return profiles @staticmethod def detect(): """ :return: an automatically detected Profile, with a "best guess" of the system settings """ profile = Profile() from conan.internal.api.profile.detect import detect_defaults_settings settings = detect_defaults_settings() for name, value in settings: profile.settings[name] = value # TODO: This profile is very incomplete, it doesn't have the processed_settings # good enough at the moment for designing the API interface, but to improve return profile def _load_profile_plugin(self): profile_plugin = self._home_paths.profile_plugin_path if not os.path.exists(profile_plugin): raise ConanException("The 'profile.py' plugin file doesn't exist. If you want " "to disable it, edit its contents instead of removing it") mod, _ = load_python_file(profile_plugin) if hasattr(mod, "profile_plugin"): return mod.profile_plugin ================================================ FILE: conan/api/subapi/remotes.py ================================================ import fnmatch import json import os from collections import OrderedDict from urllib.parse import urlparse from conan.api.model import Remote, LOCAL_RECIPES_INDEX from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan.internal.rest.remote_credentials import RemoteCredentials from conan.internal.rest.rest_client_local_recipe_index import add_local_recipes_index_remote, \ remove_local_recipes_index_remote from conan.internal.api.remotes.localdb import LocalDB from conan.errors import ConanException from conan.internal.util.files import save, load CONAN_CENTER_REMOTE_NAME = "conancenter" class RemotesAPI: """ The ``RemotesAPI`` manages the definition of remotes, contained in the "remotes.json" file in the Conan home, supporting addition, removal, update, rename, enable, disable of remotes. These operations do not contact the servers or check their existence at all. If they are not available, they will fail later when used. The ``user_xxx`` methods perform authentication related tasks, and some of them will contact the servers to perform such authentication """ def __init__(self, conan_api, api_helpers): # This method is private, the subapi is not instantiated by users self._conan_api = conan_api self._api_helpers = api_helpers self._home_folder = conan_api.home_folder self._remotes_file = HomePaths(self._home_folder).remotes_path def list(self, pattern=None, only_enabled=True): """ Obtain a list of :ref:`Remote ` objects matching the pattern. :param pattern: ``None``, single ``str`` or list of ``str``. If it is ``None``, all remotes will be returned (equivalent to ``pattern="*"``). :param only_enabled: boolean, by default return only enabled remotes :return: A list of :ref:`Remote ` objects """ remotes = _load(self._remotes_file) if only_enabled: remotes = [r for r in remotes if not r.disabled] if pattern: remotes = _filter(remotes, pattern, only_enabled) return remotes def disable(self, pattern): """ Disable all remotes matching ``pattern`` :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without wildcards like "*" and no remote is found matching that exact name, it will raise an error. :return: the list of disabled :ref:`Remote ` objects (even if they were already disabled) """ remotes = _load(self._remotes_file) disabled = _filter(remotes, pattern, only_enabled=False) result = [] if disabled: for r in disabled: r.disabled = True result.append(r) _save(self._remotes_file, remotes) return result def enable(self, pattern): """ Enable all remotes matching ``pattern``. :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without wildcards like "*" and no remote is found matching that exact name, it will raise an error. :return: the list of enabled :ref:`Remote ` objects (even if they were already enabled) """ remotes = _load(self._remotes_file) enabled = _filter(remotes, pattern, only_enabled=False) result = [] if enabled: for r in enabled: r.disabled = False result.append(r) _save(self._remotes_file, remotes) return result def get(self, remote_name): """ Obtain a :ref:`Remote ` object :param remote_name: the exact name of the remote to be returned :return: the :ref:`Remote ` object, or raise an Exception if the remote does not exist. """ remotes = _load(self._remotes_file) try: return {r.name: r for r in remotes}[remote_name] except KeyError: raise ConanException(f"Remote '{remote_name}' doesn't exist") def add(self, remote: Remote, force=False, index=None): """ Add a new :ref:`Remote ` object to the existing ones :param remote: a :ref:`Remote ` object to be added :param force: do not fail if the remote already exist (but default it fails) :param index: if not defined, the new remote will be last one. Pass an integer to insert the remote in that position instead of the last one """ add_local_recipes_index_remote(self._home_folder, remote) remotes = _load(self._remotes_file) if remote.remote_type != LOCAL_RECIPES_INDEX: _validate_url(remote.url) current = {r.name: r for r in remotes}.get(remote.name) if current: # same name remote existing! if not force: raise ConanException(f"Remote '{remote.name}' already exists in remotes " "(use --force to continue)") ConanOutput().warning(f"Remote '{remote.name}' already exists in remotes") if current.url != remote.url: ConanOutput().warning("Updating existing remote with new url") _check_urls(remotes, remote.url, force, current) if index is None: # append or replace in place d = {r.name: r for r in remotes} d[remote.name] = remote remotes = list(d.values()) else: remotes = [r for r in remotes if r.name != remote.name] remotes.insert(index, remote) _save(self._remotes_file, remotes) def remove(self, pattern): """ Remove the remotes matching the ``pattern`` :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without wildcards like "*" and no remote is found matching that exact name, it will raise an error. :return: The list of removed :ref:`Remote ` objects """ remotes = _load(self._remotes_file) removed = _filter(remotes, pattern, only_enabled=False) remotes = [r for r in remotes if r not in removed] _save(self._remotes_file, remotes) localdb = LocalDB(self._home_folder) for remote in removed: remove_local_recipes_index_remote(self._home_folder, remote) localdb.clean(remote_url=remote.url) return removed def update(self, remote_name: str, url=None, secure=None, disabled=None, index=None, allowed_packages=None, recipes_only=None): """ Update an existing remote :param remote_name: The name of the remote to update, must exist :param url: optional url to update, if not defined it will not be updated :param secure: optional ssl secure connection to update :param disabled: optional disabled state :param index: optional integer to change the order of the remote :param allowed_packages: optional list of packages allowed from this remote :param recipes_only: optional boolean to only allow recipe downloads from this remote, never package binaries """ remotes = _load(self._remotes_file) try: remote = {r.name: r for r in remotes}[remote_name] except KeyError: raise ConanException(f"Remote '{remote_name}' doesn't exist") if url is not None: if remote.remote_type != LOCAL_RECIPES_INDEX: _validate_url(url) _check_urls(remotes, url, force=False, current=remote) remote.url = url if secure is not None: remote.verify_ssl = secure if disabled is not None: remote.disabled = disabled if allowed_packages is not None: remote.allowed_packages = allowed_packages if recipes_only is not None: remote.recipes_only = recipes_only if index is not None: remotes = [r for r in remotes if r.name != remote.name] remotes.insert(index, remote) _save(self._remotes_file, remotes) def rename(self, remote_name: str, new_name: str): """ Change the name of an existing remote :param remote_name: The previous existing name :param new_name: The new name """ remotes = _load(self._remotes_file) d = {r.name: r for r in remotes} if new_name in d: raise ConanException(f"Remote '{new_name}' already exists") try: d[remote_name].name = new_name except KeyError: raise ConanException(f"Remote '{remote_name}' doesn't exist") _save(self._remotes_file, remotes) def user_info(self, remote: Remote): # TODO: Review localdb = LocalDB(self._home_folder) user_info = {} user, token, _ = localdb.get_login(remote.url) user_info["name"] = remote.name user_info["user_name"] = user user_info["authenticated"] = True if token else False return user_info def user_login(self, remote: Remote, username: str, password: str): """ Perform user authentication against the given remote with the provided username and password :param remote: a :ref:`Remote ` object :param username: the user login as ``str`` :param password: password ``str`` """ self._api_helpers.remote_manager.authenticate(remote, username, password) def login(self, remotes, username=None, password=None): creds = RemoteCredentials(self._conan_api.cache_folder, self._api_helpers.global_conf) ret = OrderedDict() for r in remotes: previous_info = self.user_info(r) if username is not None and password is not None: user, password = username, password else: user, password, _ = creds.auth(r, username) if username is not None and username != user: raise ConanException(f"User '{username}' doesn't match user '{user}' in " f"credentials.json or environment variables") self.user_login(r, user, password) info = self.user_info(r) ret[r.name] = {"previous_info": previous_info, "info": info} return ret def user_logout(self, remote: Remote): """ Logout from the given :ref:`Remote ` :param remote: The :ref:`Remote ` object to logout """ localdb = LocalDB(self._home_folder) # The localdb only stores url + username + token, not remote name, so use URL as key localdb.clean(remote_url=remote.url) def user_set(self, remote: Remote, username): # TODO: Review localdb = LocalDB(self._home_folder) if username == "": username = None localdb.store(username, token=None, refresh_token=None, remote_url=remote.url) def user_auth(self, remote: Remote, with_user=False, force=False): # TODO: Review localdb = LocalDB(self._home_folder) if with_user: user, token, _ = localdb.get_login(remote.url) if not user: var_name = f"CONAN_LOGIN_USERNAME_{remote.name.replace('-', '_').upper()}" user = os.getenv(var_name, None) or os.getenv("CONAN_LOGIN_USERNAME", None) if not user: return self._api_helpers.remote_manager.check_credentials(remote, force) user, token, _ = localdb.get_login(remote.url) return user def _load(remotes_file): if not os.path.exists(remotes_file): remote = Remote(CONAN_CENTER_REMOTE_NAME, "https://center2.conan.io", True, False) _save(remotes_file, [remote]) return [remote] try: data = json.loads(load(remotes_file)) except Exception as e: raise ConanException(f"Error loading JSON remotes file '{remotes_file}': {e}") result = [] for r in data.get("remotes", []): remote = Remote(r["name"], r["url"], r["verify_ssl"], r.get("disabled", False), r.get("allowed_packages"), r.get("remote_type"), r.get("recipes_only", False)) result.append(remote) return result def _save(remotes_file, remotes): remote_list = [] for r in remotes: remote = {"name": r.name, "url": r.url, "verify_ssl": r.verify_ssl} if r.disabled: remote["disabled"] = True if r.allowed_packages: remote["allowed_packages"] = r.allowed_packages if r.remote_type: remote["remote_type"] = r.remote_type if r.recipes_only: remote["recipes_only"] = r.recipes_only remote_list.append(remote) # This atomic replace avoids a corrupted remotes.json file if this is killed during the process save(remotes_file + ".tmp", json.dumps({"remotes": remote_list}, indent=True)) os.replace(remotes_file + ".tmp", remotes_file) def _filter(remotes, pattern, only_enabled=True): filtered_remotes = [] patterns = [pattern] if isinstance(pattern, str) else pattern for p in patterns: is_match = False for remote in remotes: if fnmatch.fnmatch(remote.name, p): is_match = True if remote not in filtered_remotes: filtered_remotes.append(remote) if not is_match: if "*" in p or "?" in p: if only_enabled: raise ConanException( f"Remotes for pattern '{p}' can't be found or are disabled") else: raise ConanException(f"Remote '{p}' can't be found or is disabled") return filtered_remotes def _validate_url(url): """ Check if URL contains protocol and address :param url: URL to be validated """ out = ConanOutput() if url: if url.startswith("https://conan.io/center"): raise ConanException("Wrong ConanCenter remote URL. You are adding the web " "https://conan.io/center the correct remote API is " "https://center2.conan.io") address = urlparse(url) if not all([address.scheme, address.netloc]): out.warning(f"The URL '{url}' is invalid. It must contain scheme and hostname.") else: out.warning("The URL is empty. It must contain scheme and hostname.") def _check_urls(remotes, url, force, current): # The remote name doesn't exist for r in remotes: if r is not current and r.url == url: msg = f"Remote url already existing in remote '{r.name}'. " \ f"Having different remotes with same URL is not recommended." if not force: raise ConanException(msg + " Use '--force' to override.") else: ConanOutput().warning(msg + " Adding duplicated remote url because '--force'.") ================================================ FILE: conan/api/subapi/remove.py ================================================ from typing import Optional from conan.api.model import Remote from conan.api.model import PkgReference from conan.api.model import RecipeReference class RemoveAPI: def __init__(self, conan_api, api_helpers): self._conan_api = conan_api self._api_helpers = api_helpers def recipe(self, ref: RecipeReference, remote: Optional[Remote] = None): assert ref.revision, "Recipe revision cannot be None to remove a recipe" """Removes the recipe (or recipe revision if present) and all the packages (with all prev)""" if remote: self._api_helpers.remote_manager.remove_recipe(ref, remote) else: self.all_recipe_packages(ref) recipe_layout = self._api_helpers.cache.recipe_layout(ref) self._api_helpers.cache.remove_recipe_layout(recipe_layout) def all_recipe_packages(self, ref: RecipeReference, remote: Optional[Remote] = None): assert ref.revision, "Recipe revision cannot be None to remove a recipe" """Removes all the packages from the provided reference""" if remote: self._api_helpers.remote_manager.remove_all_packages(ref, remote) else: # Remove all the prefs with all the prevs self._remove_all_local_packages(ref) def _remove_all_local_packages(self, ref): # Get all the prefs and all the prevs pkg_ids = self._api_helpers.cache.get_package_references(ref, only_latest_prev=False) for pref in pkg_ids: package_layout = self._api_helpers.cache.pkg_layout(pref) self._api_helpers.cache.remove_package_layout(package_layout) def package(self, pref: PkgReference, remote: Optional[Remote]): assert pref.ref.revision, "Recipe revision cannot be None to remove a package" assert pref.revision, "Package revision cannot be None to remove a package" if remote: # FIXME: Create a "packages" method to optimize remote remove? self._api_helpers.remote_manager.remove_packages([pref], remote) else: package_layout = self._api_helpers.cache.pkg_layout(pref) self._api_helpers.cache.remove_package_layout(package_layout) ================================================ FILE: conan/api/subapi/report.py ================================================ import base64 import os from io import StringIO from conan.api.output import ConanOutput from conan.errors import ConanException from conan.api.model import RecipeReference from conan.internal.conan_app import ConanApp from conan.internal.errors import conanfile_exception_formatter from conan.internal.graph.graph import CONTEXT_HOST from conan.internal.graph.profile_node_definer import initialize_conanfile_profile from conan.internal.source import config_source from conan.internal.util.runners import conan_run class ReportAPI: """ Used to compute the differences (the "diff") between two versions or revisions, for both the recipe and source code. """ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers def diff(self, old_reference, new_reference, remotes, old_path=None, new_path=None, cwd=None): """ Compare two recipes and return the differences. :param old_reference: The reference of the old recipe. :param new_reference: The reference of the new recipe. :param remotes: List of remotes to search for the recipes. :param old_path: Optional path to the old recipe's conanfile.py. :param new_path: Optional path to the new recipe's conanfile.py. :param cwd: Current working directory, used to resolve paths. :return: A dictionary with the differences between the two recipes. """ def _source(path_to_conanfile, reference): if path_to_conanfile is None: export_ref, cache_path = _get_ref_from_cache_or_remote(self._conan_api, reference, remotes) else: export_ref, cache_path = _export_recipe_from_path(self._conan_api, path_to_conanfile, reference, remotes, cwd) exported_path = self._conan_api.local.get_conanfile_path(cache_path, cwd, py=True) _configure_source(self._conan_api, self._helpers.hook_manager, exported_path, export_ref, remotes) return export_ref, cache_path old_export_ref, old_cache_path = _source(old_path, old_reference) new_export_ref, new_cache_path = _source(new_path, new_reference) old_diff_path = os.path.abspath(os.path.join(old_cache_path, os.path.pardir)).replace("\\", "/") new_diff_path = os.path.abspath(os.path.join(new_cache_path, os.path.pardir)).replace("\\", "/") src_prefix = base64.b64encode(str(new_export_ref.repr_notime()).encode()).decode() + "/" dst_prefix = base64.b64encode(str(old_export_ref.repr_notime()).encode()).decode() + "/" command = (f'git diff --no-index --src-prefix "{src_prefix}" --dst-prefix "{dst_prefix}" ' f'"{old_diff_path}" "{new_diff_path}"') ConanOutput().info( f"Generating diff from {old_export_ref.repr_notime()} to {new_export_ref.repr_notime()} " f"(this might take a while)") ConanOutput().info(command) stdout, stderr = StringIO(), StringIO() conan_run(command, stdout=stdout, stderr=stderr) diff = stdout.getvalue() if old_path: self._conan_api.remove.recipe(old_export_ref) if new_path: self._conan_api.remove.recipe(new_export_ref) return { "diff": diff, "old_export_ref": old_export_ref, "new_export_ref": new_export_ref, "old_cache_path": old_diff_path, "new_cache_path": new_diff_path, "src_prefix": src_prefix, "dst_prefix": dst_prefix, } def _configure_source(conan_api, hook_manager, conanfile_path, ref, remotes): app = ConanApp(conan_api) conanfile = app.loader.load_consumer(conanfile_path, name=ref.name, version=str(ref.version), user=ref.user, channel=ref.channel, graph_lock=None, remotes=remotes) # This profile is empty, but with the conf from global.conf profile = conan_api.profiles.get_profile([]) initialize_conanfile_profile(conanfile, profile, profile, CONTEXT_HOST, False) # This is important, otherwise the ``conan source`` doesn't define layout and fails if hasattr(conanfile, "layout"): with conanfile_exception_formatter(conanfile, "layout"): conanfile.layout() cache = conan_api._api_helpers.cache # noqa recipe_layout = cache.recipe_layout(ref) export_source_folder = recipe_layout.export_sources() source_folder = recipe_layout.source() conanfile.folders.set_base_source(source_folder) conanfile.folders.set_base_export_sources(export_source_folder) conanfile.folders.set_base_recipe_metadata(recipe_layout.metadata()) config_source(export_source_folder, conanfile, hook_manager) def _get_ref_from_cache_or_remote(conan_api, reference, enabled_remotes): ref = RecipeReference.loads(reference) full_ref, matching_remote = None, False # The first remote is None, which means local cache for the list subapi methods for remote in [None] + enabled_remotes: if ref.revision: no_rrev_ref = RecipeReference.loads(reference) no_rrev_ref.revision = None try: remote_revisions = conan_api.list.recipe_revisions(no_rrev_ref, remote) if ref in remote_revisions: full_ref = ref matching_remote = remote break except (Exception,): continue else: try: latest_recipe_revision = conan_api.list.latest_recipe_revision(ref, remote) except (Exception,): continue if full_ref is None or (latest_recipe_revision.timestamp > full_ref.timestamp): full_ref = latest_recipe_revision matching_remote = remote if full_ref is None or matching_remote is False: raise ConanException(f"No matching reference for {reference} in remotes.\n" "If you want to check against a local recipe, add an " "additional --{old,new}-path arg.\n") if matching_remote is not None: conan_api.download.recipe(full_ref, matching_remote) cache_path = conan_api.cache.export_path(full_ref) return full_ref, cache_path def _export_recipe_from_path(conan_api, path_to_conanfile, reference, enabled_remotes, cwd=None): path = conan_api.local.get_conanfile_path(path_to_conanfile, cwd, py=True) ref = RecipeReference.loads(reference) export_ref, conanfile = conan_api.export.export(path=path, name=ref.name, version=str(ref.version), user=ref.user, channel=ref.channel, lockfile=None, remotes=enabled_remotes) cache_path = conan_api.cache.export_path(export_ref) return export_ref, cache_path ================================================ FILE: conan/api/subapi/upload.py ================================================ import os import time from multiprocessing.pool import ThreadPool from typing import List from conan.api.model import PackagesList, Remote from conan.api.output import ConanOutput from conan.internal.api.upload import add_urls from conan.internal.conan_app import ConanApp from conan.internal.api.uploader import PackagePreparator, UploadExecutor, UploadUpstreamChecker from conan.internal.rest.pkg_sign import PkgSignaturesPlugin from conan.internal.rest.file_uploader import FileUploader from conan.internal.errors import AuthenticationException, ForbiddenException from conan.errors import ConanException class UploadAPI: """ This API is used to upload recipes and packages to a remote server.""" def __init__(self, conan_api, api_helpers): self._conan_api = conan_api self._api_helpers = api_helpers def check_upstream(self, package_list: PackagesList, remote: Remote, enabled_remotes: List[Remote], force=False): """ Checks ``remote`` for the existence of the recipes and packages in ``package_list``. Items that are not present in the remote will add an ``upload`` key to the entry with the value ``True``. If the recipe has an upload policy of ``skip``, it will be discarded from the upload list. :parameter package_list: A ``PackagesList`` object with the recipes and packages to check. :parameter remote: Remote to check. :parameter enabled_remotes: List of enabled remotes. This is used to possibly load python_requires from the listed recipes if necessary. :parameter force: If ``True``, it will skip the check and mark that all items need to be uploaded. A ``force_upload`` key will be added to the entries that will be uploaded. """ app = ConanApp(self._conan_api) for ref, _ in package_list.items(): layout = self._api_helpers.cache.recipe_layout(ref) conanfile_path = layout.conanfile() conanfile = app.loader.load_basic(conanfile_path, remotes=enabled_remotes) if conanfile.upload_policy == "skip": ConanOutput().info(f"{ref}: Skipping upload of binaries, " "because upload_policy='skip'") package_list.recipe_dict(ref)["packages"] = {} UploadUpstreamChecker(self._api_helpers.remote_manager).check(package_list, remote, force) def prepare(self, package_list: PackagesList, enabled_remotes: List[Remote], metadata: List[str] = None): """Compress the recipes and packages and fill the upload_data objects with the complete information. It doesn't perform the upload nor checks upstream to see if the recipe is still there :param package_list: A PackagesList object with the recipes and packages to upload. :param enabled_remotes: A list of remotes that are enabled in the client. Recipe sources will attempt to be fetched from these remotes. :param metadata: A list of patterns of metadata that should be uploaded. Default ``None`` means all metadata will be uploaded together with the package artifacts. If metadata contains an empty string (``""``), it means that no metadata files should be uploaded.""" if metadata and metadata != [''] and '' in metadata: raise ConanException("Empty string and patterns can not be mixed for metadata.") app = ConanApp(self._conan_api) preparator = PackagePreparator(app, self._api_helpers.cache, self._api_helpers.remote_manager, self._api_helpers.global_conf) preparator.prepare(package_list, enabled_remotes, metadata) signer = PkgSignaturesPlugin(self._api_helpers.cache, self._conan_api.home_folder) if signer.is_sign_configured: ConanOutput().warning("[Package sign] Implicitly signing packages in the upload " "command has been removed. Use 'conan cache sign' command before " "uploading instead.", warn_tag="deprecated") def _upload(self, package_list, remote): self._api_helpers.remote_manager.check_credentials(remote) executor = UploadExecutor(self._api_helpers.remote_manager) executor.upload(package_list, remote) def upload_full(self, package_list: PackagesList, remote: Remote, enabled_remotes: List[Remote], check_integrity=False, force=False, metadata: List[str] = None, dry_run=False): """ Does the whole process of uploading, including the possibility of parallelizing per recipe based on the ``core.upload:parallel`` conf. The steps that this method performs are: - calls ``conan_api.cache.check_integrity`` to ensure the packages are not corrupted - checks the upload policy of the recipes - (if it is ``"skip"``, it will not upload the binaries, but will still upload the metadata) - checks which revisions already exist in the server so that it can skip the upload - prepares the artifacts to upload (compresses the conan_package.tgz) - executes the actual upload - uploads associated sources backups if any :param package_list: A PackagesList object with the recipes and packages to upload. :param remote: The remote to upload the packages to. :param enabled_remotes: A list of remotes that are enabled in the client. Recipe sources will attempt to be fetched from these remotes, and to possibly load python_requires from the listed recipes if necessary. :param check_integrity: If ``True``, it will check the integrity of the cache packages before uploading them. This is useful to ensure that the packages are not corrupted. :param force: If ``True``, it will force the upload of the recipes and packages, even if they already exist in the remote. Note that this might update the timestamps :param metadata: A list of patterns of metadata that should be uploaded. Default ``None`` means all metadata will be uploaded together with the package artifacts. If metadata contains an empty string (``""``), it means that no metadata files should be uploaded. :param dry_run: If ``True``, it will not perform the actual upload, but will still prepare the artifacts and check the upstream. """ def _upload_pkglist(pkglist, subtitle=lambda _: None): if check_integrity: subtitle("Checking integrity of cache packages") self._conan_api.cache.check_integrity(pkglist) # Check if the recipes/packages are in the remote subtitle("Checking server for existing packages") self.check_upstream(pkglist, remote, enabled_remotes, force) subtitle("Preparing artifacts for upload") self.prepare(pkglist, enabled_remotes, metadata) if not dry_run: subtitle("Uploading artifacts") self._upload(pkglist, remote) backup_files = self._conan_api.cache.get_backup_sources(pkglist) self.upload_backup_sources(backup_files) t = time.time() ConanOutput().title(f"Uploading to remote {remote.name}") parallel = self._conan_api.config.get("core.upload:parallel", default=1, check_type=int) thread_pool = ThreadPool(parallel) if parallel > 1 else None if not thread_pool or len(package_list._data) <= 1: # FIXME: Iteration when multiple rrevs _upload_pkglist(package_list, subtitle=ConanOutput().subtitle) else: ConanOutput().subtitle(f"Uploading with {parallel} parallel threads") thread_pool.map(_upload_pkglist, package_list.split()) if thread_pool: thread_pool.close() thread_pool.join() elapsed = time.time() - t ConanOutput().success(f"Upload completed in {int(elapsed)}s\n") add_urls(package_list, remote) def upload_backup_sources(self, files: List) -> None: """ Upload to the server the backup sources files, that have been typically gathered by CacheAPI.get_backup_sources() :param files: The list of files that must be uploaded """ config = self._api_helpers.global_conf url = config.get("core.sources:upload_url", check_type=str) if url is None: return url = url if url.endswith("/") else url + "/" output = ConanOutput() output.subtitle("Uploading backup sources") if not files: output.info("No backup sources files to upload") return requester = self._api_helpers.requester uploader = FileUploader(requester, verify=True, config=config, source_credentials=True) # TODO: For Artifactory, we can list all files once and check from there instead # of 1 request per file, but this is more general for file in files: basename = os.path.basename(file) full_url = url + basename is_summary = file.endswith(".json") file_kind = "summary" if is_summary else "file" try: if is_summary or not uploader.exists(full_url, auth=None): output.info(f"Uploading {file_kind} '{basename}' to backup sources server") uploader.upload(full_url, file, dedup=False, auth=None) else: output.info(f"File '{basename}' already in backup sources server, " "skipping upload") except (AuthenticationException, ForbiddenException) as e: if is_summary: output.warning(f"Could not update summary '{basename}' in backup sources server. " "Skipping updating file but continuing with upload. " f"Missing permissions?: {e}") else: raise ConanException(f"Authentication to source backup server '{url}' failed, " f"please check your 'source_credentials.json': {e}") output.success("Upload backup sources complete\n") ================================================ FILE: conan/api/subapi/workspace.py ================================================ import inspect import os import shutil import textwrap from pathlib import Path from conan import ConanFile from conan.api.model import RecipeReference from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.cli.printers.graph import print_graph_basic, print_graph_packages from conan.errors import ConanException from conan.internal.conan_app import ConanApp from conan.internal.errors import conanfile_exception_formatter from conan.internal.graph.install_graph import ProfileArgs from conan.internal.methods import auto_language, auto_shared_fpic_config_options, \ auto_shared_fpic_configure from conan.internal.model.options import Options from conan.internal.model.workspace import Workspace, WORKSPACE_YML, WORKSPACE_PY, WORKSPACE_FOLDER from conan.tools.scm import Git from conan.internal.graph.graph import (RECIPE_EDITABLE, DepsGraph, CONTEXT_HOST, RECIPE_VIRTUAL, Node, RECIPE_CONSUMER) from conan.internal.graph.graph import TransitiveRequirement from conan.internal.graph.profile_node_definer import consumer_definer, initialize_conanfile_profile from conan.internal.loader import load_python_file from conan.internal.source import retrieve_exports_sources from conan.internal.util.files import merge_directories, save def _find_ws_folder(): path = Path(os.getcwd()) while path.is_dir() and len(path.parts) > 1: # finish at '/' or 'conanws/' if path.name == WORKSPACE_FOLDER: if (path / WORKSPACE_YML).is_file() or (path / WORKSPACE_PY).is_file(): return str(path) if (path / WORKSPACE_YML).is_file() or (path / WORKSPACE_PY).is_file(): return str(path) else: path = path.parent def _load_workspace(ws_folder, conan_api): """ loads a conanfile basic object without evaluating anything, returns the module too """ wspy = os.path.join(ws_folder, WORKSPACE_PY) if not os.path.isfile(wspy): ConanOutput().info(f"{WORKSPACE_PY} doesn't exist in {ws_folder}, using default behavior") assert os.path.exists(os.path.join(ws_folder, WORKSPACE_YML)) ws = Workspace(ws_folder, conan_api) else: try: module, module_id = load_python_file(wspy) ws = _parse_module(module, module_id) ws = ws(ws_folder, conan_api) except ConanException as e: raise ConanException(f"Error loading {WORKSPACE_PY} at '{wspy}': {e}") return ws def _parse_module(conanfile_module, module_id): result = None for name, attr in conanfile_module.__dict__.items(): if (name.startswith("_") or not inspect.isclass(attr) or attr.__dict__.get("__module__") != module_id): continue if issubclass(attr, Workspace) and attr != Workspace: if result is None: result = attr else: raise ConanException("More than 1 Workspace in the file") if result is None: raise ConanException("No subclass of Workspace") return result class WorkspaceAPI: TEST_ENABLED = False def __init__(self, conan_api): self._enabled = True self._conan_api = conan_api self._folder = _find_ws_folder() if self._folder: ConanOutput().warning(f"Workspace found: {self._folder}") if (WorkspaceAPI.TEST_ENABLED or os.getenv("CONAN_WORKSPACE_ENABLE")) != "will_break_next": ConanOutput().warning("Workspace ignored as CONAN_WORKSPACE_ENABLE is not set") self._folder = None else: ConanOutput().warning(f"Workspace is a dev-only feature, exclusively for testing") self._ws = _load_workspace(self._folder, conan_api) # Error if not loading def enable(self, value): self._enabled = value def name(self): self._check_ws() return self._ws.name() def folder(self): """ @return: the current workspace folder where the conanws.yml or conanws.py is located """ self._check_ws() return self._folder def packages(self): """ @return: Returns {RecipeReference: {"path": full abs-path, "output_folder": abs-path}} """ if not self._folder or not self._enabled: return packages = {} for editable_info in self._ws.packages(): rel_path = editable_info["path"] path = os.path.normpath(os.path.join(self._folder, rel_path, "conanfile.py")) if not os.path.isfile(path): raise ConanException(f"Workspace package not found: {path}") ref = editable_info.get("ref") try: if ref is None: conanfile = self._ws.load_conanfile(rel_path) reference = RecipeReference(name=conanfile.name, version=conanfile.version, user=conanfile.user, channel=conanfile.channel) else: reference = RecipeReference.loads(ref) reference.validate_ref(reference) except Exception as e: raise ConanException(f"Workspace package reference could not be deduced by" f" {rel_path}/conanfile.py or it is not" f" correctly defined in the conanws.yml file: {e}") if reference in packages: raise ConanException(f"Workspace package '{str(reference)}' already exists.") packages[reference] = {"path": path} if editable_info.get("output_folder"): packages[reference]["output_folder"] = ( os.path.normpath(os.path.join(self._folder, editable_info["output_folder"])) ) return packages def open(self, ref, remotes, cwd=None): cwd = cwd or os.getcwd() app = ConanApp(self._conan_api) ref = RecipeReference.loads(ref) if isinstance(ref, str) else ref recipe = app.proxy.get_recipe(ref, remotes, update=False, check_update=False) layout, recipe_status, remote = recipe if recipe_status == RECIPE_EDITABLE: raise ConanException(f"Can't open a dependency that is already an editable: {ref}") ref = layout.reference conanfile_path = layout.conanfile() conanfile, module = app.loader.load_basic_module(conanfile_path, remotes=remotes) scm = conanfile.conan_data.get("scm") if conanfile.conan_data else None dst_path = os.path.join(cwd, ref.name) if scm is None: conanfile.output.warning("conandata doesn't contain 'scm' information\n" "doing a local copy!!!") shutil.copytree(layout.export(), dst_path) remote_manager = self._conan_api._api_helpers.remote_manager # noqa retrieve_exports_sources(remote_manager, layout, conanfile, ref, remotes) export_sources = layout.export_sources() if os.path.exists(export_sources): conanfile.output.warning("There are export-sources, copying them, but the location" " might be incorrect, use 'scm' approach") merge_directories(export_sources, dst_path) else: git = Git(conanfile, folder=cwd) git.clone(url=scm["url"], target=ref.name) git.folder = ref.name # change to the cloned folder git.checkout(commit=scm["commit"]) return dst_path def _check_ws(self): if not self._folder: raise ConanException(f"Workspace not defined, please create a " f"'{WORKSPACE_PY}' or '{WORKSPACE_YML}' file") def add(self, path, name=None, version=None, user=None, channel=None, cwd=None, output_folder=None, remotes=None): """ Add a new editable package to the current workspace (the current workspace must exist) @param path: The path to the folder containing the conanfile.py that defines the package @param name: (optional) The name of the package to be added if not defined in recipe @param version: @param user: @param channel: @param cwd: @param output_folder: @param remotes: @return: The reference of the added package """ self._check_ws() full_path = self._conan_api.local.get_conanfile_path(path, cwd, py=True) app = ConanApp(self._conan_api) conanfile = app.loader.load_named(full_path, name, version, user, channel, remotes=remotes) if conanfile.name is None or conanfile.version is None: raise ConanException("Editable package recipe should declare its name and version") ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) ref.validate_ref() output_folder = make_abs_path(output_folder) if output_folder else None # Check the conanfile is there, and name/version matches self._ws.add(ref, full_path, output_folder) return ref def complete(self, profile_host, profile_build, lockfile, remotes, update): packages = self.packages() if not packages: ConanOutput().info("There are no packages in this workspace, nothing to complete") return for ref, info in packages.items(): ConanOutput().title(f"Computing the dependency graph for package: {ref}") gapi = self._conan_api.graph deps_graph = gapi.load_graph_requires([ref], None, profile_host, profile_build, lockfile, remotes, update) deps_graph.report_graph_error() print_graph_basic(deps_graph) nodes_to_complete = [] for node in deps_graph.nodes[1:]: # Exclude the current virtual root if node.recipe != RECIPE_EDITABLE: # sanity check, a pacakge in the cache cannot have dependencies to the workspace if any(d.node.recipe == RECIPE_EDITABLE for d in node.transitive_deps.values()): nodes_to_complete.append(node) if not nodes_to_complete: ConanOutput().info("There are no intermediate packages to add to the workspace") return for node in nodes_to_complete: full_path = os.path.join(self._folder, node.name, "conanfile.py") dep_ref = node.ref ConanOutput().info(f"Adding to workspace {dep_ref}") try: self._ws.add(dep_ref, full_path, output_folder=None) except ConanException: if os.path.isfile(full_path): raise ConanOutput().info(f"Conanfile in {node.name} not found, trying " "to open it first") self.open(dep_ref, remotes, cwd=self._folder) self._ws.add(dep_ref, full_path, output_folder=None) @staticmethod def init(path): abs_path = make_abs_path(path) os.makedirs(abs_path, exist_ok=True) ws_yml_file = Path(abs_path, WORKSPACE_YML) ws_py_file = Path(abs_path, WORKSPACE_PY) if not ws_yml_file.exists(): ConanOutput().success(f"Created empty {WORKSPACE_YML} in {path}") save(ws_yml_file, "") if not ws_py_file.exists(): ConanOutput().success(f"Created minimal {WORKSPACE_PY} in {path}") ws_name = os.path.basename(abs_path) save(ws_py_file, textwrap.dedent(f'''\ from conan import Workspace class MyWorkspace(Workspace): """ Minimal Workspace class definition. More info: https://docs.conan.io/2/incubating.html#workspaces """ def name(self): return "{ws_name}" ''')) def remove(self, path): self._check_ws() return self._ws.remove(path) def clean(self): self._check_ws() return self._ws.clean() def info(self): self._check_ws() return {"name": self._ws.name(), "folder": self._folder, "packages": self._ws.packages()} @staticmethod def _init_options(conanfile, options): if hasattr(conanfile, "config_options"): with conanfile_exception_formatter(conanfile, "config_options"): conanfile.config_options() elif "auto_shared_fpic" in conanfile.implements: auto_shared_fpic_config_options(conanfile) auto_language(conanfile) # default implementation removes `compiler.cstd` # Assign only the current package options values, but none of the dependencies conanfile.options.apply_downstream(Options(), options, None, True) if hasattr(conanfile, "configure"): with conanfile_exception_formatter(conanfile, "configure"): conanfile.configure() elif "auto_shared_fpic" in conanfile.implements: auto_shared_fpic_configure(conanfile) def super_build_graph(self, deps_graph, profile_host, profile_build): order = [] packages = self.packages() def find_folder(ref): return next(os.path.dirname(os.path.relpath(p["path"], self._folder)) for p_ref, p in packages.items() if p_ref == ref) for level in deps_graph.by_levels(): items = [item for item in level if item.recipe == "Editable"] level_order = [] for node in items: conanfile = node.conanfile if hasattr(conanfile, "layout"): with conanfile_exception_formatter(conanfile, "layout"): conanfile.layout() base_folder = find_folder(node.ref) src_folder = os.path.normpath(os.path.join(base_folder, conanfile.folders.source)) level_order.append({"ref": node.ref, "folder": src_folder.replace("\\", "/")}) order.append(level_order) self._ws.build_order(order) ConanOutput().title("Collapsing workspace packages") root_class = self._ws.root_conanfile() if root_class is not None: conanfile = root_class(f"{WORKSPACE_PY} base project Conanfile") # To inject things like cmd_wrapper to the consumer conanfile, so self.run() works helpers = ConanApp(self._conan_api).loader._conanfile_helpers # noqa conanfile._conan_helpers = helpers conanfile._conan_is_consumer = True initialize_conanfile_profile(conanfile, profile_build, profile_host, CONTEXT_HOST, is_build_require=False) # consumer_definer(conanfile, profile_host, profile_build) self._init_options(conanfile, profile_host.options) for field in ("requires", "build_requires", "test_requires", "requirements", "build", "source", "package"): if getattr(conanfile, field, None): raise ConanException(f"Conanfile in conanws.py shouldn't have '{field}'") root = Node(None, conanfile, context=CONTEXT_HOST, recipe=RECIPE_CONSUMER, path=self._folder) # path lets use the conanws.py folder root.should_build = True # It is a consumer, this is something we are building else: ConanOutput().info(f"Workspace {WORKSPACE_PY} not found in the workspace folder, " "using default behavior") conanfile = ConanFile(display_name="cli") consumer_definer(conanfile, profile_host, profile_build) root = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, recipe=RECIPE_VIRTUAL) result = DepsGraph() # TODO: We might need to copy more information from the original graph result.add_node(root) conanfile.workspace_packages = {} self._check_graph(deps_graph) for node in deps_graph.nodes[1:]: # Exclude the current root if node.recipe != RECIPE_EDITABLE: result.add_node(node) continue # At the moment we are exposing the full conanfile, docs will warn against usage of # non pure functions conanfile.workspace_packages[node.ref] = node.conanfile for r, t in node.transitive_deps.items(): if t.node.recipe == RECIPE_EDITABLE: continue existing = root.transitive_deps.pop(r, None) if existing is None: root.transitive_deps[r] = t else: require = existing.require require.aggregate(r) root.transitive_deps[require] = TransitiveRequirement(require, t.node) # The graph edges must be defined too for r, t in root.transitive_deps.items(): result.add_edge(root, t.node, r) return result @staticmethod def _check_graph(graph): for node in graph.nodes[1:]: # Exclude the current root if node.recipe != RECIPE_EDITABLE: # sanity check, a pacakge in the cache cannot have dependencies to the workspace deps_edit = [d.node for d in node.transitive_deps.values() if d.node.recipe == RECIPE_EDITABLE] if deps_edit: raise ConanException(f"Workspace definition error. Package {node} in the " f"Conan cache has dependencies to packages " f"in the workspace: {deps_edit}\n" "Try the 'conan workspace complete' to open/add " "intermediate packages") def export(self, lockfile=None, remotes=None): self._check_ws() exported = [] for ref, info in self.packages().items(): exported_ref = self._conan_api.export.export(info["path"], ref.name, str(ref.version), ref.user, ref.channel, lockfile=lockfile, remotes=remotes) ref, _ = exported_ref exported.append(ref) return exported def select_packages(self, packages): self._check_ws() editable = self.packages() packages = packages or [] selected_editables = {} for ref, info in editable.items(): if packages and not any(ref.matches(p, False) for p in packages): continue selected_editables[ref] = info if not selected_editables: raise ConanException("There are no selected packages defined in the workspace") return selected_editables def build_order(self, packages, profile_host, profile_build, build_mode, lockfile, remotes, profile_args, update=False): ConanOutput().title(f"Computing dependency graph for each package") conan_api = self._conan_api from conan.internal.graph.install_graph import InstallGraph install_order = InstallGraph(None) for ref, info in packages.items(): ConanOutput().title(f"Computing the dependency graph for package: {ref}") deps_graph = conan_api.graph.load_graph_requires([ref], None, profile_host, profile_build, lockfile, remotes, update) deps_graph.report_graph_error() print_graph_basic(deps_graph) self._check_graph(deps_graph) conan_api.graph.analyze_binaries(deps_graph, build_mode, remotes=remotes, update=update, lockfile=lockfile) print_graph_packages(deps_graph) ConanOutput().success(f"\nAggregating build-order for package: {ref}") install_graph = InstallGraph(deps_graph, order_by="recipe", profile_args=ProfileArgs.from_args(profile_args)) install_graph.raise_errors() install_order.merge(install_graph) return install_order ================================================ FILE: conan/cli/__init__.py ================================================ import os def make_abs_path(path, cwd=None): """convert 'path' to absolute if necessary (could be already absolute) if not defined (empty, or None), will return 'default' one or 'cwd' """ if path is None: return None if os.path.isabs(path): return path cwd = cwd or os.getcwd() abs_path = os.path.normpath(os.path.join(cwd, path)) return abs_path ================================================ FILE: conan/cli/args.py ================================================ import argparse from conan.cli.command import OnceArgument from conan.errors import ConanException _help_build_policies = '''Optional, specify which packages to build from source. Combining multiple '--build' options on one command line is allowed. Possible values: --build=never Disallow build for all packages, use binary packages or fail if a binary package is not found, it cannot be combined with other '--build' options. --build=missing Build packages from source whose binary package is not found. --build=cascade Build packages from source that have at least one dependency being built from source. --build=[pattern] Build packages from source whose package reference matches the pattern. The pattern uses 'fnmatch' style wildcards, so '--build="*"' will build everything from source. --build=~[pattern] Excluded packages, which will not be built from the source, whose package reference matches the pattern. The pattern uses 'fnmatch' style wildcards. --build=missing:[pattern] Build from source if a compatible binary does not exist, only for packages matching pattern. --build=compatible:[pattern] (Experimental) Build from source if a compatible binary does not exist, and the requested package is invalid, the closest package binary following the defined compatibility policies (method and compatibility.py) ''' def add_lockfile_args(parser): group = parser.add_argument_group("lockfile arguments") group.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " "existing 'conan.lock' file") group.add_argument("--lockfile-partial", action="store_true", help="Do not raise an error if some dependency is not found in lockfile") group.add_argument("--lockfile-out", action=OnceArgument, help="Filename of the updated lockfile") group.add_argument("--lockfile-packages", action="store_true", help=argparse.SUPPRESS) group.add_argument("--lockfile-clean", action="store_true", help="Remove unused entries from the lockfile") group.add_argument("--lockfile-overrides", help="Overwrite lockfile overrides") def add_common_install_arguments(parser): parser.add_argument("-b", "--build", action="append", help=_help_build_policies) group = parser.add_argument_group("remote arguments") exclusive_group = group.add_mutually_exclusive_group() exclusive_group.add_argument("-r", "--remote", action="append", default=None, help='Look in the specified remote or remotes server') exclusive_group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') update_help = ("Will install newer versions and/or revisions in the local cache " "for the given reference name, or all references in the graph if no argument is supplied. " "When using version ranges, it will install the latest version that " "satisfies the range. It will update to the " "latest revision for the resolved version range.") group.add_argument("-u", "--update", action="append", nargs="?", help=update_help, const="*") add_profiles_args(parser) def add_profiles_args(parser): contexts = ["build", "host"] group = parser.add_argument_group("profile arguments") # This comes from the _AppendAction code but modified to add to the contexts class ContextAllAction(argparse.Action): def __call__(self, action_parser, namespace, values, option_string=None): for context in contexts: items = getattr(namespace, self.dest + "_" + context, None) items = items[:] if items else [] items.append(values) setattr(namespace, self.dest + "_" + context, items) def create_config(short, long, example=None): group.add_argument(f"-{short}", f"--{long}", default=None, action="append", dest=f"{long}_host", metavar=long.upper(), help=f'Apply the specified {long}. ' f'By default, or if specifying -{short}:h (--{long}:host), it applies to the host context. ' f'Use -{short}:b (--{long}:build) to specify the build context, ' f'or -{short}:a (--{long}:all) to specify both contexts at once' + ('' if not example else f". Example: {example}")) for context in contexts: group.add_argument(f"-{short}:{context[0]}", f"--{long}:{context}", default=None, action="append", dest=f"{long}_{context}", help="") group.add_argument(f"-{short}:a", f"--{long}:all", default=None, action=ContextAllAction, dest=long, metavar=f"{long.upper()}_ALL", help="") create_config("pr", "profile") create_config("o", "options", '-o="pkg/*:with_qt=True"') create_config("s", "settings", '-s="compiler=gcc"') create_config("c", "conf", '-c="tools.cmake.cmaketoolchain:generator=Xcode"') def add_reference_args(parser): group = parser.add_argument_group("reference arguments") group.add_argument("--name", action=OnceArgument, help='Provide a package name if not specified in conanfile') group.add_argument("--version", action=OnceArgument, help='Provide a package version if not specified in conanfile') group.add_argument("--user", action=OnceArgument, help='Provide a user if not specified in conanfile') group.add_argument("--channel", action=OnceArgument, help='Provide a channel if not specified in conanfile') def common_graph_args(subparser): subparser.add_argument("path", nargs="?", help="Path to a folder containing a recipe (conanfile.py " "or conanfile.txt) or to a recipe file. e.g., " "./my_project/conanfile.txt. Defaults to the current " "directory when no --requires or --tool-requires is " "given", default=None) add_common_install_arguments(subparser) subparser.add_argument("--requires", action="append", help='Directly provide requires instead of a conanfile') subparser.add_argument("--tool-requires", action='append', help='Directly provide tool-requires instead of a conanfile') add_reference_args(subparser) add_lockfile_args(subparser) def validate_common_graph_args(args): if args.requires and (args.name or args.version or args.user or args.channel): raise ConanException("Can't use --name, --version, --user or --channel arguments with " "--requires") if args.path and (args.requires or args.tool_requires): raise ConanException("--requires and --tool-requires arguments are incompatible with " f"[path] '{args.path}' argument") if not args.requires and not args.tool_requires and args.path is None: args.path = "." # graph build-order command does not define a build-require argument if not args.path and getattr(args, "build_require", False): raise ConanException("--build-require should only be used with argument") ================================================ FILE: conan/cli/cli.py ================================================ import importlib import os import pkgutil import re import signal import sys import textwrap import traceback from collections import defaultdict from difflib import get_close_matches from inspect import getmembers from conan.api.conan_api import ConanAPI from conan.api.output import ConanOutput, Color, cli_out_write, LEVEL_TRACE from conan.cli.command import ConanSubCommand from conan.cli.exit_codes import SUCCESS, ERROR_MIGRATION, ERROR_GENERAL, USER_CTRL_C, \ ERROR_SIGTERM, USER_CTRL_BREAK, ERROR_INVALID_CONFIGURATION, ERROR_UNEXPECTED from conan import __version__ from conan.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError _CONAN_INTERNAL_CUSTOM_COMMANDS_PATH = "_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH" class Cli: """A single command of the conan application, with all the first level commands. Manages the parsing of parameters and delegates functionality to the conan python api. It can also show the help of the tool. """ _builtin_commands = None # Caching the builtin commands, no need to load them over and over def __init__(self, conan_api): assert isinstance(conan_api, ConanAPI), \ "Expected 'Conan' type, got '{}'".format(type(conan_api)) self._conan_api = conan_api self._conan_api.command.cli = self self._groups = defaultdict(list) self._commands = {} def add_commands(self): if Cli._builtin_commands is None: conan_cmd_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "commands") for module in pkgutil.iter_modules([conan_cmd_path]): module_name = module[1] self._add_command("conan.cli.commands.{}".format(module_name), module_name) Cli._builtin_commands = self._commands.copy() else: self._commands = Cli._builtin_commands.copy() self._groups = defaultdict(list) for k, v in self._commands.items(): # Fill groups data too self._groups[v.group].append(k) conan_custom_commands_path = os.path.join(self._conan_api.cache_folder, "extensions", "commands") # Important! This variable should be only used for testing/debugging purpose developer_custom_commands_path = os.getenv(_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH) # Notice that in case of having same custom commands file names, the developer one has # preference over the Conan default location because of the sys.path.append(xxxx) custom_commands_folders = [developer_custom_commands_path, conan_custom_commands_path] \ if developer_custom_commands_path else [conan_custom_commands_path] for custom_commands_path in custom_commands_folders: if not os.path.isdir(custom_commands_path): return sys.path.append(custom_commands_path) for module in pkgutil.iter_modules([custom_commands_path]): module_name = module[1] if module_name.startswith("cmd_"): try: self._add_command(module_name, module_name.replace("cmd_", "")) except Exception as e: ConanOutput().error(f"Error loading custom command '{module_name}.py': {e}", error_type="exception") # layers for folder in os.listdir(custom_commands_path): layer_folder = os.path.join(custom_commands_path, folder) sys.path.append(layer_folder) if not os.path.isdir(layer_folder): continue for module in pkgutil.iter_modules([layer_folder]): module_name = module[1] if module_name.startswith("cmd_"): module_path = f"{folder}.{module_name}" try: self._add_command(module_path, module_name.replace("cmd_", ""), package=folder) except Exception as e: ConanOutput().error(f"Error loading custom command {module_path}: {e}", error_type="exception") def _add_command(self, import_path, method_name, package=None): try: imported_module = importlib.import_module(import_path) command_wrapper = getattr(imported_module, method_name) if command_wrapper.doc: name = f"{package}:{command_wrapper.name}" if package else command_wrapper.name self._commands[name] = command_wrapper command_wrapper._prog = name # set the program name with possible package, if any # Avoiding duplicated command help messages if name not in self._groups[command_wrapper.group]: self._groups[command_wrapper.group].append(name) for name, value in getmembers(imported_module): if isinstance(value, ConanSubCommand): if name.startswith("{}_".format(method_name)): command_wrapper.add_subcommand(value) else: raise ConanException("The name for the subcommand method should " "begin with the main command name + '_'. " "i.e. {}_".format(method_name)) except AttributeError: raise ConanException("There is no {} method defined in {}".format(method_name, import_path)) def _print_similar(self, command): """ Looks for similar commands and prints them if found. """ output = ConanOutput() matches = get_close_matches( word=command, possibilities=self._commands.keys(), n=5, cutoff=0.75) if len(matches) == 0: return if len(matches) > 1: output.info("The most similar commands are") else: output.info("The most similar command is") for match in matches: output.info(" %s" % match) output.writeln("") def _output_help_cli(self): """ Prints a summary of all commands. """ max_len = max((len(c) for c in self._commands)) + 1 line_format = '{{: <{}}}'.format(max_len) for group_name, comm_names in sorted(self._groups.items()): cli_out_write("\n" + group_name + " commands", Color.BRIGHT_MAGENTA) for name in comm_names: # future-proof way to ensure tabular formatting cli_out_write(line_format.format(name), Color.GREEN, endline="") # Help will be all the lines up to the first empty one docstring_lines = self._commands[name].doc.split('\n') start = False data = [] for line in docstring_lines: line = line.strip() if not line: if start: break start = True continue data.append(line) txt = textwrap.fill(' '.join(data), 80, subsequent_indent=" " * (max_len + 2)) cli_out_write(txt) cli_out_write("") cli_out_write('Type "conan -h" for help', Color.BRIGHT_MAGENTA) def run(self, *args): """ Entry point for executing commands, dispatcher to class methods """ output = ConanOutput() self.add_commands() try: command_argument = args[0][0] except IndexError: # No parameters self._output_help_cli() return try: command = self._commands[command_argument] except KeyError as exc: if command_argument in ["-v", "--version"]: cli_out_write("Conan version %s" % __version__) return if command_argument in ["-h", "--help"]: self._output_help_cli() return output.info("'%s' is not a Conan command. See 'conan --help'." % command_argument) output.info("") self._print_similar(command_argument) raise ConanException("Unknown command %s" % str(exc)) try: command.run(self._conan_api, args[0][1:]) _warn_frozen_center(self._conan_api) except Exception as e: # must be a local-import to get updated value if ConanOutput.level_allowed(LEVEL_TRACE): print(traceback.format_exc(), file=sys.stderr) self._conan2_migrate_recipe_msg(e) raise @staticmethod def _conan2_migrate_recipe_msg(exception): message = str(exception) result = re.search(r"Package '(.*)' not resolved: .*: Cannot load recipe", message) if result: pkg = result.group(1) error = "*********************************************************\n" \ f"Recipe '{pkg}' seems broken.\n" \ f"It is possible that this recipe is not Conan 2.0 ready\n"\ "If the recipe comes from ConanCenter, report it at https://github.com/conan-io/conan-center-index/issues\n" \ "If it is your recipe, check if it is updated to 2.0\n" \ "*********************************************************\n" ConanOutput().writeln(error, fg=Color.BRIGHT_MAGENTA) @staticmethod def exception_exit_error(exception): output = ConanOutput() assert exception is not None if isinstance(exception, ConanInvalidConfiguration): output.error(exception, error_type="exception") return ERROR_INVALID_CONFIGURATION if isinstance(exception, ConanException): output.error(exception, error_type="exception") return ERROR_GENERAL if isinstance(exception, SystemExit): if exception.code != 0: output.error("Exiting with code: %d" % exception.code, error_type="exception") return exception.code assert isinstance(exception, Exception) output.error(traceback.format_exc(), error_type="exception") output.error(str(exception), error_type="exception") return ERROR_UNEXPECTED def _warn_frozen_center(conan_api): remotes = conan_api.remotes.list() for r in remotes: if r.url == "https://center.conan.io" and not r.disabled: ConanOutput().warning( "The remote 'https://center.conan.io' is now frozen and has been replaced by 'https://center2.conan.io'. \n" "Starting from Conan 2.9.2, the default remote is 'center2.conan.io'. \n" "It is recommended to update to the new remote using the following command:\n" f"'conan remote update {r.name} --url=\"https://center2.conan.io\"'", warn_tag="deprecated" ) break def main(args): """ main entry point of the conan application, using a Command to parse parameters Exit codes for conan command: 0: Success (done) 1: General ConanException error (done) 2: Migration error 3: Ctrl+C 4: Ctrl+Break 5: SIGTERM 6: Invalid configuration (done) """ try: conan_api = ConanAPI() except ConanMigrationError: # Error migrating sys.exit(ERROR_MIGRATION) except ConanException as e: sys.stderr.write("Error in Conan initialization: {}".format(e)) sys.exit(ERROR_GENERAL) def ctrl_c_handler(_, __): print('You pressed Ctrl+C!') sys.exit(USER_CTRL_C) def sigterm_handler(_, __): print('Received SIGTERM!') sys.exit(ERROR_SIGTERM) def ctrl_break_handler(_, __): print('You pressed Ctrl+Break!') sys.exit(USER_CTRL_BREAK) signal.signal(signal.SIGINT, ctrl_c_handler) signal.signal(signal.SIGTERM, sigterm_handler) if sys.platform == 'win32': signal.signal(signal.SIGBREAK, ctrl_break_handler) cli = Cli(conan_api) error = SUCCESS try: cli.run(args) _warn_python_version() except BaseException as e: error = cli.exception_exit_error(e) sys.exit(error) def _warn_python_version(): version = sys.version_info if version.minor == 7: ConanOutput().writeln("") ConanOutput().warning("*"*80, warn_tag="deprecated") ConanOutput().warning("Python 3.7 is end-of-life since June 2023. " "Conan future versions will drop support for it, " "please upgrade Python", warn_tag="deprecated") ConanOutput().warning("*" * 80, warn_tag="deprecated") ================================================ FILE: conan/cli/command.py ================================================ import argparse import os import textwrap from contextlib import redirect_stdout from conan.api.output import ConanOutput from conan.errors import ConanException class OnceArgument(argparse.Action): """Allows declaring a parameter that can have only one value, by default argparse takes the latest declared and it's very confusing. """ def __call__(self, parser, namespace, values, option_string=None): if getattr(namespace, self.dest) is not None and self.default is None: msg = '{o} can only be specified once'.format(o=option_string) raise argparse.ArgumentError(None, msg) setattr(namespace, self.dest, values) class SmartFormatter(argparse.HelpFormatter): def _fill_text(self, text, width, indent): text = textwrap.dedent(text) return ''.join(indent + line for line in text.splitlines(True)) class BaseConanCommand: def __init__(self, method, formatters=None): self._formatters = {"text": lambda x: None} self._method = method self._name = None if formatters: for kind, action in formatters.items(): if callable(action): self._formatters[kind] = action else: raise ConanException("Invalid formatter for {}. The formatter must be" " a valid function".format(kind)) if method.__doc__: self._doc = method.__doc__ else: raise ConanException("No documentation string defined for command: '{}'. Conan " "commands should provide a documentation string explaining " "its use briefly.".format(self._name)) @staticmethod def _init_core_options(parser): # Define possible levels, including "" for verbose possible_levels = list(ConanOutput.valid_log_levels().keys()) possible_levels.pop(possible_levels.index(None)) parser.add_argument("-v", default="status", nargs='?', help="Level of detail of the output. Valid options from less verbose " "to more verbose: -vquiet, -verror, -vwarning, -vnotice, -vstatus, " "-v or -vverbose, -vv or -vdebug, -vvv or -vtrace", choices=possible_levels, ) parser.add_argument("-cc", "--core-conf", action="append", help="Define core configuration, overwriting global.conf " "values. E.g.: -cc core:non_interactive=True") @property def _help_formatters(self): """ Formatters that are shown as available in help, 'text' formatter should not appear """ return [formatter for formatter in self._formatters if formatter != "text"] def _init_formatters(self, parser): formatters = self._help_formatters if formatters: help_message = "Select the output format: {}".format(", ".join(formatters)) parser.add_argument('-f', '--format', action=OnceArgument, help=help_message) parser.add_argument("--out-file", action=OnceArgument, help="Write the output of the command to the specified file instead of " "stdout.") @property def name(self): return self._name @property def method(self): return self._method @property def doc(self): return self._doc def _format(self, parser, info, *args): parser_args, _ = parser.parse_known_args(*args) formatarg = getattr(parser_args, "format", None) or "text" out_file = getattr(parser_args, "out_file", None) try: formatter = self._formatters[formatarg] except KeyError: raise ConanException("{} is not a known format. Supported formatters are: {}".format( formatarg, ", ".join(self._help_formatters))) if out_file: if os.path.dirname(out_file): os.makedirs(os.path.dirname(out_file), exist_ok=True) with open(out_file, 'w') as f: with redirect_stdout(f): formatter(info) ConanOutput().info(f"Formatted output saved to '{out_file}'") else: formatter(info) @staticmethod def _dispatch_errors(info): if info and isinstance(info, dict): if info.get("conan_error"): e = info["conan_error"] # Storing and launching an exception is better than the string, as it keeps # the correct backtrace for debugging. if isinstance(e, Exception): raise e raise ConanException(e) if info.get("conan_warning"): ConanOutput().warning(info["conan_warning"]) class ConanArgumentParser(argparse.ArgumentParser): def __init__(self, conan_api, *args, **kwargs): self._conan_api = conan_api super().__init__(*args, **kwargs) def parse_args(self, args=None, namespace=None): args = super().parse_args(args) ConanOutput.define_log_level(args.v) if getattr(args, "lockfile_packages", None): ConanOutput().error("The --lockfile-packages arg is private and shouldn't be used") if args.core_conf: self._conan_api._api_helpers.set_core_confs(args.core_conf) # noqa global_conf = self._conan_api._api_helpers.global_conf # noqa # TODO: This might be even better moved to the ConanAPI so users without doing custom # commands can benefit from it ConanOutput.set_warnings_as_errors(global_conf.get("core:warnings_as_errors", default=[], check_type=list)) ConanOutput.define_silence_warnings(global_conf.get("core:skip_warnings", default=[], check_type=list)) return args class ConanCommand(BaseConanCommand): def __init__(self, method, group=None, formatters=None): super().__init__(method, formatters=formatters) self._subcommands = {} self._group = group or "Other" self._name = method.__name__.replace("_", "-") self._prog = self._name def add_subcommand(self, subcommand): subcommand.set_name(self.name) self._subcommands[subcommand.name] = subcommand def run_cli(self, conan_api, *args): parser = ConanArgumentParser(conan_api, description=self._doc, prog="conan {}".format(self._prog), formatter_class=SmartFormatter) self._init_formatters(parser) self._init_core_options(parser) parser.suggest_on_error = True info = self._method(conan_api, parser, *args) if not self._subcommands: return info subcommand_parser = parser.add_subparsers(dest='subcommand', help='sub-command help') subcommand_parser.required = True subcmd = args[0][0] try: sub = self._subcommands[subcmd] except (KeyError, IndexError): # display help raise ConanException(f"Sub command {subcmd} does not exist") else: sub.set_parser(subcommand_parser, conan_api) return sub.run_cli(conan_api, parser, *args) def run(self, conan_api, *args): parser = ConanArgumentParser(conan_api, description=self._doc, prog="conan {}".format(self._prog), formatter_class=SmartFormatter) self._init_formatters(parser) self._init_core_options(parser) parser.suggest_on_error = True info = self._method(conan_api, parser, *args) if not self._subcommands: self._format(parser, info, *args) else: subcommand_parser = parser.add_subparsers(dest='subcommand', help='sub-command help') subcommand_parser.required = True try: sub = self._subcommands[args[0][0]] except (KeyError, IndexError): # display help for sub in self._subcommands.values(): sub.set_parser(subcommand_parser, conan_api) parser.parse_args(*args) else: sub.set_parser(subcommand_parser, conan_api) sub.run(conan_api, parser, *args) self._dispatch_errors(info) @property def group(self): return self._group class ConanSubCommand(BaseConanCommand): def __init__(self, method, formatters=None): super().__init__(method, formatters=formatters) self._parser = None self._subcommand_name = method.__name__.replace('_', '-') def run_cli(self, conan_api, parent_parser, *args): return self._method(conan_api, parent_parser, self._parser, *args) def run(self, conan_api, parent_parser, *args): info = self._method(conan_api, parent_parser, self._parser, *args) # It is necessary to do it after calling the "method" otherwise parser not complete self._format(parent_parser, info, *args) self._dispatch_errors(info) def set_name(self, parent_name): self._name = self._subcommand_name.replace(f'{parent_name}-', '', 1) def set_parser(self, subcommand_parser, conan_api): self._parser = subcommand_parser.add_parser(self._name, conan_api=conan_api, help=self._doc) self._parser.description = self._doc self._init_formatters(self._parser) self._init_core_options(self._parser) self._parser.suggest_on_error = True def conan_command(group=None, formatters=None): return lambda f: ConanCommand(f, group, formatters=formatters) def conan_subcommand(formatters=None): return lambda f: ConanSubCommand(f, formatters=formatters) ================================================ FILE: conan/cli/commands/__init__.py ================================================ ================================================ FILE: conan/cli/commands/audit.py ================================================ import json import os from conan.api.conan_api import ConanAPI from conan.api.input import UserInput from conan.api.model import MultiPackagesList, RecipeReference from conan.api.output import cli_out_write, ConanOutput from conan.api.subapi.audit import CONAN_CENTER_AUDIT_PROVIDER_NAME from conan.cli import make_abs_path from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.command import conan_command, conan_subcommand from conan.cli.formatters.audit.vulnerabilities import text_vuln_formatter, json_vuln_formatter, \ html_vuln_formatter from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_basic from conan.errors import ConanException from conan.internal.util.files import load def _add_provider_arg(subparser): subparser.add_argument("-p", "--provider", help="Provider to use for scanning") def _parse_error_threshold(result: dict, error_level: float) -> None: """Mark the result as error if any of the vulnerabilities has a severity greater than or equal to The error_level reflects the severity level configured by the user. As it uses float and CVSS score is limited to 10.0, users can use any higher number to skip it. :param result: Conan audit scan result. It's expected to find cvss there :param error_level: Threshold to raise an error in case of matching the severity level :return: None """ if "conan_error" not in result: for ref in result["data"]: if "vulnerabilities" not in result["data"][ref]: continue for edge in result["data"][ref]["vulnerabilities"]["edges"]: preferred_base_score = float(edge["node"]["cvss"].get("preferredBaseScore", 0.0)) if preferred_base_score >= error_level: result.update( {"conan_error": f"The package {ref} has a CVSS score {preferred_base_score} and " f"exceeded the threshold severity level {error_level}."}) break @conan_subcommand(formatters={"text": text_vuln_formatter, "json": json_vuln_formatter, "html": html_vuln_formatter}) def audit_scan(conan_api: ConanAPI, parser, subparser, *args) -> dict: """ Scan a given recipe for vulnerabilities in its dependencies. """ common_graph_args(subparser) # Needed for the validation of args, but this should usually be left as False here # TODO: Do we then want to hide it in the --help? subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') subparser.add_argument("-sl", "--severity-level", action="store", default=9.0, type=float, help="Set threshold for severity level to raise an error. " "By default raises an error for any critical CVSS (9.0 or higher). " " Use 100.0 to disable it.") subparser.add_argument("--context", help="Context to scan, by default both contexts are scanned " "if not specified", choices=["host", "build"], default=None) _add_provider_arg(subparser) args = parser.parse_args(*args) # This comes from install command validate_common_graph_args(args) # basic paths cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None # Basic collaborators: remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) # Graph computation (without installation of binaries) gapi = conan_api.graph if path: deps_graph = gapi.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, # TO DISCUSS: defaulting to False the is_build_require args.update, is_build_require=args.build_require) else: deps_graph = gapi.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update) print_graph_basic(deps_graph) deps_graph.report_graph_error() if deps_graph.error: return {"conan_error": deps_graph.error} provider = conan_api.audit.get_provider(args.provider or CONAN_CENTER_AUDIT_PROVIDER_NAME) scan_result = conan_api.audit.scan(deps_graph, provider, args.context) _parse_error_threshold(scan_result, args.severity_level) return scan_result @conan_subcommand(formatters={"text": text_vuln_formatter, "json": json_vuln_formatter, "html": html_vuln_formatter}) def audit_list(conan_api: ConanAPI, parser, subparser, *args): """ List the vulnerabilities of the given reference. """ input_group = subparser.add_mutually_exclusive_group(required=True) input_group.add_argument("reference", help="Reference to list vulnerabilities for", nargs="?") input_group.add_argument("-l", "--list", help="Package list file to list vulnerabilities for") input_group.add_argument("-s", "--sbom", help="SBOM file to list vulnerabilities for") input_group.add_argument("-lock", "--lockfile", help="Path to the lockfile to check for vulnerabilities") subparser.add_argument("-r", "--remote", help="Remote to use for listing") _add_provider_arg(subparser) args = parser.parse_args(*args) provider = conan_api.audit.get_provider(args.provider or CONAN_CENTER_AUDIT_PROVIDER_NAME) if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) cache_name = "Local Cache" if not args.remote else args.remote package_list = multi_package_list[cache_name] refs_to_list = package_list.serialize() references = list(refs_to_list.keys()) if not references: # the package list might contain only refs, no revs ConanOutput().warning("Nothing to list, package list does not contain recipe revisions") elif args.sbom: sbom_file = make_abs_path(args.sbom) sbom = json.loads(load(sbom_file)) if sbom.get("bomFormat") != "CycloneDX": raise ConanException(f"Unsupported SBOM format, only CycloneDX is supported.") purls = [component["purl"] for component in sbom["components"]] references = [purl.split("pkg:conan/")[1].replace("@", "/") for purl in purls] elif args.lockfile: lock_file = make_abs_path(args.lockfile) lockfile = conan_api.lockfile.get_lockfile(lock_file).serialize() references = [str(RecipeReference.loads(ref[0] if isinstance(ref, tuple) else ref)) for ref in lockfile["requires"]] else: references = [args.reference] return conan_api.audit.list(references, provider) def _text_provider_formatter(providers_action): providers = providers_action[0] action = providers_action[1] if action == "remove": cli_out_write("Provider removed successfully.") elif action == "add": cli_out_write("Provider added successfully.") elif action == "auth": cli_out_write("Provider authentication added.") elif action == "list": if not providers: cli_out_write("No providers found.") else: for provider in providers: if provider: cli_out_write(f"{provider.name} (type: {provider.type}) - {provider.url}") def _json_provider_formatter(providers_action): ret = [] for provider in providers_action[0]: if provider: ret.append({"name": provider.name, "url": provider.url, "type": provider.type}) cli_out_write(json.dumps(ret, indent=4)) @conan_subcommand(formatters={"text": _text_provider_formatter, "json": _json_provider_formatter}) def audit_provider(conan_api, parser, subparser, *args): """ Manage security providers for the 'conan audit' command. """ subparser.add_argument("action", choices=["add", "list", "auth", "remove"], help="Action to perform from 'add', 'list' , 'remove' or 'auth'") subparser.add_argument("name", help="Provider name", nargs="?") subparser.add_argument("--url", help="Provider URL") subparser.add_argument("--type", help="Provider type", choices=["conan-center-proxy", "private"]) subparser.add_argument("--token", help="Provider token") args = parser.parse_args(*args) if args.action == "add": if not args.name or not args.url or not args.type: raise ConanException("Name, URL and type are required to add a provider") if " " in args.name: raise ConanException("Name cannot contain spaces") conan_api.audit.add_provider(args.name, args.url, args.type) if not args.token: user_input = UserInput(conan_api.config.get("core:non_interactive")) ConanOutput().write(f"Please enter a token for {args.name} the provider: ") token = user_input.get_password() else: token = args.token provider = conan_api.audit.get_provider(args.name) if token: conan_api.audit.auth_provider(provider, token) return [provider], args.action elif args.action == "remove": if not args.name: raise ConanException("Name required to remove a provider") conan_api.audit.remove_provider(args.name) return [], args.action elif args.action == "list": providers = conan_api.audit.list_providers() return providers, args.action elif args.action == "auth": if not args.name: raise ConanException("Name is required to authenticate on a provider") if not args.token: user_input = UserInput(conan_api.config.get("core:non_interactive")) ConanOutput().write(f"Please enter a token for {args.name} the provider: ") token = user_input.get_password() else: token = args.token provider = conan_api.audit.get_provider(args.name) conan_api.audit.auth_provider(provider, token) return [provider], args.action @conan_command(group="Security") def audit(conan_api, parser, *args): # noqa """ Find vulnerabilities in your dependencies. """ ================================================ FILE: conan/cli/commands/build.py ================================================ import os from conan.api.output import ConanOutput from conan.cli.command import conan_command from conan.cli.formatters.graph import format_graph_json from conan.cli import make_abs_path from conan.cli.args import add_lockfile_args, add_common_install_arguments, add_reference_args from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_packages, print_graph_basic @conan_command(group='Creator', formatters={"json": format_graph_json}) def build(conan_api, parser, *args): """ Install dependencies and call the build() method. """ parser.add_argument("path", help='Path to a python-based recipe file or a folder ' 'containing a conanfile.py recipe. conanfile.txt ' 'cannot be used with conan build. ' 'Defaults to current directory', default=".", nargs='?') add_reference_args(parser) parser.add_argument("-g", "--generator", action="append", help='Generators to use') parser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') parser.add_argument("-d", "--deployer", action="append", help="Deploy using the provided deployer to the output folder. " "Built-in deployers: 'full_deploy', 'direct_deploy', 'runtime_deploy'") parser.add_argument("--deployer-folder", help="Deployer output folder, base build folder by default if not set") parser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided path is a build-require') parser.add_argument("--envs-generation", default=None, choices=["false"], help="Generation strategy for virtual environment files for the root") add_common_install_arguments(parser) add_lockfile_args(parser) args = parser.parse_args(*args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) source_folder = os.path.dirname(path) output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None deployer_folder = make_abs_path(args.deployer_folder, cwd) if args.deployer_folder else None # Basic collaborators: remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) deps_graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, is_build_require=args.build_require) print_graph_basic(deps_graph) deps_graph.report_graph_error() conan_api.graph.analyze_binaries(deps_graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) out = ConanOutput() conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) out.title("Finalizing install (deploy, generators)") conan_api.install.install_consumer(deps_graph, args.generator, source_folder, output_folder, deploy=args.deployer, deploy_folder=deployer_folder, envs_generation=args.envs_generation) out.title("Calling build()") conanfile = deps_graph.root.conanfile conan_api.local.build(conanfile) lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, source_folder) return {"graph": deps_graph} ================================================ FILE: conan/cli/commands/cache.py ================================================ import json from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, MultiPackagesList from conan.api.output import cli_out_write, ConanOutput from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.commands.list import print_list_text, print_list_json, print_serial from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference def _get_package_sign_error(pkg_list): conan_exception = ConanException("There were some errors in the package signing process. " "Please check the output.") for rref, packages in pkg_list.items(): recipe_bundle = pkg_list.recipe_dict(rref) if recipe_bundle.get("pkgsign_error"): return conan_exception for pref in packages: pkg_bundle = pkg_list.package_dict(pref) if pkg_bundle: if pkg_bundle.get("pkgsign_error"): return conan_exception return None def print_package_sign_text(data): results_dict = data.get("results", {}) signs = [] for ref_data in results_dict.values(): for revision_data in ref_data.get("revisions", {}).values(): sign = revision_data.get("pkgsign_error") signs.append(sign) for pkg in revision_data.get("packages", {}).values(): for prev in pkg.get("revisions", {}).values(): sign = prev.get("pkgsign_error") signs.append(sign) remove_keys = {"info", "timestamp", "files"} def clean_inline(obj): if not isinstance(obj, dict): return obj cleaned = {k: clean_inline(v) for k, v in obj.items() if k not in remove_keys} if "packages" in cleaned and not cleaned["packages"]: del cleaned["packages"] return cleaned items = {ref: clean_inline(item) for ref, item in results_dict.items() if item} # Output cli_out_write(f"[Package sign] Results:\n") print_serial(items) # Summary fail = sum((s is not None) for s in signs) ok = len(signs) - fail cli_out_write(f"\n[Package sign] Summary: OK={ok}, FAILED={fail}") def json_export(data): cli_out_write(json.dumps({"cache_path": data})) @conan_command(group="Consumer") def cache(conan_api: ConanAPI, parser, *args): """ Perform file operations in the local cache (of recipes and/or packages). """ pass @conan_subcommand(formatters={"text": cli_out_write, "json": json_export}) def cache_path(conan_api: ConanAPI, parser, subparser, *args): """ Show the path to the Conan cache for a given reference. """ subparser.add_argument("reference", help="Recipe reference or Package reference") subparser.add_argument("--folder", choices=['export_source', 'source', 'build', 'metadata'], help="Path to show. The 'build' requires a package reference. " "If the argument is not passed, it shows 'exports' path for recipe references " "and 'package' folder for package references.") args = parser.parse_args(*args) try: pref = PkgReference.loads(args.reference) except ConanException: pref = None if not pref: # Not a package reference ref = RecipeReference.loads(args.reference) if args.folder is None: path = conan_api.cache.export_path(ref) elif args.folder == "export_source": path = conan_api.cache.export_source_path(ref) elif args.folder == "source": path = conan_api.cache.source_path(ref) elif args.folder == "metadata": path = conan_api.cache.recipe_metadata_path(ref) else: raise ConanException(f"'--folder {args.folder}' requires a valid package reference") else: if args.folder is None: path = conan_api.cache.package_path(pref) elif args.folder == "build": path = conan_api.cache.build_path(pref) elif args.folder == "metadata": path = conan_api.cache.package_metadata_path(pref) else: raise ConanException(f"'--folder {args.folder}' requires a recipe reference") return path @conan_subcommand(formatters={"text": cli_out_write}) def cache_ref(conan_api: ConanAPI, parser, subparser, *args): """ Show the reference for a given Conan cache folder """ subparser.add_argument("path", help="Path to a Conan cache folder") args = parser.parse_args(*args) ref = conan_api.cache.path_to_ref(args.path) if ref is None: raise ConanException("Reference for this path not found in cache") return ref.repr_notime() @conan_subcommand() def cache_clean(conan_api: ConanAPI, parser, subparser, *args): """ Remove non-critical folders from the cache, like source, build and/or download (.tgz store) ones. """ subparser.add_argument("pattern", nargs="?", help="Selection pattern for references to clean") subparser.add_argument("-l", "--list", action=OnceArgument, help="Package list of packages to clean") subparser.add_argument("-s", "--source", action='store_true', default=False, help="Clean source folders") subparser.add_argument("-b", "--build", action='store_true', default=False, help="Clean build folders") subparser.add_argument("-d", "--download", action='store_true', default=False, help="Clean download and metadata folders") subparser.add_argument("-t", "--temp", action='store_true', default=False, help="Clean temporary folders") subparser.add_argument("-bs", "--backup-sources", action='store_true', default=False, help="Clean backup sources") subparser.add_argument('-p', '--package-query', action=OnceArgument, help="Remove only the packages matching a specific query, e.g., " "os=Windows AND (arch=x86 OR compiler=gcc)") args = parser.parse_args(*args) # If pattern is None, it will be replaced by "*" if args.pattern and args.list: raise ConanException("Cannot specify both pattern and list") if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list["Local Cache"] else: ref_pattern = ListPattern(args.pattern or "*", rrev="*", package_id="*", prev="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) if args.build or args.source or args.download or args.temp or args.backup_sources: conan_api.cache.clean(package_list, source=args.source, build=args.build, download=args.download, temp=args.temp, backup_sources=args.backup_sources) else: conan_api.cache.clean(package_list) def print_list_check_integrity_json(data): results = data["results"] myjson = json.dumps(results, indent=4) cli_out_write(myjson) @conan_subcommand(formatters={"text": lambda _: (), "json": print_list_check_integrity_json}) def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args): """ Check the integrity of the local cache for the given references """ subparser.add_argument("pattern", nargs="?", help="Selection pattern for references to check integrity for") subparser.add_argument("-l", "--list", action=OnceArgument, help="Package list of packages to check integrity for") subparser.add_argument('-p', '--package-query', action=OnceArgument, help="Only the packages matching a specific query, e.g., " "os=Windows AND (arch=x86 OR compiler=gcc)") args = parser.parse_args(*args) if args.pattern is None and args.list is None: raise ConanException("Missing pattern or package list file") if args.pattern and args.list: raise ConanException("Cannot specify both pattern and list") if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list["Local Cache"] else: ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) corrupted_artifacts = conan_api.cache.check_integrity(package_list, return_pkg_list=True) return {"results": {"Local Cache": corrupted_artifacts.serialize()}, "conan_error": "There are corrupted artifacts, check the error logs" if corrupted_artifacts else ""} @conan_subcommand(formatters={"text": print_package_sign_text, "json": print_list_json}) def cache_sign(conan_api: ConanAPI, parser, subparser, *args): """ Sign packages with the Package Signing Plugin """ subparser.add_argument("pattern", nargs="?", help="Selection pattern for references to be signed") subparser.add_argument("-l", "--list", action=OnceArgument, help="Package list of packages to be signed") subparser.add_argument('-p', '--package-query', action=OnceArgument, help="Only the packages matching a specific query, e.g., " "os=Windows AND (arch=x86 OR compiler=gcc)") args = parser.parse_args(*args) if args.pattern is None and args.list is None: raise ConanException("Missing pattern or package list file") if args.pattern and args.list: raise ConanException("Cannot specify both pattern and list") if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list["Local Cache"] else: ref_pattern = ListPattern(args.pattern, package_id="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) if not dict(package_list.items()): raise ConanException("No packages to process in the package list provided") conan_api.cache.sign(package_list) return { "conan_error": _get_package_sign_error(package_list), "results": package_list.serialize() } @conan_subcommand(formatters={"text": print_package_sign_text, "json": print_list_json}) def cache_verify(conan_api: ConanAPI, parser, subparser, *args): """ Check the signature of packages with the Package Signing Plugin """ subparser.add_argument("pattern", nargs="?", help="Selection pattern for references to verify their signature") subparser.add_argument("-l", "--list", action=OnceArgument, help="Package list of packages to verify their signature") subparser.add_argument('-p', '--package-query', action=OnceArgument, help="Only the packages matching a specific query, e.g., " "os=Windows AND (arch=x86 OR compiler=gcc)") args = parser.parse_args(*args) if args.pattern is None and args.list is None: raise ConanException("Missing pattern or package list file") if args.pattern and args.list: raise ConanException("Cannot specify both pattern and list") if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list["Local Cache"] else: ref_pattern = ListPattern(args.pattern, package_id="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) if not dict(package_list.items()): raise ConanException("No packages to process in the package list provided") conan_api.cache.verify(package_list) return { "conan_error": _get_package_sign_error(package_list), "results": package_list.serialize() } @conan_subcommand(formatters={"text": print_list_text, "json": print_list_json}) def cache_save(conan_api: ConanAPI, parser, subparser, *args): """ Get the artifacts from a package list and archive them """ subparser.add_argument('pattern', nargs="?", help="A pattern in the form 'pkg/version#revision:package_id#revision', " "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. " "If revision is not specified, it is assumed latest one.") subparser.add_argument("-l", "--list", help="Package list of packages to save") subparser.add_argument('--file', help="Save to this file. Allowed extensions .tgz, .txz, .tzst" " (.txz and .tzst experimental and .tzst requires " "Python>=3.14)") subparser.add_argument("--no-source", action="store_true", help="Exclude the sources") args = parser.parse_args(*args) if args.pattern is None and args.list is None: raise ConanException("Missing pattern or package list file") if args.pattern and args.list: raise ConanException("Cannot define both the pattern and the package list file") if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list["Local Cache"] else: ref_pattern = ListPattern(args.pattern) package_list = conan_api.list.select(ref_pattern) tgz_path = make_abs_path(args.file or "conan_cache_save.tgz") conan_api.cache.save(package_list, tgz_path, args.no_source) return {"results": {"Local Cache": package_list.serialize()}} @conan_subcommand(formatters={"text": print_list_text, "json": print_list_json}) def cache_restore(conan_api: ConanAPI, parser, subparser, *args): """ Put the artifacts from an archive into the cache """ subparser.add_argument("file", help="Path to archive to restore") args = parser.parse_args(*args) path = make_abs_path(args.file) package_list = conan_api.cache.restore(path) return {"results": {"Local Cache": package_list.serialize()}} @conan_subcommand() def cache_backup_upload(conan_api: ConanAPI, parser, subparser, *args): """ Upload all the source backups present in the cache """ parser.parse_args(*args) files = conan_api.cache.get_backup_sources() conan_api.upload.upload_backup_sources(files) ================================================ FILE: conan/cli/commands/config.py ================================================ import os from conan.api.model import Remote from conan.api.output import cli_out_write from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.formatters import default_json_formatter from conan.errors import ConanException @conan_command(group='Consumer') def config(conan_api, parser, *args): # noqa """ Manage the Conan configuration in the Conan home. """ @conan_subcommand() def config_install(conan_api, parser, subparser, *args): """ Install the configuration (remotes, profiles, conf), from git, http or a folder, into the Conan home folder. """ subparser.add_argument("item", help="git repository, local file or folder or zip file (local or " "http) where the configuration is stored") ssl_subgroup = subparser.add_mutually_exclusive_group() ssl_subgroup.add_argument("--verify-ssl", nargs="?", default="True", help='Verify SSL connection when downloading file') ssl_subgroup.add_argument("--insecure", action="store_false", default=None, help="Allow insecure server connections when using SSL. " "Equivalent to --verify-ssl=False", dest="verify_ssl") subparser.add_argument("-t", "--type", choices=["git", "dir", "file", "url"], help='Type of remote config') subparser.add_argument("-a", "--args", help='String with extra arguments for "git clone"') subparser.add_argument("-sf", "--source-folder", help='Install files only from a source subfolder from the ' 'specified origin') subparser.add_argument("-tf", "--target-folder", help='Install to that path in the conan cache') args = parser.parse_args(*args) def get_bool_from_text(value): # TODO: deprecate this value = value.lower() if value in ["1", "yes", "y", "true"]: return True if value in ["0", "no", "n", "false"]: return False raise ConanException("Unrecognized boolean value '%s'" % value) verify_ssl = args.verify_ssl if isinstance(args.verify_ssl, bool) \ else get_bool_from_text(args.verify_ssl) conan_api.config.install(args.item, verify_ssl, args.type, args.args, source_folder=args.source_folder, target_folder=args.target_folder) @conan_subcommand() def config_install_pkg(conan_api, parser, subparser, *args): """ (Experimental) Install the configuration (remotes, profiles, conf), from a Conan package or from a conanconfig.yml file """ subparser.add_argument("reference", nargs="?", help="Package reference 'pkg/version' to install configuration from " "or path to 'conanconfig.yml' file") subparser.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " "existing 'conan.lock' file") subparser.add_argument("--lockfile-partial", action="store_true", help="Do not raise an error if some dependency is not found in lockfile") subparser.add_argument("--lockfile-out", action=OnceArgument, help="Filename of the updated lockfile") subparser.add_argument("-f", "--force", action='store_true', help="Force the re-installation of configuration") subparser.add_argument("--url", action=OnceArgument, help="(Experimental) Provide Conan repository URL " "(for first install without remotes)") subparser.add_argument("-pr", "--profile", help="Profile to install config") subparser.add_argument("-s", "--settings", action="append", help="Settings to install config") subparser.add_argument("-o", "--options", action="append", help="Options to install config") args = parser.parse_args(*args) path = make_abs_path(args.reference or ".") if os.path.isdir(path): path = os.path.join(path, "conanconfig.yml") path = path if os.path.exists(path) else None if path is None and args.reference is None: raise ConanException("Must provide a package reference or a path to a conanconfig.yml file") lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, partial=args.lockfile_partial) try: default_profile = args.profile or conan_api.profiles.get_default_build() except ConanException: # it can fail if the default profile doesn't exist yet default_profile = None profiles = [default_profile] if default_profile else [] profile = conan_api.profiles.get_profile(profiles, args.settings, args.options) remotes = [Remote("config_install_url", url=args.url)] if args.url else None if path: conanconfig = path refs = conan_api.config.install_conanconfig(conanconfig, lockfile=lockfile, force=args.force, remotes=remotes, profile=profile) else: refs = conan_api.config.install_package(args.reference, lockfile=lockfile, force=args.force, remotes=remotes, profile=profile) lockfile = conan_api.lockfile.add_lockfile(lockfile, config_requires=refs) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out) def _list_text_formatter(confs): for k, v in confs.items(): cli_out_write(f"{k}: {v}") @conan_subcommand(formatters={"text": cli_out_write}) def config_home(conan_api, parser, subparser, *args): # noqa """ Show the Conan home folder. """ parser.parse_args(*args) return conan_api.config.home() @conan_subcommand(formatters={"text": _list_text_formatter, "json": default_json_formatter}) def config_list(conan_api, parser, subparser, *args): """ Show all the Conan available configurations: core and tools. """ subparser.add_argument('pattern', nargs="?", help="Filter configuration items that matches this pattern") args = parser.parse_args(*args) confs = conan_api.config.conf_list() if args.pattern: p = args.pattern.lower() confs = {k: v for k, v in confs.items() if p in k.lower() or p in v.lower()} return confs @conan_subcommand(formatters={"text": _list_text_formatter, "json": default_json_formatter}) def config_show(conan_api, parser, subparser, *args): """ Get the value of the specified conf """ subparser.add_argument('pattern', help='Conf item(s) pattern for which to query their value') args = parser.parse_args(*args) return conan_api.config.show(args.pattern) @conan_subcommand() def config_clean(conan_api, parser, subparser, *args): # noqa """ (Experimental) Clean the configuration files in the Conan home folder, while keeping installed packages """ parser.parse_args(*args) conan_api.config.clean() ================================================ FILE: conan/cli/commands/create.py ================================================ import os import shutil from conan.api.output import ConanOutput from conan.cli.args import add_lockfile_args, add_common_install_arguments from conan.cli.command import conan_command, OnceArgument from conan.cli.commands.export import common_args_export from conan.cli.formatters.graph import format_graph_json from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_packages, print_graph_basic from conan.errors import ConanException @conan_command(group="Creator", formatters={"json": format_graph_json}) def create(conan_api, parser, *args): """ Create a package. """ common_args_export(parser) add_lockfile_args(parser) add_common_install_arguments(parser) parser.add_argument("--build-require", action='store_true', default=False, help='Whether the package being created is a build-require (to be used' ' as tool_requires() by other packages)') parser.add_argument("-tf", "--test-folder", action=OnceArgument, help='Alternative test folder name. By default it is "test_package". ' 'Use "" to skip the test stage') parser.add_argument("-tm", "--test-missing", action='store_true', default=False, help='Run the test_package checks only if the package is built from source' ' but not if it already existed (using --build=missing)') parser.add_argument("-bt", "--build-test", action="append", help="Same as '--build' but only for the test_package requires. By default" " if not specified it will take the '--build' value if specified") raw_args = args[0] args = parser.parse_args(*args) if args.test_missing and args.test_folder == "": raise ConanException('--test-folder="" is incompatible with --test-missing') cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) ref, conanfile = conan_api.export.export(path=path, name=args.name, version=args.version, user=args.user, channel=args.channel, lockfile=lockfile, remotes=remotes) # FIXME: Dirty: package type still raw, not processed yet is_build = args.build_require or conanfile.package_type == "build-scripts" # The package_type is not fully processed at export is_python_require = conanfile.package_type == "python-require" lockfile = conan_api.lockfile.update_lockfile_export(lockfile, conanfile, ref, is_build) print_profiles(profile_host, profile_build) runner = conan_api.command.get_runner(profile_host) if runner is not None: return runner(conan_api, 'create', profile_host, profile_build, args, raw_args).run() if args.build is not None and args.build_test is None: args.build_test = args.build install_error = None if is_python_require: deps_graph = conan_api.graph.load_graph_requires([], [], profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, update=args.update, python_requires=[ref]) else: requires = [ref] if not is_build else None tool_requires = [ref] if is_build else None if conanfile.vendor: # Automatically allow repackaging for conan create pr = profile_build if is_build else profile_host pr.conf.update("&:tools.graph:vendor", "build") deps_graph = conan_api.graph.load_graph_requires(requires, tool_requires, profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, update=args.update) print_graph_basic(deps_graph) deps_graph.report_graph_error() # Not specified, force build the tested library build_modes = [ref.repr_notime()] if args.build is None else args.build if args.build is None and conanfile.build_policy == "never": raise ConanException( "This package cannot be created, 'build_policy=never', it can only be 'export-pkg'") conan_api.graph.analyze_binaries(deps_graph, build_modes, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) install_error = conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes, return_install_error=True) # We update the lockfile, so it will be updated for later ``test_package`` lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) test_package_folder = getattr(conanfile, "test_package_folder", None) \ if args.test_folder is None else args.test_folder test_conanfile_path = _get_test_conanfile_path(test_package_folder, path) # If the user provide --test-missing and the binary was not built from source, skip test_package if args.test_missing and deps_graph.root.edges\ and deps_graph.root.edges[0].dst.binary != "Build": test_conanfile_path = None # disable it if test_conanfile_path and not install_error: # TODO: We need arguments for: # - decide update policy "--test_package_update" # If it is a string, it will be injected always, if it is a RecipeReference, then it will # be replaced only if ``python_requires = "tested_reference_str"`` tested_python_requires = ref.repr_notime() if is_python_require else ref from conan.cli.commands.test import run_test # The test_package do not make the "conan create" command return a different graph or # produce a different lockfile. The result is always the same, irrespective of test_package run_test(conan_api, test_conanfile_path, ref, profile_host, profile_build, remotes, lockfile, update=None, build_modes=args.build, build_modes_test=args.build_test, tested_python_requires=tested_python_requires, tested_graph=deps_graph) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) return {"graph": deps_graph, "conan_api": conan_api, "conan_error": install_error} def _check_tested_reference_matches(deps_graph, tested_ref, out): """ Check the test_profile_override_conflict test. If we are testing a build require but we specify the build require with a different version in the profile, it has priority, it is correct but weird and likely a mistake""" # https://github.com/conan-io/conan/issues/10453 direct_refs = [n.conanfile.ref for n in deps_graph.root.neighbors()] # There is a reference with same name but different missmatch = [ref for ref in direct_refs if ref.name == tested_ref.name and ref != tested_ref] if missmatch: out.warning("The package created was '{}' but the reference being " "tested is '{}'".format(missmatch[0], tested_ref)) def test_package(conan_api, deps_graph, test_conanfile_path): out = ConanOutput() out.title("Testing the package") # TODO: Better modeling when we are testing a python_requires conanfile = deps_graph.root.conanfile if len(deps_graph.nodes) == 1 and not hasattr(conanfile, "python_requires"): raise ConanException("The conanfile at '{}' doesn't declare any requirement, " "use `self.tested_reference_str` to require the " "package being created.".format(test_conanfile_path)) conanfile_folder = os.path.dirname(test_conanfile_path) # To make sure the folders are correct conanfile.folders.set_base_folders(conanfile_folder, output_folder=None) if conanfile.build_folder and conanfile.build_folder != conanfile.source_folder: # should be the same as build folder, but we can remove it out.info("Removing previously existing 'test_package' build folder: " f"{conanfile.build_folder}") shutil.rmtree(conanfile.build_folder, ignore_errors=True) os.makedirs(conanfile.build_folder, exist_ok=True) conanfile.output.info(f"Test package build: {conanfile.folders.build}") conanfile.output.info(f"Test package build folder: {conanfile.build_folder}") conan_api.install.install_consumer(deps_graph=deps_graph, source_folder=conanfile_folder) out.title("Testing the package: Building") conan_api.local.build(conanfile) out.title("Testing the package: Executing test") conanfile.output.highlight("Running test()") conan_api.local.test(conanfile) def _get_test_conanfile_path(tf, conanfile_path): """Searches in the declared test_folder or in the standard "test_package" """ if tf == "": # Now if parameter --test-folder="" we have to skip tests return None base_folder = os.path.dirname(conanfile_path) test_conanfile_path = os.path.join(base_folder, tf or "test_package", "conanfile.py") if os.path.exists(test_conanfile_path): return test_conanfile_path elif tf: raise ConanException(f"test folder '{tf}' not available, or it doesn't have a conanfile.py") ================================================ FILE: conan/cli/commands/download.py ================================================ from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, MultiPackagesList from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.cli.command import conan_command, OnceArgument from conan.cli.commands.list import print_list_text, print_list_json from conan.errors import ConanException @conan_command(group="Creator", formatters={"text": print_list_text, "json": print_list_json}) def download(conan_api: ConanAPI, parser, *args): """ Download (without installing) a single conan package from a remote server. It downloads just the package, but not its transitive dependencies, and it will not call any generate, generators or deployers. It can download multiple packages if patterns are used, and also works with queries over the package binaries. """ parser.add_argument('pattern', nargs="?", help="A pattern in the form 'pkg/version#revision:package_id#revision', " "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. " "If revision is not specified, it is assumed latest one.") parser.add_argument("--only-recipe", action='store_true', default=False, help='Download only the recipe/s, not the binary packages.') parser.add_argument('-p', '--package-query', default=None, action=OnceArgument, help="Only download packages matching a specific query. e.g: os=Windows AND " "(arch=x86 OR compiler=gcc)") parser.add_argument("-r", "--remote", action=OnceArgument, required=True, help='Download from this specific remote') parser.add_argument("-m", "--metadata", action='append', help='Download the metadata matching the pattern, even if the package is ' 'already in the cache and not downloaded') parser.add_argument("-l", "--list", help="Package list file") args = parser.parse_args(*args) if args.pattern is None and args.list is None: raise ConanException("Missing pattern or package list file") if args.pattern and args.list: raise ConanException("Cannot define both the pattern and the package list file") remote = conan_api.remotes.get(args.remote) if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) try: package_list = multi_package_list[remote.name] except KeyError: raise ConanException(f"The current package list does not contain remote '{remote.name}'") if args.only_recipe: package_list.only_recipes() else: ref_pattern = ListPattern(args.pattern, package_id="*", only_recipe=args.only_recipe) package_list = conan_api.list.select(ref_pattern, args.package_query, remote) if package_list: conan_api.download.download_full(package_list, remote, args.metadata) else: ConanOutput().warning(f"No packages were downloaded because the package list is empty.") return {"results": {"Local Cache": package_list.serialize()}} ================================================ FILE: conan/cli/commands/editable.py ================================================ import json import os from conan.api.output import ConanOutput, cli_out_write from conan.cli.args import add_reference_args from conan.cli.command import conan_command, conan_subcommand @conan_command(group="Creator") def editable(conan_api, parser, *args): """ Allow working with a package that resides in user folder. """ @conan_subcommand() def editable_add(conan_api, parser, subparser, *args): """ Define the given location as the package , so when this package is required, it is used from this location instead of the cache. """ subparser.add_argument('path', help='Path to the package folder in the user workspace', default=".", nargs='?') add_reference_args(subparser) subparser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') group = subparser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", action="append", default=None, help='Look in the specified remote or remotes server') group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') args = parser.parse_args(*args) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] cwd = os.getcwd() ref = conan_api.local.editable_add(args.path, args.name, args.version, args.user, args.channel, cwd, args.output_folder, remotes=remotes) ConanOutput().success("Reference '{}' in editable mode".format(ref)) @conan_subcommand() def editable_remove(conan_api, parser, subparser, *args): """ Remove the "editable" mode for this reference. """ subparser.add_argument("path", nargs="?", help="Path to a folder containing a recipe conanfile.py " "or to a recipe file. e.g., " "./my_project/conanfile.py.", default=None) subparser.add_argument("-r", "--refs", action="append", help='Directly provide reference patterns') args = parser.parse_args(*args) if not args.refs and args.path is None: args.path = "." # TODO: Fix this API to use get_conanfile_path editables = conan_api.local.editable_remove(args.path, args.refs) out = ConanOutput() if editables: for ref, info in editables.items(): out.success(f"Removed editable '{ref}': {info['path']}") else: out.warning("No editables were removed") def print_editables_json(data): results = {str(k): v for k, v in data.items()} myjson = json.dumps(results, indent=4) cli_out_write(myjson) def print_editables_text(data): for k, v in data.items(): cli_out_write("%s" % k) cli_out_write(" Path: %s" % v["path"]) if v.get("output_folder"): cli_out_write(" Output: %s" % v["output_folder"]) @conan_subcommand(formatters={"text": print_editables_text, "json": print_editables_json}) def editable_list(conan_api, parser, subparser, *args): """ List all the packages in editable mode. """ parser.parse_args(*args) editables = conan_api.local.editable_list() return editables ================================================ FILE: conan/cli/commands/export.py ================================================ import json import os from conan.api.model import MultiPackagesList, PackagesList from conan.api.output import cli_out_write from conan.cli.command import conan_command, OnceArgument from conan.cli.args import add_reference_args def common_args_export(parser): parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py). " "Defaults to current directory", default=".", nargs="?") add_reference_args(parser) def json_export(data): cli_out_write(json.dumps({"reference": data["reference"].repr_notime()})) def pkglist_export(data): cli_out_write(json.dumps(data["pkglist"], indent=4)) @conan_command(group="Creator", formatters={"json": json_export, "pkglist": pkglist_export}) def export(conan_api, parser, *args): """ Export a recipe to the Conan package cache. """ common_args_export(parser) group = parser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", action="append", default=None, help='Look in the specified remote or remotes server') group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') parser.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile.") parser.add_argument("--lockfile-out", action=OnceArgument, help="Filename of the updated lockfile") parser.add_argument("--lockfile-partial", action="store_true", help="Do not raise an error if some dependency is not found in lockfile") parser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') args = parser.parse_args(*args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial) ref, conanfile = conan_api.export.export(path=path, name=args.name, version=args.version, user=args.user, channel=args.channel, lockfile=lockfile, remotes=remotes) lockfile = conan_api.lockfile.update_lockfile_export(lockfile, conanfile, ref, args.build_require) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) exported_list = PackagesList() exported_list.add_ref(ref) pkglist = MultiPackagesList() pkglist.add("Local Cache", exported_list) return { "pkglist": pkglist.serialize(), "reference": ref } ================================================ FILE: conan/cli/commands/export_pkg.py ================================================ import os from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.cli.args import add_lockfile_args, add_profiles_args, add_reference_args from conan.cli.command import conan_command, OnceArgument from conan.cli.commands.create import _get_test_conanfile_path from conan.cli.formatters.graph import format_graph_json from conan.errors import ConanException @conan_command(group="Creator", formatters={"json": format_graph_json}) def export_pkg(conan_api, parser, *args): """ Create a package directly from pre-compiled binaries. """ parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py). " "Defaults to current directory", default=".", nargs="?") parser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') parser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') parser.add_argument("-tf", "--test-folder", action=OnceArgument, help='Alternative test folder name. By default it is "test_package". ' 'Use "" to skip the test stage') parser.add_argument("-sb", "--skip-binaries", action="store_true", help="Skip installing dependencies binaries") group = parser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", action="append", default=None, help='Look in the specified remote or remotes server') group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') add_reference_args(parser) add_lockfile_args(parser) add_profiles_args(parser) args = parser.parse_args(*args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None # UX error check, for python-requires use "conan export" or "conan create" conanfile = conan_api.local.inspect(path, remotes, lockfile, name=args.name, version=args.version, user=args.user, channel=args.channel) # The package_type is not fully processed at export if conanfile.package_type == "python-require": raise ConanException("export-pkg can only be used for binaries, not for 'python-require'") # First, ensure recipe is exported ref, conanfile = conan_api.export.export(path=path, name=args.name, version=args.version, user=args.user, channel=args.channel, lockfile=lockfile, remotes=remotes) lockfile = conan_api.lockfile.update_lockfile_export(lockfile, conanfile, ref, args.build_require) # Compute the dependency graph to prepare the exporting of the package biary graph = conan_api.export.export_pkg_graph(path=path, ref=ref, profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, is_build_require=args.build_require, skip_binaries=args.skip_binaries, output_folder=output_folder) # Now export the final binary ConanOutput().title("Exporting recipe and package to the cache") conan_api.export.export_pkg(graph, output_folder) lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, args.lockfile_packages, clean=args.lockfile_clean) test_package_folder = getattr(conanfile, "test_package_folder", None) \ if args.test_folder is None else args.test_folder test_conanfile_path = _get_test_conanfile_path(test_package_folder, path) if test_conanfile_path: from conan.cli.commands.test import run_test # same as ``conan create`` the lockfile, and deps graph is the one of the exported-pkg # not the one from test_package run_test(conan_api, test_conanfile_path, ref, profile_host, profile_build, remotes=remotes, lockfile=lockfile, update=None, build_modes=None) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) return {"graph": graph, "conan_api": conan_api} ================================================ FILE: conan/cli/commands/graph.py ================================================ import json import os from conan.api.output import ConanOutput, cli_out_write, Color from conan.cli import make_abs_path from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.command import conan_command, conan_subcommand from conan.cli.commands.list import prepare_pkglist_compact, print_serial from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot from conan.cli.formatters.graph.build_order_html import format_build_order_html from conan.cli.formatters.graph.graph_info_text import format_graph_info from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_packages, print_graph_basic from conan.errors import ConanException def explain_formatter_text(data): if "closest_binaries" in data: # To be able to reuse the print_list_compact method, # we need to wrap this in a MultiPackagesList pkglist = data["closest_binaries"] prepare_pkglist_compact(pkglist) # Now we make sure that if there are no binaries we will print something that makes sense for ref, ref_info in pkglist.items(): for rrev, rrev_info in ref_info.items(): if not rrev_info: rrev_info["ERROR"] = "No package binaries exist" print_serial(pkglist) def explain_formatter_json(data): myjson = json.dumps(data, indent=4) cli_out_write(myjson) @conan_command(group="Consumer") def graph(conan_api, parser, *args): # noqa """ Compute a dependency graph, without installing or building the binaries. """ def cli_build_order(result): # TODO: Very simple cli output, probably needs to be improved build_order = result["build_order"] build_order = build_order["order"] if isinstance(build_order, dict) else build_order for level in build_order: for item in level: # If this is a configuration order, it has no packages entry, each item is a package if 'packages' in item: for package_level in item['packages']: for package in package_level: cli_out_write(f"{item['ref']}:{package['package_id']} - {package['binary']}") else: cli_out_write(f"{item['ref']}:{item['package_id']} - {item['binary']}") def json_build_order(result): cli_out_write(json.dumps(result["build_order"], indent=4)) @conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order, "html": format_build_order_html}) def graph_build_order(conan_api, parser, subparser, *args): """ Compute the build order of a dependency graph. """ common_graph_args(subparser) subparser.add_argument("--order-by", choices=['recipe', 'configuration'], help='Select how to order the output, "recipe" by default if not set.') subparser.add_argument("--reduce", action='store_true', default=False, help='Reduce the build order, output only those to build. Use this ' 'only if the result will not be merged later with other build-order') args = parser.parse_args(*args) validate_common_graph_args(args) if args.order_by is None: ConanOutput().warning("Please specify --order-by argument", warn_tag="deprecated") cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None # Basic collaborators, remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: deps_graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.build, args.update) else: deps_graph = conan_api.graph.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.build, args.update) print_graph_basic(deps_graph) deps_graph.report_graph_error() conan_api.graph.analyze_binaries(deps_graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) out = ConanOutput() out.title("Computing the build order") install_graph = conan_api.graph.build_order(deps_graph, args.order_by, args.reduce, profile_args=args) install_order_serialized = install_graph.install_build_order() if args.order_by is None: # legacy install_order_serialized = install_order_serialized["order"] lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) return {"build_order": install_order_serialized, "conan_error": install_graph.get_errors()} @conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order, "html": format_build_order_html}) def graph_build_order_merge(conan_api, parser, subparser, *args): # noqa """ Merge more than 1 build-order file. """ subparser.add_argument("--file", nargs="?", action="append", help="Files to be merged") subparser.add_argument("--reduce", action='store_true', default=False, help='Reduce the build order, output only those to build. Use this ' 'only if the result will not be merged later with other build-order') args = parser.parse_args(*args) if not args.file or len(args.file) < 2: raise ConanException("At least 2 files are needed to be merged") files = [make_abs_path(f) for f in args.file] result = conan_api.graph.build_order_merge(files, args.reduce) install_order_serialized = result.install_build_order() if getattr(result, "legacy"): install_order_serialized = install_order_serialized["order"] return {"build_order": install_order_serialized, "conan_error": result.get_errors()} @conan_subcommand(formatters={"text": format_graph_info, "html": format_graph_html, "json": format_graph_json, "dot": format_graph_dot}) def graph_info(conan_api, parser, subparser, *args): """ Compute the dependency graph and show information about it. """ common_graph_args(subparser) subparser.add_argument("--check-updates", default=False, action="store_true", help="Check if there are recipe updates") subparser.add_argument("--filter", action="append", help="Show only the specified fields") subparser.add_argument("--package-filter", action="append", help='Print information only for packages that match the patterns') subparser.add_argument("-d", "--deployer", action="append", help="Deploy using the provided deployer to the output folder. " "Built-in deployers: 'full_deploy', 'direct_deploy'. Deployers " "will only deploy recipes, as 'conan graph info' do not retrieve " "binaries") subparser.add_argument("-df", "--deployer-folder", help="Deployer output folder, base build folder by default if not set") subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') args = parser.parse_args(*args) # parameter validation validate_common_graph_args(args) if args.format in ("html", "dot") and args.filter: raise ConanException(f"Formatted output '{args.format}' cannot filter fields") cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None # Basic collaborators, remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) if path: deps_graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, check_updates=args.check_updates, is_build_require=args.build_require) else: deps_graph = conan_api.graph.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update, check_updates=args.check_updates) print_graph_basic(deps_graph) if not deps_graph.error: conan_api.graph.analyze_binaries(deps_graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) conan_api.install.install_system_requires(deps_graph, only_info=True) conan_api.install.install_sources(deps_graph, remotes=remotes) lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) if args.deployer: base_folder = args.deployer_folder or os.getcwd() conan_api.install.deploy(deps_graph, args.deployer, None, base_folder) warn_msg = None missing = set(str(n.ref.name) for n in deps_graph.nodes if n.binary == "Missing") invalid = set(str(n.ref.name) for n in deps_graph.nodes if n.binary == "Invalid") if missing or invalid: warn_msg = "There are some error(s) in the graph:" if missing: warn_msg += f"\n - Missing packages: {', '.join(missing)}" if invalid: warn_msg += f"\n - Invalid packages: {', '.join(invalid)}" return {"graph": deps_graph, "field_filter": args.filter, "package_filter": args.package_filter, "conan_api": conan_api, "conan_error": str(deps_graph.error) if deps_graph.error else None, "conan_warning": warn_msg} @conan_subcommand(formatters={"text": explain_formatter_text, "json": explain_formatter_json}) def graph_explain(conan_api, parser, subparser, *args): """ Explain what is wrong with the dependency graph, like report missing binaries closest alternatives, trying to explain why the existing binaries do not match """ common_graph_args(subparser) subparser.add_argument("--check-updates", default=False, action="store_true", help="Check if there are recipe updates") subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') subparser.add_argument('--missing', nargs="?", help="A pattern in the form 'pkg/version#revision:package_id#revision', " "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. " "If revision is not specified, it is assumed latest one.") args = parser.parse_args(*args) # parameter validation validate_common_graph_args(args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None # Basic collaborators, remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: deps_graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, check_updates=args.check_updates, is_build_require=args.build_require) else: deps_graph = conan_api.graph.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update, check_updates=args.check_updates) print_graph_basic(deps_graph) deps_graph.report_graph_error() conan_api.graph.analyze_binaries(deps_graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) ConanOutput().title("Retrieving and computing closest binaries") # compute ref and conaninfo of the first missing binary ref, conaninfo = conan_api.graph.find_first_missing_binary(deps_graph, args.missing) pkglist = conan_api.list.explain_missing_binaries(ref, conaninfo, remotes) ConanOutput().title("Closest binaries") return {"closest_binaries": pkglist.serialize()} def outdated_text_formatter(result): cli_out_write("======== Outdated dependencies ========", fg=Color.BRIGHT_MAGENTA) if len(result) == 0: cli_out_write("No outdated dependencies in graph", fg=Color.BRIGHT_YELLOW) for key, value in result.items(): current_versions_set = list({str(v) for v in value["cache_refs"]}) cli_out_write(key, fg=Color.BRIGHT_YELLOW) cli_out_write( f' Current versions: {", ".join(current_versions_set) if value["cache_refs"] else "No version found in cache"}', fg=Color.BRIGHT_CYAN) cli_out_write( f' Latest in remote(s): {value["latest_remote"]["ref"]} - {value["latest_remote"]["remote"]}', fg=Color.BRIGHT_CYAN) if value["version_ranges"]: cli_out_write(f' Version ranges: ' + str(value["version_ranges"])[1:-1], fg=Color.BRIGHT_CYAN) def outdated_json_formatter(result): output = {key: {"current_versions": list({str(v) for v in value["cache_refs"]}), "version_ranges": [str(r) for r in value["version_ranges"]], "latest_remote": [] if value["latest_remote"] is None else {"ref": str(value["latest_remote"]["ref"]), "remote": str(value["latest_remote"]["remote"])}} for key, value in result.items()} cli_out_write(json.dumps(output)) @conan_subcommand(formatters={"text": outdated_text_formatter, "json": outdated_json_formatter}) def graph_outdated(conan_api, parser, subparser, *args): """ List the dependencies in the graph and it's newer versions in the remote """ common_graph_args(subparser) subparser.add_argument("--check-updates", default=False, action="store_true", help="Check if there are recipe updates") subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') args = parser.parse_args(*args) # parameter validation validate_common_graph_args(args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None # Basic collaborators, remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: deps_graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, check_updates=args.check_updates, is_build_require=args.build_require) else: deps_graph = conan_api.graph.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update, check_updates=args.check_updates) print_graph_basic(deps_graph) # Data structure to store info per library # DO NOT USE this API call yet, it is not stable outdated = conan_api.list.outdated(deps_graph, remotes) return outdated ================================================ FILE: conan/cli/commands/inspect.py ================================================ import os from conan.api.output import cli_out_write from conan.cli.command import conan_command, OnceArgument from conan.cli.formatters import default_json_formatter def inspect_text_formatter(data): for name, value in sorted(data.items()): if value is None: continue if isinstance(value, dict): cli_out_write(f"{name}:") for k, v in value.items(): cli_out_write(f" {k}: {v}") else: cli_out_write("{}: {}".format(name, str(value))) @conan_command(group="Consumer", formatters={"text": inspect_text_formatter, "json": default_json_formatter}) def inspect(conan_api, parser, *args): """ Inspect a conanfile.py to return its public fields. """ parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py). " "Defaults to current directory", default=".", nargs="?") group = parser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", default=None, action="append", help="Remote names. Accepts wildcards ('*' means all the remotes available)") group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') parser.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " "existing 'conan.lock' file") parser.add_argument("--lockfile-partial", action="store_true", help="Do not raise an error if some dependency is not found in lockfile") args = parser.parse_args(*args) path = conan_api.local.get_conanfile_path(args.path, os.getcwd(), py=True) lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=os.getcwd(), partial=args.lockfile_partial) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] conanfile = conan_api.local.inspect(path, remotes=remotes, lockfile=lockfile) result = conanfile.serialize() # Some of the serialization info is not initialized so it's pointless to show it to the user for item in ("cpp_info", "system_requires", "recipe_folder", "conf_info"): if item in result: del result[item] return result ================================================ FILE: conan/cli/commands/install.py ================================================ import os from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.command import conan_command from conan.cli.formatters.graph import format_graph_json from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_packages, print_graph_basic @conan_command(group="Consumer", formatters={"json": format_graph_json}) def install(conan_api, parser, *args): """ Install the requirements specified in a recipe (conanfile.py or conanfile.txt). It can also be used to install packages without a conanfile, using the --requires and --tool-requires arguments. If any requirement is not found in the local cache, it will iterate the remotes looking for it. When the full dependency graph is computed, and all dependencies recipes have been found, it will look for binary packages matching the current settings. If no binary package is found for some or several dependencies, it will error, unless the '--build' argument is used to build it from source. After installation of packages, the generators and deployers will be called. """ common_graph_args(parser) parser.add_argument("-g", "--generator", action="append", help='Generators to use') parser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') parser.add_argument("-d", "--deployer", action="append", help="Deploy using the provided deployer to the output folder. " "Built-in deployers: 'full_deploy', 'direct_deploy', 'runtime_deploy'") parser.add_argument("--deployer-folder", help="Deployer output folder, base build folder by default if not set") parser.add_argument("--deployer-package", action="append", help="Execute the deploy() method of the packages matching " "the provided patterns") parser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided path is a build-require') parser.add_argument("--envs-generation", default=None, choices=["false"], help="Generation strategy for virtual environment files for the root") args = parser.parse_args(*args) validate_common_graph_args(args) cwd = os.getcwd() deps_graph, lockfile, install_error = _run_install_command(conan_api, args, cwd) # Update lockfile if necessary lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) return {"graph": deps_graph, "conan_api": conan_api, "conan_error": install_error} def _run_install_command(conan_api, args, cwd, return_install_error=True): """ This method should not be imported as-is, it is internal to the installation process and its signature might change without warning. Users are however free to copy its code and adapt it to their needs, as an example of using the Conan API to perform an installation """ # basic paths path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None source_folder = os.path.dirname(path) if args.path else cwd output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None # Basic collaborators: remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) # Graph computation (without installation of binaries) gapi = conan_api.graph if path: deps_graph = gapi.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, is_build_require=args.build_require) else: deps_graph = gapi.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update) print_graph_basic(deps_graph) deps_graph.report_graph_error() gapi.analyze_binaries(deps_graph, args.build, remotes, update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) # Installation of binaries and consumer generators install_error = conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes, return_install_error=return_install_error) if not install_error: ConanOutput().title("Finalizing install (deploy, generators)") conan_api.install.install_consumer(deps_graph, args.generator, source_folder, output_folder, deploy=getattr(args, "deployer", None), deploy_package=getattr(args, "deployer_package", None), deploy_folder=getattr(args, "deployer_folder", None), envs_generation=getattr(args, "envs_generation", None)) ConanOutput().success("Install finished successfully") return deps_graph, lockfile, install_error ================================================ FILE: conan/cli/commands/list.py ================================================ import json import datetime from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, MultiPackagesList from conan.api.output import Color, cli_out_write from conan.cli import make_abs_path from conan.cli.command import conan_command, OnceArgument from conan.cli.formatters.list import list_packages_html from conan.errors import ConanException # Keep them so we don't break other commands that import them, but TODO: Remove later remote_color = Color.BRIGHT_BLUE recipe_name_color = Color.GREEN recipe_color = Color.BRIGHT_WHITE reference_color = Color.WHITE error_color = Color.BRIGHT_RED field_color = Color.BRIGHT_YELLOW value_color = Color.CYAN def _format_timestamp_human(timestamp): # used by ref.repr_humantime() to print human readable time return datetime.datetime.fromtimestamp(int(timestamp), datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') def print_serial(item, indent=None, color_index=None): indent = "" if indent is None else (indent + " ") color_index = 0 if color_index is None else (color_index + 1) color_array = [Color.BRIGHT_BLUE, Color.BRIGHT_GREEN, Color.BRIGHT_WHITE, Color.BRIGHT_YELLOW, Color.BRIGHT_CYAN, Color.BRIGHT_MAGENTA, Color.WHITE] color = color_array[color_index % len(color_array)] if isinstance(item, dict): for k, v in item.items(): if isinstance(v, (str, int)): if "error" in k.lower(): color = Color.BRIGHT_RED k = "ERROR" elif k.lower() == "warning": color = Color.BRIGHT_YELLOW k = "WARN" color = Color.BRIGHT_RED if k == "expected" else color color = Color.BRIGHT_GREEN if k == "existing" else color cli_out_write(f"{indent}{k}: {v}", fg=color) else: cli_out_write(f"{indent}{k}", fg=color) print_serial(v, indent, color_index) elif isinstance(item, type([])): for elem in item: cli_out_write(f"{indent}{elem}", fg=color) elif isinstance(item, int): # Can print 0 cli_out_write(f"{indent}{item}", fg=color) elif item: cli_out_write(f"{indent}{item}", fg=color) def print_list_text(results): """ Do a little format modification to serialized package list, so it looks prettier on text output """ info = results["results"] # Extract command single package name new_info = {} for remote, remote_info in info.items(): new_remote_info = {} for ref, content in remote_info.items(): if ref == "error": new_remote_info[ref] = content else: name, _ = ref.split("/", 1) new_remote_info.setdefault(name, {})[ref] = content new_info[remote] = new_remote_info info = new_info # TODO: The errors are not being displayed info = {remote: {"warning": "There are no matching recipe references"} if not values else values for remote, values in info.items()} def format_timestamps(item): if isinstance(item, dict): result = {} for k, v in item.items(): if isinstance(v, dict) and v.get("timestamp"): timestamp = v.pop("timestamp") k = f"{k} ({_format_timestamp_human(timestamp)})" result[k] = format_timestamps(v) return result return item info = {remote: format_timestamps(values) for remote, values in info.items()} print_serial(info) def print_list_compact(results): info = results["results"] """ transform the dictionary into a more compact one, keeping the internals but forming full recipe and package references including revisions at the top levels """ for remote, remote_info in info.items(): if not remote_info or "error" in remote_info: info[remote] = {"warning": "There are no matching recipe references"} continue prepare_pkglist_compact(remote_info) print_serial(info) def prepare_pkglist_compact(pkglist): for ref, ref_info in pkglist.items(): new_ref_info = {} for rrev, rrev_info in ref_info.get("revisions", {}).items(): new_rrev = f"{ref}#{rrev}" timestamp = rrev_info.pop("timestamp", None) if timestamp: new_rrev += f"%{timestamp} ({_format_timestamp_human(timestamp)})" packages = rrev_info.pop("packages", None) if packages: for pid, pid_info in packages.items(): new_pid = f"{ref}#{rrev}:{pid}" rrev_info[new_pid] = pid_info new_ref_info[new_rrev] = rrev_info pkglist[ref] = new_ref_info def compute_common_options(pkgs): """ compute the common subset of existing options with same values of a set of packages """ result = {} all_package_options = [p.get("info", {}).get("options") for p in pkgs.values()] all_package_options = [p for p in all_package_options if p] # filter pkgs without options for package_options in all_package_options: # Accumulate all options for all binaries result.update(package_options) for package_options in all_package_options: # Filter those not common to all result = {k: v for k, v in result.items() if k in package_options and v == package_options[k]} for package_options in all_package_options: for k, v in package_options.items(): if v != result.get(k): result.pop(k, None) return result def compact_format_info(local_info, common_options=None): """ return a dictionary with settings and options in short form for compact format """ result = {} settings = local_info.pop("settings", None) if settings: # A bit of pretty order, first OS-ARCH values = [settings.pop(s, None) for s in ("os", "arch", "build_type", "compiler")] values = [v for v in values if v is not None] values.extend(settings.values()) result["settings"] = ", ".join(values) options = local_info.pop("options", None) if options: if common_options is not None: options = {k: v for k, v in options.items() if k not in common_options} options = ", ".join(f"{k}={v}" for k, v in options.items()) options_tag = "options(diff)" if common_options is not None else "options" result[options_tag] = options for k, v in local_info.items(): if isinstance(v, dict): v = ", ".join(f"{kv}={vv}" for kv, vv in v.items()) elif isinstance(v, type([])): v = ", ".join(v) if v: result[k] = v return result def compact_diff(diffinfo): """ return a compact and red/green diff for binary differences """ result = {} for k, v in diffinfo.items(): if not v: continue if isinstance(v, dict): result[k] = {"expected": ", ".join(value for value in v["expected"]), "existing": ", ".join(value for value in v["existing"])} else: result[k] = v return result for ref, revisions in pkglist.items(): if not isinstance(revisions, dict): continue for rrev, prefs in revisions.items(): pkg_common_options = compute_common_options(prefs) pkg_common_options = pkg_common_options if len(pkg_common_options) > 4 else None for pref, pref_contents in prefs.items(): pref_info = pref_contents.pop("info", None) if pref_info is not None: pref_contents.update(compact_format_info(pref_info, pkg_common_options)) diff_info = pref_contents.pop("diff", None) if diff_info is not None: pref_contents["diff"] = compact_diff(diff_info) def print_list_json(data): results = data["results"] myjson = json.dumps(results, indent=4) cli_out_write(myjson) @conan_command(group="Consumer", formatters={"text": print_list_text, "json": print_list_json, "html": list_packages_html, "compact": print_list_compact}) def list(conan_api: ConanAPI, parser, *args): """ List existing recipes, revisions, or packages in the cache (by default) or the remotes. """ parser.add_argument('pattern', nargs="?", help="A pattern in the form 'pkg/version#revision:package_id#revision', " "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. " "If revision is not specified, it is assumed latest one.") parser.add_argument('-p', '--package-query', default=None, action=OnceArgument, help="List only the packages matching a specific query, e.g, os=Windows AND " "(arch=x86 OR compiler=gcc)") parser.add_argument('-fp', '--filter-profile', action="append", help="Profiles to filter the binaries") parser.add_argument('-fs', '--filter-settings', action="append", help="Settings to filter the binaries") parser.add_argument('-fo', '--filter-options', action="append", help="Options to filter the binaries") parser.add_argument("-r", "--remote", default=None, action="append", help="Remote names. Accepts wildcards ('*' means all the remotes available)") parser.add_argument("-c", "--cache", action='store_true', help="Search in the local cache") parser.add_argument("-g", "--graph", help="Graph json file") parser.add_argument("-gb", "--graph-binaries", help="Which binaries are listed", action="append") parser.add_argument("-gr", "--graph-recipes", help="Which recipes are listed", action="append") parser.add_argument("-gc", "--graph-context", help="Filter the results by the given context", default=None, action=OnceArgument, choices=["build", "host", "build-only", "host-only"]) parser.add_argument('--lru', default=None, action=OnceArgument, help="List recipes and binaries that have not been recently used. Use a" " time limit like --lru=5d (days) or --lru=4w (weeks)," " h (hours), m(minutes)") args = parser.parse_args(*args) if args.pattern is None and args.graph is None: args.pattern = "*" # All packages if args.graph: # a few arguments are not compatible with this if args.pattern: raise ConanException("Cannot define both the pattern and the graph json file") if args.lru: raise ConanException("Cannot define lru when loading a graph json file") if args.filter_profile or args.filter_settings or args.filter_options: raise ConanException("Filtering binaries cannot be done when loading a graph json file") if (args.graph_recipes or args.graph_binaries) and not args.graph: raise ConanException("--graph-recipes and --graph-binaries require a --graph input") if args.remote and args.lru: raise ConanException("'--lru' cannot be used in remotes, only in cache") if args.graph: graphfile = make_abs_path(args.graph) pkglist = MultiPackagesList.load_graph(graphfile, args.graph_recipes, args.graph_binaries, context=args.graph_context) else: ref_pattern = ListPattern(args.pattern, rrev=None, prev=None) if not ref_pattern.package_id and (args.package_query or args.filter_profile or args.filter_settings or args.filter_options): raise ConanException("--package-query and --filter-xxx can only be done for binaries, " "a 'pkgname/version:*' pattern is necessary") if args.lru: if not ref_pattern.rrev and not ref_pattern.package_id: # If package_id => #latest raise ConanException("'--lru' must be used with recipe revision pattern, " "use 'pkg/version#*' argument") if ref_pattern.package_id and not ref_pattern.prev: raise ConanException("'--lru' must be used with package revision pattern, " "use 'pkg/version:*#*' argument") # If neither remote nor cache are defined, show results only from cache pkglist = MultiPackagesList() profile = conan_api.profiles.get_profile(args.filter_profile or [], args.filter_settings, args.filter_options) \ if args.filter_profile or args.filter_settings or args.filter_options else None if args.cache or not args.remote: try: cache_list = conan_api.list.select(ref_pattern, args.package_query, remote=None, lru=args.lru, profile=profile) except Exception as e: pkglist.add_error("Local Cache", str(e)) else: pkglist.add("Local Cache", cache_list) if args.remote: remotes = conan_api.remotes.list(args.remote) for remote in remotes: try: remote_list = conan_api.list.select(ref_pattern, args.package_query, remote, profile=profile) except Exception as e: pkglist.add_error(remote.name, str(e)) else: pkglist.add(remote.name, remote_list) return { "results": pkglist.serialize(), "conan_api": conan_api, "cli_args": " ".join([f"{arg}={getattr(args, arg)}" for arg in vars(args) if getattr(args, arg)]) } ================================================ FILE: conan/cli/commands/lock.py ================================================ import os from conan.api.output import ConanOutput from conan.cli.command import conan_command, OnceArgument, conan_subcommand from conan.cli import make_abs_path from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.printers.graph import print_graph_packages, print_graph_basic from conan.errors import ConanException from conan.api.model import RecipeReference @conan_command(group="Consumer") def lock(conan_api, parser, *args): # noqa """ Create or manage lockfiles. """ @conan_subcommand() def lock_create(conan_api, parser, subparser, *args): """ Create a lockfile from a conanfile or a reference. """ common_graph_args(subparser) subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') args = parser.parse_args(*args) # parameter validation validate_common_graph_args(args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=True, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, is_build_require=args.build_require) else: graph = conan_api.graph.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update) print_graph_basic(graph) graph.report_graph_error() conan_api.graph.analyze_binaries(graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(graph) lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, args.lockfile_packages, clean=args.lockfile_clean) conanfile_path = os.path.dirname(graph.root.path) \ if graph.root.path and args.lockfile_out is None else cwd conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out or "conan.lock", conanfile_path) @conan_subcommand() def lock_merge(conan_api, parser, subparser, *args): # noqa """ Merge 2 or more lockfiles. """ subparser.add_argument('--lockfile', action="append", help='Path to lockfile to be merged') subparser.add_argument("--lockfile-out", action=OnceArgument, default="conan.lock", help="Filename of the created lockfile") args = parser.parse_args(*args) result = conan_api.lockfile.merge_lockfiles(args.lockfile) lockfile_out = make_abs_path(args.lockfile_out) result.save(lockfile_out) ConanOutput().info("Generated lockfile: %s" % lockfile_out) @conan_subcommand() def lock_add(conan_api, parser, subparser, *args): """ Add requires, build-requires or python-requires to an existing or new lockfile. The resulting lockfile will be ordered, newer versions/revisions first. References can be supplied with and without revisions like "--requires=pkg/version", but they must be recipe references, including at least the version, and they cannot contain a version range. """ subparser.add_argument('--requires', action="append", help='Add references to lockfile.') subparser.add_argument('--build-requires', action="append", help='Add build-requires to lockfile') subparser.add_argument('--python-requires', action="append", help='Add python-requires to lockfile') subparser.add_argument('--config-requires', action="append", help='Add config-requires to lockfile') subparser.add_argument("--lockfile-out", action=OnceArgument, default="conan.lock", help="Filename of the created lockfile") subparser.add_argument("--lockfile", action=OnceArgument, help="Filename of the input lockfile") args = parser.parse_args(*args) lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, partial=True) allow_uppercase = conan_api.config.get("core:allow_uppercase_pkg_names", check_type=bool) def _parse_requires(reqs): if reqs: result = [RecipeReference.loads(r) for r in reqs] [r.validate_ref(allow_uppercase) for r in result] return result requires = _parse_requires(args.requires) build_requires = _parse_requires(args.build_requires) python_requires = _parse_requires(args.python_requires) config_requires = _parse_requires(args.config_requires) lockfile = conan_api.lockfile.add_lockfile(lockfile, requires=requires, python_requires=python_requires, build_requires=build_requires, config_requires=config_requires) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out) @conan_subcommand() def lock_remove(conan_api, parser, subparser, *args): """ Remove requires, build-requires or python-requires from an existing lockfile. References can be supplied with and without revisions like "--requires=pkg/version", """ subparser.add_argument('--requires', action="append", help='Remove references to lockfile.') subparser.add_argument('--build-requires', action="append", help='Remove build-requires from lockfile') subparser.add_argument('--python-requires', action="append", help='Remove python-requires from lockfile') subparser.add_argument('--config-requires', action="append", help='Remove config-requires from lockfile') subparser.add_argument("--lockfile-out", action=OnceArgument, default="conan.lock", help="Filename of the created lockfile") subparser.add_argument("--lockfile", action=OnceArgument, help="Filename of the input lockfile") args = parser.parse_args(*args) lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, partial=True) lockfile = conan_api.lockfile.remove_lockfile(lockfile, requires=args.requires, python_requires=args.python_requires, build_requires=args.build_requires, config_requires=args.config_requires) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out) @conan_subcommand() def lock_update(conan_api, parser, subparser, *args): """ Update requires, build-requires or python-requires from an existing lockfile. References that matches the arguments package names will be replaced by the arguments. References can be supplied with and without revisions like "--requires=pkg/version", """ subparser.add_argument('--requires', action="append", help='Update references to lockfile.') subparser.add_argument('--build-requires', action="append", help='Update build-requires from lockfile') subparser.add_argument('--python-requires', action="append", help='Update python-requires from lockfile') subparser.add_argument('--config-requires', action="append", help='Update config-requires from lockfile') subparser.add_argument("--lockfile-out", action=OnceArgument, default="conan.lock", help="Filename of the created lockfile") subparser.add_argument("--lockfile", action=OnceArgument, help="Filename of the input lockfile") args = parser.parse_args(*args) lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, partial=True) lockfile.update(requires=args.requires, build_requires=args.build_requires, python_requires=args.python_requires, config_requires=args.config_requires) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out) @conan_subcommand() def lock_upgrade(conan_api, parser, subparser, *args): """ (Experimental) Upgrade requires, build-requires or python-requires from an existing lockfile given a conanfile or a reference. """ common_graph_args(subparser) subparser.add_argument('--update-requires', action="append", help='Update requires from lockfile') subparser.add_argument('--update-build-requires', action="append", help='Update build-requires from lockfile') subparser.add_argument('--update-python-requires', action="append", help='Update python-requires from lockfile') subparser.add_argument('--build-require', action='store_true', default=False, help='Whether the provided reference is a build-require') args = parser.parse_args(*args) # parameter validation validate_common_graph_args(args) if not any([args.update_requires, args.update_build_requires, args.update_python_requires]): raise ConanException("At least one of --update-requires, --update-build-requires, " "--update-python-requires should be specified") cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=True, overrides=overrides) if lockfile is None: raise ConanException("No lockfile specified and default conan.lock not found") profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) # Remove the lockfile entries that will be updated lockfile = conan_api.lockfile.remove_lockfile(lockfile, requires=args.update_requires, python_requires=args.update_python_requires, build_requires=args.update_build_requires) # Resolve new graph if path: graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, is_build_require=args.build_require) else: graph = conan_api.graph.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update) print_graph_basic(graph) graph.report_graph_error() conan_api.graph.analyze_binaries(graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(graph) lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, clean=args.lockfile_clean) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out or "conan.lock") @conan_subcommand() def lock_upgrade_config(conan_api, parser, subparser, *args): """ (Experimental) Upgrade config requires in a lockfile """ common_graph_args(subparser) subparser.add_argument('--update-config-requires', action="append", help='Update config-requires from lockfile') args = parser.parse_args(*args) validate_common_graph_args(args) if not args.update_config_requires: raise ConanException("At least one --update-config-requires should be specified") cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=True) if lockfile is None: raise ConanException("No lockfile specified and default conan.lock not found") profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) # Remove the lockfile entries that will be updated lockfile = conan_api.lockfile.remove_lockfile(lockfile, config_requires=args.update_config_requires) if args.path: path = make_abs_path(args.path) reqs, remotes = conan_api.config.load_conanconfig(path, remotes) else: reqs = [RecipeReference.loads(r) for r in args.requires] pkgs = conan_api.config.fetch_packages(reqs, lockfile, remotes, profile_host) refs = [p.ref for p in pkgs] lockfile = conan_api.lockfile.add_lockfile(lockfile, config_requires=refs) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out or "conan.lock") ================================================ FILE: conan/cli/commands/new.py ================================================ import os from conan.cli.command import conan_command @conan_command(group="Creator") def new(conan_api, parser, *args): """ Create a new example recipe and source files from a template. """ parser.add_argument("template", nargs="?", default="", help="Template name, " "either a predefined built-in or a user-provided one. " "Available built-in templates: basic, cmake_lib, cmake_exe, header_lib, " "meson_lib, meson_exe, msbuild_lib, msbuild_exe, bazel_lib, bazel_exe, " "autotools_lib, autotools_exe, premake_lib, premake_exe, local_recipes_index, workspace. " "E.g. 'conan new cmake_lib -d name=hello -d version=0.1'. " "You can define your own templates too by inputting an absolute path " "as your template, or a path relative to your conan home folder." ) parser.add_argument("-d", "--define", action="append", help="Define a template argument as key=value, e.g., -d name=mypkg") parser.add_argument("-f", "--force", action='store_true', help="Overwrite file if it already exists") parser.add_argument("-o", "--output", help="Output folder for the generated files", default=os.getcwd()) args = parser.parse_args(*args) conan_api.new.save_template(args.template, args.define, args.output, args.force) ================================================ FILE: conan/cli/commands/pkglist.py ================================================ from conan.api.conan_api import ConanAPI from conan.api.model import MultiPackagesList from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand from conan.cli.commands.list import print_list_text, print_list_json from conan.cli.formatters.list import list_packages_html @conan_command(group="Consumer") def pkglist(conan_api: ConanAPI, parser, *args): # noqa """ Several operations over package lists """ @conan_subcommand(formatters={"text": print_list_text, "json": print_list_json, "html": list_packages_html}) def pkglist_find_remote(conan_api, parser, subparser, *args): """ (Experimental) Find the remotes of a list of packages in the cache """ subparser.add_argument('list', help="Input package list") subparser.add_argument("-r", "--remote", default=None, action="append", help="Remote names. Accepts wildcards " "('*' means all the remotes available)") args = parser.parse_args(*args) listfile = make_abs_path(args.list) multi_pkglist = MultiPackagesList.load(listfile) package_list = multi_pkglist["Local Cache"] selected_remotes = conan_api.remotes.list(args.remote) result = conan_api.list.find_remotes(package_list, selected_remotes) return { "results": result.serialize(), "conan_api": conan_api, "cli_args": " ".join([f"{arg}={getattr(args, arg)}" for arg in vars(args) if getattr(args, arg)]) } @conan_subcommand(formatters={"text": print_list_text, "json": print_list_json, "html": list_packages_html}) def pkglist_merge(conan_api, parser, subparser, *args): """ (Experimental) Merge several package lists into a single one """ subparser.add_argument("-l", "--list", help="Package list file", action="append") args = parser.parse_args(*args) result = MultiPackagesList() for pkg_list in args.list: listfile = make_abs_path(pkg_list) multi_pkglist = MultiPackagesList.load(listfile) result.merge(multi_pkglist) return { "results": result.serialize(), "conan_api": conan_api, "cli_args": " ".join([f"{arg}={getattr(args, arg)}" for arg in vars(args) if getattr(args, arg)]) } ================================================ FILE: conan/cli/commands/profile.py ================================================ import json import os from conan.api.output import ConanOutput, cli_out_write from conan.cli.command import conan_command, conan_subcommand from conan.cli.formatters import default_json_formatter from conan.cli.args import add_profiles_args from conan.errors import ConanException def _print_profiles(profiles): if "host" in profiles: ConanOutput().info("Host profile:") cli_out_write(profiles["host"].dumps()) if "build" in profiles: ConanOutput().info("Build profile:") cli_out_write(profiles["build"].dumps()) def profiles_list_cli_output(profiles): ConanOutput().info("Profiles found in the cache:") for p in profiles: cli_out_write(p) def _json_profiles(profiles): result = {} if "host" in profiles: result["host"] = profiles["host"].serialize() if "build" in profiles: result["build"] = profiles["build"].serialize() cli_out_write(json.dumps(result)) @conan_subcommand(formatters={"text": _print_profiles, "json": _json_profiles}) def profile_show(conan_api, parser, subparser, *args): """ Show aggregated profiles from the passed arguments. """ add_profiles_args(subparser) subparser.add_argument("-cx", "--context", choices=["host", "build"]) args = parser.parse_args(*args) profiles = conan_api.profiles.get_profiles_from_args(args) result = {} if not args.context or args.context == "host": result["host"] = profiles[0] if not args.context or args.context == "build": result["build"] = profiles[1] return result @conan_subcommand(formatters={"text": cli_out_write}) def profile_path(conan_api, parser, subparser, *args): """ Show profile path location. """ subparser.add_argument("name", help="Profile name") args = parser.parse_args(*args) return conan_api.profiles.get_path(args.name) @conan_subcommand() def profile_detect(conan_api, parser, subparser, *args): """ Generate a profile using auto-detected values. """ subparser.add_argument("--name", help="Profile name, 'default' if not specified") subparser.add_argument("-f", "--force", action='store_true', help="Overwrite if exists") subparser.add_argument("-e", "--exist-ok", action='store_true', help="If the profile already exist, do not detect a new one") args = parser.parse_args(*args) profile_name = args.name or "default" profile_pathname = conan_api.profiles.get_path(profile_name, os.getcwd(), exists=False) if os.path.exists(profile_pathname): if args.exist_ok: ConanOutput().info(f"Profile '{profile_name}' already exists, skipping detection") return if not args.force: raise ConanException(f"Profile '{profile_pathname}' already exists") detected_profile = conan_api.profiles.detect() ConanOutput().success("\nDetected profile:") cli_out_write(detected_profile.dumps()) contents = detected_profile.dumps() ConanOutput().warning("This profile is a guess of your environment, please check it.") if detected_profile.settings.get("os") == "Macos": ConanOutput().warning("Defaulted to cppstd='gnu17' for apple-clang.") ConanOutput().warning("The output of this command is not guaranteed to be stable and can " "change in future Conan versions.") ConanOutput().warning("Use your own profile files for stability.") ConanOutput().success(f"Saving detected profile to {profile_pathname}") dir_path = os.path.dirname(profile_pathname) if dir_path: os.makedirs(dir_path, exist_ok=True) with open(profile_pathname, "w", encoding="utf-8", newline="") as f: f.write(contents) @conan_subcommand(formatters={"text": profiles_list_cli_output, "json": default_json_formatter}) def profile_list(conan_api, parser, subparser, *args): # noqa """ List all profiles in the cache. """ parser.parse_args(*args) result = conan_api.profiles.list() return result @conan_command(group="Consumer") def profile(conan_api, parser, *args): # noqa """ Manage profiles. """ ================================================ FILE: conan/cli/commands/remote.py ================================================ import json import os from collections import OrderedDict from conan.api.conan_api import ConanAPI from conan.api.model import Remote, LOCAL_RECIPES_INDEX from conan.api.output import cli_out_write, Color from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.commands.list import remote_color, error_color, recipe_color, \ reference_color from conan.errors import ConanException def _print_remotes_json(remotes): info = [{"name": r.name, "url": r.url, "verify_ssl": r.verify_ssl, "enabled": not r.disabled, "allowed_packages": r.allowed_packages, "recipes_only": r.recipes_only} for r in remotes] cli_out_write(json.dumps(info, indent=4)) def _print_remote_list(remotes): for r in remotes: output_str = str(r) cli_out_write(output_str) def _print_remote_user_list(results): for remote_name, result in results.items(): cli_out_write(f"{remote_name}:", fg=remote_color) if result["user_name"] is None: cli_out_write(" No user", fg=error_color) else: cli_out_write(" Username: ", fg=recipe_color, endline="") cli_out_write(result["user_name"], fg=reference_color) cli_out_write(" authenticated: ", fg=recipe_color, endline="") cli_out_write(result["authenticated"], fg=reference_color) def _print_remote_user_set(results): for remote_name, result in results.items(): from_user = "'{}'".format(result["previous_info"]["user_name"]) from_user += " (anonymous)" \ if not result["previous_info"]["authenticated"] else " (authenticated)" to_user = "'{}'".format(result["info"]["user_name"]) to_user += " (anonymous)" \ if not result["info"]["authenticated"] else " (authenticated)" message = "Changed user of remote '{}' from {} to {}".format(remote_name, from_user, to_user) cli_out_write(message) def _print_remotes_users_json(results): cli_out_write(json.dumps(list(results.values()))) @conan_subcommand(formatters={"text": _print_remote_list, "json": _print_remotes_json}) def remote_list(conan_api: ConanAPI, parser, subparser, *args): # noqa """ List current remotes. """ parser.parse_args(*args) return conan_api.remotes.list(only_enabled=False) @conan_subcommand() def remote_add(conan_api, parser, subparser, *args): """ Add a remote. """ subparser.add_argument("name", help="Name of the remote to add") subparser.add_argument("url", help="Url of the remote") subparser.add_argument("--insecure", dest="secure", action='store_false', help="Allow insecure server connections when using SSL") subparser.add_argument("--index", action=OnceArgument, type=int, help="Insert the remote at a specific position in the remote list") subparser.add_argument("-f", "--force", action='store_true', help="Force the definition of the remote even if duplicated") subparser.add_argument("-ap", "--allowed-packages", action="append", default=None, help="Add recipe reference pattern to list of allowed packages for " "this remote") subparser.add_argument("-t", "--type", choices=[LOCAL_RECIPES_INDEX], help="Define the remote type") subparser.add_argument("--recipes-only", action="store_true", default=False, help="Disallow binary downloads from this remote, only recipes " "will be downloaded") subparser.set_defaults(secure=True) args = parser.parse_args(*args) url_folder = make_abs_path(args.url) remote_type = args.type or (LOCAL_RECIPES_INDEX if os.path.isdir(url_folder) else None) url = url_folder if remote_type == LOCAL_RECIPES_INDEX else args.url r = Remote(args.name, url, args.secure, disabled=False, remote_type=remote_type, allowed_packages=args.allowed_packages, recipes_only=args.recipes_only) conan_api.remotes.add(r, force=args.force, index=args.index) @conan_subcommand(formatters={"text": _print_remote_list}) def remote_remove(conan_api, parser, subparser, *args): """ Remove remotes. """ subparser.add_argument("remote", help="Name of the remote to remove. " "Accepts 'fnmatch' style wildcards.") # to discuss args = parser.parse_args(*args) remotes = conan_api.remotes.remove(args.remote) cli_out_write("Removed remotes:") return remotes @conan_subcommand() def remote_update(conan_api, parser, subparser, *args): """ Update a remote. """ subparser.add_argument("remote", help="Name of the remote to update") subparser.add_argument("--url", action=OnceArgument, help="New url for the remote") subparser.add_argument("--secure", dest="secure", action='store_true', help="Don't allow insecure server connections when using SSL") subparser.add_argument("--insecure", dest="secure", action='store_false', help="Allow insecure server connections when using SSL") subparser.add_argument("--index", action=OnceArgument, type=int, help="Insert the remote at a specific position in the remote list") subparser.add_argument("-ap", "--allowed-packages", action="append", default=None, help="Add recipe reference pattern to the list of allowed packages " "for this remote") subparser.add_argument("--recipes-only", default=None, const="True", nargs="?", choices=["True", "False"], help="Disallow binary downloads from this remote, only recipes will " "be downloaded") subparser.set_defaults(secure=None) args = parser.parse_args(*args) if (args.url is None and args.secure is None and args.index is None and args.allowed_packages is None and args.recipes_only is None): subparser.error("Please add at least one argument to update") args.recipes_only = None if args.recipes_only is None else args.recipes_only == "True" conan_api.remotes.update(args.remote, args.url, args.secure, index=args.index, allowed_packages=args.allowed_packages, recipes_only=args.recipes_only) @conan_subcommand() def remote_rename(conan_api, parser, subparser, *args): """ Rename a remote. """ subparser.add_argument("remote", help="Current name of the remote") subparser.add_argument("new_name", help="New name for the remote") args = parser.parse_args(*args) conan_api.remotes.rename(args.remote, args.new_name) @conan_subcommand(formatters={"text": _print_remote_list, "json": _print_remotes_json}) def remote_enable(conan_api, parser, subparser, *args): """ Enable all the remotes matching a pattern. """ subparser.add_argument("remote", help="Pattern of the remote/s to enable. " "The pattern uses 'fnmatch' style wildcards.") args = parser.parse_args(*args) return conan_api.remotes.enable(args.remote) @conan_subcommand(formatters={"text": _print_remote_list, "json": _print_remotes_json}) def remote_disable(conan_api, parser, subparser, *args): """ Disable all the remotes matching a pattern. """ subparser.add_argument("remote", help="Pattern of the remote/s to disable. " "The pattern uses 'fnmatch' style wildcards.") args = parser.parse_args(*args) return conan_api.remotes.disable(args.remote) # ### User related commands @conan_subcommand(formatters={"text": _print_remote_user_list, "json": _print_remotes_users_json}) def remote_list_users(conan_api, parser, subparser, *args): # noqa """ List the users logged into all the remotes. """ parser.parse_args(*args) remotes = conan_api.remotes.list() ret = OrderedDict() if not remotes: raise ConanException("No remotes defined") for r in remotes: ret[r.name] = conan_api.remotes.user_info(r) return ret @conan_subcommand(formatters={"text": _print_remote_user_set, "json": _print_remotes_users_json}) def remote_login(conan_api, parser, subparser, *args): """ Login into the specified remotes matching a pattern. """ subparser.add_argument("remote", help="Pattern or name of the remote to login into. " "The pattern uses 'fnmatch' style wildcards.") subparser.add_argument("username", nargs="?", help='Username') subparser.add_argument("-p", "--password", nargs='?', type=str, action=OnceArgument, help='User password. Use double quotes if password with spacing, ' 'and escape quotes if existing. If empty, the password is ' 'requested interactively (not exposed)') args = parser.parse_args(*args) remotes = conan_api.remotes.list(pattern=args.remote, only_enabled=False) if not remotes: raise ConanException("There are no remotes matching the '{}' pattern".format(args.remote)) return conan_api.remotes.login(remotes, args.username, args.password) @conan_subcommand(formatters={"text": _print_remote_user_set, "json": _print_remotes_users_json}) def remote_set_user(conan_api, parser, subparser, *args): """ Associate a username with a remote matching a pattern without performing the authentication. """ subparser.add_argument("remote", help="Pattern or name of the remote. " "The pattern uses 'fnmatch' style wildcards.") subparser.add_argument("username", help='Username') args = parser.parse_args(*args) remotes = conan_api.remotes.list(pattern=args.remote) if not remotes: raise ConanException("There are no remotes matching the '{}' pattern".format(args.remote)) ret = OrderedDict() for r in remotes: previous_info = conan_api.remotes.user_info(r) if previous_info["user_name"] != args.username: conan_api.remotes.user_logout(r) conan_api.remotes.user_set(r, args.username) ret[r.name] = {"previous_info": previous_info, "info": conan_api.remotes.user_info(r)} return ret @conan_subcommand(formatters={"text": _print_remote_user_set, "json": _print_remotes_users_json}) def remote_logout(conan_api, parser, subparser, *args): """ Clear the existing credentials for the specified remotes matching a pattern. """ subparser.add_argument("remote", help="Pattern or name of the remote to logout. " "The pattern uses 'fnmatch' style wildcards.") args = parser.parse_args(*args) remotes = conan_api.remotes.list(pattern=args.remote) if not remotes: raise ConanException("There are no remotes matching the '{}' pattern".format(args.remote)) ret = OrderedDict() for r in remotes: previous_info = conan_api.remotes.user_info(r) conan_api.remotes.user_logout(r) info = conan_api.remotes.user_info(r) ret[r.name] = {"previous_info": previous_info, "info": info} return ret def _print_auth(remotes): for remote_name, msg in remotes.items(): if msg is None: cli_out_write(f"{remote_name}: No user defined") else: cli_out_write(f"{remote_name}:") for k, v in msg.items(): cli_out_write(f" {k}: {v}", fg=Color.BRIGHT_RED if k == "error" else Color.WHITE) def _print_auth_json(results): cli_out_write(json.dumps(results)) @conan_subcommand(formatters={"text": _print_auth, "json": _print_auth_json}) def remote_auth(conan_api, parser, subparser, *args): """ Authenticate in the defined remotes. Use CONAN_LOGIN_USERNAME* and CONAN_PASSWORD* variables if available. Ask for username and password interactively in case (re-)authentication is required and there are no CONAN_LOGIN* and CONAN_PASSWORD* variables available which could be used. Usually you'd use this method over conan remote login for scripting which needs to run in CI and locally. """ subparser.add_argument("remote", help="Pattern or name of the remote/s to authenticate against." " The pattern uses 'fnmatch' style wildcards.") subparser.add_argument("--with-user", action="store_true", help="Only try to auth in those remotes that already " "have a username or a CONAN_LOGIN_USERNAME* env-var defined") subparser.add_argument("--force", action="store_true", help="Force authentication for anonymous-enabled repositories. " "Can be used for force authentication in case your Artifactory " "instance has anonymous access enabled and Conan would not ask " "for username and password even for non-anonymous repositories " "if not yet authenticated.") args = parser.parse_args(*args) remotes = conan_api.remotes.list(pattern=args.remote) if not remotes: raise ConanException("There are no remotes matching the '{}' pattern".format(args.remote)) results = {} for r in remotes: try: results[r.name] = {"user": conan_api.remotes.user_auth(r, args.with_user, args.force)} except Exception as e: results[r.name] = {"error": str(e)} return results @conan_command(group="Consumer") def remote(conan_api, parser, *args): # noqa """ Manage the remote list and the users authenticated on them. """ ================================================ FILE: conan/cli/commands/remove.py ================================================ from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, MultiPackagesList, PackagesList from conan.api.output import cli_out_write, ConanOutput from conan.api.input import UserInput from conan.cli import make_abs_path from conan.cli.command import conan_command, OnceArgument from conan.cli.commands.list import print_list_json, print_serial from conan.errors import ConanException def summary_remove_list(results): """ Do a little format modification to serialized package list so it looks prettier on text output """ cli_out_write("Remove summary:") info = results["results"] result = {} for remote, remote_info in info.items(): new_info = result.setdefault(remote, {}) for ref, content in remote_info.items(): for rev, rev_content in content.get("revisions", {}).items(): pkgids = rev_content.get('packages') if pkgids is None: new_info.setdefault(f"{ref}#{rev}", "Removed recipe and all binaries") else: new_info.setdefault(f"{ref}#{rev}", f"Removed binaries: {list(pkgids)}") print_serial(result) @conan_command(group="Consumer", formatters={"text": summary_remove_list, "json": print_list_json}) def remove(conan_api: ConanAPI, parser, *args): """ Remove recipes or packages from local cache or a remote. - If no remote is specified (-r), the removal will be done in the local conan cache. - If a recipe reference is specified, it will remove the recipe and all the packages, unless -p is specified, in that case, only the packages matching the specified query (and not the recipe) will be removed. - If a package reference is specified, it will remove only the package. """ parser.add_argument('pattern', nargs="?", help="A pattern in the form 'pkg/version#revision:package_id#revision', " "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. " "If revision is not specified, it is assumed latest one.") parser.add_argument('-c', '--confirm', default=False, action='store_true', help='Remove without requesting a confirmation') parser.add_argument('-p', '--package-query', action=OnceArgument, help="Remove all packages (empty) or provide a query: " "os=Windows AND (arch=x86 OR compiler=gcc)") parser.add_argument('-r', '--remote', action=OnceArgument, help='Will remove from the specified remote') parser.add_argument("-l", "--list", help="Package list file") parser.add_argument('--lru', default=None, action=OnceArgument, help="Remove recipes and binaries that have not been recently used. Use a" " time limit like --lru=5d (days) or --lru=4w (weeks)," " h (hours), m(minutes)") parser.add_argument("--dry-run", default=False, action="store_true", help="Do not remove any items, only print those which would be removed") args = parser.parse_args(*args) if args.pattern is None and args.list is None: raise ConanException("Missing pattern or package list file") if args.pattern and args.list: raise ConanException("Cannot define both the pattern and the package list file") if args.package_query and args.list: raise ConanException("Cannot define package-query and the package list file") if args.remote and args.lru: raise ConanException("'--lru' cannot be used in remotes, only in cache") if args.list and args.lru: raise ConanException("'--lru' cannot be used with input package list") ui = UserInput(conan_api.config.get("core:non_interactive")) remote = conan_api.remotes.get(args.remote) if args.remote else None cache_name = "Local Cache" if not remote else remote.name def confirmation(message): return args.confirm or ui.request_boolean(message) if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list[cache_name] refs_to_remove = list(package_list.items()) if not refs_to_remove: # the package list might contain only refs, no revs ConanOutput().warning("Nothing to remove, package list do not contain recipe revisions") else: ref_pattern = ListPattern(args.pattern, rrev="*", prev="*") if ref_pattern.package_id is None and args.package_query is not None: raise ConanException('--package-query supplied but the pattern does not match packages') package_list = conan_api.list.select(ref_pattern, args.package_query, remote, lru=args.lru) multi_package_list = MultiPackagesList() multi_package_list.add(cache_name, package_list) result = PackagesList() for ref, packages in package_list.items(): ref_dict = package_list.recipe_dict(ref).copy() packages_dict = ref_dict.pop("packages", None) if packages_dict is None: if confirmation(f"Remove the recipe and all the packages of '{ref.repr_notime()}'?"): if not args.dry_run: conan_api.remove.recipe(ref, remote=remote) result.add_ref(ref) result.recipe_dict(ref).update(ref_dict) # it doesn't contain "packages" else: if not packages: # weird, there is inner package-ids but without prevs ConanOutput().info(f"No binaries to remove for '{ref.repr_notime()}'") continue for pref, pkg_id_info in packages.items(): if confirmation(f"Remove the package '{pref.repr_notime()}'?"): if not args.dry_run: conan_api.remove.package(pref, remote=remote) result.add_ref(ref) result.recipe_dict(ref).update(ref_dict) # it doesn't contain "packages" result.add_pref(pref, pkg_id_info) pkg_dict = package_list.package_dict(pref) result.package_dict(pref).update(pkg_dict) multi_package_list.add(cache_name, result) return { "results": multi_package_list.serialize(), "conan_api": conan_api } ================================================ FILE: conan/cli/commands/report.py ================================================ import os from conan.cli.formatters.report import format_diff_html, format_diff_txt, format_diff_json from conan.api.conan_api import ConanAPI from conan.cli.command import conan_command, conan_subcommand @conan_command(group="Security") def report(conan_api: ConanAPI, parser, *args): """ Gets information about the recipe and its sources. """ @conan_subcommand(formatters={"text": format_diff_txt, "json": format_diff_json, "html": format_diff_html}) def report_diff(conan_api, parser, subparser, *args): """ Get the difference between two recipes with their sources. It can be used to compare two different versions of the same recipe, or two different recipe revisions. Each old/new recipe can be specified by a path to a conanfile.py and a companion reference, or by a reference only. If only a reference is specified, it will be searched in the local cache, or downloaded from the specified remotes. If no revision is specified, the latest revision will be used. """ ref_help = ("{type} reference, e.g. 'mylib/1.0'. " "If used on its own, it can contain a revision, which will be resolved to the latest one if not provided, " "but it will be ignored if a path is specified. " "If used with a path, it will be used to create the reference for the recipe to be compared.") subparser.add_argument("-op", "--old-path", help="Path to the old recipe if comparing a local recipe is desired") subparser.add_argument("-or", "--old-reference", help=ref_help.format(type="Old"), required=True) subparser.add_argument("-np", "--new-path", help="Path to the new recipe if comparing a local recipe is desired") subparser.add_argument("-nr", "--new-reference", help=ref_help.format(type="New"), required=True) subparser.add_argument("-r", "--remote", action="append", default=None, help='Look in the specified remote or remotes server') args = parser.parse_args(*args) cwd = os.getcwd() enabled_remotes = conan_api.remotes.list(args.remote or "*") result = conan_api.report.diff(args.old_reference, args.new_reference, enabled_remotes, old_path=args.old_path, new_path=args.new_path, cwd=cwd) return { "conan_api": conan_api, "diff": result["diff"], "old_export_ref": result["old_export_ref"], "new_export_ref": result["new_export_ref"], "old_cache_path": result["old_cache_path"], "new_cache_path": result["new_cache_path"], "src_prefix": result["src_prefix"], "dst_prefix": result["dst_prefix"], } ================================================ FILE: conan/cli/commands/require.py ================================================ import os import re from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, RecipeReference from conan.api.output import ConanOutput from conan.cli.command import conan_command, conan_subcommand from conan.errors import ConanException from conan.internal.util.files import save, load @conan_subcommand() def require_remove(conan_api, parser, subparser, *args): """ Removes a requirement from your local conanfile. """ subparser.add_argument("--folder", help="Path to a folder containing a recipe (conanfile.py). " "Defaults to the current directory",) subparser.add_argument("requires", nargs="*", help="Requirement name.") subparser.add_argument("-tor", "--tool", action="append", default=[], help="Tool requirement name.") subparser.add_argument("-ter", "--test", action="append", default=[], help="Test requirement name.") args = parser.parse_args(*args) path = conan_api.local.get_conanfile_path(args.folder or '.', os.getcwd(), py=True) # Check if that requirement exists in the conanfile. If yes, abort. conanfile = load(path) ConanOutput().debug(f"Loaded conanfile from {path}.") requires = [(r, "requires") for r in args.requires] tool_requires = [(r, "tool_requires") for r in args.tool] test_requires = [(r, "test_requires") for r in args.test] success_msgs = [] for (name, req_attr) in requires + tool_requires + test_requires: if not re.search(rf"self\.{req_attr}\([\"']{name}", conanfile): ConanOutput().warning(f"The {req_attr} {name} is not declared in your conanfile.") continue # Replace the whole line conanfile = re.sub(rf"^\s*self\.{req_attr}\([\"']{name}.*\n?", '', conanfile, flags=re.MULTILINE) success_msgs.append(f"Removed {name} dependency as {req_attr}.") save(path, conanfile) ConanOutput().success('\n'.join(success_msgs)) @conan_subcommand() def require_add(conan_api, parser, subparser, *args): """ Add a new requirement to your local conanfile as a version range. By default, it will look for the requirement versions remotely. """ subparser.add_argument("--folder", help="Path to a folder containing a recipe (conanfile.py). " "Defaults to the current directory",) subparser.add_argument("requires", nargs="*", help="Requirement name.") subparser.add_argument("-tor", "--tool", action="append", default=[], help="Tool requirement name.") subparser.add_argument("-ter", "--test", action="append", default=[], help="Test requirement name.") group = subparser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", default=None, action="append", help="Remote names. Accepts wildcards ('*' means all the remotes available)") group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') args = parser.parse_args(*args) requires = [(r, "requires", "requirements") for r in args.requires] tool_requires = [(r, "tool_requires", "build_requirements") for r in args.tool] test_requires = [(r, "test_requires", "build_requirements") for r in args.test] if not any(requires + tool_requires + test_requires): raise ConanException("You need to add any requires, tool_requires or test_requires.") path = conan_api.local.get_conanfile_path(args.folder or ".", os.getcwd(), py=True) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [None] conanfile = load(path) ConanOutput().debug(f"Loaded conanfile from {path}.") cached_results = {} success_msgs = [] for (name, req_attr, req_func) in requires + tool_requires + test_requires: # Check if that requirement exists in the conanfile. If yes, do nothing. if re.search(rf"self\.{req_attr}\([\"']{name}", conanfile): ConanOutput().warning(f"The {req_attr} {name} is already in use.") continue if name in cached_results: # Avoid double-search in remotes/cache, e.g., protobuf reference = RecipeReference.loads(f"{name}/{cached_results[name]}") elif "/" in name: # it already brings a version reference = RecipeReference.loads(name) cached_results[name] = reference.version # caching the result else: # Search the latest version in remotes/cache ref_pattern = ListPattern(f"{name}/*") # If neither remote nor cache are defined, show results only from cache results = {} for remote in remotes: try: pkglist = conan_api.list.select(ref_pattern, remote=remote) except Exception as e: remote_name = "Cache" if remote is None else remote.name ConanOutput().warning(f"[{remote_name}] {str(e)}") else: results = pkglist.serialize() if results: break if not results: ConanOutput().error(f"Recipe {name} not found.") continue # Put the upper limit for that requirement (next major version) reference = RecipeReference.loads(results.popitem()[0]) cached_results[name] = reference.version # caching the result try: version_range = f"{reference.name}/[>={reference.version} <{str(reference.version.bump(0))}]" except ConanException: # likely cannot bump the version, using it without ranges version_range = str(reference) full_version_range = f'self.{req_attr}("{version_range}")' if full_version_range: tab_space = " " * 4 if f"def {req_func}(" in conanfile: conanfile = conanfile.replace(f"def {req_func}(self):\n", f"def {req_func}(self):\n{tab_space * 2}{full_version_range}\n") else: requirements_func = f"\n{tab_space}def {req_func}(self):\n{tab_space * 2}{full_version_range}\n" conanfile += requirements_func success_msgs.append(f"Added '{version_range}' as a new {req_attr}.") save(path, conanfile) ConanOutput().success('\n'.join(success_msgs)) @conan_command(group="Consumer") def require(conan_api: ConanAPI, parser, *args): """ Adds/removes requirements to/from your local conanfile. """ ================================================ FILE: conan/cli/commands/run.py ================================================ import os import tempfile from conan.api.output import ConanOutput, LEVEL_STATUS, Color, LEVEL_ERROR, LEVEL_QUIET from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.command import conan_command from conan.cli.commands.install import _run_install_command from conan.errors import ConanException @conan_command(group="Consumer") def run(conan_api, parser, *args): """ (Experimental) Run a command given a set of requirements from a recipe or from command line. """ common_graph_args(parser) parser.add_argument("command", help="Command to run") parser.add_argument("--context", help="Context to use, by default both contexts are activated " "if not specified", choices=["host", "build"], default=None) parser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided path is a build-require') args = parser.parse_args(*args) validate_common_graph_args(args) cwd = os.getcwd() ConanOutput().info("Installing and building dependencies, this might take a while...", fg=Color.BRIGHT_MAGENTA) previous_log_level = ConanOutput.get_output_level() if previous_log_level == LEVEL_STATUS: ConanOutput.set_output_level(LEVEL_QUIET) with tempfile.TemporaryDirectory("conanrun") as tmpdir: # Default values for install setattr(args, "output_folder", tmpdir) setattr(args, "generator", []) try: deps_graph, lockfile, _ = _run_install_command(conan_api, args, cwd, return_install_error=False) except ConanException as e: ConanOutput.set_output_level(previous_log_level) ConanOutput().error("Error installing the dependencies. To debug this, you can either:\n" " - Re-run the command with increased verbosity (-v, -vv)\n" " - Run 'conan install' first to ensure dependencies are installed, " "or to see errors during installation\n") raise e context_env_map = { "build": "conanbuild", "host": "conanrun", } envfiles = list(context_env_map.values()) if args.context is None \ else [context_env_map.get(args.context)] ConanOutput.set_output_level(LEVEL_ERROR) deps_graph.root.conanfile.run(args.command, cwd=cwd, env=envfiles) ================================================ FILE: conan/cli/commands/search.py ================================================ from collections import OrderedDict from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern from conan.cli.command import conan_command from conan.cli.commands.list import print_list_text, print_list_json from conan.errors import ConanException @conan_command(group="Consumer", formatters={"text": print_list_text, "json": print_list_json}) def search(conan_api: ConanAPI, parser, *args): """ Search for package recipes in all the remotes (by default), or a remote. """ parser.add_argument('reference', help="Recipe reference to search for. " "It can contain * as wildcard at any reference field.") parser.add_argument("-r", "--remote", action="append", help="Remote names. Accepts wildcards. If not specified it searches " "in all the remotes") args = parser.parse_args(*args) ref_pattern = ListPattern(args.reference, rrev=None) if ref_pattern.package_id is not None or ref_pattern.rrev is not None: raise ConanException("Specifying a recipe revision or package ID is not allowed") remotes = conan_api.remotes.list(args.remote) if not remotes: raise ConanException("There are no remotes to search from") results = OrderedDict() for remote in remotes: try: pkglist = conan_api.list.select(ref_pattern, package_query=None, remote=remote) except Exception as e: results[remote.name] = {"error": str(e)} else: results[remote.name] = pkglist.serialize() return { "results": results } ================================================ FILE: conan/cli/commands/source.py ================================================ import os from conan.cli.command import conan_command from conan.cli.args import add_reference_args @conan_command(group="Creator") def source(conan_api, parser, *args): """ Call the source() method. """ parser.add_argument("path", help="Path to a folder containing a conanfile.py. " "Defaults to current directory", default=".", nargs="?") add_reference_args(parser) args = parser.parse_args(*args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) enabled_remotes = conan_api.remotes.list() # for python_requires not local # TODO: Missing lockfile for python_requires conan_api.local.source(path, name=args.name, version=args.version, user=args.user, channel=args.channel, remotes=enabled_remotes) ================================================ FILE: conan/cli/commands/test.py ================================================ import os from conan.api.output import ConanOutput from conan.cli.command import conan_command, OnceArgument from conan.cli.commands.create import test_package, _check_tested_reference_matches from conan.cli.args import add_lockfile_args, add_common_install_arguments from conan.cli.formatters.graph import format_graph_json from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_basic, print_graph_packages from conan.api.model import RecipeReference @conan_command(group="Creator", formatters={"json": format_graph_json}) def test(conan_api, parser, *args): """ Test a package from a test_package folder. """ parser.add_argument("path", action=OnceArgument, help="Path to a test_package folder containing a conanfile.py. " "Defaults to a 'test_package' folder in the current directory", default="test_package", nargs='?') parser.add_argument("reference", action=OnceArgument, help='Provide a package reference to test') add_common_install_arguments(parser) add_lockfile_args(parser) args = parser.parse_args(*args) cwd = os.getcwd() ref = RecipeReference.loads(args.reference) path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) deps_graph = run_test(conan_api, path, ref, profile_host, profile_build, remotes, lockfile, args.update, build_modes=args.build, tested_python_requires=ref) lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) return {"graph": deps_graph, "conan_api": conan_api} def run_test(conan_api, path, ref, profile_host, profile_build, remotes, lockfile, update, build_modes, tested_python_requires=None, build_modes_test=None, tested_graph=None): root_node = conan_api.graph.load_root_test_conanfile(path, ref, profile_host, profile_build, remotes=remotes, update=update, lockfile=lockfile, tested_python_requires=tested_python_requires) out = ConanOutput() out.title("Launching test_package") deps_graph = conan_api.graph.load_graph(root_node, profile_host=profile_host, profile_build=profile_build, lockfile=lockfile, remotes=remotes, update=update, check_update=update) print_graph_basic(deps_graph) deps_graph.report_graph_error() conan_api.graph.analyze_binaries(deps_graph, build_modes, remotes=remotes, update=update, lockfile=lockfile, build_modes_test=build_modes_test, tested_graph=tested_graph) print_graph_packages(deps_graph) conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) _check_tested_reference_matches(deps_graph, ref, out) test_package(conan_api, deps_graph, path) return deps_graph ================================================ FILE: conan/cli/commands/upload.py ================================================ from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, MultiPackagesList, PackagesList from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.cli.command import conan_command, OnceArgument from conan.cli.commands.list import print_list_json, print_serial from conan.api.input import UserInput from conan.errors import ConanException def summary_upload_list(results): """ Do a little format modification to serialized package list, so it looks prettier on text output """ ConanOutput().subtitle("Upload summary") info = results["results"] def format_upload(item): if isinstance(item, dict): result = {} for k, v in item.items(): if isinstance(v, dict): v.pop("info", None) v.pop("timestamp", None) v.pop("files", None) v.pop("upload-urls", None) upload_value = v.pop("upload", None) if upload_value is not None: msg = "Uploaded" if upload_value else "Skipped, already in server" force_upload = v.pop("force_upload", None) if force_upload: msg += " - forced" k = f"{k} ({msg})" result[k] = format_upload(v) return result return item info = {remote: format_upload(values) for remote, values in info.items()} print_serial(info) @conan_command(group="Creator", formatters={"text": summary_upload_list, "json": print_list_json}) def upload(conan_api: ConanAPI, parser, *args): """ Upload packages to a remote. By default, all the matching references are uploaded (all revisions). By default, if a recipe reference is specified, it will upload all the revisions for all the binary packages, unless --only-recipe is specified. You can use the "latest" placeholder at the "reference" argument to specify the latest revision of the recipe or the package. """ parser.add_argument('pattern', nargs="?", help="A pattern in the form 'pkg/version#revision:package_id#revision', " "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. " "If revision is not specified, it is assumed latest one.") parser.add_argument('-p', '--package-query', default=None, action=OnceArgument, help="Only upload packages matching a specific query. e.g: os=Windows AND " "(arch=x86 OR compiler=gcc)") # using required, we may want to pass this as a positional argument? parser.add_argument("-r", "--remote", action=OnceArgument, required=True, help='Upload to this specific remote') parser.add_argument("--only-recipe", action='store_true', default=False, help='Upload only the recipe/s, not the binary packages.') parser.add_argument("--force", action='store_true', default=False, help='Force the upload of the artifacts even if the revision already exists' ' in the server') parser.add_argument("--check", action='store_true', default=False, help='Perform an integrity check, using the manifests, before upload') parser.add_argument('-c', '--confirm', default=False, action='store_true', help='Upload all matching recipes without confirmation') parser.add_argument('--dry-run', default=False, action='store_true', help='Do not execute the real upload (experimental)') parser.add_argument("-l", "--list", help="Package list file") parser.add_argument("-m", "--metadata", action='append', help='Upload the metadata, even if the package is already in the server and ' 'not uploaded') args = parser.parse_args(*args) remote = conan_api.remotes.get(args.remote) enabled_remotes = conan_api.remotes.list() if args.pattern is None and args.list is None: raise ConanException("Missing pattern or package list file") if args.pattern and args.list: raise ConanException("Cannot define both the pattern and the package list file") if args.package_query and args.list: raise ConanException("Cannot define package-query and the package list file") if args.list: listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list["Local Cache"] if args.only_recipe: package_list.only_recipes() else: ref_pattern = ListPattern(args.pattern, package_id="*", only_recipe=args.only_recipe) package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) if package_list: # If only if search with "*" we ask for confirmation if not args.list and not args.confirm and "*" in args.pattern: package_list = _ask_confirm_upload(conan_api, package_list) conan_api.upload.upload_full(package_list, remote, enabled_remotes, args.check, args.force, args.metadata, args.dry_run) elif args.list: # Don't error on no recipes for automated workflows using list, # but warn to tell the user that no packages were uploaded ConanOutput().warning(f"No packages were uploaded because the package list is empty.") else: raise ConanException("No recipes found matching pattern '{}'".format(args.pattern)) pkglist = MultiPackagesList() pkglist.add(remote.name, package_list) return { "results": pkglist.serialize(), "conan_api": conan_api } def _ask_confirm_upload(conan_api, package_list): ui = UserInput(conan_api.config.get("core:non_interactive")) result = PackagesList() for ref, packages in package_list.items(): msg = f"Are you sure you want to upload recipe '{ref.repr_notime()}'?" if ui.request_boolean(msg): result.add_ref(ref) ref_dict = package_list.recipe_dict(ref).copy() ref_dict.pop("packages", None) result.recipe_dict(ref).update(ref_dict) for pref, pkg_id_info in packages.items(): msg = f"Are you sure you want to upload package '{pref.repr_notime()}'?" if ui.request_boolean(msg): result.add_pref(pref, pkg_id_info) result.package_dict(pref).update(package_list.package_dict(pref)) return result ================================================ FILE: conan/cli/commands/version.py ================================================ from conan.cli.commands.list import print_serial from conan.cli.command import conan_command from conan.cli.formatters import default_json_formatter from conan import conan_version import platform import sys @conan_command(group="Consumer", formatters={"text": print_serial, "json": default_json_formatter}) def version(conan_api, parser, *args): """ Give information about the Conan client version. """ parser.parse_args(*args) return { 'version': str(conan_version), 'conan_path': sys.argv[0], 'python': { 'version': platform.python_version().replace('\n', ''), 'sys_version': sys.version.replace('\n', ''), 'sys_executable': sys.executable, 'is_frozen': getattr(sys, 'frozen', False), 'architecture': platform.machine(), }, 'system': { 'version': platform.version(), 'platform': platform.platform(), 'system': platform.system(), 'release': platform.release(), 'cpu': platform.processor(), } } ================================================ FILE: conan/cli/commands/workspace.py ================================================ import json import os from conan.api.conan_api import ConanAPI from conan.api.model import RecipeReference from conan.api.output import ConanOutput, cli_out_write from conan.api.subapi.workspace import WorkspaceAPI from conan.cli import make_abs_path from conan.cli.args import add_reference_args, add_common_install_arguments, add_lockfile_args from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.commands.list import print_serial from conan.cli.formatters.graph import format_graph_json from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_packages, print_graph_basic from conan.errors import ConanException from conan.internal.graph.install_graph import ProfileArgs @conan_subcommand(formatters={"text": cli_out_write}) def workspace_root(conan_api: ConanAPI, parser, subparser, *args): # noqa """ Return the folder containing the conanws.py/conanws.yml workspace file """ parser.parse_args(*args) ws = conan_api.workspace return ws.folder() @conan_subcommand() def workspace_open(conan_api: ConanAPI, parser, subparser, *args): """ Open specific references """ subparser.add_argument("reference", help="Open this package source repository") group = subparser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", action="append", default=None, help='Look in the specified remote or remotes server') group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') args = parser.parse_args(*args) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] cwd = os.getcwd() conan_api.workspace.open(args.reference, remotes=remotes, cwd=cwd) @conan_subcommand() def workspace_add(conan_api: ConanAPI, parser, subparser, *args): """ Add packages to current workspace """ subparser.add_argument('path', nargs="?", help='Path to the package folder in the user workspace') add_reference_args(subparser) subparser.add_argument("--ref", help="Open and add this reference") subparser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') group = subparser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", action="append", default=None, help='Look in the specified remote or remotes server') group.add_argument("-nr", "--no-remote", action="store_true", help='Do not use remote, resolve exclusively in the cache') args = parser.parse_args(*args) if args.path and args.ref: raise ConanException("Do not use both 'path' and '--ref' argument") remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] cwd = os.getcwd() path = args.path if args.ref: # TODO: Use path here to open in this path path = conan_api.workspace.open(args.ref, remotes, cwd=cwd) ref = conan_api.workspace.add(path, args.name, args.version, args.user, args.channel, cwd, args.output_folder, remotes=remotes) ConanOutput().success("Reference '{}' added to workspace".format(ref)) @conan_subcommand() def workspace_complete(conan_api: ConanAPI, parser, subparser, *args): """ Complete the workspace, opening or adding intermediate packages to it that have requirements to other packages in the workspace. """ add_common_install_arguments(subparser) group = subparser.add_argument_group("lockfile arguments") group.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " "existing 'conan.lock' file") group.add_argument("--lockfile-partial", action="store_true", help="Do not raise an error if some dependency is not found in lockfile") args = parser.parse_args(*args) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] # The lockfile by default if not defined will be read from the root workspace folder ws_folder = conan_api.workspace.folder() lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) ConanOutput().box("Workspace completing intermediate packages") conan_api.workspace.complete(profile_host, profile_build, lockfile, remotes, update=args.update) @conan_subcommand() def workspace_remove(conan_api: ConanAPI, parser, subparser, *args): """ Remove packages from the current workspace """ subparser.add_argument('path', help='Path to the package folder in the user workspace') args = parser.parse_args(*args) removed = conan_api.workspace.remove(make_abs_path(args.path)) ConanOutput().info(f"Removed from workspace: {removed}") def _print_json(data): results = data["info"] myjson = json.dumps(results, indent=4) cli_out_write(myjson) def _print_workspace_info(data): ret = [] for package_info in data["info"]["packages"]: ret.append(f"- path: {package_info['path']}") ref = package_info.get('ref') if ref: ret.append(f" ref: {ref}") data["info"]["packages"] = ret or "(empty)" print_serial(data["info"]) @conan_subcommand(formatters={"text": _print_workspace_info, "json": _print_json}) def workspace_info(conan_api: ConanAPI, parser, subparser, *args): # noqa """ Display info for current workspace """ parser.parse_args(*args) return {"info": conan_api.workspace.info()} @conan_subcommand() def workspace_build(conan_api: ConanAPI, parser, subparser, *args): """ Call "conan build" for packages in the workspace, in the right order """ _install_build(conan_api, parser, subparser, True, *args) @conan_subcommand() def workspace_install(conan_api: ConanAPI, parser, subparser, *args): """ Call "conan install" for packages in the workspace, in the right order """ _install_build(conan_api, parser, subparser, False, *args) def _install_build(conan_api: ConanAPI, parser, subparser, build, *args): subparser.add_argument("--pkg", action="append", help='Define specific packages') add_common_install_arguments(subparser) group = subparser.add_argument_group("lockfile arguments") group.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " "existing 'conan.lock' file") group.add_argument("--lockfile-partial", action="store_true", help="Do not raise an error if some dependency is not found in lockfile") args = parser.parse_args(*args) # Basic collaborators: remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] # The lockfile by default if not defined will be read from the root workspace folder ws_folder = conan_api.workspace.folder() lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) buildmode = args.build if build and (not buildmode or "editable" not in buildmode): ConanOutput().info("Adding '--build=editable' as build mode") buildmode = buildmode or [] buildmode.append("editable") all_editables = conan_api.workspace.packages() packages = conan_api.workspace.select_packages(args.pkg) ConanOutput().box("Workspace computing the build order") install_order = conan_api.workspace.build_order(packages, profile_host, profile_build, buildmode, lockfile, remotes, args, update=args.update) msg = "build" if build else "install" ConanOutput().box(f"Workspace {msg}ing each package") order = install_order.install_build_order() profile_args = ProfileArgs.from_args(args) lockfile_args = [f"--lockfile={a}" for a in args.lockfile or []] if args.lockfile_partial: lockfile_args.append("--lockfile-partial") lockfile_args = " ".join(lockfile_args) for level in order["order"]: for elem in level: ref = RecipeReference.loads(elem["ref"]) for package_level in elem["packages"]: for package in package_level: ws_pkg = all_editables.get(ref) is_editable = package["binary"] in ("Editable", "EditableBuild") if ws_pkg is None: if is_editable or package["binary"] == "Build": # Build extern to Workspace cmd = f'install {package["build_args"]} {profile_args} {lockfile_args}' ConanOutput().box(f"Workspace building external {ref}") ConanOutput().info(f"Command: {cmd}\n") conan_api.command.run(cmd) else: path = ws_pkg["path"] output_folder = ws_pkg.get("output_folder") build_arg = "--build-require" if package["context"] == "build" else "" ref_args = " ".join(f"--{k}={getattr(ref, k)}" for k in ("name", "version", "user", "channel") if getattr(ref, k, None)) of_arg = f'-of="{output_folder}"' if output_folder else "" # TODO: Missing --lockfile-overrides arg here command = "build" if build else "install" cmd = (f'{command} "{path}" {profile_args} {build_arg} {ref_args} {of_arg} ' f'{lockfile_args}') ConanOutput().box(f"Workspace {command}: {ref}") ConanOutput().info(f"Command: {cmd}\n") conan_api.command.run(cmd) @conan_subcommand(formatters={"json": format_graph_json}) def workspace_super_install(conan_api: ConanAPI, parser, subparser, *args): """ Install the workspace as a monolith, installing only external dependencies to the workspace, generating a single result (generators, etc) for the whole workspace. """ subparser.add_argument("--pkg", action="append", help='Define specific packages') subparser.add_argument("-g", "--generator", action="append", help='Generators to use') subparser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') subparser.add_argument("-d", "--deployer", action="append", help="Deploy using the provided deployer to the output folder. " "Built-in deployers: 'full_deploy', 'direct_deploy', " "'runtime_deploy'") subparser.add_argument("--deployer-folder", help="Deployer output folder, base build folder by default if not set") subparser.add_argument("--deployer-package", action="append", help="Execute the deploy() method of the packages matching " "the provided patterns") subparser.add_argument("--envs-generation", default=None, choices=["false"], help="Generation strategy for virtual environment files for the root") add_common_install_arguments(subparser) add_lockfile_args(subparser) args = parser.parse_args(*args) # Basic collaborators: remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None # The lockfile by default if not defined will be read from the root workspace folder ws_folder = conan_api.workspace.folder() lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial, overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) # Build a dependency graph with all editables as requirements requires = conan_api.workspace.select_packages(args.pkg) deps_graph = conan_api.graph.load_graph_requires(requires, [], profile_host, profile_build, lockfile, remotes, update=args.update) deps_graph.report_graph_error() print_graph_basic(deps_graph) # Collapsing the graph ws_graph = conan_api.workspace.super_build_graph(deps_graph, profile_host, profile_build) ConanOutput().subtitle("Collapsed graph") print_graph_basic(ws_graph) conan_api.graph.analyze_binaries(ws_graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(ws_graph) conan_api.install.install_binaries(deps_graph=ws_graph, remotes=remotes) ConanOutput().title("Finalizing install (deploy, generators)") output_folder = make_abs_path(args.output_folder) if args.output_folder else None conan_api.install.install_consumer(ws_graph, args.generator, ws_folder, output_folder, deploy=args.deployer, deploy_package=args.deployer_package, deploy_folder=args.deployer_folder, envs_generation=args.envs_generation) # Update and save lockfile if requested lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out) ConanOutput().success("Install finished successfully") return {"graph": ws_graph, "conan_api": conan_api} @conan_subcommand() def workspace_clean(conan_api: ConanAPI, parser, subparser, *args): # noqa """ Clean the temporary build folders when possible """ parser.parse_args(*args) conan_api.workspace.clean() @conan_subcommand() def workspace_init(conan_api: ConanAPI, parser, subparser, *args): """ Clean the temporary build folders when possible """ subparser.add_argument("path", nargs="?", default=os.getcwd(), help="Path to a folder where the workspace will be initialized. " "Defaults to the current directory") args = parser.parse_args(*args) conan_api.workspace.init(args.path) @conan_subcommand() def workspace_create(conan_api: ConanAPI, parser, subparser, *args): """ Call "conan create" for packages in the workspace, in the correct order. Packages will be created in the Conan cache, not locally """ subparser.add_argument("--pkg", action="append", help='Define specific packages') add_common_install_arguments(subparser) group = subparser.add_argument_group("lockfile arguments") group.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " "existing 'conan.lock' file") group.add_argument("--lockfile-partial", action="store_true", help="Do not raise an error if some dependency is not found in lockfile") args = parser.parse_args(*args) # Basic collaborators: remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] # The lockfile by default if not defined will be read from the root workspace folder ws_folder = conan_api.workspace.folder() lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) build_mode = args.build if args.build else [] ConanOutput().box("Exporting workspace recipes to Conan cache") exported_refs = conan_api.workspace.export() build_mode.extend(f"missing:{r}" for r in exported_refs) all_packages = conan_api.workspace.packages() packages = conan_api.workspace.select_packages(args.pkg) # If we don't disable the workspace, then, the packages are not created in the Conan cache, # but locally in the user folders, are they are intercepted as editables conan_api.workspace.enable(False) install_order = conan_api.workspace.build_order(packages, profile_host, profile_build, build_mode, lockfile, remotes, args, update=args.update) ConanOutput().box("Workspace creating each package") order = install_order.install_build_order() profile_args = ProfileArgs.from_args(args) for level in order["order"]: for elem in level: ref = RecipeReference.loads(elem["ref"]) for package_level in elem["packages"]: for package in package_level: if package["binary"] not in ("Build", "EditableBuild"): ConanOutput().info(f"Skip build for {ref} binary: {package['binary']}") continue if ref not in all_packages: # Build external to Workspace cmd = f'install {package["build_args"]} {profile_args}' ConanOutput().box(f"Workspace building external {ref}") ConanOutput().info(f"Build command: {cmd}\n") conan_api.command.run(cmd) else: # Package in workspace path = packages[ref]["path"] # TODO: Missing --lockfile-overrides arg here build = "--build-require" if package["context"] == "build" else "" ref_args = " ".join(f"--{k}={getattr(ref, k)}" for k in ("name", "version", "user", "channel") if getattr(ref, k, None)) cmd = f'create "{path}" {profile_args} {build} {ref_args}' ConanOutput().box(f"Workspace create {ref}") ConanOutput().info(f"Conan create command: {cmd}\n") conan_api.command.run(cmd) @conan_subcommand() def workspace_source(conan_api: ConanAPI, parser, subparser, *args): """ Call the source() method of packages in the workspace """ subparser.add_argument("--pkg", action="append", help='Define specific packages') args = parser.parse_args(*args) remotes = conan_api.remotes.list() # In case "python_requires" are needed packages = conan_api.workspace.select_packages(args.pkg) ConanOutput().box("Workspace getting sources") for pkg, info in packages.items(): conan_api.local.source(info["path"], name=pkg.name, version=str(pkg.version), user=pkg.user, channel=pkg.channel, remotes=remotes) @conan_command(group="Consumer") def workspace(conan_api, parser, *args): # noqa """ Manage Conan workspaces (group of packages in editable mode) """ if (WorkspaceAPI.TEST_ENABLED or os.getenv("CONAN_WORKSPACE_ENABLE")) != "will_break_next": raise ConanException("Workspace command disabled without CONAN_WORKSPACE_ENABLE env var," "please read the docs about this 'incubating' feature") ================================================ FILE: conan/cli/exit_codes.py ================================================ # Exit codes for conan command: SUCCESS = 0 # 0: Success (done) ERROR_GENERAL = 1 # 1: General ConanException error (done) ERROR_MIGRATION = 2 # 2: Migration error USER_CTRL_C = 3 # 3: Ctrl+C USER_CTRL_BREAK = 4 # 4: Ctrl+Break ERROR_SIGTERM = 5 # 5: SIGTERM ERROR_INVALID_CONFIGURATION = 6 # 6: Invalid configuration (done) ERROR_UNEXPECTED = 7 ================================================ FILE: conan/cli/formatters/__init__.py ================================================ import json from conan.api.output import cli_out_write def default_json_formatter(data): myjson = json.dumps(data, indent=4) cli_out_write(myjson) ================================================ FILE: conan/cli/formatters/audit/__init__.py ================================================ ================================================ FILE: conan/cli/formatters/audit/vulnerabilities.py ================================================ import json from jinja2 import select_autoescape, Template from conan.api.output import cli_out_write, Color severity_order = { "Critical": 4, "High": 3, "Medium": 2, "Low": 1 } def text_vuln_formatter(result): severity_colors = { "Critical": Color.BRIGHT_RED, "High": Color.RED, "Medium": Color.BRIGHT_YELLOW, "Low": Color.BRIGHT_CYAN } def wrap_and_indent(txt, limit=80, indent=2): txt = txt.replace("\n", " ").strip() if len(txt) <= limit: return " " * indent + txt lines = [] while len(txt) > limit: split_index = txt.rfind(" ", 0, limit) if split_index == -1: split_index = limit lines.append(" " * indent + txt[:split_index].strip()) txt = txt[split_index:].strip() lines.append(" " * indent + txt) return "\n".join(lines) total_vulns = 0 summary_lines = [] for ref, pkg_info in result["data"].items(): edges = pkg_info.get("vulnerabilities", {}).get("edges", []) count = len(edges) border_line = "*" * (len(ref) + 4) cli_out_write("\n" + border_line, fg=Color.BRIGHT_WHITE) cli_out_write(f"* {ref} *", fg=Color.BRIGHT_WHITE) cli_out_write(border_line, fg=Color.BRIGHT_WHITE) if "error" in pkg_info: details = pkg_info["error"].get("details", "") cli_out_write(f"\n{details}\n", fg=Color.BRIGHT_YELLOW) continue if not count: cli_out_write("\nNo vulnerabilities found.\n", fg=Color.BRIGHT_GREEN) continue total_vulns += count summary_lines.append( f"{ref} {count} {'vulnerability' if count == 1 else 'vulnerabilities'} found") cli_out_write(f"\n{count} {'vulnerability' if count == 1 else 'vulnerabilities'} found:\n", fg=Color.BRIGHT_YELLOW) sorted_vulns = sorted(edges, key=lambda v: -severity_order.get(v["node"].get("severity", "Medium"), 2)) for vuln in sorted_vulns: node = vuln["node"] name = node["name"] sev = node.get("severity", "Medium") sev_color = severity_colors.get(sev, Color.BRIGHT_YELLOW) score = node.get("cvss", {}).get("preferredBaseScore") score_txt = f", CVSS: {score}" if score else "" desc = node.get("description", "") desc = (desc[:240] + "...") if len(desc) > 240 else desc desc_wrapped = wrap_and_indent(desc) isWithdrawn = node.get("withdrawn", False) publishedAt = node.get("publishedAt") cli_out_write(f"- {name}", fg=Color.BRIGHT_WHITE, endline="") if isWithdrawn: cli_out_write(" [WITHDRAWN]", fg=Color.BRIGHT_CYAN, endline="") cli_out_write(f" (Severity: {sev}{score_txt})", fg=sev_color) advisories = node.get("advisories", {}) jfrog_advisories = [adv for adv in advisories if adv.get("name", "").startswith("JFSA-")] for adv in jfrog_advisories: if adv.get("shortDescription"): cli_out_write(f" Summary provided by JFrog Research ({adv['name']})", fg=Color.BRIGHT_GREEN) cli_out_write(wrap_and_indent(f"Short description: {adv['shortDescription']}", indent=4)) if adv.get("severity"): cli_out_write(f" Severity: ", endline="") cli_out_write(adv['severity'], fg=severity_colors.get(adv['severity'])) reasons = adv.get("impactReasons", []) if reasons: cli_out_write(f" Impact reasons:") for reason in reasons: cli_out_write(wrap_and_indent(f"* {reason['name']}", indent=8), fg=Color.GREEN if reason['isPositive'] else Color.RED) if result["provider_url"]: expected_url = (result["provider_url"].rstrip("/") + f"/ui/catalog/vulnerabilities/details/{adv['name']}") cli_out_write(f" Url: {expected_url}") cli_out_write("") cli_out_write("\n" + desc_wrapped) if publishedAt: cli_out_write(f" Published at: ", endline="", fg=Color.BRIGHT_BLUE) cli_out_write(publishedAt) references = node.get("references") if references: cli_out_write(f" url: ", endline="", fg=Color.BRIGHT_BLUE) cli_out_write(references[0]) vulnerablePackages = node.get("vulnerablePackages") if vulnerablePackages: fixVersions = [fix['version'] for fix_edge in vulnerablePackages.get("edges", []) for fix in fix_edge['node'].get("fixVersions", [])] if fixVersions: cli_out_write(f" fixed in version(s): ", endline="", fg=Color.BRIGHT_BLUE) cli_out_write(', '.join(fixVersions)) cli_out_write("") color_for_total = Color.BRIGHT_RED if total_vulns else Color.BRIGHT_GREEN cli_out_write(f"Total vulnerabilities found: {total_vulns}\n", fg=color_for_total) if total_vulns > 0: cli_out_write("\nSummary:\n", fg=Color.BRIGHT_WHITE) for line in summary_lines: cli_out_write(f"- {line}", fg=Color.BRIGHT_WHITE) cli_out_write("\nIf you are using packages from Conan Center, some vulnerabilities may have already been mitigated " "through patches applied in the recipe.\nTo verify if a patch has been applied, check the recipe in Conan Center.\n", fg=Color.BRIGHT_YELLOW) if total_vulns > 0 or "error" not in result: cli_out_write("\nVulnerability information provided by JFrog Catalog. Check " "https://audit.conan.io/jfrogcuration for more information.\n", fg=Color.BRIGHT_GREEN) cli_out_write("You can send questions and report issues about " "the returned vulnerabilities to conan-research@jfrog.com.\n", fg=Color.BRIGHT_GREEN) def json_vuln_formatter(result): cli_out_write(json.dumps(result, indent=4)) def _render_vulns(vulns, template): from conan import __version__ template = Template(template, autoescape=select_autoescape(['html', 'xml'])) return template.render(vulns=vulns, version=__version__) vuln_html = """ Conan Audit Vulnerabilities Report

Conan Audit Vulnerabilities Report

{% for vuln in vulns %} {% set parts = vuln.severity.split(' - ') %} {% set severity_id = parts[0] %} {% set severity_label = parts[1] if parts|length > 1 else parts[0] %} {% endfor %}
Package Info Description
{{ vuln.package }} {{ vuln.score }} {% if vuln.withdrawn %} [WITHDRAWN]
{% endif %} {{ vuln.vuln_id }}
{% if vuln.severity not in ['N/A', ''] %} {{ severity_label }} {% else %} {{ vuln.severity }} {% endif %} {{ vuln.score }}
{% for research in vuln.advisories %} {% if research.shortDescription %}
Summary provided by JFrog Research ({{ research.name }})
Short description: {{ research.shortDescription }}
{% if research.severity %} Impact severity: {{ research.severity }}
{% if research.impactReasons %} Impact reasons:
    {% for reason in research.impactReasons %}
  • {{ reason.name }}
  • {% endfor %}
{% endif %} {% endif %} {% if vuln.provider_url %} {% set expected_url = vuln.provider_url.rstrip('/') + '/ui/catalog/vulnerabilities/details/' + research.name %} More info available in: {{ expected_url }}
{% endif %}
{% endif %} {% endfor %} Description:
{{ vuln.description }} {% if vuln.publishedAt %}

Published at: {{ vuln.publishedAt }} {% endif %} {% if vuln.fixVersions %}

Fixed in version(s):
{% for version in vuln.fixVersions %} {{ version }} {% endfor %}
{% endif %} {% if vuln.references %}
References:
    {% for ref in vuln.references %}
  • {{ ref }}
  • {% endfor %}
{% endif %} {% if vuln.aliases %}
Aliases: {{ ', '.join(vuln.aliases) }} {% endif %}
""" def html_vuln_formatter(result): vulns = [] for ref, pkg_info in result["data"].items(): edges = pkg_info.get("vulnerabilities", {}).get("edges", []) if not edges: description = "No vulnerabilities found." if "error" not in pkg_info \ else pkg_info["error"].get("details", "") vulns.append({ "package": ref, "vuln_id": "-", "aliases": [], "severity": "N/A", "score": "-", "description": description, "references": [], "withdrawn": False, "advisories": [], "provider_url": result.get("provider_url"), "fixVersions": [], "publishedAt": None }) else: sorted_vulns = sorted(edges, key=lambda v: -severity_order.get(v["node"].get("severity", "Medium"), 2)) for vuln in sorted_vulns: node = vuln["node"] name = node.get("name") sev = node.get("severity", "Medium") sev = f"{severity_order.get(sev, 2)} - {sev}" score = node.get("cvss", {}).get("preferredBaseScore") score_txt = f"CVSS: {score}" if score else "-" aliases = node.get("aliases", []) references = node.get("references", []) desc = node.get("description", "") withdrawn = node.get("withdrawn", False) advisories = node.get("advisories", []) jfrogAdvisories = [adv for adv in advisories if adv.get("name", "").startswith("JFSA-")] fixVersions = [fix['version'] for fix_edge in node.get("vulnerablePackages", {}).get("edges", []) for fix in fix_edge['node'].get("fixVersions", [])] vulns.append({ "package": ref, "vuln_id": name, "aliases": aliases, "severity": sev, "score": score_txt, "description": desc, "references": references, "withdrawn": withdrawn, "advisories": jfrogAdvisories, "provider_url": result.get("provider_url"), "fixVersions": fixVersions, "publishedAt": node.get("publishedAt") }) cli_out_write(_render_vulns(vulns, vuln_html)) ================================================ FILE: conan/cli/formatters/graph/__init__.py ================================================ from .graph import format_graph_html from .graph import format_graph_dot from .graph import format_graph_json ================================================ FILE: conan/cli/formatters/graph/build_order_html.py ================================================ from jinja2 import select_autoescape, Template from conan.api.output import cli_out_write build_order_html = r"""
""" def _render_build_order(build_order, template): from conan import __version__ context = {'build_order': build_order, 'version': __version__} return template.render(context) def format_build_order_html(result): build_order = result["build_order"] build_order = build_order["order"] if isinstance(build_order, dict) else build_order template = Template(build_order_html, autoescape=select_autoescape(['html', 'xml'])) cli_out_write(_render_build_order(build_order, template)) ================================================ FILE: conan/cli/formatters/graph/graph.py ================================================ import json import os from jinja2 import Template, select_autoescape from conan.api.output import cli_out_write from conan.cli.formatters.graph.graph_info_text import filter_graph from conan.cli.formatters.graph.info_graph_dot import graph_info_dot from conan.cli.formatters.graph.info_graph_html import graph_info_html def _render_graph(graph, template, template_folder): deps_graph = graph.serialize() from conan import __version__ template = Template(template, autoescape=select_autoescape(['html', 'xml'])) return template.render(deps_graph=deps_graph, base_template_path=template_folder, version=__version__) def format_graph_html(result): graph = result["graph"] conan_api = result["conan_api"] template_folder = os.path.join(conan_api.cache_folder, "templates") user_template = os.path.join(template_folder, "graph.html") template = graph_info_html if os.path.isfile(user_template): with open(user_template, 'r', encoding="utf-8", newline="") as handle: template = handle.read() cli_out_write(_render_graph(graph, template, template_folder)) def format_graph_dot(result): graph = result["graph"] conan_api = result["conan_api"] template_folder = os.path.join(conan_api.cache_folder, "templates") user_template = os.path.join(template_folder, "graph.dot") template = graph_info_dot if os.path.isfile(user_template): with open(user_template, 'r', encoding="utf-8", newline="") as handle: template = handle.read() cli_out_write(_render_graph(graph, template, template_folder)) def format_graph_json(result): graph = result["graph"] field_filter = result.get("field_filter") package_filter = result.get("package_filter") serial = graph.serialize() serial = filter_graph(serial, package_filter=package_filter, field_filter=field_filter) json_result = json.dumps({"graph": serial}, indent=4) cli_out_write(json_result) ================================================ FILE: conan/cli/formatters/graph/graph_info_text.py ================================================ import fnmatch from collections import OrderedDict from conan.api.model import RecipeReference from conan.api.output import ConanOutput, cli_out_write def filter_graph(graph, package_filter=None, field_filter=None): if package_filter is not None: def _matching(node, pattern): if fnmatch.fnmatch(node["ref"] or "", pattern): return True if pattern == "&": # Handle the consumer pattern if node["recipe"] == "Consumer": return True # How to deal with --requires=xxx --package-filter=& "consumers" root = graph["nodes"]["0"] if root["recipe"] == "Cli" and node is not root: # We look if the current node is a direct dependency of the root node node_ref = RecipeReference.loads(node["ref"]) for dep in root["dependencies"].values(): if dep["direct"] and node_ref == RecipeReference.loads(dep["ref"]): return True graph["nodes"] = {id_: n for id_, n in graph["nodes"].items() if any(_matching(n, p) for p in package_filter)} if field_filter is not None: if "ref" not in field_filter: field_filter.append("ref") result = {} for id_, n in graph["nodes"].items(): new_node = OrderedDict((k, v) for k, v in n.items() if k in field_filter) result[id_] = new_node graph["nodes"] = result return graph def format_graph_info(result): """ More complete graph output, including information for every node in the graph Used for 'graph info' command """ graph = result["graph"] field_filter = result["field_filter"] package_filter = result["package_filter"] ConanOutput().subtitle("Basic graph information") serial = graph.serialize() serial = filter_graph(serial, package_filter, field_filter) for n in serial["nodes"].values(): cli_out_write(f"{n['ref']}:") # FIXME: This can be empty for consumers and it is ugly ":" _serial_pretty_printer(n, indent=" ") def _serial_pretty_printer(data, indent=""): for k, v in data.items(): if isinstance(v, dict): cli_out_write(f"{indent}{k}:") # TODO: increment color too _serial_pretty_printer(v, indent=indent+" ") else: cli_out_write(f"{indent}{k}: {v}") ================================================ FILE: conan/cli/formatters/graph/info_graph_dot.py ================================================ graph_info_dot = """\ digraph { {%- for node_id, node in deps_graph["nodes"].items() %} {%- for dep_id, dep in node["dependencies"].items() %} {%- if dep["direct"] %} "{{ node["label"] }}" -> "{{ deps_graph["nodes"][dep_id]["label"] }}" {%- endif %} {%- endfor %} {%- endfor %} } """ ================================================ FILE: conan/cli/formatters/graph/info_graph_html.py ================================================ graph_info_html = r"""
Package info: Click on one package to show information
""" ================================================ FILE: conan/cli/formatters/list/__init__.py ================================================ from .list import list_packages_html ================================================ FILE: conan/cli/formatters/list/list.py ================================================ import json import os from jinja2 import Template, select_autoescape from conan.api.output import cli_out_write from conan.cli.formatters.list.search_table_html import list_packages_html_template from conan import __version__ def list_packages_html(result): results = result["results"] cli_args = result["cli_args"] conan_api = result["conan_api"] template_folder = os.path.join(conan_api.cache_folder, "templates") user_template = os.path.join(template_folder, "list_packages.html") template = list_packages_html_template if os.path.isfile(user_template): with open(user_template, 'r', encoding="utf-8", newline="") as handle: template = handle.read() template = Template(template, autoescape=select_autoescape(['html', 'xml'])) content = template.render(results=json.dumps(results), base_template_path=template_folder, version=__version__, cli_args=cli_args) cli_out_write(content) ================================================ FILE: conan/cli/formatters/list/search_table_html.py ================================================ list_packages_html_template = r""" conan list results
Conan {{ version }} JFrog LTD. https://conan.io
""" ================================================ FILE: conan/cli/formatters/report/__init__.py ================================================ from .diff import format_diff_html, format_diff_txt, format_diff_json ================================================ FILE: conan/cli/formatters/report/diff.py ================================================ import json import os import base64 from jinja2 import Template from conan.api.output import cli_out_write from conan.cli.formatters.report.diff_html import diff_html def _generate_json(result): diff_text = result["diff"] src_prefix = result["src_prefix"] dst_prefix = result["dst_prefix"] ret = {} current_filename = None for line in diff_text.splitlines(): if line.startswith("diff --git "): src_filename, dst_filename = _get_filenames(line, src_prefix, dst_prefix) current_filename = src_filename ret[current_filename] = [line] else: ret[current_filename].append(line) return ret def _get_filenames(line, src_prefix, dst_prefix): """ Extracts the source and destination filenames from a diff line. """ src_index = line.find(src_prefix) dst_index = line.find(dst_prefix) if src_index == -1 or dst_index == -1: return None, None src_filename = line[src_index + len(src_prefix) - 1:dst_index - 1].strip() dst_filename = line[dst_index + len(dst_prefix) - 1:].strip() return src_filename, dst_filename def _render_diff(content, template, template_folder, **kwargs): from conan import __version__ template = Template(template, autoescape=True) def _safe_filename(filename): # Calculate base64 of the filename return base64.b64encode(filename.encode(), altchars=b'-_').decode() def _remove_prefixes(line): return line.replace(kwargs["src_prefix"][:-1], "").replace(kwargs["dst_prefix"][:-1], "") def _replace_cache_paths(line): return line.replace(kwargs["old_cache_path"], "(old)").replace(kwargs["new_cache_path"], "(new)") def _replace_paths(line): return _remove_prefixes(_replace_cache_paths(line)) def _extract_header(diff_lines): # Header ends at the first occurrence of +++ line, # and it can be at most 10 lines long for i, line in enumerate(diff_lines[:10]): if line.startswith("+++ "): return diff_lines[:i + 1] return diff_lines[:10] def _parse_header_is_deleted(header_contents): return ("+++ /dev/null" in header_contents or any("deleted file mode" in line for line in header_contents)) def _parse_header_rename_to(header_contents): if not any("similarity index" in line for line in header_contents): return None for line in header_contents: if line.startswith("rename to "): return line[len("rename to "):] return None per_folder = {"folders": {}, "files": {}} for file in content: header = _extract_header(content[file]) renamed_to = _parse_header_rename_to(header) replaced_path = _replace_paths(renamed_to or file) replaced_file = replaced_path.replace("(old)", "").replace("(new)", "").replace("\\", "/") bits = replaced_file.split("/")[1:] cur = per_folder for folder in bits[:-1]: cur = cur["folders"].setdefault(folder, {"folders": {}, "files": {}}) filename = bits[-1] cur["files"][filename] = {"filename": file, # This is file so renamed use old name "is_new": "(new)" in replaced_path, "is_deleted": _parse_header_is_deleted(header), "renamed_to": renamed_to, "relative_path": replaced_path} def flatten_empty_folders(current_node): for folder_data in current_node["folders"].values(): flatten_empty_folders(folder_data) promoted_folders = {} # The list here is important to avoid modifying the dict while iterating for folder_name, folder_data in list(current_node["folders"].items()): if not folder_data["files"]: for sub_folder_name, sub_folder_data in folder_data['folders'].items(): new_key = os.path.join(folder_name, sub_folder_name) promoted_folders[new_key] = sub_folder_data del current_node["folders"][folder_name] current_node["folders"].update(promoted_folders) flatten_empty_folders(per_folder) # Now sort each folder and file recursively def sort_folders_and_files(node): node["folders"] = dict(sorted(node["folders"].items())) node["files"] = dict(sorted(node["files"].items(), key=lambda x: x[0].lower())) for folder_data in node["folders"].values(): sort_folders_and_files(folder_data) sort_folders_and_files(per_folder) return template.render(content=content, per_folder=per_folder, base_template_path=template_folder, version=__version__, safe_filename=_safe_filename, replace_paths=_replace_paths, replace_cache_paths=_replace_cache_paths, remove_prefixes=_remove_prefixes, **kwargs) def format_diff_html(result): conan_api = result["conan_api"] template_folder = os.path.join(conan_api.cache_folder, "templates") user_template = os.path.join(template_folder, "diff.html") template = diff_html if os.path.isfile(user_template): with open(user_template, 'r', encoding="utf-8", newline="") as handle: template = handle.read() content = _generate_json(result) cli_out_write(_render_diff(content, template, template_folder, old_reference=result["old_export_ref"], new_reference=result["new_export_ref"], old_cache_path=result["old_cache_path"], new_cache_path=result["new_cache_path"], src_prefix=result["src_prefix"], dst_prefix=result["dst_prefix"])) def format_diff_txt(result): diff_text = result["diff"] cli_out_write(diff_text) def format_diff_json(result): cli_out_write(json.dumps(_generate_json(result), indent=2)) ================================================ FILE: conan/cli/formatters/report/diff_html.py ================================================ diff_html = r""" {% macro render_sidebar_folder(folder, folder_info) %} {%- for name, sub_folder_info in folder_info["folders"].items() %} {% set folder_name = folder + "/" + name %}
  • {{ name }}
      {{ render_sidebar_folder(folder_name, sub_folder_info) }}
  • {%- endfor %} {%- for name, file_info in folder_info["files"].items() %} {% set file_type = "renamed" if file_info["renamed_to"] else ( "deleted" if file_info["is_deleted"] else ( "new" if file_info["is_new"] else "old")) %}
  • {% if file_info["renamed_to"] %} {{ file_info["renamed_to"].split("/")[1:][-1] }} {% else %} {{ name }} {% endif %}
  • {%- endfor %} {% endmacro %} {% macro render_diff_folder(folder_info) %} {%- for name, sub_folder_info in folder_info["folders"].items() %} {{ render_diff_folder(sub_folder_info) }} {%- endfor %} {%- for name, file_info in folder_info["files"].items() %} {% set filename = file_info["filename"] %}
    {{ replace_cache_paths(filename) | replace("(old)/", "") | replace("(new)/", "") }} {% if file_info["renamed_to"] %}  →  {{ replace_cache_paths(file_info["renamed_to"]) | replace("(old)/", "") | replace("(new)/", "") }} {% endif %}
    {%- endfor %} {% endmacro %} Diff report for {{ old_reference }} - {{ new_reference }}

    Diff Report Between {{ old_reference.repr_notime() }} And {{ new_reference.repr_notime() }}

    {{ render_diff_folder(per_folder) }}
    """ ================================================ FILE: conan/cli/printers/__init__.py ================================================ from conan.api.output import ConanOutput, Color def print_profiles(profile_host, profile_build): out = ConanOutput() out.title("Input profiles") out.info("Profile host:", fg=Color.BRIGHT_CYAN) out.info(profile_host.dumps()) out.info("Profile build:", fg=Color.BRIGHT_CYAN) out.info(profile_build.dumps()) ================================================ FILE: conan/cli/printers/graph.py ================================================ from conan.api.output import ConanOutput, Color, LEVEL_VERBOSE, LEVEL_DEBUG def print_graph_basic(graph): # I am excluding the "download"-"cache" or remote information, that is not # the definition of the graph, but some history how it was computed # maybe we want to summarize that info after the "GraphBuilder" ends? # TODO: Should all of this be printed from a json representation of the graph? (the same json # that would be in the json_formatter for the graph?) output = ConanOutput() requires = {} build_requires = {} test_requires = {} python_requires = {} deprecated = {} for node in graph.nodes: if hasattr(node.conanfile, "python_requires"): for _, r in node.conanfile.python_requires.items(): python_requires[r.ref] = r.recipe, r.remote if node.recipe in ("Consumer", "Cli"): continue if node.context == "build": build_requires[node.ref] = node.recipe, node.remote else: if node.test: test_requires[node.ref] = node.recipe, node.remote else: requires[node.ref] = node.recipe, node.remote if node.conanfile.deprecated: deprecated[node.ref] = node.conanfile.deprecated output.info("Graph root", Color.BRIGHT_YELLOW) path = ": {}".format(graph.root.path) if graph.root.path else "" output.info(" {}{}".format(graph.root, path), Color.BRIGHT_CYAN) def _format_requires(title, reqs_to_print): if not reqs_to_print: return output.info(title, Color.BRIGHT_YELLOW) for ref_, (recipe, remote) in sorted(reqs_to_print.items()): if remote is not None: recipe = "{} ({})".format(recipe, remote.name) output.info(" {} - {}".format(ref_.repr_notime(), recipe), Color.BRIGHT_CYAN) _format_requires("Requirements", requires) _format_requires("Test requirements", test_requires) _format_requires("Build requirements", build_requires) _format_requires("Python requires", python_requires) def _format_resolved(title, reqs_to_print): if not reqs_to_print: return output.info(title, Color.BRIGHT_YELLOW) for k_, v_ in sorted(reqs_to_print.items()): output.info(" {}: {}".format(k_, v_), Color.BRIGHT_CYAN) if graph.replaced_requires: output.info("Replaced requires", Color.BRIGHT_YELLOW) for k, v in graph.replaced_requires.items(): output.info(" {}: {}".format(k, v), Color.BRIGHT_CYAN) _format_resolved("Resolved alias", graph.aliased) if graph.aliased: output.warning("'alias' is a Conan 1.X legacy, unsupported and undocumented feature, " "completely discouraged. " "It might be removed in future Conan versions", warn_tag="deprecated") output.warning("Consider using version-ranges instead.") _format_resolved("Resolved version ranges", graph.resolved_ranges) for req in graph.resolved_ranges: if str(req.version) == "[]": output.warning("Empty version range usage is discouraged. Use [*] instead", warn_tag="deprecated") break overrides = graph.overrides() if overrides: output.info("Overrides", Color.BRIGHT_YELLOW) for req, override_info in overrides.serialize().items(): output.info(" {}: {}".format(req, override_info), Color.BRIGHT_CYAN) if deprecated: output.info("Deprecated", Color.BRIGHT_YELLOW) for d, reason in deprecated.items(): reason = f": {reason}" if reason else "" output.info(" {}{}".format(d, reason), Color.BRIGHT_CYAN) if graph.options_conflicts: output.info("Options conflicts", Color.BRIGHT_YELLOW) for ref, ref_conflicts in graph.options_conflicts.items(): for option, conflict_info in ref_conflicts.items(): prev_value = conflict_info['value'] output.info(f" {ref}:{option}={prev_value} (current value)", Color.BRIGHT_CYAN) for src_ref, conflict_value in conflict_info["conflicts"]: output.info(f" {src_ref}->{option}={conflict_value}", Color.BRIGHT_CYAN) output.info(" It is recommended to define options values in profiles, not in recipes", Color.BRIGHT_CYAN) output.warning("There are options conflicts in the dependency graph", warn_tag="risk") if deprecated: output.warning("There are deprecated packages in the graph", warn_tag="risk") if graph.visibility_conflicts: msg = ["Packages required both with visible=True and visible=False"] for ref, consumers in graph.visibility_conflicts.items(): msg.append(f" {ref}: Required by {', '.join(str(c) for c in consumers)}") output.warning("\n".join(msg), warn_tag="risk") def print_graph_packages(graph): # I am excluding the "download"-"cache" or remote information, that is not # the definition of the graph, but some history how it was computed # maybe we want to summarize that info after the "GraphBuilder" ends? output = ConanOutput() requires = {} build_requires = {} test_requires = {} skipped_requires = [] tab = " " for node in graph.nodes: if node.recipe in ("Consumer", "Cli"): continue node_info = node.conanfile.info if node.context == "build": existing = build_requires.setdefault(node.pref, [node.binary, node.binary_remote, node_info]) else: if node.test: existing = test_requires.setdefault(node.pref, [node.binary, node.binary_remote, node_info]) else: existing = requires.setdefault(node.pref, [node.binary, node.binary_remote, node_info]) # TO avoid showing as "skip" something that is used in other node of the graph if existing[0] == "Skip": existing[0] = node.binary def _format_requires(title, reqs_to_print): if not reqs_to_print: return output.info(title, Color.BRIGHT_YELLOW) for pref, (status, remote, info) in sorted(reqs_to_print.items(), key=repr): name = pref.repr_notime() if status != "Platform" else str(pref.ref) msg = f"{tab}{name} - " if status == "Skip": skipped_requires.append(str(pref.ref)) output.verbose(f"{msg}{status}", Color.BRIGHT_CYAN) elif status == "Missing" or status == "Invalid": output.write(msg, Color.BRIGHT_CYAN) output.writeln(status, Color.BRIGHT_RED) elif status == "Build": output.write(msg, Color.BRIGHT_CYAN) output.writeln(status, Color.BRIGHT_YELLOW) else: # Support python36 msg += status if remote: msg += f" ({remote.name})" output.info(msg, Color.BRIGHT_CYAN) if output.level_allowed(LEVEL_DEBUG): compact_dumps = info.summarize_compact() for line in compact_dumps: output.debug(f"{tab}{tab}{line}", Color.BRIGHT_GREEN) _format_requires("Requirements", requires) _format_requires("Test requirements", test_requires) _format_requires("Build requirements", build_requires) if skipped_requires and not output.level_allowed(LEVEL_VERBOSE): output.info("Skipped binaries", Color.BRIGHT_YELLOW) output.info(f"{tab}{', '.join(skipped_requires)}", Color.BRIGHT_CYAN) ================================================ FILE: conan/cps/__init__.py ================================================ from conan.cps.cps import CPS ================================================ FILE: conan/cps/cps.py ================================================ import json import os from enum import Enum from conan.internal.model.cpp_info import CppInfo from conan.internal.util.files import save, load class CPSComponentType(Enum): DYLIB = "dylib" ARCHIVE = "archive" INTERFACE = "interface" EXE = "executable" JAR = "jar" UNKNOWN = "unknown" def __str__(self): return self.value def __eq__(self, other): # This is useful for comparing with string type at user code, like ``type == "xxx"`` return super().__eq__(CPSComponentType(other)) @staticmethod def from_conan(pkg_type): _package_type_map = { "shared-library": "dylib", "static-library": "archive", "header-library": "interface", "application": "executable" } return CPSComponentType(_package_type_map.get(str(pkg_type), "unknown")) class CPSComponent: def __init__(self, component_type=None): self.includes = [] self.type = component_type or "unknown" self.definitions = {} self.requires = [] self.link_requires = [] self.location = None self.link_location = None self.link_languages = [] self.link_libraries = [] # system libraries def serialize(self): component = {"type": str(self.type)} if self.requires: component["requires"] = self.requires if self.link_requires: component["link_requires"] = self.link_requires if self.includes: component["includes"] = [x.replace("\\", "/") for x in self.includes] if self.definitions: component["definitions"] = self.definitions if self.location: # TODO: @prefix@ component["location"] = self.location if self.link_location: component["link_location"] = self.link_location if self.link_libraries: component["link_libraries"] = self.link_libraries if self.link_languages: component["link_languages"] = self.link_languages return component @staticmethod def deserialize(data): comp = CPSComponent() comp.type = CPSComponentType(data.get("type")) comp.requires = data.get("requires", []) comp.link_requires = data.get("link_requires", []) comp.includes = data.get("includes", []) comp.definitions = data.get("definitions", {}) comp.location = data.get("location") comp.link_location = data.get("link_location") comp.link_libraries = data.get("link_libraries", []) comp.link_languages = data.get("link_languages", []) return comp @staticmethod def from_cpp_info(cpp_info, conanfile, libname=None): cps_langs_mapping = {"C": "c", "C++": "cpp"} comp_langs = cpp_info.languages or conanfile.languages or [] def definitions_from_conan(defines): result = {} for define in defines: if "=" in define: k, v = define.split("=", 1) result[k] = v else: result[define] = None return result cps_comp = CPSComponent() if not libname: cps_comp.definitions = { cps_langs_mapping.get(lang, "*"): definitions_from_conan(cpp_info.defines) for lang in (comp_langs or ["*"]) } if cpp_info.defines else {} cps_comp.includes = [x.replace("\\", "/") for x in cpp_info.includedirs] if not cpp_info.libs: cps_comp.type = CPSComponentType.INTERFACE return cps_comp if len(cpp_info.libs) > 1 and not libname: # Multi-lib pkg without components defined cps_comp.type = CPSComponentType.INTERFACE return cps_comp cpp_info.deduce_locations(conanfile) cps_comp.type = CPSComponentType.from_conan(cpp_info.type) cps_comp.location = cpp_info.location cps_comp.link_location = cpp_info.link_location cps_comp.link_libraries = cpp_info.system_libs cps_comp.link_languages = [cps_langs_mapping[lang] for lang in comp_langs] required = cpp_info.requires cps_comp.requires = [f":{c}" if "::" not in c else c.replace("::", ":") for c in required] cps_comp.definitions = { cps_langs_mapping.get(lang, "*"): definitions_from_conan(cpp_info.defines) for lang in (comp_langs or ["*"]) } if cpp_info.defines else {} return cps_comp def update(self, conf, conf_def): # TODO: conf not used at the moent self.link_languages = self.link_languages or conf_def.get("link_languages") self.location = self.location or conf_def.get("location") self.link_location = self.link_location or conf_def.get("link_location") self.link_libraries = self.link_libraries or conf_def.get("link_libraries") class CPS: """ represents the CPS file for 1 package """ def __init__(self, name=None, version=None): self.name = name self.version = version self.default_components = [] self.components = {} self.configurations = [] self.requires = [] # Supplemental self.description = None self.license = None self.website = None self.prefix = None def serialize(self): cps = {"cps_version": "0.13.0", "name": self.name, "version": self.version} if self.prefix is not None: cps["prefix"] = self.prefix # Supplemental for data in "license", "description", "website": if getattr(self, data, None): cps[data] = getattr(self, data) if self.requires: cps["requires"] = self.requires if self.configurations: cps["configurations"] = self.configurations cps["default_components"] = self.default_components cps["components"] = {} for name, comp in self.components.items(): cps["components"][name] = comp.serialize() return cps @staticmethod def deserialize(data): cps = CPS() cps.name = data.get("name") cps.prefix = data.get("prefix") cps.version = data.get("version") cps.license = data.get("license") cps.description = data.get("description") cps.website = data.get("website") cps.requires = data.get("requires") cps.configurations = data.get("configurations") cps.default_components = data.get("default_components") cps.components = {k: CPSComponent.deserialize(v) for k, v in data.get("components", {}).items()} return cps @staticmethod def from_conan(dep): cps = CPS(dep.ref.name, str(dep.ref.version)) cps.prefix = dep.package_folder.replace("\\", "/") # supplemental cps.license = dep.license cps.description = dep.description cps.website = dep.homepage cps.requires = {d.ref.name: None for d in dep.dependencies.host.values()} if dep.settings.get_safe("build_type"): cps.configurations = [str(dep.settings.build_type).lower()] if not dep.cpp_info.has_components: if dep.cpp_info.libs and len(dep.cpp_info.libs) > 1: comp = CPSComponent.from_cpp_info(dep.cpp_info, dep) # base base_name = f"_{cps.name}" cps.components[base_name] = comp for lib in dep.cpp_info.libs: comp = CPSComponent.from_cpp_info(dep.cpp_info, dep, lib) comp.requires.insert(0, f":{base_name}") # dep to the common one cps.components[lib] = comp cps.default_components = dep.cpp_info.libs # FIXME: What if one lib is named equal to the package? else: # single component, called same as library component = CPSComponent.from_cpp_info(dep.cpp_info, dep) if not component.requires and dep.dependencies: for transitive_dep in dep.dependencies.host.items(): dep_name = transitive_dep[0].ref.name component.requires.append(f"{dep_name}:{dep_name}") # the component will be just the package name cps.default_components = [f"{dep.ref.name}"] cps.components[f"{dep.ref.name}"] = component else: sorted_comps = dep.cpp_info.get_sorted_components() for comp_name, comp in sorted_comps.items(): component = CPSComponent.from_cpp_info(comp, dep) cps.components[comp_name] = component # Now by default all are default_components cps.default_components = [comp_name for comp_name in sorted_comps] return cps def to_conan(self): def strip_prefix(dirs): return [d.replace("@prefix@/", "") for d in dirs] def lib_location(loc, info): loc = loc.replace("@prefix@/", "") info.libdirs = [os.path.dirname(loc)] filename = os.path.basename(loc) basefile, ext = os.path.splitext(filename) if basefile.startswith("lib") and ext != ".lib": basefile = basefile[3:] info.libs = [basefile] def definitions(defs): # TODO: C/CPP specific as per CPS spec # "*" has less priority than specific language aggregated = {} aggregated.update(defs.get("*", {})) aggregated.update(defs.get("c", {})) aggregated.update(defs.get("cpp", {})) result = list(f"{k}={v}" if v is not None else k for k, v in aggregated.items()) return result cpp_info = CppInfo() cpp_info.default_components = self.default_components for comp_name, comp in self.components.items(): cpp_comp = cpp_info if len(self.components) == 1 else cpp_info.components[comp_name] cpp_comp.includedirs = strip_prefix(comp.includes) cpp_comp.defines = definitions(comp.definitions) cpp_info.set_property("cmake_file_name", self.name) cpp_info.set_property("cmake_target_name", f"{self.name}::{comp_name}") if comp.link_location: link_location = comp.link_location lib_location(link_location, cpp_comp) location = comp.location location = location.replace("@prefix@/", "") cpp_comp.bindirs = [os.path.dirname(location)] elif comp.location: location = comp.location lib_location(location, cpp_comp) requires = comp.link_requires + comp.requires for r in requires: cpp_comp.requires.append(r[1:] if r.startswith(":") else r.replace(":", "::")) cpp_comp.system_libs = comp.link_libraries return cpp_info def save(self, folder): filename = os.path.join(folder, f"{self.name}.cps") save(filename, json.dumps(self.serialize(), indent=2)) return filename @staticmethod def load(file): contents = load(file) base = CPS.deserialize(json.loads(contents)) path, name = os.path.split(file) basename, ext = os.path.splitext(name) # Find, load and merge configuration specific files for conf in "release", "debug": full_conf = os.path.join(path, f"{basename}@{conf}{ext}") if os.path.exists(full_conf): conf_content = json.loads(load(full_conf)) for comp, comp_def in conf_content.get("components", {}).items(): existing = base.components.get(comp) if existing: existing.update(conf, comp_def) else: base.components[comp] = comp_def return base ================================================ FILE: conan/errors.py ================================================ class ConanException(Exception): """ Generic conan exception """ def __init__(self, msg=None, remote=None): self.remote = remote super().__init__(msg) def __str__(self): msg = super().__str__() if self.remote: return f"{msg}. [Remote: {self.remote.name}]" return msg class ConanInvalidConfiguration(ConanException): """ This binary, for the requested configuration and package-id cannot be built """ pass class ConanMigrationError(ConanException): pass ================================================ FILE: conan/internal/__init__.py ================================================ from conan.errors import ConanException REVISIONS = "revisions" # capability def check_duplicated_generator(generator, conanfile): if generator.__class__.__name__ in conanfile.generators: raise ConanException(f"{generator.__class__.__name__} is declared in the generators " "attribute, but was instantiated in the generate() method too. " "It should only be present in one of them.") ================================================ FILE: conan/internal/api/__init__.py ================================================ ================================================ FILE: conan/internal/api/audit/__init__.py ================================================ ================================================ FILE: conan/internal/api/audit/providers.py ================================================ import textwrap from urllib.parse import urljoin from conan.api.output import Color, ConanOutput from conan.errors import ConanException def _build_headers(token): return {"Content-Type": "application/json", "Accept": "application/json", "Authorization": f"Bearer {token}"} class ConanCenterProvider: def __init__(self, conan_api, name, provider_data): self.name = name self.url = provider_data["url"] self.type = provider_data["type"] self._token = provider_data.get("token") self._session = conan_api._api_helpers.requester # noqa self._query_url = urljoin(self.url, "api/v1/query") def get_cves(self, refs): if not self._token: from conan.api.subapi.audit import CONAN_CENTER_AUDIT_PROVIDER_NAME if self.name == CONAN_CENTER_AUDIT_PROVIDER_NAME: output = ConanOutput() output.write("\n") output.write("Authentication required for the CVE provider: ", fg=Color.BRIGHT_RED, newline=False) output.write(f"'{self.name}'\n", fg=Color.BRIGHT_WHITE) output.write("\nTo resolve, please:\n") output.write(" 1. Visit: ", fg=Color.BRIGHT_WHITE, newline=False) output.write("https://audit.conan.io/register\n", fg=Color.BRIGHT_BLUE) output.write(" 2. Register to obtain the access token and activate it.\n", fg=Color.BRIGHT_WHITE) output.write(" 3. Use the command below to authenticate:\n", fg=Color.BRIGHT_WHITE) output.write(f"\n conan audit provider auth {self.name} --token=", fg=Color.BRIGHT_GREEN, newline=True) output.write("\nOnce authenticated, re-run the command.\n\n") raise ConanException("Missing authentication token. Please authenticate and retry.") result = {"data": {}, "error": None, "provider_url": None} for ref in refs: ConanOutput().info(f"Requesting vulnerability info for: {ref}") response = self._session.post(self._query_url, headers=_build_headers(self._token), json={"reference": str(ref)}) if response.status_code == 200: result["data"][str(ref)] = response.json().get("data", {}).get("query", {}) elif response.status_code == 400: ConanOutput().warning(f"Package '{ref}' not found.\n" f"Only libraries available in Conan Center can be queried for vulnerabilities.\n" f"Please ensure the package exists in the official repository: https://conan.io/center\n" f"If the package exists in the repository, please report it to conan-research@jfrog.com.\n") result["data"][str(ref)] = {"error": {"details": f"Package '{ref}' not scanned: Not found."}} continue elif response.status_code == 403: # TODO: How to report auth error to the user ConanOutput().error(f"Authentication error ({response.status_code}).\n" f"Your token may be invalid or not yet validated. If you recently registered, please check your email to validate your token.\n" f" - Set a valid token using: 'conan audit provider auth {self.name} --token='\n" f" - If you don’t have a token, register at: https://audit.conan.io/register") result["conan_error"] = f"Authentication error ({response.status_code})." break elif response.status_code == 429: reset_seconds = int(response.headers.get("retry-after", 0)) reset_in_hours = reset_seconds // 3600 reset_in_minutes = (reset_seconds % 3600) // 60 output = ConanOutput() output.write("\n") if reset_in_hours > 0: output.write( f"You have exceeded the number of allowed requests. " f"The limit will reset in {reset_in_hours} " f"hour{'s' if reset_in_hours > 1 else ''} and {reset_in_minutes} " f"minute{'s' if reset_in_minutes > 1 else ''}.\n", fg=Color.BRIGHT_WHITE, ) else: output.write( f"You have exceeded the number of allowed requests. " f"The limit will reset in {reset_in_minutes} " f"minute{'s'if reset_in_minutes > 1 else ''}.\n", fg=Color.BRIGHT_WHITE, ) output.write("Visit our website to learn more about JFrog's DevSecOps solution: ", fg=Color.BRIGHT_WHITE, newline=False) output.write("https://audit.conan.io/limit", newline=True, fg=Color.BRIGHT_BLUE) output.write("\n") result["conan_error"] = "Rate limit exceeded." break elif response.status_code == 500: # TODO: How to report internal server error to the user result["conan_error"] = f"Internal server error ({response.status_code})" break else: result["conan_error"] = f"Error in {ref} ({response.status_code})" break return result class PrivateProvider: def __init__(self, conan_api, name, provider_data): self.name = name self.url = provider_data["url"] self.type = provider_data["type"] self._token = provider_data.get("token") self._session = conan_api._api_helpers.requester # noqa self._query_url = urljoin(self.url, "catalog/api/v0/public/graphql") def get_cves(self, refs): if not self._token: raise ConanException(f"Missing authentication token for '{self.name}' provider.\n" f"Please authenticate using 'conan audit provider auth' and retry.") result = {"data": {}, "error": None, "provider_url": self.url} for ref in refs: try: response = self._get(ref) if "error" in response: result["data"][str(ref)] = {"error": {"details": response["error"]}} else: result["data"][str(ref)] = response.get("data", {}).get("query", {}) except Exception as e: result["conan_error"] = str(e) break return result @staticmethod def _build_query(ref): name, version = ref.name, ref.version full_query = f"""query packageVersionDetails {{ query: packageVersion(name: "{name}", type: "conan", version: "{version}") {{ vulnerabilities(first: 100) {{ totalCount edges {{ node {{ name description severity cvss {{ preferredBaseScore }} aliases withdrawn publishedAt advisories {{ name ...on JfrogAdvisory {{ name shortDescription severity impactReasons {{ name isPositive }} }} }} references vulnerablePackages(first: 100) {{ totalCount edges {{ node {{ fixVersions {{ version }} }} }} }} }} }} }} }} }}""" return full_query @staticmethod def _parse_error(errors, ref): """This function removes the errors array that comes from the catalog and returns a more user-friendly message if we know how to parse it, or a generic one if we don't find such one""" def _replace_message(message): if "not found" in message: return f"Package '{ref}' not scanned: Not found." return None error_msgs = filter(bool, [_replace_message(error["message"]) for error in errors]) return next(error_msgs, "Unknown error") def _get(self, ref): full_query = self._build_query(ref) try: response = self._session.post( self._query_url, headers=_build_headers(self._token), json={ "query": textwrap.dedent(full_query) } ) if response.status_code == 404: ConanOutput().write("\n") ConanOutput().warning( f"An error occurred while connecting to the '{self.name}' provider.\n" "This is likely because your JFrog Platform instance does not have JFrog Curation.\n" "For more information, visit: https://audit.conan.io/missing-curation\n" ) # Raises if some HTTP error was found response.raise_for_status() except Exception as e: raise e response_json = response.json() # filter the extensions key with graphql data response_json.pop('extensions', None) if "errors" in response_json: return {"error": self._parse_error(response_json["errors"], ref)} return response_json ================================================ FILE: conan/internal/api/config/__init__.py ================================================ ================================================ FILE: conan/internal/api/config/config_installer.py ================================================ import os import shutil import fnmatch import zipfile from urllib.parse import urlparse, urlsplit from contextlib import contextmanager from conan.api.output import ConanOutput from conan.internal.paths import find_file_walk_up from conan.internal.rest.file_downloader import FileDownloader from conan.errors import ConanException from conan.internal.util.files import mkdir, rmdir, remove, chdir from conan.internal.util.runners import detect_runner from conan.tools.files.files import untargz class _ConanIgnoreMatcher: def __init__(self, conanignore_path, ignore=None): self._ignored_entries = {".conanignore"} self._included_entries = set() if ignore: self._ignored_entries.update(ignore) if conanignore_path is None or not os.path.exists(conanignore_path): return with open(conanignore_path, 'r') as conanignore: for line in conanignore: line_content = line.split("#", maxsplit=1)[0].strip() if line_content: if line_content.startswith("!"): self._included_entries.add(line_content[1:]) else: self._ignored_entries.add(line_content) def matches(self, path): """Returns whether the path should be ignored It's ignored if: - The path does not match any of the included entries - And the path matches any of the ignored entries In any other, the path is not ignored""" for include_entry in self._included_entries: if fnmatch.fnmatch(path, include_entry): return False for ignore_entry in self._ignored_entries: if fnmatch.fnmatch(path, ignore_entry): return True return False def _hide_password(resource): """ Hide password from url/file path :param resource: string with url or file path :return: resource with hidden password if present """ password = urlparse(resource).password return resource.replace(password, "") if password else resource @contextmanager def tmp_config_install_folder(cache_folder): tmp_folder = os.path.join(cache_folder, "tmp_config_install") # necessary for Mac OSX, where the temp folders in /var/ are symlinks to /private/var/ tmp_folder = os.path.abspath(tmp_folder) rmdir(tmp_folder) mkdir(tmp_folder) try: yield tmp_folder finally: rmdir(tmp_folder) def _process_git_repo(config, cache_folder): output = ConanOutput() output.info("Trying to clone repo: %s" % config.uri) with tmp_config_install_folder(cache_folder) as tmp_folder: with chdir(tmp_folder): args = config.args or "" ret, out = detect_runner('git clone "{}" . {}'.format(config.uri, args)) if ret != 0: raise ConanException("Can't clone repo: {}".format(out)) output.info("Repo cloned!") _process_folder(config, tmp_folder, cache_folder) def _process_zip_file(config, zippath, cache_folder, tmp_folder, first_remove=False): # First, unzip. This is repeated with the tools.unzip, but better do not mess # Same list as below tgz_exts = (".tar.gz", ".tgz", ".tbz2", ".tar.bz2", ".tar", ".tar.xz", ".txz") if any(zippath.endswith(e) for e in tgz_exts): untargz(zippath, tmp_folder) else: full_path = os.path.normpath(os.path.join(os.getcwd(), tmp_folder)) with zipfile.ZipFile(zippath, "r") as z: zip_info = z.infolist() extracted_size = 0 for file_ in zip_info: extracted_size += file_.file_size z.extract(file_, full_path) if first_remove: os.unlink(zippath) _process_folder(config, tmp_folder, cache_folder) def _filecopy(src, filename, dst): # https://github.com/conan-io/conan/issues/6556 # This is just a local convenience for "conan config install", using copyfile to avoid # copying with permissions that later cause bugs src = os.path.join(src, filename) dst = os.path.join(dst, filename) # Clear the destination file if os.path.exists(dst): if os.path.isdir(dst): # dst was a directory and now src is a file rmdir(dst) else: remove(dst) shutil.copyfile(src, dst) def _process_file(directory, filename, config, cache_folder, folder): output = ConanOutput() if filename == "settings.yml": output.info("Installing settings.yml") _filecopy(directory, filename, cache_folder) elif filename == "remotes.json": output.info("Defining remotes from remotes.json") _filecopy(directory, filename, cache_folder) else: relpath = os.path.relpath(directory, folder) if config.target_folder: target_folder = os.path.join(cache_folder, config.target_folder, relpath) else: target_folder = os.path.join(cache_folder, relpath) if os.path.isfile(target_folder): # Existed as a file and now should be a folder remove(target_folder) mkdir(target_folder) output.info("Copying file %s to %s" % (filename, target_folder)) _filecopy(directory, filename, target_folder) def _process_folder(config, folder, cache_folder, ignore=None): if not os.path.isdir(folder): raise ConanException("No such directory: '%s'" % str(folder)) original_folder = folder if config.source_folder: folder = os.path.join(folder, config.source_folder) conanignore_path = find_file_walk_up(folder, ".conanignore", end=original_folder) conanignore = _ConanIgnoreMatcher(conanignore_path, ignore) for root, dirs, files in os.walk(folder): # .git is always ignored by default, even if not present in .conanignore dirs[:] = [d for d in dirs if d != ".git"] for f in files: rel_path = os.path.relpath(os.path.join(root, f), folder) if not conanignore.matches(rel_path): _process_file(root, f, config, cache_folder, folder) def _process_download(config, cache_folder, requester): output = ConanOutput() with tmp_config_install_folder(cache_folder) as tmp_folder: output.info("Trying to download %s" % _hide_password(config.uri)) path = urlsplit(config.uri).path filename = os.path.basename(path) zippath = os.path.join(tmp_folder, filename) try: downloader = FileDownloader(requester=requester, source_credentials=True) downloader.download(url=config.uri, file_path=zippath, verify_ssl=config.verify_ssl, retry=1) _process_zip_file(config, zippath, cache_folder, tmp_folder, first_remove=True) except Exception as e: raise ConanException("Error while installing config from %s\n%s" % (config.uri, str(e))) class _ConfigOrigin: def __init__(self, uri, config_type, verify_ssl, args, source_folder, target_folder): if config_type: self.type = config_type else: if uri.endswith(".git"): self.type = "git" elif os.path.isdir(uri): self.type = "dir" elif os.path.isfile(uri): self.type = "file" elif uri.startswith("http"): self.type = "url" else: raise ConanException("Unable to deduce type config install: %s" % uri) self.source_folder = source_folder self.target_folder = target_folder self.args = args self.verify_ssl = verify_ssl if os.path.exists(uri): uri = os.path.abspath(uri) self.uri = uri def _is_compressed_file(filename): open(filename, "r") # Check if the file exist and can be opened import zipfile if zipfile.is_zipfile(filename): return True tgz_exts = (".tar.gz", ".tgz", ".tbz2", ".tar.bz2", ".tar", ".tar.xz", ".txz") return any(filename.endswith(e) for e in tgz_exts) def configuration_install(cache_folder, requester, uri, verify_ssl, config_type=None, args=None, source_folder=None, target_folder=None, ignore=None): config = _ConfigOrigin(uri, config_type, verify_ssl, args, source_folder, target_folder) try: if config.type == "git": _process_git_repo(config, cache_folder) elif config.type == "dir": _process_folder(config, config.uri, cache_folder, ignore) elif config.type == "file": if _is_compressed_file(config.uri): with tmp_config_install_folder(cache_folder) as tmp_folder: _process_zip_file(config, config.uri, cache_folder, tmp_folder) else: dirname, filename = os.path.split(config.uri) _process_file(dirname, filename, config, cache_folder, dirname) elif config.type == "url": _process_download(config, cache_folder, requester=requester) else: raise ConanException("Unable to process config install: %s" % config.uri) except Exception as e: raise ConanException("Failed conan config install: %s" % str(e)) ================================================ FILE: conan/internal/api/detect/__init__.py ================================================ ================================================ FILE: conan/internal/api/detect/detect_api.py ================================================ import os import platform import re import tempfile import textwrap from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.model.version import Version from conan.internal.util.files import load, save from conan.internal.util.runners import check_output_runner, detect_runner def detect_os(): the_os = platform.system() if the_os == "Darwin": the_os = "Macos" return the_os def detect_arch(): machine = platform.machine() arch = None system = platform.system() # special detectors if system == "SunOS": arch = _get_solaris_architecture() elif system == "AIX": arch = _get_aix_architecture() if arch: return arch if "ppc64le" in machine: return "ppc64le" elif "ppc64" in machine: return "ppc64" elif "ppc" in machine: return "ppc32" elif "mips64" in machine: return "mips64" elif "mips" in machine: return "mips" elif "sparc64" in machine: return "sparcv9" elif "sparc" in machine: return "sparc" elif "aarch64" in machine: return "armv8" elif "arm64" in machine: return "armv8" elif "ARM64" in machine: return "armv8" elif 'riscv64' in machine: return "riscv64" elif "riscv32" in machine: return 'riscv32' elif "64" in machine: return "x86_64" elif "86" in machine: return "x86" elif "armv8" in machine: return "armv8" elif "armv7" in machine: return "armv7" elif "arm" in machine: return "armv6" elif "s390x" in machine: return "s390x" elif "s390" in machine: return "s390" elif "sun4v" in machine: return "sparc" elif "e2k" in machine: return _get_e2k_architecture() return None def _get_solaris_architecture(): # under intel solaris, platform.machine()=='i86pc' so we need to handle # it early to suport 64-bit processor = platform.processor() kernel_bitness, elf = platform.architecture() if "sparc" in processor: return "sparcv9" if kernel_bitness == "64bit" else "sparc" elif "i386" in processor: return "x86_64" if kernel_bitness == "64bit" else "x86" def _get_aix_conf(options=None): options = " %s" % options if options else "" try: ret = check_output_runner("getconf%s" % options).strip() return ret except Exception as e: ConanOutput(scope="detect_api").warning(f"Couldn't get aix getconf {e}") return None def _get_aix_architecture(): processor = platform.processor() if "powerpc" in processor: kernel_bitness = _get_aix_conf("KERNEL_BITMODE") if kernel_bitness: return "ppc64" if kernel_bitness == "64" else "ppc32" elif "rs6000" in processor: return "ppc32" def _get_e2k_architecture(): return { "E1C+": "e2k-v4", # Elbrus 1C+ and Elbrus 1CK "E2C+": "e2k-v2", # Elbrus 2CM "E2C+DSP": "e2k-v2", # Elbrus 2C+ "E2C3": "e2k-v6", # Elbrus 2C3 "E2S": "e2k-v3", # Elbrus 2S (aka Elbrus 4C) "E8C": "e2k-v4", # Elbrus 8C and Elbrus 8C1 "E8C2": "e2k-v5", # Elbrus 8C2 (aka Elbrus 8CB) "E12C": "e2k-v6", # Elbrus 12C "E16C": "e2k-v6", # Elbrus 16C "E32C": "e2k-v7", # Elbrus 32C }.get(platform.processor()) def _parse_gnu_libc(ldd_output): first_line = ldd_output.partition("\n")[0] if any(glibc_indicator in first_line for glibc_indicator in ["GNU libc", "GLIBC"]): return first_line.split()[-1].strip() return None def _detect_gnu_libc(ldd="/usr/bin/ldd"): if platform.system() != "Linux": ConanOutput(scope="detect_api").warning("detect_gnu_libc() only works on Linux") return None try: ldd_output = check_output_runner(f"{ldd} --version") version = _parse_gnu_libc(ldd_output) if version is None: first_line = ldd_output.partition("\n")[0] ConanOutput(scope="detect_api").warning( f"detect_gnu_libc() did not detect glibc in the first line of output from '{ldd} --version': '{first_line}'" ) return None return version except Exception as e: ConanOutput(scope="detect_api").debug( f"Couldn't determine the glibc version from the output of the '{ldd} --version' command {e}" ) return None def _parse_musl_libc(ldd_output): lines = ldd_output.splitlines() if "musl libc" not in lines[0]: return None return lines[1].split()[-1].strip() def _detect_musl_libc(ldd="/usr/bin/ldd"): if platform.system() != "Linux": ConanOutput(scope="detect_api").warning( "detect_musl_libc() only works on Linux" ) return None d = tempfile.mkdtemp() tmp_file = os.path.join(d, "err") try: with open(tmp_file, 'w') as stderr: check_output_runner(f"{ldd}", stderr=stderr, ignore_error=True) ldd_output = load(tmp_file) version = _parse_musl_libc(ldd_output) if version is None: first_line = ldd_output.partition("\n")[0] ConanOutput(scope="detect_api").warning( f"detect_musl_libc() did not detect musl libc in the first line of output from '{ldd}': '{first_line}'" ) return None return version except Exception as e: ConanOutput(scope="detect_api").debug( f"Couldn't determine the musl libc version from the output of the '{ldd}' command {e}" ) finally: try: os.unlink(tmp_file) except OSError: pass return None def detect_libc(ldd="/usr/bin/ldd"): if platform.system() != "Linux": ConanOutput(scope="detect_api").warning( f"detect_libc() is only supported on Linux currently" ) return None, None version = _detect_gnu_libc(ldd) if version is not None: return "gnu", version version = _detect_musl_libc(ldd) if version is not None: return "musl", version ConanOutput(scope="detect_api").warning( f"Couldn't detect the libc provider and version" ) return None, None def detect_libcxx(compiler, version, compiler_exe=None): assert isinstance(version, Version) def _detect_gcc_libcxx(version_, executable): output = ConanOutput(scope="detect_api") # Assumes a working g++ executable if executable == "g++": # we can rule out old gcc versions new_abi_available = version_ >= "5.1" if not new_abi_available: return "libstdc++" main = textwrap.dedent(""" #include using namespace std; static_assert(sizeof(std::string) != sizeof(void*), "using libstdc++"); int main(){} """) t = tempfile.mkdtemp() filename = os.path.join(t, "main.cpp") save(filename, main) old_path = os.getcwd() os.chdir(t) try: error, out_str = detect_runner(f'"{executable}" main.cpp -std=c++11') if error: if "using libstdc++" in out_str: output.info("gcc C++ standard library: libstdc++") return "libstdc++" # Other error, but can't know, lets keep libstdc++11 output.warning("compiler.libcxx check error: %s" % out_str) output.warning("Couldn't deduce compiler.libcxx for gcc>=5.1, assuming libstdc++11") else: output.info("gcc C++ standard library: libstdc++11") return "libstdc++11" finally: os.chdir(old_path) # This is not really a detection in most cases # Get compiler C++ stdlib if compiler == "apple-clang": return "libc++" elif compiler == "gcc": libcxx = _detect_gcc_libcxx(version, compiler_exe or "g++") return libcxx elif compiler == "cc": if platform.system() == "SunOS": return "libstdcxx4" elif compiler == "clang": if platform.system() == "FreeBSD": return "libc++" elif platform.system() == "Darwin": return "libc++" elif platform.system() == "Windows": return # by default windows will assume LLVM/Clang with VS backend else: # Linux libcxx = _detect_gcc_libcxx(version, compiler_exe or "clang++") return libcxx elif compiler == "sun-cc": return "libCstd" elif compiler == "mcst-lcc": return "libstdc++" elif compiler == "intel-cc": return "libstdc++11" def default_msvc_runtime(compiler): if platform.system() != "Windows": return None, None if compiler == "clang": # It could be LLVM/Clang with VS runtime or Msys2 with libcxx ConanOutput(scope="detect_api").warning("Assuming LLVM/Clang in Windows with VS 17 2022") ConanOutput(scope="detect_api").warning("If Msys2/Clang need to remove compiler.runtime* " "and define compiler.libcxx") return "dynamic", "v143" elif compiler == "msvc": # Add default mandatory fields for MSVC compiler return "dynamic", None return None, None def detect_msvc_update(version): from conan.internal.api.detect.detect_vs import vs_detect_update return vs_detect_update(version) def default_cppstd(compiler, compiler_version): """ returns the default cppstd for the compiler-version. This is not detected, just the default """ def _clang_cppstd_default(version): if version >= "16": return "gnu17" # Official docs are wrong, in 6.0 the default is gnu14 to follow gcc's choice return "gnu98" if version < "6" else "gnu14" def _gcc_cppstd_default(version): if version >= "11": return "gnu17" return "gnu98" if version < "6" else "gnu14" def _visual_cppstd_default(version): if version >= "190": # VS 2015 update 3 only return "14" return None def _mcst_lcc_cppstd_default(version): return "gnu14" if version >= "1.24" else "gnu98" def _intel_cppstd_default(version): tokens = version.main major = tokens[0] # https://www.intel.com/content/www/us/en/developer/articles/troubleshooting/icx-changes-default-cpp-std-to-cpp17-with-2023.html return "17" if major >= "2023" else "14" def _apple_clang_cppstd_default(version): return "gnu98" if version < "17" else "gnu14" default = {"gcc": _gcc_cppstd_default(compiler_version), "clang": _clang_cppstd_default(compiler_version), "apple-clang": _apple_clang_cppstd_default(compiler_version), "intel-cc": _intel_cppstd_default(compiler_version), "msvc": _visual_cppstd_default(compiler_version), "mcst-lcc": _mcst_lcc_cppstd_default(compiler_version)}.get(str(compiler), None) return default def detect_cppstd(compiler, compiler_version): cppstd = default_cppstd(compiler, compiler_version) if compiler == "apple-clang" and compiler_version >= "11": # Conan does not detect the default cppstd for apple-clang, # because it's still 98/14 for the compiler (even though xcode uses newer in projects) # and having it be so old would be annoying for users cppstd = "gnu17" return cppstd def default_cstd(compiler, compiler_version): """returns the default cstd for the compiler-version. This is not detected, just the default""" def _clang_cstd_default(version): if version >= "11": return "gnu17" # https://releases.llvm.org/11.0.0/tools/clang/docs/ReleaseNotes.html#c-language-changes-in-clang elif version >= "4": # 3.5 actually return "gnu11" else: return "gnu99" # It was gnu89 actually def _gcc_cstd_default(version): if version >= "15": # https://www.gnu.org/software/gcc/gcc-15/changes.html#c return "gnu23" elif version >= "8": return "gnu17" # https://www.gnu.org/software/gcc/gcc-8/changes.html#c elif version >= "5": return "gnu11" # https://www.gnu.org/software/gcc/gcc-5/changes.html#c else: return "gnu99" # It was gnu89 actually def _visual_cstd_default(version): return None def _apple_clang_cstd_default(version): # Based on which LLVM/Clang versions these are based on if version >= "12": return "gnu17" if version >= "10": return "gnu11" return "gnu99" def _intel_cstd_default(version): return None def _mcst_lcc_cstd_default(version): return None default = { "gcc": _gcc_cstd_default(compiler_version), "clang": _clang_cstd_default(compiler_version), "apple-clang": _apple_clang_cstd_default(compiler_version), "intel-cc": _intel_cstd_default(compiler_version), "msvc": _visual_cstd_default(compiler_version), "mcst-lcc": _mcst_lcc_cstd_default(compiler_version), }.get(str(compiler), None) return default def detect_default_compiler(): """ find the default compiler on the build machine search order and priority: 1. CC and CXX environment variables are always top priority 2. Visual Studio detection (Windows only) via vswhere or registry or environment variables 3. Apple Clang (Mac only) 4. cc executable 5. gcc executable 6. clang executable """ output = ConanOutput(scope="detect_api") cc = os.environ.get("CC", "") cxx = os.environ.get("CXX", "") if cc or cxx: # Env defined, use them output.info("CC and CXX: %s, %s " % (cc or "None", cxx or "None")) command = cc or cxx if "/usr/bin/cc" == command or "/usr/bin/c++" == command: # Symlinks of linux "alternatives" return _cc_compiler(command) if "clang" in command.lower(): return detect_clang_compiler(command) if "gnu-cc" in command or "gcc" in command or "g++" in command or "c++" in command: gcc, gcc_version, compiler_exe = detect_gcc_compiler(command) if platform.system() == "Darwin" and gcc is None: output.error("%s detected as a frontend using apple-clang. " "Compiler not supported" % command) return gcc, gcc_version, compiler_exe if "icpx" in command or "icx" in command: intel, intel_version, compiler_exe = detect_intel_compiler(command) return intel, intel_version, compiler_exe if platform.system() == "SunOS" and command.lower() == "cc": return detect_suncc_compiler(command) if (platform.system() == "Windows" and command.rstrip('"').endswith(("cl", "cl.exe")) and "clang" not in command): return detect_cl_compiler(command) # I am not able to find its version output.error("Not able to automatically detect '%s' version" % command) return None, None, None if platform.system() == "Windows": compiler, version, compiler_exe = detect_msvc_compiler() if compiler: return compiler, version, compiler_exe if platform.system() == "SunOS": sun_cc, sun_cc_version, compiler_exe = detect_suncc_compiler() if sun_cc: return sun_cc, sun_cc_version, compiler_exe if platform.system() in ["Darwin", "FreeBSD"]: clang, clang_version, compiler_exe = detect_clang_compiler() # prioritize clang if clang: return clang, clang_version, compiler_exe return None, None, None else: # linux like system compiler, compiler_version, compiler_exe = _cc_compiler() if compiler: return compiler, compiler_version, compiler_exe gcc, gcc_version, compiler_exe = detect_gcc_compiler() if gcc: return gcc, gcc_version, compiler_exe return detect_clang_compiler() def default_msvc_ide_version(version): version = {"195": "18", "194": "17", "193": "17", "192": "16", "191": "15"}.get(str(version)) if version: return Version(version) def _detect_vs_ide_version(): from conan.internal.api.detect.detect_vs import vs_installation_path msvc_versions = "18", "17", "16", "15" for version in msvc_versions: vs_path = os.getenv('vs%s0comntools' % version) path = vs_path or vs_installation_path(version) if path: ConanOutput(scope="detect_api").info("Found msvc %s" % version) return Version(version) return None def _cc_compiler(compiler_exe="cc"): # Try to detect the "cc" linux system "alternative". It could point to gcc or clang try: ret, out = detect_runner(f'"{compiler_exe}" --version') if ret != 0: return None, None, None compiler = "clang" if "clang" in out else "gcc" # clang and gcc have version after a space, first try to find that to skip extra numbers # that might appear in the first line of the output before the version # There might also be a leading parenthesis that contains build information, # so we try to skip it installed_version = re.search(r"(?:\(.*\))? ([0-9]+(\.[0-9]+)*)", out) # Fallback to the first number we find optionally followed by other version fields installed_version = installed_version or re.search(r"([0-9]+(\.[0-9]+)*)", out) if installed_version and installed_version.group(1): installed_version = installed_version.group(1) ConanOutput(scope="detect_api").info("Found cc=%s-%s" % (compiler, installed_version)) return compiler, Version(installed_version), compiler_exe except (Exception,): # to disable broad-except return None, None, None def detect_gcc_compiler(compiler_exe="gcc"): try: if platform.system() == "Darwin": # In Mac OS X check if gcc is a fronted using apple-clang _, out = detect_runner(f'"{compiler_exe}" --version') out = out.lower() if "clang" in out: return None, None, None ret, out = detect_runner(f'"{compiler_exe}" -dumpversion') if ret != 0: return None, None, None compiler = "gcc" installed_version = re.search(r"([0-9]+(\.[0-9]+)?)", out).group() if installed_version: ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version)) return compiler, Version(installed_version), compiler_exe except (Exception,): # to disable broad-except return None, None, None def detect_compiler(): ConanOutput(scope="detect_api").warning("detect_compiler() is deprecated, " "use detect_default_compiler()", warn_tag="deprecated") compiler, version, _ = detect_default_compiler() return compiler, version def detect_intel_compiler(compiler_exe="icx"): try: ret, out = detect_runner(f'"{compiler_exe}" --version') if ret != 0: return None, None, None compiler = "intel-cc" installed_version = re.search(r"(202[0-9]+(\.[0-9])?)", out).group() if installed_version: ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version)) return compiler, Version(installed_version), compiler_exe except (Exception,): # to disable broad-except return None, None, None def detect_suncc_compiler(compiler_exe="cc"): try: _, out = detect_runner(f'"{compiler_exe}" -V') compiler = "sun-cc" installed_version = re.search(r"Sun C.*([0-9]+\.[0-9]+)", out) if installed_version: installed_version = installed_version.group(1) else: installed_version = re.search(r"([0-9]+\.[0-9]+)", out).group() if installed_version: ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version)) return compiler, Version(installed_version), compiler_exe except (Exception,): # to disable broad-except return None, None, None def detect_clang_compiler(compiler_exe="clang"): try: ret, out = detect_runner(f'"{compiler_exe}" --version') if ret != 0: return None, None, None if "Apple" in out: compiler = "apple-clang" elif "clang version" in out: compiler = "clang" else: return None, None, None installed_version = re.search(r"([0-9]+\.[0-9])", out).group() if installed_version: ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version)) return compiler, Version(installed_version), compiler_exe except (Exception,): # to disable broad-except return None, None, None def detect_msvc_compiler(): ide_version = _detect_vs_ide_version() # Map to compiler version = {"18": "195", "17": "193", "16": "192", "15": "191"}.get(str(ide_version)) if ide_version == "17": update = detect_msvc_update(version) # FIXME weird passing here the 193 compiler version if update and int(update) >= 10: version = "194" if version: return 'msvc', Version(version), None return None, None, None def detect_cl_compiler(compiler_exe="cl"): """ only if CC/CXX env-vars are defined pointing to cl.exe, and the VS environment must be active to have them in the path """ try: compiler_exe = compiler_exe.strip('"') ret, out = detect_runner(f'"{compiler_exe}" /?') if ret != 0: return None, None, None first_line = out.splitlines()[0] if "Microsoft" not in first_line: return None, None, None compiler = "msvc" version_regex = re.search(r"(?P[0-9]+)\.(?P[0-9]+)\.([0-9]+)\.?([0-9]+)?", first_line) if not version_regex: return None, None, None # 19.36.32535 -> 193 version = f"{version_regex.group('major')}{version_regex.group('minor')[0]}" return compiler, Version(version), compiler_exe except (Exception,): # to disable broad-except return None, None, None def detect_emcc_compiler(compiler_exe="emcc"): ret, out = detect_runner(f'"{compiler_exe}" --version') if ret != 0: return None, None, None if "Emscripten" not in out: return None, None, None compiler = "emcc" version_match = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", out) if not version_match: return None, None, None version = version_match.group() ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, version)) return compiler, Version(version), compiler_exe def default_compiler_version(compiler, version): """ returns the default version that Conan uses in profiles, typically dropping some of the minor or patch digits, that do not affect binary compatibility """ output = ConanOutput(scope="detect_api") if not version: raise ConanException( f"No version provided to 'detect_api.default_compiler_version()' for {compiler} compiler") tokens = version.main major = tokens[0] minor = tokens[1] if len(tokens) > 1 else 0 if compiler == "clang" and major >= 8: output.info("clang>=8, using the major as version") return major elif compiler == "gcc": if major >= 5: output.info("gcc>=5, using the major as version") return major else: output.info("gcc<5, using the major.minor as version") return Version(f"{major}.{minor}") elif compiler == "apple-clang" and major >= 13: output.info("apple-clang>=13, using the major as version") return major elif compiler == "intel" and (major < 19 or (major == 19 and minor == 0)): return major elif compiler == "msvc": return major elif compiler == "intel-cc": return major return version def detect_sdk_version(sdk): if platform.system() != "Darwin": return cmd = f'xcrun -sdk {sdk} --show-sdk-version' _, result = detect_runner(cmd) result = result.strip() return result ================================================ FILE: conan/internal/api/detect/detect_vs.py ================================================ import json import os from shutil import which from conan.tools.build import cmd_args_to_string from conan.errors import ConanException def vs_installation_path(version): return _vs_installation_path(version)[0] def vs_detect_update(version): version = {"195": "18", "194": "17", "193": "17", "192": "16", "191": "15"}.get(str(version)) full_version = _vs_installation_path(version)[1] components = full_version.split(".") if len(components) > 1: return components[1] def _vs_installation_path(version): # TODO: Preference hardcoded, [conf] must be defined preference = ["Enterprise", "Professional", "Community", "BuildTools"] # Try with vswhere() try: legacy_products = vswhere(legacy=True) products = vswhere(products=["*"]) products.extend(p for p in legacy_products if p not in products) except ConanException: products = None if products: # First matching for product_type in preference: for product in products: if product["installationVersion"].startswith(f"{version}."): if product_type in product.get("productId", ""): return product["installationPath"], product["installationVersion"] # Append products without "productId" (Legacy installations) for product in products: if (product["installationVersion"].startswith(f"{version}.") and "productId" not in product): return product["installationPath"], product["installationVersion"] # If vswhere does not find anything or not available, try with vs_comntools vs_path = os.getenv("vs%s0comntools" % version) if vs_path: sub_path_to_remove = os.path.join("", "Common7", "Tools", "") # Remove '\\Common7\\Tools\\' to get same output as vswhere if vs_path.endswith(sub_path_to_remove): vs_path = vs_path[:-(len(sub_path_to_remove)+1)] return vs_path, None def vswhere(all_=False, prerelease=True, products=None, requires=None, version="", latest=False, legacy=False, property_="", nologo=True): # 'version' option only works if Visual Studio 2017 is installed: # https://github.com/Microsoft/vswhere/issues/91 products = list() if products is None else products requires = list() if requires is None else requires if legacy and (products or requires): raise ConanException("The 'legacy' parameter cannot be specified with either the " "'products' or 'requires' parameter") installer_path = None program_files = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") if program_files: expected_path = os.path.join(program_files, "Microsoft Visual Studio", "Installer", "vswhere.exe") if os.path.isfile(expected_path): installer_path = expected_path vswhere_path = installer_path or which("vswhere") if not vswhere_path: raise ConanException("Cannot locate vswhere in 'Program Files'/'Program Files (x86)' " "directory nor in PATH") arguments = list() arguments.append(vswhere_path) # Output json format arguments.append("-utf8") arguments.append("-format") arguments.append("json") if all_: arguments.append("-all") if prerelease: arguments.append("-prerelease") if products: arguments.append("-products") arguments.extend(products) if requires: arguments.append("-requires") arguments.extend(requires) if len(version) != 0: arguments.append("-version") arguments.append(version) if latest: arguments.append("-latest") if legacy: arguments.append("-legacy") if len(property_) != 0: arguments.append("-property") arguments.append(property_) if nologo: arguments.append("-nologo") try: from conan.internal.util.runners import check_output_runner cmd = cmd_args_to_string(arguments) output = check_output_runner(cmd).strip() # Ignore the "description" field, that even decoded contains non valid charsets for json # (ignored ones) output = "\n".join([line for line in output.splitlines() if not line.strip().startswith('"description"')]) except (ValueError, UnicodeDecodeError) as e: raise ConanException("vswhere error: %s" % str(e)) return json.loads(output) ================================================ FILE: conan/internal/api/export.py ================================================ import os import shutil from conan.tools.files import copy from conan.api.output import ConanOutput from conan.tools.scm import Git from conan.internal.errors import conanfile_exception_formatter from conan.errors import ConanException from conan.internal.model.manifest import FileTreeManifest from conan.api.model import RecipeReference from conan.internal.paths import DATA_YML from conan.internal.util.files import is_dirty, rmdir, set_dirty, mkdir, clean_dirty, chdir def cmd_export(loader, cache, hook_manager, global_conf, conanfile_path, name, version, user, channel, graph_lock=None, remotes=None): """ Export the recipe param conanfile_path: the original source directory of the user containing a conanfile.py """ conanfile = loader.load_export(conanfile_path, name, version, user, channel, graph_lock, remotes=remotes) ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) ref.validate_ref(allow_uppercase=global_conf.get("core:allow_uppercase_pkg_names", check_type=bool)) conanfile.conf = global_conf.get_conanfile_conf(ref, is_consumer=True) conanfile.display_name = str(ref) conanfile.output.scope = conanfile.display_name scoped_output = conanfile.output # Even though the package_id_non_embed_mode is minor_mode by default, # and package_id_unknown_mode is semver_mode by default, # recipes with buggy versions that do not define the attribute will have # the same problem regardless if not isinstance(ref.version.major.value, int) and ref.version.minor is not None: modes = [getattr(conanfile, f"package_id_{m}_mode", None) for m in ("embed", "non_embed", "unknown")] if (any(m in ("semver", "major", "minor", "patch") for m in modes) or all(m is None for m in modes)): msg = (f"Version '{ref.version}' contains an alphanumeric major alongside a minor " f"version, without correct 'package_id_xxx_mode' attributes.\n" f"This is highly discouraged due to unexpected package ID calculation " f"risks. Either a different version scheme should be used " f"(e.g., semantic versioning), or the 'package_id_xxx_mode' attributes " f"should be set (to something other than major, minor, patch or semver modes).\n" f"Refer to the documentation for more details: " f"https://docs.conan.io/2/knowledge/guidelines.html#guidelines-bad-alphanumeric-majors") scoped_output.warning(msg, warn_tag="risk") recipe_layout = cache.create_export_recipe_layout(ref) hook_manager.execute("pre_export", conanfile=conanfile) scoped_output.info(f"Exporting package recipe: {conanfile_path}") export_folder = recipe_layout.export() export_src_folder = recipe_layout.export_sources() # TODO: cache2.0 move this creation to other place mkdir(export_folder) mkdir(export_src_folder) recipe_metadata = recipe_layout.metadata() mkdir(recipe_metadata) conanfile.folders.set_base_recipe_metadata(recipe_metadata) _export_recipe(conanfile, export_folder) _export_source(conanfile, export_src_folder) shutil.copy2(conanfile_path, recipe_layout.conanfile()) # Execute post-export hook before computing the digest hook_manager.execute("post_export", conanfile=conanfile) conanfile.folders.set_base_export(None) conanfile.folders.set_base_export_sources(None) # Compute the new digest manifest = FileTreeManifest.create(export_folder, export_src_folder) manifest.save(export_folder) manifest.report_summary(scoped_output) # Compute the revision for the recipe revision = _calc_revision(scoped_output=conanfile.output, path=os.path.dirname(conanfile_path), manifest=manifest, revision_mode=conanfile.revision_mode, conanfile=conanfile) ref.revision = revision recipe_layout.reference = ref cache.assign_rrev(recipe_layout) scoped_output.info('Exported to cache folder: %s' % recipe_layout.export()) # TODO: cache2.0: check this part source_folder = recipe_layout.source() if os.path.exists(source_folder): try: if is_dirty(source_folder): scoped_output.info("Source folder is corrupted, forcing removal") rmdir(source_folder) clean_dirty(source_folder) except BaseException as e: scoped_output.error("Unable to delete source folder. Will be marked as corrupted " "for deletion", error_type="exception") scoped_output.warning(str(e)) set_dirty(source_folder) scoped_output.success(f"Exported: {ref.repr_humantime()}") return ref, conanfile def _calc_revision(scoped_output, path, manifest, revision_mode, conanfile): if revision_mode not in ["scm", "scm_folder", "hash"]: raise ConanException("Revision mode should be one of 'hash' (default) or 'scm'") # Use the proper approach depending on 'revision_mode' if revision_mode == "hash": revision = manifest.summary_hash else: # Exception to the rule that tools should only be used in recipes, this Git helper is ok excluded = getattr(conanfile, "revision_mode_excluded", None) git = Git(conanfile, folder=path, excluded=excluded) try: revision = git.get_commit(repository=(revision_mode == "scm")) except Exception as exc: error_msg = "Cannot detect revision using '{}' mode from repository at " \ "'{}'".format(revision_mode, path) raise ConanException("{}: {}".format(error_msg, exc)) if git.is_dirty(): raise ConanException("Can't have a dirty repository using revision_mode='scm' and doing" " 'conan export', please commit the changes and run again, or " "use 'revision_mode_excluded' recipe attribute or " "'core.scm:excluded' global configuration to define the list of " "excluded file patterns") scoped_output.info("Using git commit as the recipe revision: %s" % revision) return revision def _classify_patterns(patterns): patterns = patterns or [] included, excluded = [], [] for p in patterns: if p.startswith("!"): excluded.append(p[1:]) else: included.append(p) return included, excluded def _export_source(conanfile, destination_source_folder): if callable(conanfile.exports_sources): raise ConanException("conanfile 'exports_sources' shouldn't be a method, " "use 'export_sources()' instead") if isinstance(conanfile.exports_sources, str): conanfile.exports_sources = (conanfile.exports_sources,) included_sources, excluded_sources = _classify_patterns(conanfile.exports_sources) for pattern in included_sources: copy(conanfile, pattern, src=conanfile.recipe_folder, dst=destination_source_folder, excludes=excluded_sources) conanfile.folders.set_base_export_sources(destination_source_folder) _run_method(conanfile, "export_sources") def _export_recipe(conanfile, destination_folder): if callable(conanfile.exports): raise ConanException("conanfile 'exports' shouldn't be a method, use 'export()' instead") if isinstance(conanfile.exports, str): conanfile.exports = (conanfile.exports,) package_output = ConanOutput(scope="%s: exports" % conanfile.output.scope) if os.path.exists(os.path.join(conanfile.recipe_folder, DATA_YML)): package_output.info("File '{}' found. Exporting it...".format(DATA_YML)) tmp = [DATA_YML] if conanfile.exports: tmp.extend(conanfile.exports) # conanfile.exports could be a tuple (immutable) conanfile.exports = tmp included_exports, excluded_exports = _classify_patterns(conanfile.exports) for pattern in included_exports: copy(conanfile, pattern, conanfile.recipe_folder, destination_folder, excludes=excluded_exports) conanfile.folders.set_base_export(destination_folder) _run_method(conanfile, "export") def _run_method(conanfile, method): export_method = getattr(conanfile, method, None) if export_method: if not callable(export_method): raise ConanException("conanfile '%s' must be a method" % method) conanfile.output.highlight("Calling %s()" % method) default_options = conanfile.default_options options = conanfile.options try: # TODO: Poor man attribute control access. Convert to nice decorator conanfile.default_options = None conanfile.options = None with chdir(conanfile.recipe_folder): with conanfile_exception_formatter(conanfile, method): export_method() finally: conanfile.default_options = default_options conanfile.options = options ================================================ FILE: conan/internal/api/install/__init__.py ================================================ ================================================ FILE: conan/internal/api/install/generators.py ================================================ import importlib import inspect import os import traceback from conan.errors import ConanException from conan.internal.cache.home_paths import HomePaths from conan.internal.errors import conanfile_exception_formatter from conan.internal.util.files import mkdir, chdir _generators = {"CMakeToolchain": "conan.tools.cmake", "CMakeDeps": "conan.tools.cmake", "CMakeConfigDeps": "conan.tools.cmake", "MesonToolchain": "conan.tools.meson", "MSBuildDeps": "conan.tools.microsoft", "MSBuildToolchain": "conan.tools.microsoft", "NMakeToolchain": "conan.tools.microsoft", "NMakeDeps": "conan.tools.microsoft", "VCVars": "conan.tools.microsoft", "VirtualRunEnv": "conan.tools.env.virtualrunenv", "VirtualBuildEnv": "conan.tools.env.virtualbuildenv", "AutotoolsDeps": "conan.tools.gnu", "AutotoolsToolchain": "conan.tools.gnu", "GnuToolchain": "conan.tools.gnu", "PkgConfigDeps": "conan.tools.gnu", "BazelDeps": "conan.tools.google", "BazelToolchain": "conan.tools.google", "IntelCC": "conan.tools.intel", "XcodeDeps": "conan.tools.apple", "XcodeToolchain": "conan.tools.apple", "PremakeDeps": "conan.tools.premake", "PremakeToolchain": "conan.tools.premake", "MakeDeps": "conan.tools.gnu", "SConsDeps": "conan.tools.scons", "QbsDeps": "conan.tools.qbs", "QbsProfile": "conan.tools.qbs", "CPSDeps": "conan.tools.cps", "ROSEnv": "conan.tools.ros" } def _get_generator_class(generator_name): try: generator_class = _generators[generator_name] # This is identical to import ... form ... in terms of cacheing except KeyError as e: raise ConanException(f"Invalid generator '{generator_name}'. " f"Available types: {', '.join(_generators)}") from e try: return getattr(importlib.import_module(generator_class), generator_name) except ImportError as e: raise ConanException("Internal Conan error: " f"Could not find module {generator_class}") from e except AttributeError as e: raise ConanException("Internal Conan error: " f"Could not find name {generator_name} " f"inside module {generator_class}") from e def load_cache_generators(path): from conan.internal.loader import load_python_file result = {} # Name of the generator: Class if not os.path.isdir(path): return result for f in os.listdir(path): if not f.endswith(".py") or f.startswith("_"): continue full_path = os.path.join(path, f) mod, _ = load_python_file(full_path) for name, value in inspect.getmembers(mod): if inspect.isclass(value) and not name.startswith("_"): result[name] = value return result def write_generators(conanfile, hook_manager, home_folder, envs_generation=None): new_gen_folder = conanfile.generators_folder _receive_conf(conanfile) _receive_generators(conanfile) # TODO: Optimize this, so the global generators are not loaded every call to write_generators global_generators = load_cache_generators(HomePaths(home_folder).custom_generators_path) hook_manager.execute("pre_generate", conanfile=conanfile) if conanfile.generators: conanfile.output.highlight(f"Writing generators to {new_gen_folder}") # generators check that they are not present in the generators field, # to avoid duplicates between the generators attribute and the generate() method # They would raise an exception here if we don't invalidate the field while we call them old_generators = [] for gen in conanfile.generators: if gen not in old_generators: old_generators.append(gen) conanfile.generators = [] for generator_name in old_generators: if isinstance(generator_name, str): global_generator = global_generators.get(generator_name) generator_class = global_generator or _get_generator_class(generator_name) else: generator_class = generator_name generator_name = generator_class.__name__ assert generator_class try: generator = generator_class(conanfile) mkdir(new_gen_folder) conanfile.output.info(f"Generator '{generator_name}' calling 'generate()'") with chdir(new_gen_folder): generator.generate() except Exception as e: # When a generator fails, it is very useful to have the whole stacktrace if not isinstance(e, ConanException): conanfile.output.error(traceback.format_exc(), error_type="exception") raise ConanException(f"Error in generator '{generator_name}': {str(e)}") from e # restore the generators attribute, so it can raise # if the user tries to instantiate a generator already present in generators conanfile.generators = old_generators if hasattr(conanfile, "generate"): conanfile.output.highlight("Calling generate()") conanfile.output.info(f"Generators folder: {new_gen_folder}") mkdir(new_gen_folder) with chdir(new_gen_folder): with conanfile_exception_formatter(conanfile, "generate"): conanfile.generate() if envs_generation is None: if conanfile.virtualbuildenv: mkdir(new_gen_folder) with chdir(new_gen_folder): from conan.tools.env.virtualbuildenv import VirtualBuildEnv env = VirtualBuildEnv(conanfile) # TODO: Check length of env.vars().keys() when adding NotEmpty env.generate() if conanfile.virtualrunenv: mkdir(new_gen_folder) with chdir(new_gen_folder): from conan.tools.env import VirtualRunEnv env = VirtualRunEnv(conanfile) env.generate() from conan.tools.env.environment import generate_aggregated_env generate_aggregated_env(conanfile) hook_manager.execute("post_generate", conanfile=conanfile) def _receive_conf(conanfile): """ collect conf_info from the immediate build_requires, aggregate it and injects/update current conf """ # TODO: Open question 1: Only build_requires can define config? # TODO: Only direct build_requires? # TODO: Is really the best mechanism to define this info? Better than env-vars? # Conf only for first level build_requires for build_require in conanfile.dependencies.direct_build.values(): if build_require.conf_info: conanfile.conf.compose_conf(build_require.conf_info) def _receive_generators(conanfile): """ Collect generators_info from the immediate build_requires""" for build_req in conanfile.dependencies.direct_build.values(): if build_req.generator_info: if not isinstance(build_req.generator_info, list): raise ConanException(f"{build_req} 'generator_info' must be a list") names = [c.__name__ if not isinstance(c, str) else c for c in build_req.generator_info] conanfile.output.warning(f"Tool-require {build_req} adding generators: {names}", warn_tag="experimental") # Generators can be defined as a tuple in recipes, ensure we don't break if so conanfile.generators = build_req.generator_info + list(conanfile.generators) def relativize_path(path, conanfile, placeholder, normalize=True): """ relative path from the "generators_folder" to "path", asuming the root file, like conan_toolchain.cmake will be directly in the "generators_folder" """ base_common_folder = conanfile.folders._base_generators # noqa if not base_common_folder or not os.path.isabs(base_common_folder): return path try: common_path = os.path.commonpath([path, conanfile.generators_folder, base_common_folder]) if common_path.replace("\\", "/") == base_common_folder.replace("\\", "/"): rel_path = os.path.relpath(path, conanfile.generators_folder) new_path = os.path.join(placeholder, rel_path) return new_path.replace("\\", "/") if normalize else new_path except ValueError: # In case the unit in Windows is different, path cannot be made relative pass return path ================================================ FILE: conan/internal/api/list/__init__.py ================================================ ================================================ FILE: conan/internal/api/list/query_parse.py ================================================ from collections import OrderedDict def filter_package_configs(pkg_configurations, query): postfix = _infix_to_postfix(query) if query else [] result = OrderedDict() for pref, data in pkg_configurations.items(): if _evaluate_postfix_with_info(postfix, data): result[pref] = data return result def _evaluate_postfix_with_info(postfix, binary_info): # Evaluate conaninfo with the expression def evaluate_info(expression): """Receives an expression like compiler.version="12" Uses conan_vars_info in the closure to evaluate it""" name, value = expression.split("=", 1) value = value.replace("\"", "") return _evaluate(name, value, binary_info) return _evaluate_postfix(postfix, evaluate_info) def _evaluate(prop_name, prop_value, binary_info): """ Evaluates a single prop_name, prop_value like "os", "Windows" against conan_vars_info.serialize_min() """ def compatible_prop(setting_value, _prop_value): return (_prop_value == setting_value) or (_prop_value == "None" and setting_value is None) # TODO: Necessary to generalize this query evaluation to include all possible fields info_settings = binary_info.get("settings", {}) info_options = binary_info.get("options", {}) if not prop_name.startswith("options."): return compatible_prop(info_settings.get(prop_name), prop_value) else: prop_name = prop_name[len("options."):] return compatible_prop(info_options.get(prop_name), prop_value) def _is_operator(el): return el in ["|", "&"] def _parse_expression(subexp): """Expressions like: compiler.version=12 compiler="Visual Studio" arch="x86" Could be replaced with another one to parse different queries """ ret = "" quoted = False for char in subexp: if char in ['"', "'"]: # Fixme: Mix quotes quoted = not quoted ret += char continue if quoted: ret += char elif char == " " or _is_operator(char) or char in [")", "("]: break else: ret += char if "=" not in ret: raise Exception("Invalid expression: %s" % ret) return ret def _evaluate_postfix(postfix, evaluator): """ Evaluates a postfix expression and returns a boolean @param postfix: Postfix expression as a list @param evaluator: Function that will return a bool receiving expressions like "compiler.version=12" @return: bool """ if not postfix: # If no query return all? return True stack = [] for el in postfix: if not _is_operator(el): stack.append(el) else: o1 = stack.pop() o2 = stack.pop() if not isinstance(o1, bool): o1 = evaluator(o1) if not isinstance(o2, bool): o2 = evaluator(o2) if el == "|": res = o1 or o2 elif el == "&": res = o1 and o2 stack.append(res) if len(stack) != 1: raise Exception("Bad stack: %s" % str(stack)) elif not isinstance(stack[0], bool): return evaluator(stack[0]) # Single Expression without AND or OR else: return stack[0] def _infix_to_postfix(exp): """ Translates an infix expression to postfix using an standard algorithm with little hacks for parse complex expressions like "compiler.version=4" instead of just numbers and without taking in account the operands priority except the priority specified by the "(" @param exp: String with an expression with & and | operators, e.g.: "os=Windows & (compiler=gcc | compiler.version=3)" e.g.: "os=Windows AND (compiler=gcc or compiler.version=3)" @return List with the postfix expression """ # To ease the parser, operators only with one character exp = exp.replace(" AND ", "&").replace(" OR ", "|").replace(" and ", "&").replace(" or ", "|") output = [] stack = [] i = -1 while i < len(exp) - 1: i += 1 char = exp[i] if char == " ": # Ignore spaces between expressions and operators continue if char == ")": # Pop the stack until "(" and send them to output popped = None while popped != "(" and stack: popped = stack.pop() if popped != "(": output.append(popped) if popped != "(": raise Exception("Bad expression, not balanced parenthesis") elif _is_operator(char): # Same operations has the same priority # replace this lines if the operators need to have # some priority if stack and _is_operator(stack[:-1]): popped = stack.pop() output.append(popped) stack.append(char) elif char == "(": stack.append("(") else: # Parse an expression, in our case something like "compiler=gcc" expr = _parse_expression(exp[i:]) i += len(expr) - 1 output.append(expr) # Append remaining elements if "(" in stack: raise Exception("Bad expression, not balanced parenthesis") output.extend(stack) return output ================================================ FILE: conan/internal/api/local/__init__.py ================================================ ================================================ FILE: conan/internal/api/local/editable.py ================================================ import copy import fnmatch import json import os from os.path import join, normpath from conan.api.model import RecipeReference from conan.internal.util.files import load, save EDITABLE_PACKAGES_FILE = 'editable_packages.json' class EditablePackages: def __init__(self, cache_folder=None): if cache_folder is None: self._edited_refs = {} return self._edited_file = normpath(join(cache_folder, EDITABLE_PACKAGES_FILE)) if os.path.exists(self._edited_file): edited = load(self._edited_file) edited_js = json.loads(edited) self._edited_refs = {RecipeReference.loads(r): d for r, d in edited_js.items()} else: self._edited_refs = {} # {ref: {"path": path, "layout": layout}} def update_copy(self, ws_editables): """ Create a new instance with the union of the editable packages of self and other """ if ws_editables is None: return self result = EditablePackages() result._edited_refs = self._edited_refs.copy() result._edited_refs.update(ws_editables) return result @property def edited_refs(self): return self._edited_refs def save(self): d = {str(ref): d for ref, d in self._edited_refs.items()} save(self._edited_file, json.dumps(d)) def get(self, ref): _tmp = copy.copy(ref) _tmp.revision = None return self._edited_refs.get(_tmp) def get_path(self, ref): editable = self.get(ref) if editable is not None: return editable["path"] def add(self, ref, path, output_folder=None): assert isinstance(ref, RecipeReference) _tmp = copy.copy(ref) _tmp.revision = None self._edited_refs[ref] = {"path": path, "output_folder": output_folder} self.save() def remove(self, path, requires): removed = {} kept = {} for ref, info in self._edited_refs.items(): to_remove = False if path and info["path"] == path: to_remove = True else: for r in requires or []: if fnmatch.fnmatch(str(ref), r): to_remove = True if to_remove: removed[ref] = info else: kept[ref] = info self._edited_refs = kept self.save() return removed ================================================ FILE: conan/internal/api/migrations.py ================================================ import os import textwrap from conan.api.output import ConanOutput from conan.internal.default_settings import migrate_settings_file from conans.migrations import Migrator from conan.internal.util.files import load, save CONAN_GENERATED_COMMENT = "This file was generated by Conan" def update_file(file_path, new_content): """ Update any file path given with the new content. Notice that the file is only updated whether it contains the ``CONAN_GENERATED_COMMENT``. :param file_path: ``str`` path to the file. :param new_content: ``str`` content to be saved. """ out = ConanOutput() file_name = os.path.basename(file_path) if not os.path.exists(file_path): save(file_path, new_content) else: content = load(file_path) first_line = content.lstrip().split("\n", 1)[0] if CONAN_GENERATED_COMMENT in first_line and content != new_content: save(file_path, new_content) out.success(f"Migration: Successfully updated {file_name}") class ClientMigrator(Migrator): def __init__(self, cache_folder, current_version): self.cache_folder = cache_folder super(ClientMigrator, self).__init__(cache_folder, current_version) def _apply_migrations(self, old_version): # Migrate the settings if they were the default for that version # Time for migrations! # Update settings.yml migrate_settings_file(self.cache_folder) # Update compatibility.py, app_compat.py, and cppstd_compat.py. from conan.internal.graph.compatibility import migrate_compatibility_files migrate_compatibility_files(self.cache_folder) # Update profile plugin from conan.internal.api.profile.profile_loader import migrate_profile_plugin migrate_profile_plugin(self.cache_folder) # let the back migration files be stored # if there was not a previous install (old_version==None) if old_version is None or old_version < "2.4": _migrate_default_compatibility(self.cache_folder) def _migrate_default_compatibility(cache_folder): # just the back migration undo = textwrap.dedent("""\ import os def migrate(home_folder): from conans.client.graph.compatibility import migrate_compatibility_files migrate_compatibility_files(home_folder) """) path = os.path.join(cache_folder, "migrations", "2.4_1-migrate.py") save(path, undo) ================================================ FILE: conan/internal/api/new/__init__.py ================================================ ================================================ FILE: conan/internal/api/new/alias_new.py ================================================ _conanfile = '''\ from conan import ConanFile class AliasConanfile(ConanFile): name = "{{name}}" {% if version %}version = "{{version}}"{%endif%} alias = "{{name}}/{{target}}" revision_mode = "{{revision_mode|default('hash')}}" ''' alias_file = {"conanfile.py": _conanfile} ================================================ FILE: conan/internal/api/new/autoools_exe.py ================================================ from conan.internal.api.new.autotools_lib import makefile_am from conan.internal.api.new.cmake_lib import source_cpp, test_main, source_h conanfile_exe = """\ import os from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools, AutotoolsDeps from conan.tools.layout import basic_layout from conan.tools.files import chdir class {{package_name}}Conan(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "application" win_bash = True # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Binary configuration settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "configure.ac", "Makefile.am", "src/*" {% if requires is defined -%} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} def layout(self): basic_layout(self) def generate(self): deps = AutotoolsDeps(self) deps.generate() at_toolchain = AutotoolsToolchain(self) at_toolchain.generate() def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def package(self): autotools = Autotools(self) autotools.install() """ makefile_am_exe = """\ bin_PROGRAMS = {{name}} {{name}}_SOURCES = main.cpp {{name}}.cpp """ test_conanfile_exe_v2 = """\ import os from conan import ConanFile from conan.tools.build import can_run from conan.tools.layout import basic_layout class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def layout(self): basic_layout(self) def test(self): if can_run(self): self.run("{{name}}", env="conanrun") """ configure_ac = """\ AC_INIT([{{name}}], [{{version}}], []) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_PROG_CXX AC_PROG_RANLIB AM_PROG_AR AC_CONFIG_FILES([Makefile src/Makefile]) AC_OUTPUT """ autotools_exe_files = {"conanfile.py": conanfile_exe, "src/{{name}}.cpp": source_cpp, "src/{{name}}.h": source_h, "src/main.cpp": test_main, "configure.ac": configure_ac, "Makefile.am": makefile_am, "src/Makefile.am": makefile_am_exe, "test_package/conanfile.py": test_conanfile_exe_v2 } ================================================ FILE: conan/internal/api/new/autotools_lib.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_sources_v2 = """ import os from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools from conan.tools.layout import basic_layout from conan.tools.apple import fix_apple_shared_install_name class {{package_name}}Conan(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "library" win_bash = True # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "configure.ac", "Makefile.am", "src/*" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): basic_layout(self) def generate(self): at_toolchain = AutotoolsToolchain(self) at_toolchain.generate() def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def package(self): autotools = Autotools(self) autotools.install() fix_apple_shared_install_name(self) def package_info(self): self.cpp_info.libs = ["{{name}}"] """ configure_ac = """ AC_INIT([{{name}}], [{{version}}], []) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_PROG_CXX AM_PROG_AR LT_INIT AC_CONFIG_FILES([Makefile src/Makefile]) AC_OUTPUT """ makefile_am = """ SUBDIRS = src """ makefile_am_lib = """ lib_LTLIBRARIES = lib{{name}}.la lib{{name}}_la_SOURCES = {{name}}.cpp {{name}}.h lib{{name}}_la_HEADERS = {{name}}.h lib{{name}}_ladir = $(includedir) """ test_conanfile_v2 = """ import os from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools, AutotoolsDeps from conan.tools.layout import basic_layout from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "AutotoolsDeps", "AutotoolsToolchain" win_bash = True def requirements(self): self.requires(self.tested_reference_str) def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def layout(self): basic_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "main") self.run(cmd, env="conanrun") """ test_configure_ac = """ AC_INIT([main], [1.0], []) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_PROG_CXX AC_PROG_RANLIB AM_PROG_AR AC_CONFIG_FILES([Makefile]) AC_OUTPUT """ test_makefile_am = """ bin_PROGRAMS = main main_SOURCES = main.cpp """ autotools_lib_files = {"conanfile.py": conanfile_sources_v2, "src/{{name}}.cpp": source_cpp, "src/{{name}}.h": source_h, "src/Makefile.am": makefile_am_lib, "configure.ac": configure_ac, "Makefile.am": makefile_am, "test_package/conanfile.py": test_conanfile_v2, "test_package/main.cpp": test_main, "test_package/configure.ac": test_configure_ac, "test_package/Makefile.am": test_makefile_am } ================================================ FILE: conan/internal/api/new/basic.py ================================================ def inject_get_or_else(variable, default): return variable + ' = "{% if ' + variable + " is defined %}{{ " + variable + " }}{% else %}" + default + '{% endif %}"' _conanfile_header = f'''\ {inject_get_or_else("name", "pkg")} {inject_get_or_else("version", "1.0")} {inject_get_or_else("description", "A basic recipe")} {inject_get_or_else("license", "")} {inject_get_or_else("homepage", "")} ''' _conanfile = '''\ from conan import ConanFile class BasicConanfile(ConanFile): ''' + _conanfile_header + '''\ # Check the documentation for the rest of the available attributes # The requirements method allows you to define the dependencies of your recipe def requirements(self): # Each call to self.requires() will add the corresponding requirement # to the current list of requirements {% if requires is defined -%} {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {% else -%} # Uncommenting this line will add the zlib/1.2.13 dependency to your project # self.requires("zlib/1.2.13") pass {%- endif %} # The build_requirements() method is functionally equivalent to the requirements() one, # being executed just after it. It's a good place to define tool requirements, # dependencies necessary at build time, not at application runtime def build_requirements(self): # Each call to self.tool_requires() will add the corresponding build requirement {% if tool_requires is defined -%} {% for require in tool_requires -%} self.tool_requires("{{ require }}") {% endfor %} {% else -%} # Uncommenting this line will add the cmake >=3.15 build dependency to your project # self.requires("cmake/[>=3.15]") pass {%- endif %} # The purpose of generate() is to prepare the build, generating the necessary files, such as # Files containing information to locate the dependencies, environment activation scripts, # and specific build system files among others def generate(self): pass # This method is used to build the source code of the recipe using the desired commands. def build(self): # You can use your command line tools to invoke your build system # or any of the build helpers provided with Conan in conan.tools # self.run("g++ ...") pass # The actual creation of the package, once it's built, is done in the package() method. # Using the copy() method from tools.files, artifacts are copied # from the build folder to the package folder def package(self): # copy(self, "*.h", self.source_folder, join(self.package_folder, "include"), keep_path=False) pass ''' _conanfile_default = '''from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" settings = "os", "compiler", "build_type", "arch" def layout(self): cmake_layout(self) {% if requires is defined %} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} {%- if tool_requires is defined %} def build_requirements(self): {% for require in tool_requires -%} self.tool_requires("{{ require }}") {% endfor %} {%- endif %} def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() # This recipe is aimed at a consumer scenario. If you want to create # a package, then you should add the following attributes/methods: # * exports_sources attribute (https://docs.conan.io/2/reference/conanfile/attributes.html#exports-sources) # * package method (https://docs.conan.io/2/reference/conanfile/methods/package.html) # * package_info method (https://docs.conan.io/2/reference/conanfile/methods/package_info.html) ''' basic_file = {"conanfile.py": _conanfile} basic_default_file = {"conanfile.py": _conanfile_default} ================================================ FILE: conan/internal/api/new/bazel_7_exe.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_exe = """ import os from conan import ConanFile from conan.tools.google import Bazel, bazel_layout from conan.tools.files import copy class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "application" # Binary configuration settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "main/*", "MODULE.bazel", ".bazelrc" generators = "BazelToolchain" def layout(self): bazel_layout(self) def build(self): bazel = Bazel(self) bazel.build(target="//main:{{name}}") def package(self): dest_bin = os.path.join(self.package_folder, "bin") build = os.path.join(self.build_folder, "bazel-bin", "main") copy(self, "{{name}}", build, dest_bin, keep_path=False) copy(self, "{{name}}.exe", build, dest_bin, keep_path=False) """ test_conanfile_exe_v2 = """from conan import ConanFile from conan.tools.build import can_run class {{package_name}}Test(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if can_run(self): self.run("{{name}}", env="conanrun") """ _bazel_build_exe = """\ cc_binary( name = "{{name}}", srcs = ["main.cpp", "{{name}}.cpp", "{{name}}.h"] ) """ _bazel_workspace = " " # Important not empty, so template doesn't discard it _bazel_rc = """\ {% if output_root_dir is defined %}startup --output_user_root={{output_root_dir}}{% endif %} """ bazel_exe_files_7 = {"conanfile.py": conanfile_exe, "main/{{name}}.cpp": source_cpp, "main/{{name}}.h": source_h, "main/main.cpp": test_main, "main/BUILD": _bazel_build_exe, "MODULE.bazel": _bazel_workspace, ".bazelrc": _bazel_rc, "test_package/conanfile.py": test_conanfile_exe_v2 } ================================================ FILE: conan/internal/api/new/bazel_7_lib.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_sources_v2 = """ import os from conan import ConanFile from conan.tools.google import Bazel, bazel_layout from conan.tools.files import copy class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "library" # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "main/*", "MODULE.bazel", ".bazelrc" generators = "BazelToolchain" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): bazel_layout(self) def build(self): bazel = Bazel(self) # On Linux platforms, Bazel creates both shared and static libraries by default, and # it is getting naming conflicts if we use the cc_shared_library rule if self.options.shared and self.settings.os != "Linux": # We need to add '--experimental_cc_shared_library' because the project uses # cc_shared_library to create shared libraries bazel.build(args=["--experimental_cc_shared_library"], target="//main:{{name}}_shared") else: bazel.build(target="//main:{{name}}") def package(self): dest_lib = os.path.join(self.package_folder, "lib") dest_bin = os.path.join(self.package_folder, "bin") build = os.path.join(self.build_folder, "bazel-bin", "main") copy(self, "*.so", build, dest_lib, keep_path=False) copy(self, "*.dll", build, dest_bin, keep_path=False) copy(self, "*.dylib", build, dest_lib, keep_path=False) if self.settings.os == "Linux" and self.options.get_safe("fPIC"): copy(self, "*.pic.a", build, dest_lib, keep_path=False) else: copy(self, "*.a", build, dest_lib, keep_path=False) copy(self, "*.lib", build, dest_lib, keep_path=False) copy(self, "{{name}}.h", os.path.join(self.source_folder, "main"), os.path.join(self.package_folder, "include"), keep_path=False) def package_info(self): if self.options.shared and self.settings.os != "Linux": self.cpp_info.libs = ["{{name}}_shared"] else: self.cpp_info.libs = ["{{name}}"] """ test_conanfile_v2 = """import os from conan import ConanFile from conan.tools.google import Bazel, bazel_layout from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "BazelToolchain", "BazelDeps" def requirements(self): self.requires(self.tested_reference_str) def build(self): bazel = Bazel(self) bazel.build(target="//main:example") def layout(self): bazel_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "main", "example") self.run(cmd, env="conanrun") """ _bazel_build_test = """\ cc_binary( name = "example", srcs = ["example.cpp"], deps = [ "@{{name}}//:{{name}}", ], ) """ _bazel_build = """\ cc_library( name = "{{name}}", srcs = ["{{name}}.cpp"], hdrs = ["{{name}}.h"], ) """ _bazel_build_shared = """ cc_shared_library( name = "{{name}}_shared", shared_lib_name = "lib{{name}}_shared.%s", deps = [":{{name}}"], ) """ _bazel_workspace = " " # Important not empty, so template doesn't discard it _bazel_rc = """\ {% if output_root_dir is defined %}startup --output_user_root={{output_root_dir}}{% endif %} """ _test_bazel_module_bazel = """\ load_conan_dependencies = use_extension("//conan:conan_deps_module_extension.bzl", "conan_extension") use_repo(load_conan_dependencies, "{{name}}") """ def _get_bazel_build(): import platform os_ = platform.system() ret = _bazel_build if os_ != "Linux": ret += _bazel_build_shared % ("dylib" if os_ == "Darwin" else "dll") return ret bazel_lib_files_7 = {"conanfile.py": conanfile_sources_v2, "main/{{name}}.cpp": source_cpp, "main/{{name}}.h": source_h, "main/BUILD": _get_bazel_build(), "MODULE.bazel": _bazel_workspace, ".bazelrc": _bazel_rc, "test_package/conanfile.py": test_conanfile_v2, "test_package/main/example.cpp": test_main, "test_package/main/BUILD": _bazel_build_test, "test_package/MODULE.bazel": _test_bazel_module_bazel, "test_package/.bazelrc": _bazel_rc} ================================================ FILE: conan/internal/api/new/bazel_exe.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_exe = """ import os from conan import ConanFile from conan.tools.google import Bazel, bazel_layout from conan.tools.files import copy class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "application" # Binary configuration settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "main/*", "WORKSPACE", ".bazelrc" generators = "BazelToolchain" def layout(self): bazel_layout(self) def build(self): from conan.api.output import ConanOutput ConanOutput().warning("This is the template for Bazel 6.x version, " "but it will be overridden by the 'bazel_7_exe' template " "(Bazel >= 7.1 compatible).", warn_tag="deprecated") bazel = Bazel(self) bazel.build(target="//main:{{name}}") def package(self): dest_bin = os.path.join(self.package_folder, "bin") build = os.path.join(self.build_folder, "bazel-bin", "main") copy(self, "{{name}}", build, dest_bin, keep_path=False) copy(self, "{{name}}.exe", build, dest_bin, keep_path=False) """ test_conanfile_exe_v2 = """from conan import ConanFile from conan.tools.build import can_run class {{package_name}}Test(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if can_run(self): self.run("{{name}}", env="conanrun") """ _bazel_build_exe = """\ cc_binary( name = "{{name}}", srcs = ["main.cpp", "{{name}}.cpp", "{{name}}.h"] ) """ _bazel_workspace = " " # Important not empty, so template doesn't discard it _bazel_rc = """\ {% if output_root_dir is defined %}startup --output_user_root={{output_root_dir}}{% endif %} """ bazel_exe_files = {"conanfile.py": conanfile_exe, "main/{{name}}.cpp": source_cpp, "main/{{name}}.h": source_h, "main/main.cpp": test_main, "main/BUILD": _bazel_build_exe, "WORKSPACE": _bazel_workspace, ".bazelrc": _bazel_rc, "test_package/conanfile.py": test_conanfile_exe_v2 } ================================================ FILE: conan/internal/api/new/bazel_lib.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_sources_v2 = """ import os from conan import ConanFile from conan.tools.google import Bazel, bazel_layout from conan.tools.files import copy class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "library" # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "main/*", "WORKSPACE", ".bazelrc" generators = "BazelToolchain" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): bazel_layout(self) def build(self): from conan.api.output import ConanOutput ConanOutput().warning("This is the template for Bazel 6.x version, " "but it will be overridden by the 'bazel_7_lib' template " "(Bazel >= 7.1 compatible).", warn_tag="deprecated") bazel = Bazel(self) # On Linux platforms, Bazel creates both shared and static libraries by default, and # it is getting naming conflicts if we use the cc_shared_library rule if self.options.shared and self.settings.os != "Linux": # We need to add '--experimental_cc_shared_library' because the project uses # cc_shared_library to create shared libraries bazel.build(args=["--experimental_cc_shared_library"], target="//main:{{name}}_shared") else: bazel.build(target="//main:{{name}}") def package(self): dest_lib = os.path.join(self.package_folder, "lib") dest_bin = os.path.join(self.package_folder, "bin") build = os.path.join(self.build_folder, "bazel-bin", "main") copy(self, "*.so", build, dest_lib, keep_path=False) copy(self, "*.dll", build, dest_bin, keep_path=False) copy(self, "*.dylib", build, dest_lib, keep_path=False) if self.settings.os == "Linux" and self.options.get_safe("fPIC"): copy(self, "*.pic.a", build, dest_lib, keep_path=False) else: copy(self, "*.a", build, dest_lib, keep_path=False) copy(self, "*.lib", build, dest_lib, keep_path=False) copy(self, "{{name}}.h", os.path.join(self.source_folder, "main"), os.path.join(self.package_folder, "include"), keep_path=False) def package_info(self): if self.options.shared and self.settings.os != "Linux": self.cpp_info.libs = ["{{name}}_shared"] else: self.cpp_info.libs = ["{{name}}"] """ test_conanfile_v2 = """import os from conan import ConanFile from conan.tools.google import Bazel, bazel_layout from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "BazelToolchain", "BazelDeps" def requirements(self): self.requires(self.tested_reference_str) def build(self): bazel = Bazel(self) bazel.build() def layout(self): bazel_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "main", "example") self.run(cmd, env="conanrun") """ _bazel_build_test = """\ cc_binary( name = "example", srcs = ["example.cpp"], deps = [ "@{{name}}//:{{name}}", ], ) """ _bazel_build = """\ cc_library( name = "{{name}}", srcs = ["{{name}}.cpp"], hdrs = ["{{name}}.h"], ) """ _bazel_build_shared = """ cc_shared_library( name = "{{name}}_shared", shared_lib_name = "lib{{name}}_shared.%s", deps = [":{{name}}"], ) """ _bazel_workspace = " " # Important not empty, so template doesn't discard it _bazel_rc = """\ {% if output_root_dir is defined %}startup --output_user_root={{output_root_dir}}{% endif %} """ _test_bazel_workspace = """ load("@//conan:dependencies.bzl", "load_conan_dependencies") load_conan_dependencies() """ def _get_bazel_build(): import platform os_ = platform.system() ret = _bazel_build if os_ != "Linux": ret += _bazel_build_shared % ("dylib" if os_ == "Darwin" else "dll") return ret bazel_lib_files = {"conanfile.py": conanfile_sources_v2, "main/{{name}}.cpp": source_cpp, "main/{{name}}.h": source_h, "main/BUILD": _get_bazel_build(), "WORKSPACE": _bazel_workspace, ".bazelrc": _bazel_rc, "test_package/conanfile.py": test_conanfile_v2, "test_package/main/example.cpp": test_main, "test_package/main/BUILD": _bazel_build_test, "test_package/WORKSPACE": _test_bazel_workspace, "test_package/.bazelrc": _bazel_rc} ================================================ FILE: conan/internal/api/new/cmake_exe.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_exe = '''from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "application" # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Binary configuration settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "CMakeLists.txt", "src/*" def layout(self): cmake_layout(self) def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() {% if requires is defined -%} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} {% if tool_requires is defined -%} def build_requirements(self): {% for require in tool_requires -%} self.tool_requires("{{ require }}") {% endfor %} {%- endif %} ''' cmake_exe_v2 = """cmake_minimum_required(VERSION 3.15) project({{name}} CXX) {% if requires is defined -%} {% for require in requires -%} find_package({{as_name(require)}} CONFIG REQUIRED) {% endfor %} {%- endif %} add_executable({{name}} src/{{name}}.cpp src/main.cpp) {% if requires is defined -%} {% for require in requires -%} target_link_libraries({{name}} PRIVATE {{as_name(require)}}::{{as_name(require)}}) {% endfor %} {%- endif %} install(TARGETS {{name}} DESTINATION "." RUNTIME DESTINATION bin ARCHIVE DESTINATION lib LIBRARY DESTINATION lib ) """ test_conanfile_exe_v2 = """import os from conan import ConanFile from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if can_run(self): self.run("{{name}}", env="conanrun") """ cmake_exe_files = {"conanfile.py": conanfile_exe, "src/{{name}}.cpp": source_cpp, "src/{{name}}.h": source_h, "src/main.cpp": test_main, "CMakeLists.txt": cmake_exe_v2, "test_package/conanfile.py": test_conanfile_exe_v2 } ================================================ FILE: conan/internal/api/new/cmake_lib.py ================================================ conanfile_sources_v2 = '''from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "library" # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "CMakeLists.txt", "src/*", "include/*" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): cmake_layout(self) {% if requires is defined %} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} {%- if tool_requires is defined %} def build_requirements(self): {% for require in tool_requires -%} self.tool_requires("{{ require }}") {% endfor %} {%- endif %} def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["{{name}}"] ''' cmake_v2 = """cmake_minimum_required(VERSION 3.15) project({{name}} CXX) {% if requires is defined -%} {% for require in requires -%} find_package({{as_name(require)}} CONFIG REQUIRED) {% endfor %} {%- endif %} add_library({{name}} src/{{name}}.cpp) target_include_directories({{name}} PUBLIC include) {% if requires is defined -%} {% for require in requires -%} target_link_libraries({{name}} PRIVATE {{as_name(require)}}::{{as_name(require)}}) {% endfor %} {%- endif %} set_target_properties({{name}} PROPERTIES PUBLIC_HEADER "include/{{name}}.h") install(TARGETS {{name}}) """ source_h = """#pragma once #include #include {% set define_name = package_name.upper() %} #ifdef _WIN32 #define {{define_name}}_EXPORT __declspec(dllexport) #else #define {{define_name}}_EXPORT #endif {{define_name}}_EXPORT void {{package_name}}(); {{define_name}}_EXPORT void {{package_name}}_print_vector(const std::vector &strings); """ source_cpp = r"""#include #include "{{name}}.h" {% if requires is defined -%} {% for require in requires -%} #include "{{ as_name(require) }}.h" {% endfor %} {%- endif %} void {{package_name}}(){ {% if requires is defined -%} {% for require in requires -%} {{ as_name(require).replace(".", "_") }}(); {% endfor %} {%- endif %} #ifdef NDEBUG std::cout << "{{name}}/{{version}}: Hello World Release!\n"; #else std::cout << "{{name}}/{{version}}: Hello World Debug!\n"; #endif // ARCHITECTURES #ifdef _M_X64 std::cout << " {{name}}/{{version}}: _M_X64 defined\n"; #endif #ifdef _M_IX86 std::cout << " {{name}}/{{version}}: _M_IX86 defined\n"; #endif #ifdef _M_ARM64 std::cout << " {{name}}/{{version}}: _M_ARM64 defined\n"; #endif #if __i386__ std::cout << " {{name}}/{{version}}: __i386__ defined\n"; #endif #if __x86_64__ std::cout << " {{name}}/{{version}}: __x86_64__ defined\n"; #endif #if __aarch64__ std::cout << " {{name}}/{{version}}: __aarch64__ defined\n"; #endif // Libstdc++ #if defined _GLIBCXX_USE_CXX11_ABI std::cout << " {{name}}/{{version}}: _GLIBCXX_USE_CXX11_ABI "<< _GLIBCXX_USE_CXX11_ABI << "\n"; #endif // MSVC runtime #if defined(_DEBUG) #if defined(_MT) && defined(_DLL) std::cout << " {{name}}/{{version}}: MSVC runtime: MultiThreadedDebugDLL\n"; #elif defined(_MT) std::cout << " {{name}}/{{version}}: MSVC runtime: MultiThreadedDebug\n"; #endif #else #if defined(_MT) && defined(_DLL) std::cout << " {{name}}/{{version}}: MSVC runtime: MultiThreadedDLL\n"; #elif defined(_MT) std::cout << " {{name}}/{{version}}: MSVC runtime: MultiThreaded\n"; #endif #endif // COMPILER VERSIONS #if _MSC_VER std::cout << " {{name}}/{{version}}: _MSC_VER" << _MSC_VER<< "\n"; #endif #if _MSVC_LANG std::cout << " {{name}}/{{version}}: _MSVC_LANG" << _MSVC_LANG<< "\n"; #endif #if __cplusplus std::cout << " {{name}}/{{version}}: __cplusplus" << __cplusplus<< "\n"; #endif #if __INTEL_COMPILER std::cout << " {{name}}/{{version}}: __INTEL_COMPILER" << __INTEL_COMPILER<< "\n"; #endif #if __GNUC__ std::cout << " {{name}}/{{version}}: __GNUC__" << __GNUC__<< "\n"; #endif #if __GNUC_MINOR__ std::cout << " {{name}}/{{version}}: __GNUC_MINOR__" << __GNUC_MINOR__<< "\n"; #endif #if __clang_major__ std::cout << " {{name}}/{{version}}: __clang_major__" << __clang_major__<< "\n"; #endif #if __clang_minor__ std::cout << " {{name}}/{{version}}: __clang_minor__" << __clang_minor__<< "\n"; #endif #if __apple_build_version__ std::cout << " {{name}}/{{version}}: __apple_build_version__" << __apple_build_version__<< "\n"; #endif // SUBSYSTEMS #if __MSYS__ std::cout << " {{name}}/{{version}}: __MSYS__" << __MSYS__<< "\n"; #endif #if __MINGW32__ std::cout << " {{name}}/{{version}}: __MINGW32__" << __MINGW32__<< "\n"; #endif #if __MINGW64__ std::cout << " {{name}}/{{version}}: __MINGW64__" << __MINGW64__<< "\n"; #endif #if __CYGWIN__ std::cout << " {{name}}/{{version}}: __CYGWIN__" << __CYGWIN__<< "\n"; #endif } void {{package_name}}_print_vector(const std::vector &strings) { for(std::vector::const_iterator it = strings.begin(); it != strings.end(); ++it) { std::cout << "{{package_name}}/{{version}} " << *it << std::endl; } } """ test_conanfile_v2 = """import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """ test_cmake_v2 = """cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) find_package({{name}} CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example {{name}}::{{name}}) """ test_main = """#include "{{name}}.h" #include #include int main() { {{package_name}}(); std::vector vec; vec.push_back("test_package"); {{package_name}}_print_vector(vec); } """ cmake_lib_files = {"conanfile.py": conanfile_sources_v2, "src/{{name}}.cpp": source_cpp, "include/{{name}}.h": source_h, "CMakeLists.txt": cmake_v2, "test_package/conanfile.py": test_conanfile_v2, "test_package/src/example.cpp": test_main, "test_package/CMakeLists.txt": test_cmake_v2} ================================================ FILE: conan/internal/api/new/header_lib.py ================================================ conanfile_sources_v2 = '''from conan import ConanFile from conan.tools.layout import basic_layout from conan.tools.files import copy class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "header-library" # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "include/*" # Automatically manage the package ID clearing of settings and options implements = ["auto_header_only"] def layout(self): basic_layout(self) {% if requires is defined %} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} def package(self): copy(self, "include/*", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.bindirs = [] self.cpp_info.libdirs = [] ''' source_h = """#pragma once #include #include #include {% if requires is defined -%} {% for require in requires -%} #include "{{ as_name(require) }}.h" {% endfor %} {%- endif %} void {{package_name}}(){ {% if requires is defined -%} {% for require in requires -%} {{ as_name(require).replace(".", "_") }}(); {% endfor %} {%- endif %} std::cout << "{{package_name}}/{{version}} header only called" << std::endl; } """ test_conanfile_v2 = """import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """ test_cmake_v2 = """cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) find_package({{name}} CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example {{name}}::{{name}}) """ test_main = """#include "{{name}}.h" #include #include int main() { {{package_name}}(); } """ header_only_lib_files = {"conanfile.py": conanfile_sources_v2, "include/{{name}}.h": source_h, "test_package/conanfile.py": test_conanfile_v2, "test_package/src/example.cpp": test_main, "test_package/CMakeLists.txt": test_cmake_v2} ================================================ FILE: conan/internal/api/new/local_recipes_index.py ================================================ from conan.internal.api.new.cmake_lib import test_conanfile_v2, test_cmake_v2 config_yml = """\ versions: "{{version}}": folder: all """ conandata_yml = """\ sources: "{{version}}": url: {% if url is defined -%} - "{{url}}" {% else -%} - "http://put/here/the/url/to/release.1.2.3.zip" {% endif %} {% if sha256 is defined -%} sha256: "{{sha256}}" {%- else -%} sha256: "Put here your tarball sha256" {% endif -%} """ conanfile = """\ from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps from conan.tools.files import apply_conandata_patches, export_conandata_patches, get class {{package_name}}Recipe(ConanFile): name = "{{name}}" package_type = "library" # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def export_sources(self): export_conandata_patches(self) def source(self): get(self, **self.conan_data["sources"][self.version], destination=self.source_folder, strip_root=True) apply_conandata_patches(self) def layout(self): cmake_layout(self, src_folder="src") def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["{{name}}"] {% if requires is defined -%} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} {% if tool_requires is defined -%} def build_requirements(self): {% for require in tool_requires -%} self.tool_requires("{{ require }}") {% endfor %} {%- endif %} """ test_main = """#include "{{name}}.h" int main() { {{package_name}}(); } """ local_recipes_index_files = {"recipes/{{name}}/config.yml": config_yml, "recipes/{{name}}/all/conandata.yml": conandata_yml, "recipes/{{name}}/all/conanfile.py": conanfile, "recipes/{{name}}/all/test_package/conanfile.py": test_conanfile_v2, "recipes/{{name}}/all/test_package/CMakeLists.txt": test_cmake_v2, "recipes/{{name}}/all/test_package/src/example.cpp": test_main} ================================================ FILE: conan/internal/api/new/meson_exe.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_exe = """from conan import ConanFile from conan.tools.meson import MesonToolchain, Meson from conan.tools.gnu import PkgConfigDeps class {{package_name}}Conan(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "application" # Binary configuration settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "meson.build", "src/*" {% if requires is defined -%} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} def layout(self): self.folders.build = "build" def generate(self): deps = PkgConfigDeps(self) deps.generate() tc = MesonToolchain(self) tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() def package(self): meson = Meson(self) meson.install() """ test_conanfile_exe_v2 = """import os from conan import ConanFile from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if can_run(self): self.run("{{name}}", env="conanrun") """ _meson_build_exe = """\ project('{{name}} ', 'cpp') {% if requires is defined -%} cxx = meson.get_compiler('cpp') {% for require in requires -%} {{as_name(require)}} = dependency('{{as_name(require)}}', required: true) {% endfor %} {%- endif %} {% if requires is defined -%} executable('{{name}}', 'src/{{name}}.cpp', 'src/main.cpp', install: true, dependencies: {{ names(requires) }} ) {% else %} executable('{{name}}', 'src/{{name}}.cpp', 'src/main.cpp', install: true) {% endif %} """ meson_exe_files = {"conanfile.py": conanfile_exe, "src/{{name}}.cpp": source_cpp, "src/{{name}}.h": source_h, "src/main.cpp": test_main, "meson.build": _meson_build_exe, "test_package/conanfile.py": test_conanfile_exe_v2 } ================================================ FILE: conan/internal/api/new/meson_lib.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_sources_v2 = """import os from conan import ConanFile from conan.tools.meson import MesonToolchain, Meson from conan.tools.layout import basic_layout from conan.tools.files import copy {% if requires is defined %} from conan.tools.gnu import PkgConfigDeps {% endif %} class {{package_name}}Conan(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "library" # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "meson.build", "src/*" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): basic_layout(self) {% if requires is defined %} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} def generate(self): {% if requires is defined %} deps = PkgConfigDeps(self) deps.generate() {%- endif %} tc = MesonToolchain(self) tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() def package(self): meson = Meson(self) meson.install() def package_info(self): self.cpp_info.libs = ["{{name}}"] """ test_conanfile_v2 = """import os from conan import ConanFile from conan.tools.build import can_run from conan.tools.meson import MesonToolchain, Meson from conan.tools.layout import basic_layout class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps", "MesonToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): meson = Meson(self) meson.configure() meson.build() def layout(self): basic_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """ _meson_build_test = """\ project('Test{{name}}', 'cpp') {{name}} = dependency('{{name}}', version : '>=0.1') executable('example', 'src/example.cpp', dependencies: {{name}}) """ _meson_build = """\ project('{{name}} ', 'cpp') library('{{name}}', 'src/{{name}}.cpp', install: true) install_headers('src/{{name}}.h') """ meson_lib_files = {"conanfile.py": conanfile_sources_v2, "src/{{name}}.cpp": source_cpp, "src/{{name}}.h": source_h, "meson.build": _meson_build, "test_package/conanfile.py": test_conanfile_v2, "test_package/src/example.cpp": test_main, "test_package/meson.build": _meson_build_test} ================================================ FILE: conan/internal/api/new/msbuild_exe.py ================================================ from conan.internal.api.new.msbuild_lib import vcxproj, sln_file test_main = """#include "{{name}}.h" int main() { {{name}}(); } """ conanfile_exe = """import os from conan import ConanFile from conan.tools.microsoft import MSBuildToolchain, MSBuild, vs_layout from conan.tools.files import copy class {{package_name}}Conan(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "application" # Binary configuration settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "{{name}}.sln", "{{name}}.vcxproj", "src/*" def layout(self): vs_layout(self) def generate(self): tc = MSBuildToolchain(self) tc.generate() def build(self): msbuild = MSBuild(self) msbuild.build("{{name}}.sln") def package(self): copy(self, "*{{name}}.exe", src=self.build_folder, dst=os.path.join(self.package_folder, "bin"), keep_path=False) """ test_conanfile_exe_v2 = """import os from conan import ConanFile from conan.tools.build import can_run from conan.tools.layout import basic_layout class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def layout(self): basic_layout(self) def test(self): if can_run(self): self.run("{{name}}", env="conanrun") """ test_exe = r"""#include int main() { #ifdef NDEBUG std::cout << "{{name}}/{{version}}: Hello World Release!\n"; #else std::cout << "{{name}}/{{version}}: Hello World Debug!\n"; #endif } """ msbuild_exe_files = {"conanfile.py": conanfile_exe, "src/{{name}}.cpp": test_exe, "{{name}}.sln": sln_file.replace("test_", ""), "{{name}}.vcxproj": vcxproj.replace("TYPE_PLACEHOLDER", "Application") .replace("DEPENDENCIES", "").replace("test_", ""), "test_package/conanfile.py": test_conanfile_exe_v2 } ================================================ FILE: conan/internal/api/new/msbuild_lib.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main sln_file = r""" Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.757 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_{{name}}", "test_{{name}}.vcxproj", "{6F392A05-B151-490C-9505-B2A49720C4D9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x64.ActiveCfg = Debug|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x64.Build.0 = Debug|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x86.ActiveCfg = Debug|Win32 {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x86.Build.0 = Debug|Win32 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x64.ActiveCfg = Release|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x64.Build.0 = Release|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x86.ActiveCfg = Release|Win32 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DE6E462F-E299-4F9C-951A-F9404EB51521} EndGlobalSection EndGlobal """ vcxproj = r""" Debug Win32 Release Win32 Debug x64 Release x64 15.0 {6F392A05-B151-490C-9505-B2A49720C4D9} Win32Proj test_{{name}} DEPENDENCIES TYPE_PLACEHOLDER true Unicode TYPE_PLACEHOLDER false true Unicode TYPE_PLACEHOLDER true Unicode TYPE_PLACEHOLDER false true Unicode true true false false NotUsing Level3 Disabled true WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true include;%(AdditionalIncludeDirectories) Console true NotUsing Level3 Disabled true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true include;%(AdditionalIncludeDirectories) Console true NotUsing Level3 MaxSpeed true true true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true include;%(AdditionalIncludeDirectories) Console true true true NotUsing Level3 MaxSpeed true true true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true include;%(AdditionalIncludeDirectories) Console true true true """ conanfile_sources_v2 = """import os from conan import ConanFile from conan.tools.microsoft import MSBuildToolchain, MSBuild, vs_layout from conan.tools.files import copy class {{package_name}}Conan(ConanFile): name = "{{name}}" version = "{{version}}" # Binary configuration package_type = "static-library" # hardcoded in .vcxproj settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "{{name}}.sln", "{{name}}.vcxproj", "src/*", "include/*" def layout(self): vs_layout(self) def generate(self): tc = MSBuildToolchain(self) tc.generate() def build(self): msbuild = MSBuild(self) msbuild.build("{{name}}.sln") def package(self): copy(self, "*.h", os.path.join(self.source_folder, "include"), dst=os.path.join(self.package_folder, "include")) copy(self, "*.lib", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False) def package_info(self): self.cpp_info.libs = ["{{name}}"] """ test_conanfile_v2 = """import os from conan import ConanFile from conan.tools.microsoft import MSBuildDeps, MSBuildToolchain, MSBuild, vs_layout from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "MSBuildDeps" def requirements(self): self.requires(self.tested_reference_str) def layout(self): vs_layout(self) def generate(self): tc = MSBuildToolchain(self) tc.generate() def build(self): msbuild = MSBuild(self) msbuild.build("test_{{name}}.sln") def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "test_{{name}}") self.run(cmd, env="conanrun") """ msbuild_lib_files = {"conanfile.py": conanfile_sources_v2, "src/{{name}}.cpp": source_cpp, "include/{{name}}.h": source_h, "{{name}}.sln": sln_file.replace("test_", ""), "{{name}}.vcxproj": vcxproj.replace("TYPE_PLACEHOLDER", "StaticLibrary") .replace("DEPENDENCIES", "").replace("test_", ""), "test_package/conanfile.py": test_conanfile_v2, "test_package/src/test_{{name}}.cpp": test_main, "test_package/test_{{name}}.sln": sln_file, "test_package/test_{{name}}.vcxproj": vcxproj.replace("TYPE_PLACEHOLDER", "Application") .replace("DEPENDENCIES", r'') } ================================================ FILE: conan/internal/api/new/premake_exe.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_exe = """import os from conan import ConanFile from conan.tools.files import copy from conan.tools.layout import basic_layout from conan.tools.premake import PremakeDeps, PremakeToolchain, Premake class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "application" # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Binary configuration settings = "os", "compiler", "build_type", "arch" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "premake5.lua", "src/*" def layout(self): basic_layout(self) def generate(self): deps = PremakeDeps(self) deps.generate() tc = PremakeToolchain(self) tc.generate() def build(self): premake = Premake(self) premake.configure() premake.build(workspace="{{name.capitalize()}}") def package(self): dest_bin = os.path.join(self.package_folder, "bin") build = os.path.join(self.build_folder, "bin") copy(self, "{{name}}", build, dest_bin, keep_path=False) copy(self, "{{name}}.exe", build, dest_bin, keep_path=False) {% if requires is defined -%} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} {% if tool_requires is defined -%} def build_requirements(self): {% for require in tool_requires -%} self.tool_requires("{{ require }}") {% endfor %} {%- endif %} """ premake5 = """workspace "{{name.capitalize()}}" configurations { "Debug", "Release" } project "{{name}}" kind "ConsoleApp" language "C++" files { "src/main.cpp", "src/{{name}}.cpp", "src/{{name}}.h" } filter "configurations:Debug" defines { "DEBUG" } symbols "On" filter "configurations:Release" defines { "NDEBUG" } optimize "On" """ test_conanfile_exe_v2 = """ from conan import ConanFile from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if can_run(self): self.run("{{name}}", env="conanrun") """ premake_exe_files = { "conanfile.py": conanfile_exe, "src/{{name}}.cpp": source_cpp, "src/{{name}}.h": source_h, "src/main.cpp": test_main, "premake5.lua": premake5, "test_package/conanfile.py": test_conanfile_exe_v2, } ================================================ FILE: conan/internal/api/new/premake_lib.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_sources = """import os from conan import ConanFile from conan.tools.files import copy from conan.tools.layout import basic_layout from conan.tools.premake import PremakeDeps, PremakeToolchain, Premake class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" package_type = "library" # Optional metadata license = "" author = " " url = "" description = "" topics = ("", "", "") # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} implements = ["auto_shared_fpic"] # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "premake5.lua", "src/*", "include/*" def layout(self): basic_layout(self) {% if requires is defined %} def requirements(self): {% for require in requires -%} self.requires("{{ require }}") {% endfor %} {%- endif %} {%- if tool_requires is defined %} def build_requirements(self): {% for require in tool_requires -%} self.tool_requires("{{ require }}") {% endfor %} {%- endif %} def generate(self): deps = PremakeDeps(self) deps.generate() tc = PremakeToolchain(self) tc.generate() def build(self): premake = Premake(self) premake.configure() premake.build(workspace="{{name.capitalize()}}") def package(self): copy(self, "*.h", os.path.join(self.source_folder, "include"), os.path.join(self.package_folder, "include")) for pattern in ("*.lib", "*.a", "*.so*", "*.dylib"): copy(self, pattern, os.path.join(self.build_folder, "bin"), os.path.join(self.package_folder, "lib")) copy(self, "*.dll", os.path.join(self.build_folder, "bin"), os.path.join(self.package_folder, "bin")) def package_info(self): self.cpp_info.libs = ["{{name}}"] """ premake5 = """workspace "{{name.capitalize()}}" configurations { "Debug", "Release" } fatalwarnings {"All"} floatingpoint "Fast" includedirs { ".", "src", "include" } project "{{name}}" cppdialect "C++17" -- To let conan take control over `kind` of the libraries, DO NOT SET `kind` (StaticLib or -- SharedLib) in `project` block. -- kind "" language "C++" files { "include/*.hpp", "include/*.h", "src/*.cpp" } filter "configurations:Debug" defines { "DEBUG" } symbols "On" filter "configurations:Release" defines { "NDEBUG" } optimize "On" """ test_conanfile = """import os from conan import ConanFile from conan.tools.layout import basic_layout from conan.tools.premake import Premake from conan.tools.build import can_run class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PremakeDeps", "PremakeToolchain" def layout(self): basic_layout(self) def requirements(self): self.requires(self.tested_reference_str) def build(self): premake = Premake(self) premake.configure() premake.build(workspace="Example") def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "bin", "example") self.run(cmd, env="conanrun") """ test_premake5 = """workspace "Example" configurations { "Debug", "Release" } project "example" kind "ConsoleApp" language "C++" files { "src/example.cpp" } filter "configurations:Debug" defines { "DEBUG" } symbols "On" filter "configurations:Release" defines { "NDEBUG" } optimize "On" """ premake_lib_files = { "conanfile.py": conanfile_sources, "src/{{name}}.cpp": source_cpp, "include/{{name}}.h": source_h, "premake5.lua": premake5, "test_package/conanfile.py": test_conanfile, "test_package/src/example.cpp": test_main, "test_package/premake5.lua": test_premake5, } ================================================ FILE: conan/internal/api/new/qbs_lib.py ================================================ from conan.internal.api.new.cmake_lib import source_cpp, source_h, test_main conanfile_sources = ''' import os from conan import ConanFile from conan.tools.qbs import Qbs class {{package_name}}Recipe(ConanFile): name = "{{name}}" version = "{{version}}" exports_sources = "*.cpp", "*.h", "*.qbs" settings = "os", "compiler", "arch" options = {"shared": [True, False]} default_options = {"shared": False} def build(self): qbs = Qbs(self) qbs_config = {"products.{{name}}.isShared": "true" if self.options.shared else "false"} qbs.add_configuration("default", qbs_config) qbs.resolve() qbs.build() def package(self): qbs = Qbs(self) qbs.install() def package_info(self): self.cpp_info.libs = ["{{name}}"] ''' qbs_lib_file = ''' Library { property bool isShared: true name: "{{name}}" type: isShared ? "dynamiclibrary" : "staticlibrary" files: [ "{{name}}.cpp" ] Group { name: "headers" files: [ "{{name}}.h" ] qbs.install: true qbs.installDir: "include" } Depends { name: "cpp" } Depends { name: "bundle" } bundle.isBundle: false install: true qbs.installPrefix: "" } ''' test_conanfile_v2 = """import os from conan import ConanFile from conan.tools.build import can_run from conan.tools.qbs import Qbs from conan.tools.build import cmd_args_to_string class {{package_name}}TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps" def requirements(self): self.requires(self.tested_reference_str) def build(self): qbs = Qbs(self) qbs.resolve() qbs.build() qbs.install() def test(self): if can_run(self): cmd = os.path.join(self.package_folder, "bin", "example") self.run(cmd_args_to_string([cmd]), env="conanrun") """ qbs_test_file = ''' Application { name: "example" consoleApplication: true files: [ "example.cpp" ] Depends { name: "cpp" } install: true qbs.installPrefix: "" // external dependency via pkg-config qbsModuleProviders: ["qbspkgconfig"] moduleProviders.qbspkgconfig.libDirs: path Depends { name: "{{name}}" } } ''' qbs_lib_files = {"conanfile.py": conanfile_sources, "{{name}}.qbs": qbs_lib_file, "{{name}}.cpp": source_cpp, "{{name}}.h": source_h, "test_package/conanfile.py": test_conanfile_v2, "test_package/example.cpp": test_main, "test_package/example.qbs": qbs_test_file} ================================================ FILE: conan/internal/api/new/workspace.py ================================================ from conan.api.subapi.new import NewAPI from conan.internal.api.new.cmake_exe import cmake_exe_files from conan.internal.api.new.cmake_lib import cmake_lib_files conanws_yml = """\ packages: - path: liba ref: liba/0.1 - path: libb ref: libb/0.1 - path: app1 ref: app1/0.1 """ cmake = """\ cmake_minimum_required(VERSION 3.25) project(monorepo CXX) include(FetchContent) function(add_project PACKAGE_NAME SUBFOLDER) message(STATUS "Adding project: ${PACKAGE_NAME}. Folder: ${SUBFOLDER}") FetchContent_Declare( ${PACKAGE_NAME} SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/${SUBFOLDER} SYSTEM OVERRIDE_FIND_PACKAGE ) FetchContent_MakeAvailable(${PACKAGE_NAME}) endfunction() include(build/conanws_build_order.cmake) foreach(pair ${CONAN_WS_BUILD_ORDER}) string(FIND "${pair}" ":" pos) string(SUBSTRING "${pair}" 0 "${pos}" pkg) math(EXPR pos "${pos} + 1") # Skip the separator string(SUBSTRING "${pair}" "${pos}" -1 folder) add_project(${pkg} ${folder}) # This target should be defined in the liba/CMakeLists.txt, but we can fix it here get_target_property(target_type ${pkg} TYPE) if (NOT target_type STREQUAL "EXECUTABLE") add_library(${pkg}::${pkg} ALIAS ${pkg}) endif() endforeach() """ conanfile = r'''\ import json from conan import Workspace from conan import ConanFile from conan.tools.files import save from conan.tools.cmake import CMakeDeps, CMakeToolchain, cmake_layout class MyWs(ConanFile): """ This is a special conanfile, used only for workspace definition of layout and generators. It shouldn't have requirements, tool_requirements. It shouldn't have build() or package() methods """ settings = "os", "compiler", "build_type", "arch" def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def layout(self): cmake_layout(self) class Ws(Workspace): def root_conanfile(self): return MyWs def build_order(self, order): super().build_order(order) # default behavior prints the build order pkglist = " ".join([f'{it["ref"].name}:{it["folder"]}' for level in order for it in level]) save(self, "build/conanws_build_order.cmake", f"set(CONAN_WS_BUILD_ORDER {pkglist})") ''' workspace_files = {"conanws.yml": conanws_yml, "CMakeLists.txt": cmake, "conanws.py": conanfile, ".gitignore": "build"} # liba files = {f"liba/{k}": v for k, v in cmake_lib_files.items()} workspace_files.update(files) # libb files = NewAPI.render(cmake_lib_files, {"requires": ["liba/0.1"], "name": "libb"}) files = {f"libb/{k}": v for k, v in files.items()} workspace_files.update(files) # app files = NewAPI.render(cmake_exe_files, definitions={"name": "app1", "requires": ["libb/0.1"]}) files = {f"app1/{k}": v for k, v in files.items()} workspace_files.update(files) ================================================ FILE: conan/internal/api/profile/__init__.py ================================================ ================================================ FILE: conan/internal/api/profile/detect.py ================================================ from conan.api.output import ConanOutput from conan.internal.api.detect.detect_api import detect_os, detect_arch, default_msvc_runtime, \ detect_libcxx, detect_cppstd, detect_default_compiler, default_compiler_version def detect_defaults_settings(): """ try to deduce current machine values without any constraints at all :return: A list with default settings """ result = [] the_os = detect_os() result.append(("os", the_os)) arch = detect_arch() if arch: result.append(("arch", arch)) compiler, version, compiler_exe = detect_default_compiler() if not compiler: result.append(("build_type", "Release")) ConanOutput().warning("No compiler was detected (one may not be needed)") return result result.append(("compiler", compiler)) result.append(("compiler.version", default_compiler_version(compiler, version))) runtime, runtime_version = default_msvc_runtime(compiler) if runtime: result.append(("compiler.runtime", runtime)) if runtime_version: result.append(("compiler.runtime_version", runtime_version)) libcxx = detect_libcxx(compiler, version, compiler_exe) if libcxx: result.append(("compiler.libcxx", libcxx)) cppstd = detect_cppstd(compiler, version) if cppstd: result.append(("compiler.cppstd", cppstd)) result.append(("build_type", "Release")) return result ================================================ FILE: conan/internal/api/profile/profile_loader.py ================================================ import os import platform import subprocess from collections import OrderedDict, defaultdict from jinja2 import Environment, FileSystemLoader from conan import conan_version from conan.api.output import ConanOutput from conan.internal.api.detect import detect_api from conan.internal.cache.home_paths import HomePaths from conan.tools.env.environment import ProfileEnvironment from conan.errors import ConanException from conan.internal.model.conf import ConfDefinition, CORE_CONF_PATTERN from conan.internal.model.options import Options from conan.internal.model.profile import Profile from conan.api.model import RecipeReference from conan.internal.util.config_parser import TextINIParse from conan.internal.util.files import mkdir, load_user_encoded def _unquote(text): text = text.strip() if len(text) > 1 and (text[0] == text[-1]) and text[0] in "'\"": return text[1:-1] return text _default_profile_plugin = """\ # This file was generated by Conan. Remove this comment if you edit this file or Conan # will destroy your changes. def profile_plugin(profile): settings = profile.settings if settings.get("compiler") in ("msvc", "clang") and settings.get("compiler.runtime"): if settings.get("compiler.runtime_type") is None: runtime = "Debug" if settings.get("build_type") == "Debug" else "Release" try: settings["compiler.runtime_type"] = runtime except ConanException: pass _check_correct_cppstd(settings) _check_correct_cstd(settings) def _check_correct_cppstd(settings): cppstd = settings.get("compiler.cppstd") version = settings.get("compiler.version") if cppstd and version: compiler = settings.get("compiler") from conan.tools.build.cppstd import supported_cppstd supported = supported_cppstd(None, compiler, version) # supported is None when we don't have information about the compiler # but an empty list when no flags are supported for this version if supported is not None and cppstd not in supported: from conan.errors import ConanException raise ConanException(f"The provided compiler.cppstd={cppstd} is not supported by {compiler} {version}. " f"Supported values are: {supported}") def _check_correct_cstd(settings): cstd = settings.get("compiler.cstd") version = settings.get("compiler.version") if cstd and version: compiler = settings.get("compiler") from conan.tools.build.cstd import supported_cstd supported = supported_cstd(None, compiler, version) # supported is None when we don't have information about the compiler # but an empty list when no flags are supported for this version if supported is not None and cstd not in supported: from conan.errors import ConanException raise ConanException(f"The provided compiler.cstd={cstd} is not supported by {compiler} {version}. " f"Supported values are: {supported}") """ class ProfileLoader: def __init__(self, cache_folder): self._home_paths = HomePaths(cache_folder) def from_cli_args(self, profiles, settings, options, conf, cwd, context=None): """ Return a Profile object, as the result of merging a potentially existing Profile file and the args command-line arguments """ if conf and any(CORE_CONF_PATTERN.match(c) for c in conf): raise ConanException("[conf] 'core.*' configurations are not allowed in profiles.") result = Profile() for p in profiles: tmp = self.load_profile(p, cwd, context) result.compose_profile(tmp) args_profile = _profile_parse_args(settings, options, conf) result.compose_profile(args_profile) return result def load_profile(self, profile_name, cwd=None, context=None): # TODO: This can be made private, only used in testing now cwd = cwd or os.getcwd() profile = self._load_profile(profile_name, cwd, context, root_profile_name=profile_name) return profile def _load_profile(self, profile_name, cwd, context, root_profile_name): """ Will look for "profile_name" in disk if profile_name is absolute path, in current folder if path is relative or in the default folder otherwise. return: a Profile object """ profiles_folder = self._home_paths.profiles_path profile_path = self.get_profile_path(profiles_folder, profile_name, cwd) try: text = load_user_encoded(profile_path) except Exception as e: raise ConanException(f"Cannot load profile:\n{e}") # All profiles will be now rendered with jinja2 as first pass base_path = os.path.dirname(profile_path) file_path = os.path.basename(profile_path) renderc = {"platform": platform, "os": os, "subprocess": subprocess, "profile_dir": base_path, "profile_name": file_path, "root_profile_name": root_profile_name, "conan_version": conan_version, "detect_api": detect_api, "context": context} # Always include the root Conan home "profiles" folder as secondary route for loading # imports and includes from jinja2 templates. loader_paths = [base_path, profiles_folder] try: rtemplate = Environment(loader=FileSystemLoader(loader_paths)).from_string(text) text = rtemplate.render(renderc) except Exception as e: raise ConanException(f"Error while rendering the profile template file '{profile_path}'. " f"Check your Jinja2 syntax: {str(e)}") try: return self._recurse_load_profile(text, profile_path, context, profile_name) except ConanException as exc: raise ConanException("Error reading '%s' profile: %s" % (profile_name, exc)) def _recurse_load_profile(self, text, profile_path, context, root_profile_name): """ Parse and return a Profile object from a text config like representation. cwd is needed to be able to load the includes """ try: inherited_profile = Profile() cwd = os.path.dirname(os.path.abspath(profile_path)) if profile_path else None profile_parser = _ProfileParser(text) # Iterate the includes and call recursive to get the profile and variables # from parent profiles for include in profile_parser.includes: # Recursion !! profile = self._load_profile(include, cwd, context, root_profile_name) inherited_profile.compose_profile(profile) # Current profile before update with parents (but parent variables already applied) inherited_profile = _ProfileValueParser.get_profile(profile_parser.profile_text, inherited_profile) return inherited_profile except ConanException: raise except Exception as exc: raise ConanException("Error parsing the profile text file: %s" % str(exc)) @staticmethod def get_profile_path(profiles_path, profile_name, cwd, exists=True): def valid_path(_profile_path, _profile_name=None): if exists and not os.path.isfile(_profile_path): raise ConanException("Profile not found: {}".format(_profile_name or _profile_path)) return _profile_path if os.path.isabs(profile_name): return valid_path(profile_name) if profile_name[:2] in ("./", ".\\") or profile_name.startswith(".."): # local profile_path = os.path.abspath(os.path.join(cwd, profile_name)) return valid_path(profile_path, profile_name) default_folder = profiles_path if not os.path.exists(default_folder): mkdir(default_folder) profile_path = os.path.join(default_folder, profile_name) if exists: if not os.path.isfile(profile_path): profile_path = os.path.abspath(os.path.join(cwd, profile_name)) if not os.path.isfile(profile_path): raise ConanException("Profile not found: %s" % profile_name) return profile_path # TODO: This class can be removed/simplified now to a function, it reduced to just __init__ class _ProfileParser: def __init__(self, text): """ divides the text in 3 items: - self.includes: List of other profiles to include - self.profile_text: the remaining, containing settings, options, env, etc """ self.includes = [] self.profile_text = "" for counter, line in enumerate(text.splitlines()): line = line.strip() if not line or line.startswith("#"): continue if line.startswith("["): self.profile_text = "\n".join(text.splitlines()[counter:]) break elif line.startswith("include("): include = line.split("include(", 1)[1] if not include.endswith(")"): raise ConanException("Invalid include statement") include = include[:-1] self.includes.append(include) else: raise ConanException("Error while parsing line %i: '%s'" % (counter, line)) class _ProfileValueParser: """ parses a "pure" or "effective" profile, with no includes, no variables, as the one in the lockfiles, or once these things have been processed by ProfileParser """ @staticmethod def get_profile(profile_text, base_profile=None): # Trying to strip comments might be problematic if things contain # doc = TextINIParse(profile_text, allowed_fields=["tool_requires", "system_tools", # DEPRECATED: platform_tool_requires "platform_requires", "platform_tool_requires", "settings", "options", "conf", "buildenv", "runenv", "replace_requires", "replace_tool_requires", "runner"]) # Parse doc sections into Conan model, Settings, Options, etc settings, package_settings = _ProfileValueParser._parse_settings(doc) options = Options.loads(doc.options) if doc.options else None tool_requires = _ProfileValueParser._parse_tool_requires(doc) doc_platform_requires = doc.platform_requires or "" doc_platform_tool_requires = doc.platform_tool_requires or doc.system_tools or "" if doc.system_tools: ConanOutput().warning("Profile [system_tools] is deprecated," " please use [platform_tool_requires]") def parse_replaces(replaces): result = [RecipeReference.loads(r) for r in replaces.splitlines()] errors = [r for r in result if str(r.version).startswith("[")] if errors: raise ConanException("Profile [platform_requires]/[platform_tool_requires] must " f"be exact versions, not version ranges: {errors}") return result platform_tool_requires = parse_replaces(doc_platform_tool_requires) platform_requires = parse_replaces(doc_platform_requires) def load_replace(doc_replace_requires): result = {} for r in doc_replace_requires.splitlines(): r = r.strip() if not r or r.startswith("#"): continue try: src, target = r.split(":") target = RecipeReference.loads(target.strip()) src = RecipeReference.loads(src.strip()) except Exception as e: raise ConanException(f"Error in [replace_xxx] '{r}'.\nIt should be in the form" f" 'pattern: replacement', without package-ids.\n" f"Original error: {str(e)}") result[src] = target return result replace_requires = load_replace(doc.replace_requires) if doc.replace_requires else {} replace_tool = load_replace(doc.replace_tool_requires) if doc.replace_tool_requires else {} if doc.conf: conf = ConfDefinition() conf.loads(doc.conf, profile=True) else: conf = None buildenv = ProfileEnvironment.loads(doc.buildenv) if doc.buildenv else None runenv = ProfileEnvironment.loads(doc.runenv) if doc.runenv else None # Create or update the profile base_profile = base_profile or Profile() base_profile.replace_requires.update(replace_requires) base_profile.replace_tool_requires.update(replace_tool) current_platform_tool_requires = {r.name: r for r in base_profile.platform_tool_requires} current_platform_tool_requires.update({r.name: r for r in platform_tool_requires}) base_profile.platform_tool_requires = list(current_platform_tool_requires.values()) current_platform_requires = {r.name: r for r in base_profile.platform_requires} current_platform_requires.update({r.name: r for r in platform_requires}) base_profile.platform_requires = list(current_platform_requires.values()) base_profile.settings.update(settings) for pkg_name, values_dict in package_settings.items(): existing = base_profile.package_settings[pkg_name] existing.update(values_dict) # Make sure they are ordered, so `compiler=xxx` goes before `compiler.cppstd` always base_profile.package_settings[pkg_name] = OrderedDict(sorted(existing.items())) for pattern, refs in tool_requires.items(): # If the same package, different version is added, the latest version prevail current = base_profile.tool_requires.setdefault(pattern, []) current_dict = {r.name: r for r in current} current_dict.update({r.name: r for r in refs}) current[:] = list(current_dict.values()) if options is not None: base_profile.options.update_options(options) if conf is not None: base_profile.conf.update_conf_definition(conf) if buildenv is not None: base_profile.buildenv.update_profile_env(buildenv) if runenv is not None: base_profile.runenv.update_profile_env(runenv) runner = _ProfileValueParser._parse_key_value(doc.runner) if doc.runner else {} base_profile.runner.update(runner) return base_profile @staticmethod def _parse_key_value(raw_info): result = OrderedDict() for br_line in raw_info.splitlines(): tokens = br_line.split("=", 1) pattern, req_list = tokens result[pattern.strip()] = req_list.strip() return result @staticmethod def _parse_tool_requires(doc): result = OrderedDict() if doc.tool_requires: # FIXME CHECKS OF DUPLICATED? for br_line in doc.tool_requires.splitlines(): tokens = br_line.split(":", 1) if len(tokens) == 1: pattern, req_list = "*", br_line else: pattern, req_list = tokens if "[" in pattern: ConanOutput().warning( f"Tool requires pattern {pattern} contains a version range, which has no effect. " f"Only '&' for consumer and '*' as wildcard are supported in this context.", warn_tag="risk") refs = [RecipeReference.loads(r.strip()) for r in req_list.split(",")] result.setdefault(pattern, []).extend(refs) return result @staticmethod def _parse_settings(doc): def get_package_name_value(item): """Parse items like package:name=value or name=value""" packagename = None if ":" in item: tmp = item.split(":", 1) packagename, item = tmp if "[" in packagename: ConanOutput().warning(f"Settings pattern {packagename} contains a version range, which has no effect. " f"Only '&' for consumer and '*' as wildcard are supported in this context.", warn_tag="risk") result_name, result_value = item.split("=", 1) result_name = result_name.strip() result_value = _unquote(result_value) if result_value == "~": # Can be used to undefine an already defined setting result_value = None return packagename, result_name, result_value package_settings = OrderedDict() settings = OrderedDict() for setting in doc.settings.splitlines(): setting = setting.strip() if not setting or setting.startswith("#"): continue if "=" not in setting: raise ConanException("Invalid setting line '%s'" % setting) package_name, name, value = get_package_name_value(setting) if package_name: package_settings.setdefault(package_name, OrderedDict())[name] = value else: settings[name] = value return settings, package_settings def _profile_parse_args(settings, options, conf): """ return a Profile object result of parsing raw data """ def _get_tuples_list_from_extender_arg(items): if not items: return [] # Validate the pairs for item in items: chunks = item.split("=", 1) if len(chunks) != 2: raise ConanException("Invalid input '%s', use 'name=value'" % item) return [(item[0], item[1]) for item in [item.split("=", 1) for item in items]] def _get_simple_and_package_tuples(items): """Parse items like "thing:item=value or item2=value2 and returns a tuple list for the simple items (name, value) and a dict for the package items {package: [(item, value)...)], ...} """ simple_items = [] package_items = defaultdict(list) tuples = _get_tuples_list_from_extender_arg(items) for name, value in tuples: if value == "~": # Can be used to undefine an already defined setting value = None if ":" in name: # Scoped items tmp = name.split(":", 1) ref_name = tmp[0] name = tmp[1] if "[" in ref_name: ConanOutput().warning( f"Pattern {ref_name} contains a version range, which has no effect. " f"Only '&' for consumer and '*' as wildcard are supported in this context.", warn_tag="risk") package_items[ref_name].append((name, value)) else: simple_items.append((name, value)) return simple_items, package_items settings, package_settings = _get_simple_and_package_tuples(settings) result = Profile() result.options = Options.loads("\n".join(options or [])) result.settings = OrderedDict(settings) if conf: result.conf = ConfDefinition() result.conf.loads("\n".join(conf)) for pkg, values in package_settings.items(): result.package_settings[pkg] = OrderedDict(values) return result def migrate_profile_plugin(cache_folder): from conan.internal.api.migrations import update_file profile_plugin_file = HomePaths(cache_folder).profile_plugin_path update_file(profile_plugin_file, _default_profile_plugin) ================================================ FILE: conan/internal/api/remotes/__init__.py ================================================ ================================================ FILE: conan/internal/api/remotes/encrypt.py ================================================ # WARNING: These functions implements a Vigenere cypher, they are NO WAY OK FOR SECURITY! CHARS = [c for c in (chr(i) for i in range(32, 127))] def _ascii_key(key): key = "".join([it for it in key if it in CHARS]) assert len(key), "Provide a key containing ASCII characters" return key def encode(text, key): assert isinstance(text, str), "Expected string type, got '{}'".format(type(text)) assert isinstance(key, str), "Expected 'str' type, got '{}'".format(type(key)) key = _ascii_key(key) res = "" for i, c in enumerate(text): if c not in CHARS: res += c else: text_index = CHARS.index(c) key_index = CHARS.index(key[i % len(key)]) res += CHARS[(text_index + key_index) % len(CHARS)] return res def decode(text, key): assert isinstance(text, str), "Expected 'bytes', got '{}'".format(type(text)) assert isinstance(key, str), "Expected 'str' type, got '{}'".format(type(key)) key = _ascii_key(key) res = "" for i, c in enumerate(text): if c not in CHARS: res += c else: text_index = CHARS.index(c) key_index = CHARS.index(key[i % len(key)]) res += CHARS[(text_index - key_index) % len(CHARS)] return res ================================================ FILE: conan/internal/api/remotes/localdb.py ================================================ import os import sqlite3 from contextlib import contextmanager from sqlite3 import OperationalError from conan.errors import ConanException from conan.internal.api.remotes import encrypt REMOTES_USER_TABLE = "users_remotes" LOCALDB = ".conan.db" _localdb_encryption_key = os.environ.pop('CONAN_LOGIN_ENCRYPTION_KEY', None) class LocalDB: def __init__(self, dbfolder): self.dbfile = os.path.join(dbfolder, LOCALDB) self.encryption_key = _localdb_encryption_key # Create the database file if it doesn't exist if not os.path.exists(self.dbfile): par = os.path.dirname(self.dbfile) os.makedirs(par, exist_ok=True) open(self.dbfile, 'w').close() with self._connect() as connection: try: cursor = connection.cursor() cursor.execute("create table if not exists %s " "(remote_url TEXT UNIQUE, user TEXT, " "token TEXT, refresh_token TEXT)" % REMOTES_USER_TABLE) except Exception as e: message = f"Could not initialize local sqlite database {self.dbfile}" raise ConanException(message, e) def _encode(self, value): if value and self.encryption_key: return encrypt.encode(value, self.encryption_key) return value def _decode(self, value): if value and self.encryption_key: return encrypt.decode(value, self.encryption_key) return value def clean(self, remote_url=None): with self._connect() as connection: try: cursor = connection.cursor() query = "DELETE FROM %s" % REMOTES_USER_TABLE if remote_url: query += " WHERE remote_url='{}'".format(remote_url) cursor.execute(query) try: # https://github.com/ghaering/pysqlite/issues/109 connection.isolation_level = None cursor.execute('VACUUM') # Make sure the DB is cleaned, drop doesn't do that except OperationalError: pass except Exception as e: raise ConanException("Could not initialize local sqlite database", e) @contextmanager def _connect(self): connection = sqlite3.connect(self.dbfile, detect_types=sqlite3.PARSE_DECLTYPES) connection.text_factory = str try: yield connection finally: connection.close() def get_login(self, remote_url): """ Returns login credentials. This method is also in charge of expiring them. """ with self._connect() as connection: try: statement = connection.cursor() statement.execute("select user, token, refresh_token from %s where remote_url='%s'" % (REMOTES_USER_TABLE, remote_url)) rs = statement.fetchone() if not rs: return None, None, None name = rs[0] token = self._decode(rs[1]) refresh_token = self._decode(rs[2]) return name, token, refresh_token except Exception: raise ConanException("Couldn't read login\n Try removing '%s' file" % self.dbfile) def get_username(self, remote_url): return self.get_login(remote_url)[0] def store(self, user, token, refresh_token, remote_url): """ Login is a tuple of (user, token) """ with self._connect() as connection: try: token = self._encode(token) refresh_token = self._encode(refresh_token) statement = connection.cursor() statement.execute("INSERT OR REPLACE INTO %s (remote_url, user, token, " "refresh_token) " "VALUES (?, ?, ?, ?)" % REMOTES_USER_TABLE, (remote_url, user, token, refresh_token)) connection.commit() except Exception as e: raise ConanException("Could not store credentials %s" % str(e)) ================================================ FILE: conan/internal/api/upload.py ================================================ from conan.internal.rest.client_routes import ClientV2Router from conan.internal.util.files import sha1sum def add_urls(package_list, remote): router = ClientV2Router(remote.url.rstrip("/")) for ref, packages in package_list.items(): ref_info = package_list.recipe_dict(ref) for f, fp in ref_info.get("files", {}).items(): ref_info.setdefault("upload-urls", {})[f] = { 'url': router.recipe_file(ref, f), 'checksum': sha1sum(fp) } for pref in packages: pref_info = package_list.package_dict(pref) for f, fp in pref_info.get("files", {}).items(): pref_info.setdefault("upload-urls", {})[f] = { 'url': router.package_file(pref, f), 'checksum': sha1sum(fp) } ================================================ FILE: conan/internal/api/uploader.py ================================================ import fnmatch import gzip import os import shutil import sys import tarfile import time from conan.internal.conan_app import ConanApp from conan.api.output import ConanOutput from conan.internal.source import retrieve_exports_sources from conan.internal.errors import NotFoundException from conan.errors import ConanException from conan.internal.paths import CONAN_MANIFEST, CONANFILE, CONANINFO, COMPRESSIONS, \ EXPORT_SOURCES_FILE_NAME, EXPORT_FILE_NAME, PACKAGE_FILE_NAME from conan.internal.util.files import (clean_dirty, is_dirty, gather_files, set_dirty_context_manager, mkdir, human_size) UPLOAD_POLICY_FORCE = "force-upload" UPLOAD_POLICY_SKIP = "skip-upload" class UploadUpstreamChecker: """ decides if something needs to be uploaded or force-uploaded checking if that exact revision already exists in the remote server, or if the --force parameter is forcing the upload This is completely irrespective of the actual package contents, it only uses the local computed revision and the remote one """ def __init__(self, remote_manager): self._remote_manager = remote_manager def check(self, package_list, remote, force): for ref, packages in package_list.items(): recipe_info = package_list.recipe_dict(ref) self._check_upstream_recipe(ref, recipe_info, remote, force) for pref in packages: pkg_dict = package_list.package_dict(pref) self._check_upstream_package(pref, pkg_dict, remote, force) def _check_upstream_recipe(self, ref, ref_bundle, remote, force): output = ConanOutput(scope=str(ref)) output.info("Checking which revisions exist in the remote server") try: assert ref.revision # TODO: It is a bit ugly, interface-wise to ask for revisions to check existence server_ref = self._remote_manager.get_recipe_revision(ref, remote) assert server_ref # If successful (not raising NotFoundException), this will exist except NotFoundException: ref_bundle["force_upload"] = False ref_bundle["upload"] = True else: if force: output.info(f"Recipe '{ref.repr_notime()}' already in server, forcing upload") ref_bundle["force_upload"] = True ref_bundle["upload"] = True else: output.info(f"Recipe '{ref.repr_notime()}' already in server, skipping upload") ref_bundle["upload"] = False ref_bundle["force_upload"] = False def _check_upstream_package(self, pref, prev_bundle, remote, force): assert (pref.revision is not None), "Cannot upload a package without PREV" assert (pref.ref.revision is not None), "Cannot upload a package without RREV" try: # TODO: It is a bit ugly, interface-wise to ask for revisions to check existence server_revisions = self._remote_manager.get_package_revision(pref, remote) assert server_revisions except NotFoundException: prev_bundle["force_upload"] = False prev_bundle["upload"] = True else: output = ConanOutput(scope=str(pref.ref)) if force: output.info(f"Package '{pref.repr_notime()}' already in server, forcing upload") prev_bundle["force_upload"] = True prev_bundle["upload"] = True else: output.info(f"Package '{pref.repr_notime()}' already in server, skipping upload") prev_bundle["force_upload"] = False prev_bundle["upload"] = False def get_compress_level(compressformat, global_conf): if compressformat == "xz": msg = ("The 'xz' compression is experimental. " "Consumers using older Conan versions will not be able to install these packages. " "Feedback is welcome, please report any issues as GitHub tickets.") ConanOutput().warning(msg, warn_tag="experimental") elif compressformat == "zst": msg = ("The 'zst' compression is experimental. " "Consumers installing packages created with this format must use Python >= 3.14. " "Consumers using older Conan or Python versions will not be able to install these " "packages. Feedback is welcome, please report any issues as GitHub tickets.") ConanOutput().warning(msg, warn_tag="experimental") if compressformat == "zst" and sys.version_info.minor < 14: raise ConanException("The 'core.upload:compression_format=zst' is only for Python>=3.14") compresslevel = global_conf.get("core:compresslevel", check_type=int) if compresslevel is None and compressformat == "gz": compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int) # do not deprecate yet core.gzip:compresslevel, wait a bit to stabilize core:compresslevel return compresslevel class PackagePreparator: def __init__(self, app: ConanApp, cache, remote_manager, global_conf): self._app = app self._remote_manager = remote_manager self._cache = cache self._global_conf = global_conf compressformat = self._global_conf.get("core.upload:compression_format", default="gz", choices=COMPRESSIONS) compresslevel = get_compress_level(compressformat, global_conf) self._compressformat = compressformat self._compresslevel = compresslevel def prepare(self, pkg_list, enabled_remotes, metadata, force=False): local_url = self._global_conf.get("core.scm:local_url", choices=["allow", "block"]) for ref, packages in pkg_list.items(): recipe_layout = self._cache.recipe_layout(ref) conanfile_path = recipe_layout.conanfile() conanfile = self._app.loader.load_basic(conanfile_path) url = conanfile.conan_data.get("scm", {}).get("url") if conanfile.conan_data else None if local_url != "allow" and url is not None: if not any(url.startswith(v) for v in ("ssh", "git", "http", "file")): raise ConanException(f"Package {ref} contains conandata scm url={url}\n" "This isn't a remote URL, the build won't be reproducible\n" "Failing because conf 'core.scm:local_url!=allow'") # Just in case it was defined from a previous run bundle = pkg_list.recipe_dict(ref) bundle.pop("files", None) bundle.pop("upload-urls", None) if bundle.get("upload") or force: self._prepare_recipe(recipe_layout, ref, bundle, conanfile, enabled_remotes) # Package metadata files too if metadata != [""] and (metadata or bundle.get("upload")): metadata_folder = recipe_layout.metadata() files = _metadata_files(metadata_folder, metadata) if files: ConanOutput(scope=str(ref)).info(f"Recipe metadata: {len(files)} files") bundle.setdefault("files", {}).update(files) bundle["upload"] = True for pref in packages: prev_bundle = pkg_list.package_dict(pref) prev_bundle.pop("files", None) # If defined from a previous upload prev_bundle.pop("upload-urls", None) self._prepare_package(pref, prev_bundle, metadata, force=force) def _prepare_recipe(self, recipe_layout, ref, ref_bundle, conanfile, remotes): """ do a bunch of things that are necessary before actually executing the upload: - retrieve exports_sources to complete the recipe if necessary - compress the artifacts in conan_export.tgz and conan_export_sources.tgz """ try: retrieve_exports_sources(self._remote_manager, recipe_layout, conanfile, ref, remotes) cache_files = self._compress_recipe_files(recipe_layout, ref) ref_bundle["files"] = cache_files except Exception as e: raise ConanException(f"{ref} Error while compressing: {e}") def _compress_recipe_files(self, layout, ref): download_export_folder = layout.download_export() export_folder = layout.export() files, symlinked_folders = gather_files(export_folder) files.update(symlinked_folders) if CONANFILE not in files or CONAN_MANIFEST not in files: raise ConanException("Cannot upload corrupted recipe '%s'" % str(ref)) export_src_folder = layout.export_sources() src_files, src_symlinked_folders = gather_files(export_src_folder) src_files.update(src_symlinked_folders) # We do a copy of conanfile and conanmanifest to the download_export_folder # so it is identical as when it is downloaded, and all files are from the same location # to be uploaded mkdir(download_export_folder) shutil.copy2(os.path.join(export_folder, CONANFILE), os.path.join(download_export_folder, CONANFILE)) shutil.copy2(os.path.join(export_folder, CONAN_MANIFEST), os.path.join(download_export_folder, CONAN_MANIFEST)) result = {CONANFILE: os.path.join(download_export_folder, CONANFILE), CONAN_MANIFEST: os.path.join(download_export_folder, CONAN_MANIFEST)} # Files NOT included in the tgz files.pop(CONANFILE) files.pop(CONAN_MANIFEST) if files: comp = self._compressed_file(EXPORT_FILE_NAME, files, download_export_folder, ref) result[comp] = os.path.join(download_export_folder, comp) if src_files: comp = self._compressed_file(EXPORT_SOURCES_FILE_NAME, src_files, download_export_folder, ref) result[comp] = os.path.join(download_export_folder, comp) return result def _prepare_package(self, pref, prev_bundle, metadata, force=False): pkg_layout = None if prev_bundle.get("upload") or force: pkg_layout = self._cache.pkg_layout(pref) if pkg_layout.package_is_dirty(): raise ConanException(f"Package {pref} is corrupted, aborting upload.\n" f"Remove it with 'conan remove {pref}'") cache_files = self._compress_package_files(pkg_layout, pref) prev_bundle["files"] = cache_files # Package metadata files too if metadata != [""] and (metadata or prev_bundle.get("upload")): pkg_layout = pkg_layout or self._cache.pkg_layout(pref) metadata_folder = pkg_layout.metadata() files = _metadata_files(metadata_folder, metadata) if files: ConanOutput(scope=str(pref)).info(f"Package metadata: {len(files)} files") prev_bundle.setdefault("files", {}).update(files) prev_bundle["upload"] = True def _compressed_file(self, filename, files, download_folder, ref): output = ConanOutput(scope=str(ref)) # Check if there is some existing compressed file already matches = [] for extension in COMPRESSIONS: file_name = filename + extension package_file = os.path.join(download_folder, file_name) if is_dirty(package_file): output.warning(f"Removing {file_name}, marked as dirty") os.remove(package_file) clean_dirty(package_file) if os.path.isfile(package_file): matches.append(file_name) if len(matches) > 1: raise ConanException(f"{ref}: Multiple package files found for {filename}: {matches}") if len(matches) == 1: existing = matches[0] if not existing.endswith(self._compressformat): output.info(f"Existing {existing} compressed file, " f"keeping it, not using '{self._compressformat}' format") return existing file_name = filename + self._compressformat package_file = os.path.join(download_folder, file_name) compressed_path = compress_files(files, file_name, download_folder, compresslevel=self._compresslevel, scope=str(ref)) assert compressed_path == package_file assert os.path.exists(package_file) return file_name def _compress_package_files(self, layout, pref): download_pkg_folder = layout.download_package() # Get all the files in that directory # existing package package_folder = layout.package() files, symlinked_folders = gather_files(package_folder) files.update(symlinked_folders) if CONANINFO not in files or CONAN_MANIFEST not in files: raise ConanException("Cannot upload corrupted package '%s'" % str(pref)) # Do a copy so the location of CONANINFO and MANIFEST is the "download" folder one mkdir(download_pkg_folder) shutil.copy2(os.path.join(package_folder, CONANINFO), os.path.join(download_pkg_folder, CONANINFO)) shutil.copy2(os.path.join(package_folder, CONAN_MANIFEST), os.path.join(download_pkg_folder, CONAN_MANIFEST)) # Files NOT included in the tgz files.pop(CONANINFO) files.pop(CONAN_MANIFEST) compressed_file = self._compressed_file(PACKAGE_FILE_NAME, files, download_pkg_folder, pref) return {compressed_file: os.path.join(download_pkg_folder, compressed_file), CONANINFO: os.path.join(download_pkg_folder, CONANINFO), CONAN_MANIFEST: os.path.join(download_pkg_folder, CONAN_MANIFEST)} class UploadExecutor: """ does the actual file transfer to the remote. The files to be uploaded have already been computed and are passed in the ``upload_data`` parameter, so this executor is also agnostic about which files are transferred """ def __init__(self, remote_manager): self._remote_manager = remote_manager def upload(self, upload_data, remote): for ref, packages in upload_data.items(): bundle = upload_data.recipe_dict(ref) if bundle.get("upload"): self.upload_recipe(ref, bundle, remote) for pref in packages: prev_bundle = upload_data.package_dict(pref) if prev_bundle.get("upload"): self.upload_package(pref, prev_bundle, remote) def upload_recipe(self, ref, bundle, remote): output = ConanOutput(scope=str(ref)) cache_files = bundle["files"] output.info(f"Uploading recipe '{ref.repr_notime()}' ({_total_size(cache_files)})") t1 = time.time() self._remote_manager.upload_recipe(ref, cache_files, remote) duration = time.time() - t1 output.debug(f"Upload {ref} in {duration} time") return ref def upload_package(self, pref, prev_bundle, remote): output = ConanOutput(scope=str(pref.ref)) cache_files = prev_bundle["files"] assert (pref.revision is not None), "Cannot upload a package without PREV" assert (pref.ref.revision is not None), "Cannot upload a package without RREV" output.info(f"Uploading package '{pref.repr_notime()}' ({_total_size(cache_files)})") t1 = time.time() self._remote_manager.upload_package(pref, cache_files, remote) duration = time.time() - t1 output.debug(f"Upload {pref} in {duration} time") def gzopen_without_timestamps(name, fileobj, compresslevel=None): """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was setted in Gzip file causing md5 to change. Not possible using the previous tarfile open because arguments are not passed to GzipFile constructor """ compresslevel = compresslevel if compresslevel is not None else 9 # default Gzip = 9 fileobj = gzip.GzipFile(name, "w", compresslevel, fileobj, mtime=0) # Format is forced because in Python3.8, it changed and it generates different tarfiles # with different checksums, which break hashes of tgzs # PAX_FORMAT is the default for Py38, lets make it explicit for older Python versions t = tarfile.TarFile.taropen(name, "w", fileobj, format=tarfile.PAX_FORMAT) t._extfileobj = False return t def compress_files(files, name, dest_dir, compresslevel=None, scope=None, recursive=False): t1 = time.time() tgz_path = os.path.join(dest_dir, name) out = ConanOutput(scope=scope) out.info(f"Compressing {name}") if name.endswith("zst"): with tarfile.open(tgz_path, "w:zst", level=compresslevel) as tar: # noqa Py314 only for filename, abs_path in sorted(files.items()): tar.add(abs_path, filename, recursive=recursive) out.debug(f"{name} compressed in {time.time() - t1} time") return tgz_path if name.endswith("xz"): # The default to PAX_FORMAT in case of Python 3.7 with tarfile.open(tgz_path, "w:xz", preset=compresslevel, format=tarfile.PAX_FORMAT) as tar: for filename, abs_path in sorted(files.items()): tar.add(abs_path, filename, recursive=recursive) out.debug(f"{name} compressed in {time.time() - t1} time") return tgz_path with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) for filename, abs_path in sorted(files.items()): # recursive is False by default in case it is a symlink to a folder tgz.add(abs_path, filename, recursive=recursive) tgz.close() out.debug(f"{name} compressed in {time.time() - t1} time") return tgz_path def _total_size(cache_files): total_size = 0 for file in cache_files.values(): stat = os.stat(file) total_size += stat.st_size return human_size(total_size) def _metadata_files(folder, metadata): result = {} for root, _, files in os.walk(folder): for f in files: abs_path = os.path.join(root, f) relpath = os.path.relpath(abs_path, folder) if metadata: if not any(fnmatch.fnmatch(relpath, m) for m in metadata): continue path = os.path.join("metadata", relpath).replace("\\", "/") result[path] = abs_path return result ================================================ FILE: conan/internal/cache/__init__.py ================================================ ================================================ FILE: conan/internal/cache/cache.py ================================================ import hashlib import os import re import shutil import uuid from fnmatch import translate from typing import List from conan.internal.cache.conan_reference_layout import RecipeLayout, PackageLayout # TODO: Random folders are no longer accessible, how to get rid of them asap? # TODO: We need the workflow to remove existing references. from conan.internal.cache.db.cache_database import CacheDatabase from conan.internal.errors import ConanReferenceAlreadyExistsInDB from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import rmdir, renamedir, mkdir class PkgCache: """ Class to represent the recipes and packages storage in disk """ def __init__(self, cache_folder, global_conf): # paths self._store_folder = global_conf.get("core.cache:storage_path") or \ os.path.join(cache_folder, "p") try: mkdir(self._store_folder) db_filename = os.path.join(self._store_folder, 'cache.sqlite3') self._base_folder = os.path.abspath(self._store_folder) self._db = CacheDatabase(filename=db_filename) except Exception as e: raise ConanException(f"Couldn't initialize storage in {self._store_folder}: {e}") @property def store(self): return self._base_folder @property def temp_folder(self): """ temporary folder where Conan puts exports and packages before the final revision is computed""" # TODO: Improve the path definitions, this is very hardcoded return os.path.join(self._base_folder, "t") @property def builds_folder(self): return os.path.join(self._base_folder, "b") def _create_path(self, relative_path, remove_contents=True): path = self._full_path(relative_path) if os.path.exists(path) and remove_contents: rmdir(path) os.makedirs(path, exist_ok=True) def _full_path(self, relative_path): # This one is used only for rmdir and mkdir operations, not returned to user # or stored in DB path = os.path.realpath(os.path.join(self._base_folder, relative_path)) return path @staticmethod def _short_hash_path(h): """:param h: Unicode text to reduce""" h = h.encode("utf-8") md = hashlib.sha256() md.update(h) sha_bytes = md.hexdigest() # len based on: https://github.com/conan-io/conan/pull/9595#issuecomment-918976451 # Reduce length in 3 characters 16 - 3 = 13 return sha_bytes[0:13] @staticmethod def _get_path(ref): return ref.name[:5] + PkgCache._short_hash_path(ref.repr_notime()) @staticmethod def _get_path_pref(pref): return pref.ref.name[:5] + PkgCache._short_hash_path(pref.repr_notime()) def create_export_recipe_layout(self, ref: RecipeReference): """ This is a temporary layout while exporting a new recipe, because the revision is not computed until later. The entry is not added to DB, just a temp folder is created This temporary export folder will be moved to permanent when revision is computed by the assign_rrev() method """ assert ref.revision is None, "Recipe revision should be None" assert ref.timestamp is None h = ref.name[:5] + PkgCache._short_hash_path(ref.repr_notime()) reference_path = os.path.join("t", h) self._create_path(reference_path) return RecipeLayout(ref, os.path.join(self._base_folder, reference_path)) def create_build_pkg_layout(self, pref: PkgReference): # Temporary layout to build a new package, when we don't know the package revision yet assert pref.ref.revision, "Recipe revision must be known to get or create the package layout" assert pref.package_id, "Package id must be known to get or create the package layout" assert pref.revision is None, "Package revision should be None" assert pref.timestamp is None random_id = str(uuid.uuid4()) h = pref.ref.name[:5] + PkgCache._short_hash_path(pref.repr_notime() + random_id) package_path = os.path.join("b", h) self._create_path(package_path) return PackageLayout(pref, os.path.join(self._base_folder, package_path)) def recipe_layout(self, ref: RecipeReference): """ the revision must exists, the folder must exist The regular graph building will use this method if the revision is defined, like when using lockfiles or explicit, or recipe_layout_latest() if not, to do one single DB query """ assert ref.revision is not None ref_data = self._db.get_recipe(ref) ref_path = ref_data.get("path") ref = ref_data.get("ref") # new revision with timestamp return RecipeLayout(ref, os.path.join(self._base_folder, ref_path)) def recipe_layout_latest(self, ref: RecipeReference): """ the revision must be None, the folder must exist This method was added so the ConanProxy used to resolve the dependency graph avoid doing 2 DB calls when the revision is not defined """ assert ref.revision is None ref_data = self._db.get_latest_recipe(ref) ref_path = ref_data.get("path") ref = ref_data.get("ref") # new revision with timestamp return RecipeLayout(ref, os.path.join(self._base_folder, ref_path)) def get_latest_recipe_revision(self, ref: RecipeReference) -> RecipeReference: assert ref.revision is None ref_data = self._db.get_latest_recipe(ref) return ref_data.get("ref") def get_recipe_revisions(self, ref: RecipeReference): # For listing multiple revisions only assert ref.revision is None return self._db.get_recipe_revisions_references(ref) def pkg_layout(self, pref: PkgReference): """ the revision must exists, the folder must exist No longer used by GraphBinariesAnalyzer """ assert pref.ref.revision, "Recipe revision must be known to get the package layout" assert pref.package_id, "Package id must be known to get the package layout" assert pref.revision, "Package revision must be known to get the package layout" pref_data = self._db.try_get_package(pref) pref_path = pref_data.get("path") # we use abspath to convert cache forward slash in Windows to backslash return PackageLayout(pref, os.path.abspath(os.path.join(self._base_folder, pref_path))) def pkg_layout_latest(self, pref: PkgReference): """ GraphBinariesAnalyzer will call this method to avoid doing 2 DB calls, previously it was using pkg_layout() after a get_latest_package_revision() """ assert pref.ref.revision, "Recipe revision must be known to get the package layout" assert pref.package_id, "Package id must be known to get the package layout" assert pref.revision is None pref_data = self._db.get_latest_package_reference_data(pref) if pref_data is None: return None pref_path = pref_data.get("path") pref = pref_data.get("pref") # new revision with timestamp # we use abspath to convert cache forward slash in Windows to backslash return PackageLayout(pref, os.path.abspath(os.path.join(self._base_folder, pref_path))) def create_ref_layout(self, ref: RecipeReference): """ called exclusively by: - RemoteManager.get_recipe() - cache restore """ assert ref.revision, "Recipe revision must be known to create the package layout" reference_path = self._get_path(ref) self._db.create_recipe(reference_path, ref) self._create_path(reference_path, remove_contents=False) return RecipeLayout(ref, os.path.join(self._base_folder, reference_path)) def create_pkg_layout(self, pref: PkgReference): """ called by: - RemoteManager.get_package() - cache restore """ assert pref.ref.revision, "Recipe revision must be known to create the package layout" assert pref.package_id, "Package id must be known to create the package layout" assert pref.revision, "Package revision should be known to create the package layout" package_path = self._get_path_pref(pref) self._db.create_package(package_path, pref, None) self._create_path(package_path, remove_contents=False) return PackageLayout(pref, os.path.join(self._base_folder, package_path)) def update_recipe_timestamp(self, ref: RecipeReference): """ when the recipe already exists in cache, but we get a new timestamp from a server that would affect its order in our cache """ assert ref.revision assert ref.timestamp self._db.update_recipe_timestamp(ref) def search_recipes(self, pattern=None, ignorecase=True): # Conan references in main storage if pattern: if isinstance(pattern, RecipeReference): pattern = repr(pattern) pattern = translate(pattern) pattern = re.compile(pattern, re.IGNORECASE if ignorecase else 0) return self._db.list_references(pattern) def exists_prev(self, pref): # Used just by download to skip downloads if prev already exists in cache return self._db.exists_prev(pref) def get_latest_package_revision(self, pref: PkgReference) -> PkgReference: # This is no longer needed by the Graph resolution functionality, only by ListAPI # its usage in graph resolution has been replaced by a single call to pkg_layout_latest() return self._db.get_latest_package_reference(pref) def get_package_references(self, ref: RecipeReference, only_latest_prev=True) -> List[PkgReference]: """Get the latest package references""" return self._db.get_package_references(ref, only_latest_prev) def get_package_revisions(self, pref: PkgReference) -> List[PkgReference]: return self._db.get_package_revisions_references(pref) def get_matching_build_id(self, ref, build_id): return self._db.get_matching_build_id(ref, build_id) def remove_recipe_layout(self, layout: RecipeLayout): layout.remove() # FIXME: This is clearing package binaries from DB, but not from disk/layout self._db.remove_recipe(layout.reference) def remove_package_layout(self, layout: PackageLayout): layout.remove() self._db.remove_package(layout.reference) def remove_build_id(self, pref): self._db.remove_build_id(pref) def assign_prev(self, layout: PackageLayout): pref = layout.reference build_id = layout.build_id pref.timestamp = revision_timestamp_now() # Wait until it finish to really update the DB relpath = os.path.relpath(layout.base_folder, self._base_folder) relpath = relpath.replace("\\", "/") # Uniform for Windows and Linux try: self._db.create_package(relpath, pref, build_id) except ConanReferenceAlreadyExistsInDB: # TODO: Optimize this into 1 single UPSERT operation # There was a previous package folder for this same package reference (and prev) pkg_layout = self.pkg_layout(pref) # We remove the old one and move the new one to the path of the previous one # this can be necessary in case of new metadata or build-folder because of "build_id()" pkg_layout.remove() shutil.move(layout.base_folder, pkg_layout.base_folder) # clean unused temporary build layout._base_folder = pkg_layout.base_folder # reuse existing one # TODO: The relpath would be the same as the previous one, it shouldn't be ncessary to # update it, the update_package_timestamp() can be simplified and path dropped relpath = os.path.relpath(layout.base_folder, self._base_folder) self._db.update_package_timestamp(pref, path=relpath, build_id=build_id) def assign_rrev(self, layout: RecipeLayout): """ called at export, once the exported recipe revision has been computed, it can register for the first time the new RecipeReference""" ref = layout.reference assert ref.revision is not None, "Revision must exist after export" assert ref.timestamp is None, "Timestamp no defined yet" ref.timestamp = revision_timestamp_now() # TODO: here maybe we should block the recipe and all the packages too # This is the destination path for the temporary created export and export_sources folders # with the hash created based on the recipe revision new_path_relative = self._get_path(ref) new_path_absolute = self._full_path(new_path_relative) if os.path.exists(new_path_absolute): # If there source folder exists, export and export_sources # folders are already copied so we can remove the tmp ones rmdir(self._full_path(layout.base_folder)) else: # Destination folder is empty, move all the tmp contents renamedir(self._full_path(layout.base_folder), new_path_absolute) layout._base_folder = os.path.join(self._base_folder, new_path_relative) # Wait until it finish to really update the DB try: self._db.create_recipe(new_path_relative, ref) except ConanReferenceAlreadyExistsInDB: # This was exported before, making it latest again, update timestamp ref = layout.reference self._db.update_recipe_timestamp(ref) def get_recipe_lru(self, ref): path = self._full_path(self._get_path(ref)) return os.path.getmtime(path) # seconds since EPOCH def update_recipes_lru(self, refs): for r in refs: path = self._full_path(self._get_path(r)) os.utime(path, None) def get_package_lru(self, pref): layout = self.pkg_layout(pref) # It can be a build folder too return os.path.getmtime(layout.base_folder) # seconds since EPOCH def path_to_ref(self, path): try: path = os.path.relpath(path, self._base_folder) path = path.replace("\\", "/") # Uniform for Windows and Linux except ValueError: raise ConanException(f"Invalid path: {path}") return self._db.path_to_ref(path) ================================================ FILE: conan/internal/cache/conan_reference_layout.py ================================================ import os from contextlib import contextmanager from conan.internal.model.manifest import FileTreeManifest from conan.internal.paths import CONANFILE, DATA_YML from conan.internal.util.files import set_dirty, clean_dirty, is_dirty, rmdir # To be able to change them later to something shorter SRC_FOLDER = "s" BUILD_FOLDER = "b" PACKAGES_FOLDER = "p" FINALIZE_FOLDER = "f" EXPORT_FOLDER = "e" EXPORT_SRC_FOLDER = "es" DOWNLOAD_EXPORT_FOLDER = "d" METADATA = "metadata" class LayoutBase: def __init__(self, ref, base_folder): self.reference = ref self._base_folder = base_folder @property def base_folder(self): return self._base_folder def remove(self): rmdir(self._base_folder) class BasicLayout(LayoutBase): # For editables and platform_requires def conanfile(self): # the full conanfile path (including other other.py names) for editables # None for platform_requires return self._base_folder def metadata(self): if self._base_folder is None: return return os.path.join(os.path.dirname(self._base_folder), METADATA) class RecipeLayout(LayoutBase): @contextmanager def conanfile_write_lock(self, output): yield def export(self): return os.path.join(self._base_folder, EXPORT_FOLDER) def export_sources(self): return os.path.join(self._base_folder, EXPORT_SRC_FOLDER) def metadata(self): return os.path.join(self.download_export(), METADATA) def download_export(self): return os.path.join(self._base_folder, DOWNLOAD_EXPORT_FOLDER) def source(self): return os.path.join(self._base_folder, SRC_FOLDER) def conanfile(self): return os.path.join(self.export(), CONANFILE) def conandata(self): return os.path.join(self.export(), DATA_YML) def recipe_manifests(self): # Used for comparison and integrity check export_folder = self.export() readed_manifest = FileTreeManifest.load(export_folder) exports_source_folder = self.export_sources() expected_manifest = FileTreeManifest.create(export_folder, exports_source_folder) return readed_manifest, expected_manifest def sources_remove(self): src_folder = self.source() rmdir(src_folder) class PackageLayout(LayoutBase): def __init__(self, ref, base_folder): super().__init__(ref, base_folder) self.build_id = None # TODO: cache2.0 locks implementation @contextmanager def package_lock(self): yield def build(self): return os.path.join(self._base_folder, BUILD_FOLDER) def package(self): return os.path.join(self._base_folder, PACKAGES_FOLDER) def finalize(self): return os.path.join(self._base_folder, FINALIZE_FOLDER) def download_package(self): return os.path.join(self._base_folder, DOWNLOAD_EXPORT_FOLDER) def metadata(self): return os.path.join(self.download_package(), METADATA) def package_manifests(self): package_folder = self.package() readed_manifest = FileTreeManifest.load(package_folder) expected_manifest = FileTreeManifest.create(package_folder) return readed_manifest, expected_manifest @contextmanager def set_dirty_context_manager(self): set_dirty(self.package()) yield clean_dirty(self.package()) # TODO: cache2.0 check this def package_is_dirty(self): return is_dirty(self.package()) def build_remove(self): rmdir(self.build()) # TODO: cache2.0 locks def package_remove(self): # Here we could validate and check we own a write lock over this package tgz_folder = self.download_package() rmdir(tgz_folder) rmdir(self.package()) if is_dirty(self.package()): clean_dirty(self.package()) ================================================ FILE: conan/internal/cache/db/__init__.py ================================================ ================================================ FILE: conan/internal/cache/db/cache_database.py ================================================ import os import sqlite3 from conan.api.output import ConanOutput from conan.internal.cache.db.packages_table import PackagesDBTable from conan.internal.cache.db.recipes_table import RecipesDBTable from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.model.version import Version class CacheDatabase: def __init__(self, filename): version = sqlite3.sqlite_version if Version(version) < "3.7.11": ConanOutput().error(f"Your sqlite3 '{version} < 3.7.11' version is not supported") self._recipes = RecipesDBTable(filename) self._packages = PackagesDBTable(filename) if not os.path.isfile(filename): self._recipes.create_table() self._packages.create_table() def exists_prev(self, ref): return self._packages.get_package_revisions_reference_exists(ref) def get_latest_package_reference(self, pref): prevs = list(self._packages.get_package_revisions_references(pref, only_latest_prev=True)) return prevs[0]["pref"] if prevs else None def get_latest_package_reference_data(self, pref): # Used just for PkgCache.pkg_layout_latest() # TODO: This can be refactored, unified with get_latest_package_reference() prevs = list(self._packages.get_package_revisions_references(pref, only_latest_prev=True)) return prevs[0] if prevs else None def update_recipe_timestamp(self, ref): self._recipes.update_timestamp(ref) def update_package_timestamp(self, pref: PkgReference, path: str, build_id: str): self._packages.update_timestamp(pref, path=path, build_id=build_id) def remove_recipe(self, ref: RecipeReference): # Removing the recipe must remove all the package binaries too from DB self._recipes.remove(ref) self._packages.remove_recipe(ref) def remove_package(self, ref: PkgReference): # Removing the recipe must remove all the package binaries too from DB self._packages.remove(ref) def remove_build_id(self, pref): self._packages.remove_build_id(pref) def get_matching_build_id(self, ref, build_id): result = self._packages.get_package_references_with_build_id_match(ref, build_id) if result: return result["pref"] return None def get_recipe(self, ref: RecipeReference): """ Returns the reference data as a dictionary (or fails) """ return self._recipes.get_recipe(ref) def get_latest_recipe(self, ref: RecipeReference): """ Returns the reference data as a dictionary (or fails) """ return self._recipes.get_latest_recipe(ref) def get_recipe_revisions_references(self, ref: RecipeReference): return self._recipes.get_recipe_revisions_references(ref) def try_get_package(self, ref: PkgReference): """ Returns the reference data as a dictionary (or fails) """ ref_data = self._packages.get(ref) return ref_data def create_recipe(self, path, ref: RecipeReference): self._recipes.create(path, ref) def create_package(self, path, ref: PkgReference, build_id): self._packages.create(path, ref, build_id=build_id) def list_references(self, pattern=None): """Returns a list of all RecipeReference in the cache, optionally filtering by pattern. The references have their revision and timestamp attributes unset""" return [ref for ref in self._recipes.all_references() if pattern is None or ref.partial_match(pattern)] def get_package_revisions_references(self, pref: PkgReference): return [d["pref"] for d in self._packages.get_package_revisions_references(pref, only_latest_prev=False)] def get_package_references(self, ref: RecipeReference, only_latest_prev=True): return [d["pref"] for d in self._packages.get_package_references(ref, only_latest_prev)] def path_to_ref(self, path): ref = self._recipes.path_to_ref(path) if ref is not None: return ref return self._packages.path_to_ref(path) ================================================ FILE: conan/internal/cache/db/packages_table.py ================================================ import sqlite3 from conan.internal.cache.db.table import BaseDbTable from conan.internal.errors import ConanReferenceDoesNotExistInDB, ConanReferenceAlreadyExistsInDB from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.dates import timestamp_now class PackagesDBTable(BaseDbTable): table_name = 'packages' columns_description = [('reference', str), ('rrev', str), ('pkgid', str, True), ('prev', str, True), ('path', str, False, True), ('timestamp', float), ('build_id', str, True), ('lru', int)] unique_together = ('reference', 'rrev', 'pkgid', 'prev') @staticmethod def _as_dict(row): ref = RecipeReference.loads(row.reference) ref.revision = row.rrev pref = PkgReference(ref, row.pkgid, row.prev, row.timestamp) return { "pref": pref, "build_id": row.build_id, "path": row.path, "lru": row.lru } def _where_clause(self, pref: PkgReference): where_dict = { self.columns.reference: str(pref.ref), self.columns.rrev: pref.ref.revision, self.columns.pkgid: pref.package_id, self.columns.prev: pref.revision, } where_expr = ' AND '.join( [f"{k}='{v}' " if v is not None else f'{k} IS NULL' for k, v in where_dict.items()]) return where_expr def _set_clause(self, pref: PkgReference, path=None, build_id=None): set_dict = { self.columns.reference: str(pref.ref), self.columns.rrev: pref.ref.revision, self.columns.pkgid: pref.package_id, self.columns.prev: pref.revision, self.columns.path: path, self.columns.timestamp: pref.timestamp, self.columns.build_id: build_id, } set_expr = ', '.join([f"{k} = '{v}'" for k, v in set_dict.items() if v is not None]) return set_expr def get(self, pref: PkgReference): """ Returns the row matching the reference or fails """ where_clause = self._where_clause(pref) query = f'SELECT * FROM {self.table_name} ' \ f'WHERE {where_clause};' with self.db_connection() as conn: r = conn.execute(query) row = r.fetchone() if not row: raise ConanReferenceDoesNotExistInDB(f"No entry for package '{repr(pref)}'") return self._as_dict(self.row_type(*row)) def create(self, path, pref: PkgReference, build_id): assert pref.revision assert pref.timestamp # we set the timestamp to 0 until they get a complete reference, here they # are saved with the temporary uuid one, we don't want to consider these # not yet built packages for search and so on placeholders = ', '.join(['?' for _ in range(len(self.columns))]) lru = timestamp_now() with self.db_connection() as conn: try: conn.execute(f'INSERT INTO {self.table_name} ' f'VALUES ({placeholders})', [str(pref.ref), pref.ref.revision, pref.package_id, pref.revision, path, pref.timestamp, build_id, lru]) except sqlite3.IntegrityError: raise ConanReferenceAlreadyExistsInDB(f"Reference '{repr(pref)}' already exists") def update_timestamp(self, pref: PkgReference, path: str, build_id: str): assert pref.revision assert pref.timestamp where_clause = self._where_clause(pref) set_clause = self._set_clause(pref, path=path, build_id=build_id) query = f"UPDATE {self.table_name} " \ f"SET {set_clause} " \ f"WHERE {where_clause};" with self.db_connection() as conn: try: conn.execute(query) except sqlite3.IntegrityError: raise ConanReferenceAlreadyExistsInDB(f"Reference '{repr(pref)}' already exists") def remove_build_id(self, pref): where_clause = self._where_clause(pref) query = f"UPDATE {self.table_name} " \ f"SET {self.columns.build_id} = 'null' " \ f"WHERE {where_clause};" with self.db_connection() as conn: try: conn.execute(query) except sqlite3.IntegrityError: raise ConanReferenceAlreadyExistsInDB(f"Reference '{repr(pref)}' already exists") def remove_recipe(self, ref: RecipeReference): # can't use the _where_clause, because that is an exact match on the package_id, etc query = f"DELETE FROM {self.table_name} " \ f"WHERE {self.columns.reference} = '{str(ref)}' " \ f"AND {self.columns.rrev} = '{ref.revision}' " with self.db_connection() as conn: conn.execute(query) def remove(self, pref: PkgReference): where_clause = self._where_clause(pref) query = f"DELETE FROM {self.table_name} " \ f"WHERE {where_clause};" with self.db_connection() as conn: conn.execute(query) def get_package_revisions_references(self, pref: PkgReference, only_latest_prev=False): assert pref.ref.revision, "To search package revisions you must provide a recipe revision." assert pref.package_id, "To search package revisions you must provide a package id." check_prev = f"AND {self.columns.prev} = '{pref.revision}' " if pref.revision else "" if only_latest_prev: query = f'SELECT {self.columns.reference}, ' \ f'{self.columns.rrev}, ' \ f'{self.columns.pkgid}, ' \ f'{self.columns.prev}, ' \ f'{self.columns.path}, ' \ f'MAX({self.columns.timestamp}), ' \ f'{self.columns.build_id}, ' \ f'{self.columns.lru} ' \ f'FROM {self.table_name} ' \ f"WHERE {self.columns.rrev} = '{pref.ref.revision}' " \ f"AND {self.columns.reference} = '{str(pref.ref)}' " \ f"AND {self.columns.pkgid} = '{pref.package_id}' " \ f'{check_prev} ' \ f'AND {self.columns.prev} IS NOT NULL ' \ f'GROUP BY {self.columns.pkgid} ' else: query = f'SELECT * FROM {self.table_name} ' \ f"WHERE {self.columns.rrev} = '{pref.ref.revision}' " \ f"AND {self.columns.reference} = '{str(pref.ref)}' " \ f"AND {self.columns.pkgid} = '{pref.package_id}' " \ f'{check_prev} ' \ f'AND {self.columns.prev} IS NOT NULL ' \ f'ORDER BY {self.columns.timestamp} DESC' with self.db_connection() as conn: r = conn.execute(query) rows = r.fetchall() return [self._as_dict(self.row_type(*row)) for row in rows] def get_package_revisions_reference_exists(self, pref: PkgReference): assert pref.ref.revision, "To check package revision existence you must provide a recipe revision." assert pref.package_id, "To check package revisions existence you must provide a package id." check_prev = f"AND {self.columns.prev} = '{pref.revision}' " if pref.revision else "" query = f'SELECT 1 FROM {self.table_name} ' \ f"WHERE {self.columns.rrev} = '{pref.ref.revision}' " \ f"AND {self.columns.reference} = '{str(pref.ref)}' " \ f"AND {self.columns.pkgid} = '{pref.package_id}' " \ f'{check_prev} ' \ f'AND {self.columns.prev} IS NOT NULL ' \ 'LIMIT 1 ' with self.db_connection() as conn: r = conn.execute(query) row = r.fetchone() return bool(row) def get_package_references(self, ref: RecipeReference, only_latest_prev=True): # Return the latest revisions assert ref.revision, "To search for package id's you must provide a recipe revision." # we select the latest prev for each package_id if only_latest_prev: query = f'SELECT {self.columns.reference}, ' \ f'{self.columns.rrev}, ' \ f'{self.columns.pkgid}, ' \ f'{self.columns.prev}, ' \ f'{self.columns.path}, ' \ f'MAX({self.columns.timestamp}), ' \ f'{self.columns.build_id}, ' \ f'{self.columns.lru} ' \ f'FROM {self.table_name} ' \ f"WHERE {self.columns.rrev} = '{ref.revision}' " \ f"AND {self.columns.reference} = '{str(ref)}' " \ f'GROUP BY {self.columns.pkgid} ' else: query = f'SELECT * FROM {self.table_name} ' \ f"WHERE {self.columns.rrev} = '{ref.revision}' " \ f"AND {self.columns.reference} = '{str(ref)}' " \ f'AND {self.columns.prev} IS NOT NULL ' \ f'ORDER BY {self.columns.timestamp} DESC' with self.db_connection() as conn: r = conn.execute(query) rows = r.fetchall() return [self._as_dict(self.row_type(*row)) for row in rows] def get_package_references_with_build_id_match(self, ref: RecipeReference, build_id): # Return the latest revisions assert ref.revision, "To search for package id's by build_id you must provide a recipe revision." # we select the latest prev for each package_id # This is the same query as get_package_references, but with an additional filter query = f'SELECT {self.columns.reference}, ' \ f'{self.columns.rrev}, ' \ f'{self.columns.pkgid}, ' \ f'{self.columns.prev}, ' \ f'{self.columns.path}, ' \ f'MAX({self.columns.timestamp}), ' \ f'{self.columns.build_id}, ' \ f'{self.columns.lru} ' \ f'FROM {self.table_name} ' \ f"WHERE {self.columns.rrev} = '{ref.revision}' " \ f"AND {self.columns.reference} = '{str(ref)}' " \ f"AND {self.columns.build_id} = '{build_id}' " \ f'GROUP BY {self.columns.pkgid} ' with self.db_connection() as conn: r = conn.execute(query) row = r.fetchone() if row: return self._as_dict(self.row_type(*row)) return None def path_to_ref(self, path): query = f'SELECT * FROM {self.table_name} ' \ f"WHERE {self.columns.path}='{path}'" with self.db_connection() as conn: r = conn.execute(query) row = r.fetchone() if not row: return None ref = RecipeReference.loads(row[0]) ref.revision = row[1] pref = PkgReference(ref, row[2], row[3], row[5]) return pref ================================================ FILE: conan/internal/cache/db/recipes_table.py ================================================ import sqlite3 from conan.internal.cache.db.table import BaseDbTable from conan.internal.errors import ConanReferenceDoesNotExistInDB, ConanReferenceAlreadyExistsInDB from conan.api.model import RecipeReference from conan.internal.util.dates import timestamp_now class RecipesDBTable(BaseDbTable): table_name = 'recipes' columns_description = [('reference', str), ('rrev', str), ('path', str, False, True), ('timestamp', float), ('lru', int)] unique_together = ('reference', 'rrev') @staticmethod def _as_dict(row): ref = RecipeReference.loads(row.reference) ref.revision = row.rrev ref.timestamp = row.timestamp return { "ref": ref, "path": row.path, "lru": row.lru, } def _where_clause(self, ref): assert isinstance(ref, RecipeReference) where_dict = { self.columns.reference: str(ref), self.columns.rrev: ref.revision, } where_expr = ' AND '.join( [f"{k}='{v}' " if v is not None else f'{k} IS NULL' for k, v in where_dict.items()]) return where_expr def create(self, path, ref: RecipeReference): assert ref is not None assert ref.revision is not None assert ref.timestamp is not None placeholders = ', '.join(['?' for _ in range(len(self.columns))]) lru = timestamp_now() with self.db_connection() as conn: try: conn.execute(f'INSERT INTO {self.table_name} ' f'VALUES ({placeholders})', [str(ref), ref.revision, path, ref.timestamp, lru]) except sqlite3.IntegrityError: raise ConanReferenceAlreadyExistsInDB(f"Reference '{repr(ref)}' already exists") def update_timestamp(self, ref: RecipeReference): assert ref.revision is not None assert ref.timestamp is not None query = f"UPDATE {self.table_name} " \ f"SET {self.columns.timestamp} = '{ref.timestamp}' " \ f"WHERE {self.columns.reference}='{str(ref)}' " \ f"AND {self.columns.rrev} = '{ref.revision}' " with self.db_connection() as conn: conn.execute(query) def remove(self, ref: RecipeReference): where_clause = self._where_clause(ref) query = f"DELETE FROM {self.table_name} " \ f"WHERE {where_clause};" with self.db_connection() as conn: conn.execute(query) # returns all different conan references (name/version@user/channel) def all_references(self): query = f'SELECT DISTINCT {self.columns.reference} FROM {self.table_name}' with self.db_connection() as conn: r = conn.execute(query) rows = r.fetchall() return [RecipeReference.loads(row[0]) for row in rows] def get_recipe(self, ref: RecipeReference): query = f'SELECT * FROM {self.table_name} ' \ f"WHERE {self.columns.reference}='{str(ref)}' " \ f"AND {self.columns.rrev} = '{ref.revision}' " with self.db_connection() as conn: r = conn.execute(query) row = r.fetchone() if not row: raise ConanReferenceDoesNotExistInDB(f"Recipe '{ref.repr_notime()}' not found") ret = self._as_dict(self.row_type(*row)) return ret def get_latest_recipe(self, ref: RecipeReference): query = f'SELECT {self.columns.reference}, ' \ f'{self.columns.rrev}, ' \ f'{self.columns.path}, ' \ f'MAX({self.columns.timestamp}), ' \ f'{self.columns.lru} ' \ f'FROM {self.table_name} ' \ f"WHERE {self.columns.reference} = '{str(ref)}' " \ f'GROUP BY {self.columns.reference} ' # OTHERWISE IT FAILS THE MAX() with self.db_connection() as conn: r = conn.execute(query) row = r.fetchone() if row is None: raise ConanReferenceDoesNotExistInDB(f"Recipe '{ref}' not found") ret = self._as_dict(self.row_type(*row)) return ret def get_recipe_revisions_references(self, ref: RecipeReference): assert ref.revision is None query = f'SELECT * FROM {self.table_name} ' \ f"WHERE {self.columns.reference} = '{str(ref)}' " \ f'ORDER BY {self.columns.timestamp} DESC' with self.db_connection() as conn: r = conn.execute(query) rows = r.fetchall() ret = [self._as_dict(self.row_type(*row))["ref"] for row in rows] return ret def path_to_ref(self, path): query = f'SELECT * FROM {self.table_name} ' \ f"WHERE {self.columns.path}='{path}'" with self.db_connection() as conn: r = conn.execute(query) row = r.fetchone() if not row: return None ref = RecipeReference.loads(row[0]) ref.revision = row[1] ref.timestamp = row[3] return ref ================================================ FILE: conan/internal/cache/db/table.py ================================================ import sqlite3 import threading import traceback from collections import defaultdict, namedtuple from contextlib import contextmanager from typing import Tuple, List from conan.api.output import ConanOutput from conan.errors import ConanException class BaseDbTable: table_name: str = None columns_description: List[Tuple[str, type]] = None row_type: namedtuple = None columns: namedtuple = None unique_together: tuple = None _lock: threading.Lock = None _lock_storage = defaultdict(threading.Lock) def __init__(self, filename): self.filename = filename column_names: List[str] = [it[0] for it in self.columns_description] self.row_type = namedtuple('_', column_names) self.columns = self.row_type(*column_names) self._lock = self._lock_storage[self.filename] @contextmanager def db_connection(self): if not self._lock.acquire(timeout=20): m = traceback.format_exc() + "\n" ConanOutput().error(m) raise ConanException("Conan failed to acquire database lock in 20s. Maybe the system is " "under very heavy load. Please report it to Github tickets") # isolation_level=None, puts it in regular SQLITE autocommit mode, every # connection.execute() will autocommit connection = sqlite3.connect(self.filename, isolation_level=None, timeout=20) try: yield connection finally: connection.close() self._lock.release() def create_table(self): def field(name, typename, nullable=False, unique=False): field_str = name if typename is str: field_str += ' text' elif typename is int: field_str += ' integer' else: assert typename is float, f"sqlite3 type not mapped for type '{typename}'" field_str += ' real' if not nullable: field_str += ' NOT NULL' if unique: field_str += ' UNIQUE' return field_str fields = ', '.join([field(*it) for it in self.columns_description]) guard = 'IF NOT EXISTS' table_checks = f", UNIQUE({', '.join(self.unique_together)})" if self.unique_together else '' with self.db_connection() as conn: conn.execute(f"CREATE TABLE {guard} {self.table_name} ({fields} {table_checks});") ================================================ FILE: conan/internal/cache/home_paths.py ================================================ import os from conan.api.output import ConanOutput _EXTENSIONS_FOLDER = "extensions" _PLUGINS = "plugins" class HomePaths: """ pure computing of paths in the home, not caching anything """ def __init__(self, home_folder): self._home = home_folder @property def local_recipes_index_path(self): return os.path.join(self._home, ".local_recipes_index") @property def global_conf_path(self): return os.path.join(self._home, "global.conf") @property def deployers_path(self): deploy = os.path.join(self._home, _EXTENSIONS_FOLDER, "deploy") if os.path.exists(deploy): ConanOutput().warning("Use 'deployers' cache folder for deployers instead of 'deploy'", warn_tag="deprecated") return deploy return os.path.join(self._home, _EXTENSIONS_FOLDER, "deployers") @property def custom_generators_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, "generators") @property def hooks_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, "hooks") @property def wrapper_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "cmd_wrapper.py") @property def profiles_path(self): return os.path.join(self._home, "profiles") @property def profile_plugin_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "profile.py") @property def auth_remote_plugin_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "auth_remote.py") @property def auth_source_plugin_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "auth_source.py") @property def sign_plugin_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "sign", "sign.py") @property def remotes_path(self): return os.path.join(self._home, "remotes.json") @property def providers_path(self): return os.path.join(self._home, "audit_providers.json") @property def compatibility_plugin_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "compatibility") @property def default_sources_backup_folder(self): return os.path.join(self._home, "sources") @property def settings_path(self): return os.path.join(self._home, "settings.yml") @property def settings_path_user(self): return os.path.join(self._home, "settings_user.yml") @property def config_version_path(self): return os.path.join(self._home, "config_version.json") ================================================ FILE: conan/internal/cache/integrity_check.py ================================================ import os from conan.api.model.list import PackagesList from conan.api.output import ConanOutput from conan.api.model import PkgReference from conan.api.model import RecipeReference class IntegrityChecker: """ Check: - Performs a corruption integrity check in the cache. This is done by loading the existing conanmanifest.txt and comparing against a computed conanmanifest.txt. It doesn't address someone tampering with the conanmanifest.txt, just accidental modifying of a package contents, like if some file has been added after computing the manifest. This is to be done over the package contents, not the compressed conan_package.tgz artifacts """ def __init__(self, cache): self._cache = cache def check(self, pkg_list) -> PackagesList: corrupted_pkglist = PackagesList() for ref, packages in pkg_list.items(): # Check if any of the packages are corrupted if self._recipe_corrupted(ref): # If the recipe is corrupted, all its packages are considered corrupted corrupted_pkglist.add_ref(ref) else: # Do not check any binary if the recipe is corrupted for pref in packages: if self._package_corrupted(pref): corrupted_pkglist.add_ref(ref) # Cannot add package reference without having the recipe reference already added corrupted_pkglist.add_pref(pref) return corrupted_pkglist def _recipe_corrupted(self, ref: RecipeReference): layout = self._cache.recipe_layout(ref) output = ConanOutput(scope=f"{ref.repr_notime()}") try: read_manifest, expected_manifest = layout.recipe_manifests() except FileNotFoundError: output.error("Manifest missing", error_type="exception") return True # Filter exports_sources from read manifest if there are no exports_sources locally # This happens when recipe is downloaded without sources (not built from source) export_sources_folder = layout.export_sources() if not os.path.exists(export_sources_folder): read_manifest.file_sums = {k: v for k, v in read_manifest.file_sums.items() if not k.startswith("export_source")} if read_manifest != expected_manifest: output_lines = ["", "Manifest mismatch", f" Folder: {layout.export()}"] diff = read_manifest.difference(expected_manifest) for fname, (h1, h2) in diff.items(): output_lines.append(f" {fname} (manifest: {h1}, file: {h2})") output.error("\n".join(output_lines), error_type="exception") return True output.info("Integrity check: ok") def _package_corrupted(self, ref: PkgReference): layout = self._cache.pkg_layout(ref) output = ConanOutput(scope=f"{ref.repr_notime()}") try: read_manifest, expected_manifest = layout.package_manifests() except FileNotFoundError: output.error("Manifest missing", error_type="exception") return True if read_manifest != expected_manifest: output_lines = ["", "Manifest mismatch", f" Folder: {layout.package()}"] diff = read_manifest.difference(expected_manifest) for fname, (h1, h2) in diff.items(): output_lines.append(f" {fname} (manifest: {h1}, file: {h2})") output.error("\n".join(output_lines), error_type="exception") return True output.info("Integrity check: ok") ================================================ FILE: conan/internal/conan_app.py ================================================ import os from conan.internal.api.local.editable import EditablePackages from conan.internal.cache.cache import PkgCache from conan.internal.cache.home_paths import HomePaths from conan.internal.model.conf import ConfDefinition from conan.internal.graph.proxy import ConanProxy from conan.internal.graph.python_requires import PyRequireLoader from conan.internal.graph.range_resolver import RangeResolver from conan.internal.loader import ConanFileLoader, load_python_file from conan.internal.rest.remote_manager import RemoteManager class CmdWrapper: def __init__(self, wrapper): if os.path.isfile(wrapper): mod, _ = load_python_file(wrapper) self._wrapper = mod.cmd_wrapper else: self._wrapper = None def wrap(self, cmd, conanfile, **kwargs): if self._wrapper is None: return cmd return self._wrapper(cmd, conanfile=conanfile, **kwargs) class ConanFileHelpers: def __init__(self, requester, cmd_wrapper, global_conf, cache, home_folder, conan_api): self.requester = requester self.cmd_wrapper = cmd_wrapper self.global_conf = global_conf self.cache = cache self.home_folder = home_folder self.conan_api = conan_api # Might be None for local-recipes-index class ConanBasicApp: def __init__(self, conan_api): """ Needs: - Global configuration - Cache home folder """ # TODO: Remove this global_conf from here global_conf = conan_api._api_helpers.global_conf # noqa # TODO: Temporary while refactoring, remove i nthe future self._cache = conan_api._api_helpers.cache # noqa self._remote_manager = conan_api._api_helpers.remote_manager # noqa self._global_conf = global_conf self.cache_folder = conan_api.home_folder global_editables = conan_api.local.editable_packages ws_editables = conan_api.workspace.packages() self.editable_packages = global_editables.update_copy(ws_editables) class ConanApp(ConanBasicApp): def __init__(self, conan_api): """ Needs: - LocalAPI to read editable packages """ super().__init__(conan_api) legacy_update = self._global_conf.get("core:update_policy", choices=["legacy"]) self.proxy = ConanProxy(self._cache, self._remote_manager, self.editable_packages, legacy_update=legacy_update) self.range_resolver = RangeResolver(self._cache, self._remote_manager, self._global_conf, self.editable_packages) cmd_wrap = CmdWrapper(HomePaths(self.cache_folder).wrapper_path) requester = conan_api._api_helpers.requester # noqa conanfile_helpers = ConanFileHelpers(requester, cmd_wrap, self._global_conf, self._cache, self.cache_folder, conan_api) pyreq_loader = PyRequireLoader(self.proxy, self.range_resolver, self._global_conf) self.loader = ConanFileLoader(pyreq_loader, conanfile_helpers) class LocalRecipesIndexApp: """ Simplified one, without full API, for the LocalRecipesIndex. Only publicly used fields are: - cache - loader (for the export phase of local-recipes-index) The others are internally use by other collaborators """ def __init__(self, cache_folder): self.global_conf = ConfDefinition() self.cache = PkgCache(cache_folder, self.global_conf) self.remote_manager = RemoteManager(self.cache, auth_manager=None, home_folder=cache_folder) editable_packages = EditablePackages() self.proxy = ConanProxy(self.cache, self.remote_manager, editable_packages) self.range_resolver = RangeResolver(self.cache, self.remote_manager, self.global_conf, editable_packages) pyreq_loader = PyRequireLoader(self.proxy, self.range_resolver, self.global_conf) helpers = ConanFileHelpers(None, CmdWrapper(""), self.global_conf, self.cache, cache_folder, None) self.loader = ConanFileLoader(pyreq_loader, helpers) ================================================ FILE: conan/internal/default_settings.py ================================================ import os default_settings_yml = """\ # This file was generated by Conan. Remove this comment if you edit this file or Conan # will destroy your changes. os: Windows: subsystem: [null, cygwin, msys, msys2, wsl] WindowsStore: version: ["8.1", "10.0"] WindowsCE: platform: [ANY] version: ["5.0", "6.0", "7.0", "8.0"] Linux: iOS: version: &ios_version ["7.0", "7.1", "8.0", "8.1", "8.2", "8.3", "8.4", "9.0", "9.1", "9.2", "9.3", "10.0", "10.1", "10.2", "10.3", "11.0", "11.1", "11.2", "11.3", "11.4", "12.0", "12.1", "12.2", "12.3", "12.4", "12.5", "13.0", "13.1", "13.2", "13.3", "13.4", "13.5", "13.6", "13.7", "14.0", "14.1", "14.2", "14.3", "14.4", "14.5", "14.6", "14.7", "14.8", "15.0", "15.1", "15.2", "15.3", "15.4", "15.5", "15.6", "15.7", "15.8", "16.0", "16.1", "16.2", "16.3", "16.4", "16.5", "16.6", "16.7", "17.0", "17.1", "17.2", "17.3", "17.4", "17.5", "17.6", "17.7", "17.8", "18.0", "18.1", "18.2", "18.3", "18.4", "18.5", "18.6", "18.7", "26.0", "26.1", "26.2", "26.3"] sdk: ["iphoneos", "iphonesimulator"] sdk_version: [null, "11.3", "11.4", "12.0", "12.1", "12.2", "12.4", "13.0", "13.1", "13.2", "13.3", "13.4", "13.5", "13.6", "13.7", "14.0", "14.1", "14.2", "14.3", "14.4", "14.5", "15.0", "15.2", "15.4", "15.5", "16.0", "16.1", "16.2", "16.4", "17.0", "17.1", "17.2", "17.4", "17.5", "18.0", "18.1", "18.2", "18.4", "18.5", "26.0", "26.1", "26.2"] watchOS: version: ["4.0", "4.1", "4.2", "4.3", "5.0", "5.1", "5.2", "5.3", "6.0", "6.1", "6.2", "6.3", "7.0", "7.1", "7.2", "7.3", "7.4", "7.5", "7.6", "8.0", "8.1", "8.3", "8.4", "8.5", "8.6", "8.7", "9.0","9.1", "9.2", "9.3", "9.4", "9.5", "9.6", "10.0", "10.1", "10.2", "10.3", "10.4", "10.5", "10.6", "11.0", "11.1", "11.2", "11.3", "11.4", "11.5", "11.6", "26.0", "26.1", "26.2", "26.3"] sdk: ["watchos", "watchsimulator"] sdk_version: [null, "4.3", "5.0", "5.1", "5.2", "5.3", "6.0", "6.1", "6.2", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.0.1", "8.3", "8.5", "9.0", "9.1", "9.4", "10.0", "10.1", "10.2", "10.4", "10.5", "11.0", "11.1", "11.2", "11.4", "11.5", "26.0", "26.1", "26.2"] tvOS: version: ["11.0", "11.1", "11.2", "11.3", "11.4", "12.0", "12.1", "12.2", "12.3", "12.4", "13.0", "13.2", "13.3", "13.4", "14.0", "14.2", "14.3", "14.4", "14.5", "14.6", "14.7", "15.0", "15.1", "15.2", "15.3", "15.4", "15.5", "15.6", "16.0", "16.1", "16.2", "16.3", "16.4", "16.5", "16.6", "17.0", "17.1", "17.2", "17.3", "17.4", "17.5", "17.6", "18.0", "18.1", "18.2", "18.3", "18.4", "18.5", "18.6", "26.0", "26.1", "26.2", "26.3"] sdk: ["appletvos", "appletvsimulator"] sdk_version: [null, "11.3", "11.4", "12.0", "12.1", "12.2", "12.4", "13.0", "13.2", "13.3", "13.4", "14.0", "14.2", "14.3", "14.4", "14.5", "15.0", "15.2", "15.4", "15.5", "16.0", "16.1", "16.4", "17.0", "17.1", "17.2", "17.4", "17.5", "18.0", "18.1", "18.2", "18.4", "18.5", "26.0", "26.1", "26.2"] visionOS: version: ["1.0", "1.1", "1.2", "1.3", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "26.0", "26.1", "26.2", "26.3"] sdk: ["xros", "xrsimulator"] sdk_version: [null, "1.0", "1.1", "1.2", "1.3", "2.0", "2.1", "2.2", "2.4", "2.5", "26.0", "26.1", "26.2"] Macos: version: [null, "10.6", "10.7", "10.8", "10.9", "10.10", "10.11", "10.12", "10.13", "10.14", "10.15", "11.0", "11.1", "11.2", "11.3", "11.4", "11.5", "11.6", "11.7", "12.0", "12.1", "12.2", "12.3", "12.4", "12.5", "12.6", "12.7", "13.0", "13.1", "13.2", "13.3", "13.4", "13.5", "13.6", "13.7", "14.0", "14.1", "14.2", "14.3", "14.4", "14.5", "14.6", "14.7", "15.0", "15.1", "15.2", "15.3", "15.4", "15.5", "15.6", "15.7", "26.0", "26.1", "26.2", "26.3"] sdk_version: [null, "10.13", "10.14", "10.15", "11.0", "11.1", "11.2", "11.3", "12.0", "12.1", "12.3", "12.4", "13.0", "13.1", "13.3", "14.0", "14.2", "14.4", "14.5", "15.0", "15.1", "15.2", "15.4", "15.5", "26.0", "26.1", "26.2"] subsystem: null: catalyst: ios_version: *ios_version Android: api_level: [ANY] ndk_version: [null, ANY] FreeBSD: SunOS: AIX: Arduino: board: [ANY] Emscripten: Neutrino: version: ["6.4", "6.5", "6.6", "7.0", "7.1"] baremetal: VxWorks: version: ["7"] arch: [x86, x86_64, ppc32be, ppc32, ppc64le, ppc64, armv4, armv4i, armv5el, armv5hf, armv6, armv7, armv7hf, armv7s, armv7k, armv8, armv8_32, armv8.3, arm64ec, sparc, sparcv9, mips, mips64, avr, s390, s390x, asm.js, wasm, wasm64, sh4le, e2k-v2, e2k-v3, e2k-v4, e2k-v5, e2k-v6, e2k-v7, riscv64, riscv32, xtensalx6, xtensalx106, xtensalx7, tc131, tc16, tc161, tc162, tc18] compiler: sun-cc: version: ["5.10", "5.11", "5.12", "5.13", "5.14", "5.15"] threads: [null, posix] libcxx: [libCstd, libstdcxx, libstlport, libstdc++] gcc: version: ["4.1", "4.4", "4.5", "4.6", "4.7", "4.8", "4.9", "5", "5.1", "5.2", "5.3", "5.4", "5.5", "6", "6.1", "6.2", "6.3", "6.4", "6.5", "7", "7.1", "7.2", "7.3", "7.4", "7.5", "8", "8.1", "8.2", "8.3", "8.4", "8.5", "9", "9.1", "9.2", "9.3", "9.4", "9.5", "10", "10.1", "10.2", "10.3", "10.4", "10.5", "11", "11.1", "11.2", "11.3", "11.4", "11.5", "12", "12.1", "12.2", "12.3", "12.4", "12.5", "13", "13.1", "13.2", "13.3", "13.4", "14", "14.1", "14.2", "14.3", "15", "15.1", "15.2"] libcxx: [libstdc++, libstdc++11] threads: [null, posix, win32, mcf] # Windows MinGW exception: [null, dwarf2, sjlj, seh] # Windows MinGW cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23, 26, gnu26] cstd: [null, 99, gnu99, 11, gnu11, 17, gnu17, 23, gnu23] msvc: version: [170, 180, 190, 191, 192, 193, 194, 195] update: [null, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] runtime: [static, dynamic] runtime_type: [Debug, Release] cppstd: [null, 14, 17, 20, 23] toolset: [null, v110_xp, v120_xp, v140_xp, v141_xp] cstd: [null, 11, 17] clang: version: ["3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "3.9", "4.0", "5.0", "6.0", "7.0", "7.1", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22"] libcxx: [null, libstdc++, libstdc++11, libc++, c++_shared, c++_static] cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23, 26, gnu26] runtime: [null, static, dynamic] runtime_type: [null, Debug, Release] runtime_version: [null, v140, v141, v142, v143, v144, v145] cstd: [null, 99, gnu99, 11, gnu11, 17, gnu17, 23, gnu23] apple-clang: version: ["5.0", "5.1", "6.0", "6.1", "7.0", "7.3", "8.0", "8.1", "9.0", "9.1", "10.0", "11.0", "12.0", "13", "13.0", "13.1", "14", "14.0", "15", "15.0", "16", "16.0", "17", "17.0"] libcxx: [libstdc++, libc++] cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23, 26, gnu26] cstd: [null, 99, gnu99, 11, gnu11, 17, gnu17, 23, gnu23] intel-cc: version: ["2021.1", "2021.2", "2021.3", "2021.4", "2022.1", "2022.2", "2022.3", "2023.0", "2023.1", "2023.2", "2024.0", "2024.1", "2025.0", "2025.1"] update: [null, ANY] mode: ["icx", "classic", "dpcpp"] libcxx: [null, libstdc++, libstdc++11, libc++] cppstd: [null, 98, gnu98, "03", gnu03, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23] runtime: [null, static, dynamic] runtime_type: [null, Debug, Release] qcc: version: ["4.4", "5.4", "8.3"] libcxx: [cxx, gpp, cpp, cpp-ne, accp, acpp-ne, ecpp, ecpp-ne] cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17] mcst-lcc: version: ["1.19", "1.20", "1.21", "1.22", "1.23", "1.24", "1.25"] libcxx: [libstdc++, libstdc++11] cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23] emcc: # From https://github.com/emscripten-core/emscripten/blob/main/ChangeLog.md # There is no ABI compatibility guarantee between versions version: [ANY] libcxx: [null, libstdc++, libstdc++11, libc++] threads: [null, posix, wasm_workers] cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23, 26, gnu26] cstd: [null, 99, gnu99, 11, gnu11, 17, gnu17, 23, gnu23] build_type: [null, Debug, Release, RelWithDebInfo, MinSizeRel] """ def migrate_settings_file(cache_folder): from conan.internal.api.migrations import update_file settings_path = os.path.join(cache_folder, "settings.yml") update_file(settings_path, default_settings_yml) ================================================ FILE: conan/internal/deploy.py ================================================ import filecmp import os import shutil import fnmatch from conan.internal.cache.home_paths import HomePaths from conan.api.output import ConanOutput from conan.internal.loader import load_python_file from conan.internal.errors import conanfile_exception_formatter from conan.errors import ConanException from conan.internal.util.files import rmdir, mkdir, save def _find_deployer(d, cache_deploy_folder): """ Implements the logic of finding a deployer, with priority: - 1) absolute paths - 2) relative to cwd - 3) in the cache/extensions/deploy folder - 4) built-in """ def _load(path): mod, _ = load_python_file(path) try: return mod.deploy except AttributeError: raise ConanException(f"Deployer does not contain 'deploy()' function: {path}") if not d.endswith(".py"): d += ".py" # Deployers must be python files if os.path.isabs(d): return _load(d) cwd = os.getcwd() local_path = os.path.normpath(os.path.join(cwd, d)) if os.path.isfile(local_path): return _load(local_path) cache_path = os.path.join(cache_deploy_folder, d) if os.path.isfile(cache_path): return _load(cache_path) builtin_deploy = {"full_deploy.py": full_deploy, "direct_deploy.py": direct_deploy, "runtime_deploy.py": runtime_deploy, "cyclone_1.6.py": cyclonedx_1_6, "cyclone_1.4.py": cyclonedx_1_4}.get(d) if builtin_deploy is not None: return builtin_deploy raise ConanException(f"Cannot find deployer '{d}'") def do_deploys(home_folder, graph, deploy, deploy_package, deploy_folder): try: mkdir(deploy_folder) except Exception as e: raise ConanException(f"Deployer folder cannot be created '{deploy_folder}':\n{e}") # handle the recipe deploy() if deploy_package: # Similar processing as BuildMode class excluded = [p[1:] for p in deploy_package if p[0] in ["!", "~"]] included = [p for p in deploy_package if p[0] not in ["!", "~"]] for node in graph.ordered_iterate(): conanfile = node.conanfile if not conanfile.ref: # virtual or conanfile.txt, can't have deployer continue consumer = conanfile._conan_is_consumer # noqa if any(conanfile.ref.matches(p, consumer) for p in excluded): continue if not any(conanfile.ref.matches(p, consumer) for p in included): continue if hasattr(conanfile, "deploy"): conanfile.output.info("Executing deploy()") conanfile.deploy_folder = deploy_folder with conanfile_exception_formatter(conanfile, "deploy"): conanfile.deploy() # Handle the deploys cache = HomePaths(home_folder) for d in deploy or []: deployer = _find_deployer(d, cache.deployers_path) # IMPORTANT: Use always kwargs to not break if it changes in the future deployer(graph=graph, output_folder=deploy_folder) def full_deploy(graph, output_folder): """ Deploys to output_folder + host/dep/0.1/Release/x86_64 subfolder """ # TODO: This deployer needs to be put somewhere else # TODO: Document that this will NOT work with editables conanfile = graph.root.conanfile conanfile.output.info(f"Conan built-in full deployer to {output_folder}") for dep in conanfile.dependencies.values(): if dep.package_folder is None: continue folder_name = os.path.join("full_deploy", dep.context, dep.ref.name, str(dep.ref.version)) build_type = dep.info.settings.get_safe("build_type") arch = dep.info.settings.get_safe("arch") if build_type: folder_name = os.path.join(folder_name, build_type) if arch: folder_name = os.path.join(folder_name, arch) _deploy_single(dep, conanfile, output_folder, folder_name) def runtime_deploy(graph, output_folder): """ Deploy all the shared libraries and the executables of the dependencies in a flat directory. It preserves symlinks in case the configuration tools.deployer:symlinks is True. It preserves the directory structure when having subfolders """ conanfile = graph.root.conanfile output = ConanOutput(scope="runtime_deploy") output.info(f"Deploying dependencies runtime to folder: {output_folder}") output.warning("This deployer is experimental and subject to change. " "Please give feedback at https://github.com/conan-io/conan/issues") mkdir(output_folder) symlinks = conanfile.conf.get("tools.deployer:symlinks", check_type=bool, default=True) for req, dep in conanfile.dependencies.host.items(): if not req.run: # Avoid deploying unused binaries at runtime continue if dep.package_folder is None: output.warning(f"{dep.ref} does not have any package folder, skipping binary") continue count = 0 cpp_info = dep.cpp_info.aggregated_components() for bindir in cpp_info.bindirs: if not os.path.isdir(bindir): output.warning(f"{dep.ref} {bindir} does not exist") continue count += _flatten_directory(dep, bindir, output_folder, symlinks) for libdir in cpp_info.libdirs: if not os.path.isdir(libdir): output.warning(f"{dep.ref} {libdir} does not exist") continue count += _flatten_directory(dep, libdir, output_folder, symlinks, [".dylib*", ".so*"]) output.info(f"Copied {count} files from {dep.ref}") conanfile.output.success(f"Runtime deployed to folder: {output_folder}") def cyclonedx_1_4(graph, output_folder): from conan.tools.sbom import cyclonedx_1_4 import json sbom = cyclonedx_1_4(graph.root.conanfile) save(os.path.join(output_folder, "sbom-cyclonedx-1.4.json"), json.dumps(sbom, indent=2)) def cyclonedx_1_6(graph, output_folder): from conan.tools.sbom import cyclonedx_1_6 import json sbom = cyclonedx_1_6(graph.root.conanfile) save(os.path.join(output_folder, "sbom-cyclonedx-1.6.json"), json.dumps(sbom, indent=2)) def _flatten_directory(dep, src_dir, output_dir, symlinks, extension_filter=None): """ Copy all the files from the source directory in a flat output directory, respecting subfolders. An optional string, named extension_filter, can be set to copy only the files with the listed extensions. """ file_count = 0 output = ConanOutput(scope="runtime_deploy") for src_dirpath, _, src_filenames in os.walk(src_dir, followlinks=symlinks): rel_path = os.path.relpath(src_dirpath, src_dir) for src_filename in src_filenames: if extension_filter and not any(fnmatch.fnmatch(src_filename, f'*{ext}') for ext in extension_filter): continue src_filepath = os.path.join(src_dirpath, src_filename) dest_filepath = os.path.join(output_dir, rel_path, src_filename) if not symlinks and os.path.islink(src_filepath): continue if not os.path.exists(os.path.dirname(dest_filepath)): os.makedirs(os.path.dirname(dest_filepath)) if os.path.exists(dest_filepath): if filecmp.cmp(src_filepath, dest_filepath): # Be efficient, do not copy output.verbose(f"{dest_filepath} exists with same contents, skipping copy") continue else: output.warning(f"{dest_filepath} exists and will be overwritten") try: file_count += 1 # INFO: When follow_symlinks is false, and src is a symbolic link, it tries to # copy all metadata from the src symbolic link to the newly created dst link shutil.copy2(src_filepath, dest_filepath, follow_symlinks=not symlinks) output.verbose(f"Copied {src_filepath} into {output_dir}") except Exception as e: if "WinError 1314" in str(e): ConanOutput().error("runtime_deploy: Windows symlinks require admin privileges " "or 'Developer mode = ON'", error_type="exception") raise ConanException(f"runtime_deploy: Copy of '{dep}' files failed: {e}.\nYou can " f"use 'tools.deployer:symlinks' conf to disable symlinks") return file_count def _deploy_single(dep, conanfile, output_folder, folder_name): new_folder = os.path.join(output_folder, folder_name) rmdir(new_folder) symlinks = conanfile.conf.get("tools.deployer:symlinks", check_type=bool, default=True) try: shutil.copytree(dep.package_folder, new_folder, symlinks=symlinks) except Exception as e: if "WinError 1314" in str(e): ConanOutput().error("full_deploy: Symlinks in Windows require admin privileges " "or 'Developer mode = ON'", error_type="exception") raise ConanException(f"full_deploy: The copy of '{dep}' files failed: {e}.\nYou can " f"use 'tools.deployer:symlinks' conf to disable symlinks") dep.set_deploy_folder(new_folder) def direct_deploy(graph, output_folder): """ Deploys to output_folder a single package, """ # TODO: This deployer needs to be put somewhere else # TODO: Document that this will NOT work with editables output_folder = os.path.join(output_folder, "direct_deploy") conanfile = graph.root.conanfile conanfile.output.info(f"Conan built-in pkg deployer to {output_folder}") # If the argument is --requires, the current conanfile is a virtual one with 1 single # dependency, the "reference" package. If the argument is a local path, then all direct # dependencies for dep in conanfile.dependencies.filter({"direct": True}).values(): _deploy_single(dep, conanfile, output_folder, dep.ref.name) ================================================ FILE: conan/internal/errors.py ================================================ import traceback from contextlib import contextmanager from conan.errors import ConanException, ConanInvalidConfiguration @contextmanager def conanfile_remove_attr(conanfile, names, method): """ remove some self.xxxx attribute from the class, so it raises an exception if used within a given conanfile method """ original_class = type(conanfile) def _prop(attr_name): def _m(_): raise ConanException(f"'self.{attr_name}' access in '{method}()' method is forbidden") return property(_m) try: new_class = type(original_class.__name__, (original_class, ), {}) conanfile.__class__ = new_class for name in names: setattr(new_class, name, _prop(name)) yield finally: conanfile.__class__ = original_class @contextmanager def conanfile_exception_formatter(conanfile, funcname): """ Decorator to throw an exception formatted with the line of the conanfile where the error ocurrs. """ try: yield except ConanInvalidConfiguration as exc: # TODO: This is never called from `conanfile.validate()` but could be called from others msg = "{}: Invalid configuration: {}".format(str(conanfile), exc) raise ConanInvalidConfiguration(msg) except Exception as exc: m = scoped_traceback(f"{conanfile}: Error in {funcname}() method", exc, scope="conanfile.py") from conan.api.output import LEVEL_DEBUG, ConanOutput if ConanOutput.level_allowed(LEVEL_DEBUG): m = traceback.format_exc() + "\n" + m raise ConanException(m) def scoped_traceback(header_msg, exception, scope): """ It will iterate the traceback lines, when it finds that the source code is inside the users conanfile it "start recording" the messages, when the trace exits the conanfile we return the traces. """ import sys content_lines = [] try: scope_reached = False tb = sys.exc_info()[2] index = 0 while True: # If out of index will raise and will be captured later # 40 levels of nested functions max, get the latest filepath, line, name, contents = traceback.extract_tb(tb, 40)[index] filepath = filepath.replace("\\", "/") if scope not in filepath: # Avoid show trace from internal conan source code if scope_reached: # The error goes to internal code, exit print break else: if not scope_reached: # First line msg = f"{header_msg}, line {line}\n\t{contents}" else: msg = (f"while calling '{name}', line {line}\n\t{contents}" if line else "\n\t%s" % contents) content_lines.append(msg) scope_reached = True index += 1 except IndexError: pass ret = "\n".join(content_lines) ret += "\n\t%s: %s" % (exception.__class__.__name__, str(exception)) return ret class ConanReferenceDoesNotExistInDB(ConanException): """ Reference does not exist in cache db """ pass class ConanReferenceAlreadyExistsInDB(ConanException): """ Reference already exists in cache db """ pass class ConanConnectionError(ConanException): pass class InternalErrorException(ConanException): """ Generic 500 error """ pass class RequestErrorException(ConanException): """ Generic 400 error """ pass class AuthenticationException(ConanException): # 401 """ 401 error """ pass class ForbiddenException(ConanException): # 403 """ 403 error """ pass class NotFoundException(ConanException): # 404 """ 404 error """ pass class RecipeNotFoundException(NotFoundException): def __init__(self, ref): super().__init__(f"Recipe not found: '{ref.repr_notime()}'") class PackageNotFoundException(NotFoundException): def __init__(self, pref): super().__init__(f"Binary package not found: '{pref.repr_notime()}'") EXCEPTION_CODE_MAPPING = {InternalErrorException: 500, RequestErrorException: 400, AuthenticationException: 401, ForbiddenException: 403, NotFoundException: 404, RecipeNotFoundException: 404, PackageNotFoundException: 404} ================================================ FILE: conan/internal/graph/__init__.py ================================================ ================================================ FILE: conan/internal/graph/build_mode.py ================================================ from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.model.recipe_ref import ref_matches class BuildMode: """ build_mode => ["*"] if user wrote "--build=*" => ["hello", "bye"] if user wrote "--build hello --build bye" => ["hello/0.1@foo/bar"] if user wrote "--build hello/0.1@foo/bar" => ["!foo"] or ["~foo"] means exclude when building all from sources """ def __init__(self, params): self._missing = False self._never = False self.cascade = False self._editable = False self._patterns = [] self._build_missing_patterns = [] self._build_missing_excluded = [] self._build_compatible_patterns = [] self._build_compatible_excluded = [] self._excluded_patterns = [] if params is None: return assert isinstance(params, list) assert len(params) > 0 # Not empty list for param in params: if param == "missing": self._missing = True elif param == "editable": self._editable = True elif param == "never": self._never = True elif param == "cascade": self.cascade = True ConanOutput().warning("Using build-mode 'cascade' is generally inefficient and it " "shouldn't be used. Use 'package_id' and 'package_id_modes' " "for more efficient re-builds") else: if param.startswith("missing:"): clean_pattern = param[len("missing:"):] if clean_pattern and clean_pattern[0] in ["!", "~"]: self._build_missing_excluded.append(clean_pattern[1:]) else: self._build_missing_patterns.append(clean_pattern) elif param == "compatible": self._build_compatible_patterns = ["*"] elif param.startswith("compatible:"): clean_pattern = param[len("compatible:"):] if clean_pattern and clean_pattern[0] in ["!", "~"]: self._build_compatible_excluded.append(clean_pattern[1:]) else: self._build_compatible_patterns.append(clean_pattern) else: clean_pattern = param if clean_pattern and clean_pattern[0] in ["!", "~"]: self._excluded_patterns.append(clean_pattern[1:]) else: self._patterns.append(clean_pattern) if self._never and (self._missing or self._patterns or self.cascade): raise ConanException("--build=never not compatible with other options") @property def editable(self): # we can make this conditional on the context in the future return self._editable def forced(self, conan_file, ref, with_deps_to_build=False): # TODO: ref can be obtained from conan_file for pattern in self._excluded_patterns: if ref_matches(ref, pattern, is_consumer=conan_file._conan_is_consumer): # noqa conan_file.output.info("Excluded build from source") return False if conan_file.build_policy == "never": # this package has been export-pkg return False if self._never: return False if conan_file.build_policy == "always": raise ConanException("{}: build_policy='always' has been removed. " "Please use 'missing' only".format(conan_file)) if self.cascade and with_deps_to_build: return True # Patterns to match, if package matches pattern, build is forced for pattern in self._patterns: if ref_matches(ref, pattern, is_consumer=conan_file._conan_is_consumer): # noqa return True return False def allowed(self, conan_file): if self._never or conan_file.build_policy == "never": # this package has been export-pkg return False if self._missing: return True if conan_file.build_policy == "missing": conan_file.output.info("Building package from source as defined by " "build_policy='missing'") return True if self.should_build_missing(conan_file): return True if self.allowed_compatible(conan_file): return True return False def allowed_compatible(self, conanfile): if self._build_compatible_excluded: for pattern in self._build_compatible_excluded: if ref_matches(conanfile.ref, pattern, is_consumer=False): return False return True # If it has not been excluded by the negated patterns, it is included for pattern in self._build_compatible_patterns: if ref_matches(conanfile.ref, pattern, is_consumer=conanfile._conan_is_consumer): # noqa return True def should_build_missing(self, conanfile): if self._build_missing_excluded: for pattern in self._build_missing_excluded: if ref_matches(conanfile.ref, pattern, is_consumer=False): return False return True # If it has not been excluded by the negated patterns, it is included for pattern in self._build_missing_patterns: if ref_matches(conanfile.ref, pattern, is_consumer=conanfile._conan_is_consumer): # noqa return True ================================================ FILE: conan/internal/graph/compatibility.py ================================================ import os from collections import OrderedDict from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan.internal.graph.compute_pid import run_validate_package_id from conan.internal.loader import load_python_file from conan.internal.errors import conanfile_exception_formatter, scoped_traceback from conan.errors import ConanException from conan.internal.api.migrations import CONAN_GENERATED_COMMENT # TODO: Define other compatibility besides applications from conan.internal.util.files import load, save _default_compat = """\ # This file was generated by Conan. Remove this comment if you edit this file or Conan # will destroy your changes. from conan.tools.build import supported_cppstd, supported_cstd from conan.errors import ConanException def cppstd_compat(conanfile): # It will try to find packages with all the cppstd versions extension_properties = getattr(conanfile, "extension_properties", {}) compiler = conanfile.settings.get_safe("compiler") compiler_version = conanfile.settings.get_safe("compiler.version") cppstd = conanfile.settings.get_safe("compiler.cppstd") if not compiler or not compiler_version: return [] factors = [] # List of list, each sublist is a potential combination if cppstd is not None and extension_properties.get("compatibility_cppstd") is not False: cppstd_possible_values = supported_cppstd(conanfile) if cppstd_possible_values is None: conanfile.output.warning(f'No cppstd compatibility defined for compiler "{compiler}"') else: # The current cppst must be included in case there is other factor factors.append([{"compiler.cppstd": v} for v in cppstd_possible_values]) cstd = conanfile.settings.get_safe("compiler.cstd") if cstd is not None and extension_properties.get("compatibility_cstd") is not False: cstd_possible_values = supported_cstd(conanfile) if cstd_possible_values is None: conanfile.output.warning(f'No cstd compatibility defined for compiler "{compiler}"') else: factors.append([{"compiler.cstd": v} for v in cstd_possible_values if v != cstd]) return factors def compatibility(conanfile): # By default, different compiler.cppstd are compatible # factors is a list of lists factors = cppstd_compat(conanfile) # MSVC 194->193 fallback compatibility compiler = conanfile.settings.get_safe("compiler") compiler_version = conanfile.settings.get_safe("compiler.version") if compiler == "msvc": msvc_fallback = {"194": "193"}.get(compiler_version) if msvc_fallback: factors.append([{"compiler.version": msvc_fallback}]) # Append more factors for your custom compatibility rules here # Combine factors to compute all possible configurations combinations = _factors_combinations(factors) # Final compatibility settings combinations to check return [{"settings": [(k, v) for k, v in comb.items()]} for comb in combinations] def _factors_combinations(factors): combinations = [] for factor in factors: if not combinations: combinations = factor continue new_combinations = [] for comb in combinations: for f in factor: new_comb = comb.copy() new_comb.update(f) new_combinations.append(new_comb) combinations.extend(new_combinations) return combinations """ def migrate_compatibility_files(cache_folder): compatible_folder = HomePaths(cache_folder).compatibility_plugin_path compatibility_file = os.path.join(compatible_folder, "compatibility.py") cppstd_compat_file = os.path.join(compatible_folder, "cppstd_compat.py") def _is_migratable(file_path): if not os.path.exists(file_path): return True content = load(file_path) first_line = content.lstrip().split("\n", 1)[0] return CONAN_GENERATED_COMMENT in first_line if _is_migratable(compatibility_file) and _is_migratable(cppstd_compat_file): compatibility_exists = os.path.exists(compatibility_file) needs_update = not compatibility_exists or load(compatibility_file) != _default_compat if needs_update: save(compatibility_file, _default_compat) if compatibility_exists: ConanOutput().success("Migration: Successfully updated compatibility.py") if os.path.exists(cppstd_compat_file): os.remove(cppstd_compat_file) class BinaryCompatibility: def __init__(self, compatibility_plugin_folder, hook_manager): self._hook_manager = hook_manager compatibility_file = os.path.join(compatibility_plugin_folder, "compatibility.py") if not os.path.exists(compatibility_file): raise ConanException("The 'compatibility.py' plugin file doesn't exist. If you want " "to disable it, edit its contents instead of removing it") mod, _ = load_python_file(compatibility_file) self._compatibility = mod.compatibility def compatibles(self, conanfile): compat_infos = [] if hasattr(conanfile, "compatibility"): with conanfile_exception_formatter(conanfile, "compatibility"): recipe_compatibles = conanfile.compatibility() compat_infos.extend(self._compatible_infos(conanfile, recipe_compatibles)) try: plugin_compatibles = self._compatibility(conanfile) except Exception as e: msg = f"Error while processing 'compatibility.py' plugin for '{conanfile}'" msg = scoped_traceback(msg, e, scope="plugins/compatibility") raise ConanException(msg) compat_infos.extend(self._compatible_infos(conanfile, plugin_compatibles)) if not compat_infos: return {} result = OrderedDict() original_info = conanfile.info original_settings = conanfile.settings original_settings_target = conanfile.settings_target original_options = conanfile.options for c in compat_infos: # we replace the conanfile, so ``validate()`` and ``package_id()`` can # use the compatible ones conanfile.info = c conanfile.settings = c.settings conanfile.settings_target = c.settings_target conanfile.options = c.options run_validate_package_id(conanfile, self._hook_manager) pid = c.package_id() if pid not in result and not c.invalid: result[pid] = c # Restore the original state conanfile.info = original_info conanfile.settings = original_settings conanfile.settings_target = original_settings_target conanfile.options = original_options return result @staticmethod def _compatible_infos(conanfile, compatibles): result = [] if compatibles: for elem in compatibles: compat_info = conanfile.original_info.clone() compat_info.compatibility_delta = elem settings = elem.get("settings") if settings: compat_info.settings.update_values(settings, raise_undefined=False) options = elem.get("options") if options: compat_info.options.update(options_values=OrderedDict(options)) result.append(compat_info) settings_target = elem.get("settings_target") if settings_target and compat_info.settings_target: compat_info.settings_target.update_values(settings_target, raise_undefined=False) return result ================================================ FILE: conan/internal/graph/compute_pid.py ================================================ from collections import OrderedDict from conan.internal.errors import conanfile_remove_attr, conanfile_exception_formatter from conan.errors import ConanException, ConanInvalidConfiguration from conan.internal.methods import auto_header_only_package_id from conan.internal.model.info import (ConanInfo, RequirementsInfo, RequirementInfo, PythonRequiresInfo) from conan.internal.model.pkg_type import PackageType def compute_package_id(node, modes, config_version, hook_manager): """ Compute the binary package ID of this node """ conanfile = node.conanfile unknown_mode, non_embed_mode, embed_mode, python_mode, build_mode = modes python_requires = getattr(conanfile, "python_requires", None) if python_requires: python_requires = python_requires.info_requires() data = OrderedDict() build_data = OrderedDict() for require, transitive in node.transitive_deps.items(): dep_node = transitive.node require.deduce_package_id_mode(conanfile.package_type, dep_node, non_embed_mode, embed_mode, build_mode, unknown_mode) if require.package_id_mode is not None: req_info = RequirementInfo(dep_node.pref.ref, dep_node.pref.package_id, require.package_id_mode) if require.build: build_data[require] = req_info else: data[require] = req_info if conanfile.vendor: # Make the package_id fully independent of dependencies versions data, build_data = OrderedDict(), OrderedDict() # TODO, cleaner, now minimal diff reqs_info = RequirementsInfo(data) build_requires_info = RequirementsInfo(build_data) python_requires = PythonRequiresInfo(python_requires, python_mode) try: copied_options = conanfile.options.copy_conaninfo_options() except ConanException as e: raise ConanException(f"{conanfile}: {e}") conanfile.info = ConanInfo(settings=conanfile.settings.copy_conaninfo_settings(), options=copied_options, reqs_info=reqs_info, build_requires_info=build_requires_info, python_requires=python_requires, conf=conanfile.conf.copy_conaninfo_conf(), config_version=config_version.copy() if config_version else None) conanfile.original_info = conanfile.info.clone() # To account for effect of headers into consumers, like shared/static variability # It affects to both embed and not embed, that would imply some "repetition" of the information # in embed cases embedding the full package_id, but it is useful to have that info explicit too if conanfile.package_type and conanfile.package_type in [PackageType.SHARED, PackageType.STATIC, PackageType.APP]: for require, transitive in node.transitive_deps.items(): if require.headers: header_opts = getattr(transitive.node.conanfile, "package_id_abi_options", ()) for pkg_id_option in header_opts: v = getattr(transitive.node.conanfile.options, pkg_id_option) setattr(conanfile.info.options[f"{transitive.node.name}/*"], pkg_id_option, v) run_validate_package_id(conanfile, hook_manager) if conanfile.info.settings_target: # settings_target has beed added to conan package via package_id api conanfile.original_info.settings_target = conanfile.info.settings_target info = conanfile.info node.package_id = info.package_id() def run_validate_package_id(conanfile, hook_manager): # IMPORTANT: This validation code must run before calling info.package_id(), to mark "invalid" if hasattr(conanfile, "validate_build"): with conanfile_exception_formatter(conanfile, "validate_build"): with conanfile_remove_attr(conanfile, ['cpp_info'], "validate_build"): try: conanfile.validate_build() except ConanInvalidConfiguration as e: # This 'cant_build' will be ignored if we don't have to build the node. conanfile.info.cant_build = str(e) if hasattr(conanfile, "validate") or hook_manager.validate_hook: with conanfile_exception_formatter(conanfile, "validate"): with conanfile_remove_attr(conanfile, ['cpp_info'], "validate"): try: if hook_manager.validate_hook: hook_manager.execute("pre_validate", conanfile=conanfile) if hasattr(conanfile, "validate"): conanfile.validate() if hook_manager.validate_hook: hook_manager.execute("post_validate", conanfile=conanfile) except ConanInvalidConfiguration as e: conanfile.info.invalid = str(e) # Once we are done, call package_id() to narrow and change possible values if hasattr(conanfile, "package_id"): with conanfile_exception_formatter(conanfile, "package_id"): with conanfile_remove_attr(conanfile, ['cpp_info', 'settings', 'options'], "package_id"): conanfile.package_id() elif "auto_header_only" in conanfile.implements: auto_header_only_package_id(conanfile) if hook_manager.post_package_id_hook: with conanfile_exception_formatter(conanfile, "package_id"): with conanfile_remove_attr(conanfile, ['cpp_info', 'settings', 'options'], "package_id"): hook_manager.execute("post_package_id", conanfile=conanfile) conanfile.info.validate() ================================================ FILE: conan/internal/graph/graph.py ================================================ from collections import OrderedDict from conan.internal.graph.graph_error import GraphError, GraphConflictError from conan.api.output import ConanOutput from conan.api.model import PkgReference from conan.api.model import RecipeReference RECIPE_DOWNLOADED = "Downloaded" RECIPE_INCACHE = "Cache" # The previously installed recipe in cache is being used RECIPE_UPDATED = "Updated" RECIPE_INCACHE_DATE_UPDATED = "Cache (Updated date)" RECIPE_NEWER = "Newer" # The local recipe is modified and newer timestamp than server RECIPE_NOT_IN_REMOTE = "Not in remote" RECIPE_UPDATEABLE = "Update available" # The update of recipe is available (only in conan info) # These recipes do not have a full reference, not in the cache RECIPE_EDITABLE = "Editable" RECIPE_CONSUMER = "Consumer" # A conanfile from the user RECIPE_VIRTUAL = "Cli" # A virtual conanfile (dynamic in memory conanfile) RECIPE_PLATFORM = "Platform" BINARY_CACHE = "Cache" BINARY_DOWNLOAD = "Download" BINARY_UPDATE = "Update" BINARY_BUILD = "Build" BINARY_MISSING = "Missing" BINARY_SKIP = "Skip" BINARY_EDITABLE = "Editable" BINARY_EDITABLE_BUILD = "EditableBuild" BINARY_INVALID = "Invalid" BINARY_PLATFORM = "Platform" CONTEXT_HOST = "host" CONTEXT_BUILD = "build" class TransitiveRequirement: def __init__(self, require, node): self.require = require self.node = node def __repr__(self): return "Require: {}, Node: {}".format(repr(self.require), repr(self.node)) class Node: def __init__(self, ref, conanfile, context, recipe=None, path=None, test=False): self.ref = ref self.path = path # path to the consumer conanfile.xx for consumer, None otherwise self._package_id = None self.prev = None self.pref_timestamp = None if conanfile is not None: conanfile._conan_node = self # Reference to self, to access data self.conanfile = conanfile self.binary = None self.recipe = recipe self.remote = None self.binary_remote = None self.context = context self.test = test # real graph model self.transitive_deps = OrderedDict() # of _TransitiveRequirement self.edges = [] # Ordered Edges self.dependants = [] # Edges self.error = None self.should_build = False # If the --build or policy wants to build this binary self.build_allowed = False self.is_conf = False self.replaced_requires = {} # To track the replaced requires for self.edges[old-ref] self.skipped_build_requires = False @property def dependencies(self): ConanOutput().warning("Node.dependencies is private and shouldn't be used. It is now " "node.edges. Please fix your code, Node.dependencies will be removed " "in future versions", warn_tag="deprecated") return self.edges def subgraph(self): nodes = [self] opened = [self] while opened: new_opened = [] for o in opened: for n in o.neighbors(): if n not in nodes: nodes.append(n) if n not in opened: new_opened.append(n) opened = new_opened graph = DepsGraph() graph.nodes = nodes return graph def __lt__(self, other): """ :type other: Node """ # TODO: Remove this order, shouldn't be necessary return (str(self.ref), self._package_id) < (str(other.ref), other._package_id) def propagate_closing_loop(self, require, prev_node, visibility_conflicts): self.propagate_downstream(require, prev_node, visibility_conflicts) # List to avoid mutating the dict for transitive in list(prev_node.transitive_deps.values()): # TODO: possibly optimize in a bulk propagate if transitive.require.override: continue prev_node.propagate_downstream(transitive.require, transitive.node, visibility_conflicts, self) def propagate_downstream(self, require, node, visibility_conflicts, src_node=None): # print(" Propagating downstream ", self, "<-", require) assert node is not None # This sets the transitive_deps node if it was None (overrides) # Take into account that while propagating we can find RUNTIME shared conflicts we # didn't find at check_downstream_exist, because we didn't know the shared/static existing = self.transitive_deps.get(require) if existing is not None and existing.require is not require: if existing.node is not None and existing.node.ref != node.ref: # print(" +++++Runtime conflict!", require, "with", node.ref) raise GraphConflictError(self, require, existing.node, existing.require, node) ill_formed = ((require.direct or existing.require.direct) and require.visible != existing.require.visible) if ill_formed and not (require.test or existing.require.test): visibility_conflicts.setdefault(require.ref, set()).add(self.ref) require.aggregate(existing.require) # An override can be overriden by a downstream force/override if existing.require.override and existing.require.ref != require.ref: # If it is an override, but other value, it has been overriden too existing.require.overriden_ref = existing.require.ref existing.require.override_ref = require.ref assert not require.version_range # No ranges slip into transitive_deps definitions # TODO: Might need to move to an update() for performance poped = self.transitive_deps.pop(require, None) self.transitive_deps[require] = TransitiveRequirement(require, node) if poped is not None: # adjust .edges, to avoid orphans for e in self.edges: if e.dst is poped.node: # check for identity, pointing to that node e.dst = node break if self.conanfile.vendor: return # Check if need to propagate downstream if not self.dependants: return if src_node is not None: # This happens when closing a loop, and we need to know the edge d = next(d for d in self.dependants if d.src is src_node) else: assert len(self.dependants) == 1 d = self.dependants[0] down_require = d.require.transform_downstream(self.conanfile.package_type, require, node.conanfile.package_type) if down_require is None: return down_require.defining_require = require.defining_require # If the requirement propagates .files downstream, cannot be skipped # But if the files are not needed in this graph branch, can be marked "Skip" if down_require.files: down_require.required_nodes = require.required_nodes.copy() down_require.required_nodes.add(self) d.src.propagate_downstream(down_require, node, visibility_conflicts) def check_downstream_exists(self, require): # First, a check against self, could be a loop-conflict # This is equivalent as the Requirement hash and eq methods # TODO: Make self.ref always exist, but with name=None if name not defined if self.ref is not None and require.ref.name == self.ref.name: if require.build and (self.context == CONTEXT_HOST or # switch context require.ref.version != self.ref.version): # or different version pass elif require.visible is False: # and require.ref.version != self.ref.version: # Experimental, to support repackaging of openssl previous versions FIPS plugins pass # An invisible require doesn't conflict with itself else: return None, self, self # First is the require, as it is a loop => None # First do a check against the current node dependencies prev = self.transitive_deps.get(require) # print(" Transitive deps", self.transitive_deps) # (" THERE IS A PREV ", prev, "in node ", self, " for require ", require) # Overrides: The existing require could be itself, that was just added result = None if prev and (prev.require is not require or prev.node is not None): result = prev.require, prev.node, self # Do not return yet, keep checking downstream, because downstream overrides or forces # have priority # Check if need to propagate downstream # Then propagate downstream if self.conanfile.vendor: return result # Seems the algrithm depth-first, would only have 1 dependant at most to propagate down # at any given time if not self.dependants: return result assert len(self.dependants) == 1 dependant = self.dependants[0] # TODO: Implement an optimization where the requires is checked against a graph global # print(" Lets check_downstream one more") down_require = dependant.require.transform_downstream(self.conanfile.package_type, require, None) if down_require is None: # print(" No need to check downstream more") return result down_require.defining_require = require.defining_require source_node = dependant.src return source_node.check_downstream_exists(down_require) or result def check_loops(self, new_node, count=0): if self.ref == new_node.ref and self.context == new_node.context: if count >= 1: return self count += 1 if not self.dependants: return assert len(self.dependants) == 1 dependant = self.dependants[0] source_node = dependant.src return source_node.check_loops(new_node, count) @property def package_id(self): return self._package_id @package_id.setter def package_id(self, pkg_id): assert self._package_id is None, "Trying to override an existing package_id" self._package_id = pkg_id @property def name(self): return self.ref.name if self.ref else None @property def pref(self): assert self.ref is not None and self.package_id is not None, "Node %s" % self.recipe return PkgReference(self.ref, self.package_id, self.prev, self.pref_timestamp) def add_edge(self, edge): if edge.src == self: self.edges.append(edge) else: self.dependants.append(edge) def neighbors(self): return [edge.dst for edge in self.edges] def inverse_neighbors(self): return [edge.src for edge in self.dependants] def __repr__(self): return repr(self.conanfile) def serialize(self): result = OrderedDict() result["ref"] = self.ref.repr_notime() if self.ref is not None else "conanfile" result["id"] = getattr(self, "id") # Must be assigned by graph.serialize() result["recipe"] = self.recipe result["package_id"] = self.package_id result["prev"] = self.prev result["rrev"] = self.ref.revision if self.ref is not None else None result["rrev_timestamp"] = self.ref.timestamp if self.ref is not None else None result["prev_timestamp"] = self.pref_timestamp result["remote"] = self.remote.name if self.remote else None result["binary_remote"] = self.binary_remote.name if self.binary_remote else None from conan.internal.graph.installer import build_id result["build_id"] = build_id(self.conanfile) result["binary"] = self.binary # TODO: This doesn't match the model, check it result["invalid_build"] = getattr(getattr(self.conanfile, "info", None), "cant_build", False) result["info_invalid"] = getattr(getattr(self.conanfile, "info", None), "invalid", None) # Adding the conanfile information: settings, options, etc result.update(self.conanfile.serialize()) result.pop("requires", None) # superseded by "dependencies" (graph.transitive_deps) result["dependencies"] = {d.node.id: d.require.serialize() for d in self.transitive_deps.values() if d.node is not None} result["context"] = self.context result["test"] = self.test return result def overrides(self): def transitive_subgraph(): result = set() opened = {self} while opened: new_opened = set() for o in opened: result.add(o) new_opened.update(set(o.neighbors()).difference(result)) opened = new_opened return result nodes = transitive_subgraph() return Overrides.create(nodes) class Edge: def __init__(self, src, dst, require): self.src = src self.dst = dst self.require = require class Overrides: def __init__(self): self._overrides = {} # {require_ref: {override_ref1, override_ref2}} def __bool__(self): return bool(self._overrides) def __repr__(self): return repr(self.serialize()) @staticmethod def create(nodes): overrides = {} for n in nodes: for r in n.conanfile.requires.values(): if r.override and not r.overriden_ref: # overrides are not real graph edges continue if r.overriden_ref: overrides.setdefault(r.overriden_ref, set()).add(r.override_ref) else: overrides.setdefault(r.ref, set()).add(None) # reduce, eliminate those overrides definitions that only override to None, that is, not # really an override result = Overrides() for require, override_info in overrides.items(): if len(override_info) != 1 or None not in override_info: result._overrides[require] = override_info return result def get(self, require): return self._overrides.get(require) def update(self, other): """ :type other: Overrides """ for require, override_info in other._overrides.items(): self._overrides.setdefault(require, set()).update(override_info) def items(self): return self._overrides.items() def serialize(self): return {k.repr_notime(): sorted([e.repr_notime() if e else None for e in v], key=lambda e: "" if e is None else e) for k, v in self._overrides.items()} @staticmethod def deserialize(data): result = Overrides() result._overrides = {RecipeReference.loads(k): set([RecipeReference.loads(e) if e else None for e in v]) for k, v in data.items()} return result class DepsGraph: def __init__(self): self.nodes = [] self.aliased = {} self.resolved_ranges = {} self.replaced_requires = {} self.options_conflicts = {} self.visibility_conflicts = {} self.error = False def lockfile(self): from conan.internal.model.lockfile import Lockfile return Lockfile(self) def overrides(self): return Overrides.create(self.nodes) def __repr__(self): return "\n".join((repr(n) for n in self.nodes)) @property def root(self): return self.nodes[0] if self.nodes else None def add_node(self, node): self.nodes.append(node) @staticmethod def add_edge(src, dst, require): edge = Edge(src, dst, require) src.add_edge(edge) dst.add_edge(edge) def ordered_iterate(self): ordered = self.by_levels() for level in ordered: for node in level: yield node def by_levels(self): """ order by node degree. The first level will be the one which nodes dont have dependencies. Second level will be with nodes that only have dependencies to first level nodes, and so on return [[node1, node34], [node3], [node23, node8],...] """ result = [] # We make it a dict to preserve insertion order and be deterministic, s # sets are not deterministic order. dict is fast for look up operations opened = dict.fromkeys(self.nodes) while opened: current_level = [] for o in opened: o_neighs = o.neighbors() if not any(n in opened for n in o_neighs): current_level.append(o) # TODO: SORTING seems only necessary for test order current_level.sort() result.append(current_level) # now start new level, removing the current level items for item in current_level: opened.pop(item) return result def report_graph_error(self): if self.error: raise self.error def serialize(self): for i, n in enumerate(self.nodes): n.id = str(i) result = OrderedDict() result["nodes"] = {n.id: n.serialize() for n in self.nodes} result["root"] = {self.root.id: repr(self.root.ref)} # TODO: ref of consumer/virtual result["overrides"] = self.overrides().serialize() result["resolved_ranges"] = {repr(r): s.repr_notime() for r, s in self.resolved_ranges.items()} result["replaced_requires"] = {k: v for k, v in self.replaced_requires.items()} result["error"] = self.error.serialize() if isinstance(self.error, GraphError) else None return result ================================================ FILE: conan/internal/graph/graph_binaries.py ================================================ import os from conan.api.output import ConanOutput, Color from conan.internal.cache.home_paths import HomePaths from conan.internal.graph.build_mode import BuildMode from conan.internal.graph.compatibility import BinaryCompatibility from conan.internal.graph.compute_pid import compute_package_id from conan.internal.graph.graph import (BINARY_BUILD, BINARY_CACHE, BINARY_DOWNLOAD, BINARY_MISSING, BINARY_UPDATE, RECIPE_EDITABLE, BINARY_EDITABLE, RECIPE_CONSUMER, RECIPE_VIRTUAL, BINARY_SKIP, BINARY_INVALID, BINARY_EDITABLE_BUILD, RECIPE_PLATFORM, BINARY_PLATFORM) from conan.internal.graph.proxy import should_update_reference from conan.internal.errors import (conanfile_exception_formatter, ConanConnectionError, NotFoundException, PackageNotFoundException) from conan.errors import ConanException from conan.internal.model.conanconfig import loadconanconfig from conan.internal.model.info import RequirementInfo, RequirementsInfo from conan.internal.model.pkg_type import PackageType class GraphBinariesAnalyzer: def __init__(self, cache, remote_manager, home_folder, global_conf, hook_manager): self._cache = cache self._home_folder = home_folder self._global_conf = global_conf self._remote_manager = remote_manager self._hook_manager = hook_manager # These are the nodes with pref (not including PREV) that have been evaluated self._evaluated = {} # {pref: [nodes]} compat_folder = HomePaths(home_folder).compatibility_plugin_path self._compatibility = BinaryCompatibility(compat_folder, hook_manager) unknown_mode = global_conf.get("core.package_id:default_unknown_mode", default="semver_mode") non_embed = global_conf.get("core.package_id:default_non_embed_mode", default="minor_mode") # recipe_revision_mode already takes into account the package_id embed_mode = global_conf.get("core.package_id:default_embed_mode", default="full_mode") python_mode = global_conf.get("core.package_id:default_python_mode", default="minor_mode") build_mode = global_conf.get("core.package_id:default_build_mode", default=None) self._modes = unknown_mode, non_embed, embed_mode, python_mode, build_mode self._warn_about_new_compatibility = False @staticmethod def _evaluate_build(node, build_mode): ref, conanfile = node.ref, node.conanfile with_deps_to_build = False # check dependencies, if they are being built, "cascade" will try to build this one too if build_mode.cascade: with_deps_to_build = any(edge.dst.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD) for edge in node.edges) if build_mode.forced(conanfile, ref, with_deps_to_build): node.should_build = True conanfile.output.info('Forced build from source') node.binary = BINARY_BUILD if not conanfile.info.cant_build else BINARY_INVALID node.prev = None return True @staticmethod def _evaluate_clean_pkg_folder_dirty(node, package_layout): # Check if dirty, to remove it with package_layout.package_lock(): assert node.recipe != RECIPE_EDITABLE, "Editable package shouldn't reach this code" if package_layout.package_is_dirty(): node.conanfile.output.warning("Package binary is corrupted, " "removing: %s" % node.package_id) package_layout.package_remove() return True # check through all the selected remotes: # - if not --update: get the first package found # - if --update: get the latest remote searching in all of them def _get_package_from_remotes(self, node, remotes, update): results = [] pref = node.pref for r in remotes: try: info = node.conanfile.info latest_pref = self._remote_manager.get_latest_package_revision(pref, r, info) results.append({'pref': latest_pref, 'remote': r}) if len(results) > 0 and not should_update_reference(node.ref, update): break except NotFoundException: pass except ConanConnectionError: ConanOutput().error(f"Failed checking for binary '{pref}' in remote '{r.name}': " "remote not available") raise if not remotes and should_update_reference(node.ref, update): node.conanfile.output.warning("Can't update, there are no remotes defined") if len(results) > 0: remotes_results = sorted(results, key=lambda k: k['pref'].timestamp, reverse=True) result = remotes_results[0] node.prev = result.get("pref").revision node.pref_timestamp = result.get("pref").timestamp node.binary_remote = result.get('remote') else: node.binary_remote = None node.prev = None raise PackageNotFoundException(pref) def _evaluate_is_cached(self, node): """ Each pref has to be evaluated just once, and the action for all of them should be exactly the same """ pref = node.pref previous_nodes = self._evaluated.get(pref) if previous_nodes: previous_nodes.append(node) previous_node = previous_nodes[0] node.binary = previous_node.binary node.binary_remote = previous_node.binary_remote node.prev = previous_node.prev node.pref_timestamp = previous_node.pref_timestamp node.should_build = previous_node.should_build node.build_allowed = previous_node.build_allowed # this line fixed the compatible_packages with private case. # https://github.com/conan-io/conan/issues/9880 node._package_id = previous_node.package_id return True self._evaluated[pref] = [node] def _compatible_get_packages(self, node): # Evaluate the ``compatibility() methods to compute the compatible hypothesis variants conanfile = node.conanfile original_package_id = node.package_id compatibles = self._compatibility.compatibles(conanfile) existing = compatibles.pop(original_package_id, None) # Skip main package_id if existing: # Skip the check if same package_id conanfile.output.debug(f"Compatible package ID {original_package_id} equal to " "the default package ID: Skipping it.") return compatibles @staticmethod def _compatible_found(conanfile, pkg_id, compatible_pkg): diff = conanfile.info.dump_diff(compatible_pkg) conanfile.output.success(f"Found compatible package '{pkg_id}': {diff}") # So they are available in package_info() method conanfile.info = compatible_pkg # Redefine current # TODO: Improve this interface # The package_id method might have modified the settings to erase information, # ensure we allow those new values conanfile.settings = conanfile.settings.copy_conaninfo_settings() conanfile.settings.update_values(compatible_pkg.settings.values_list) # Trick to allow mutating the options (they were freeze=True) conanfile.options = conanfile.options.copy_conaninfo_options() conanfile.options.update_options(compatible_pkg.options) def _compatible_find_existing_binaries(self, node, compatibles, remotes, update): # Do the actual search in the cache and remotes for the compatible package-ids conanfile = node.conanfile original_binary = node.binary original_package_id = node.package_id conanfile.output.info(f"Main binary package '{original_package_id}' missing") conanfile.output.info(f"Checking {len(compatibles)} compatible configurations") compatibility_mode = self._global_conf.get("core.graph:compatibility_mode", choices=("optimized",)) use_compatibility_optimization = compatibility_mode == "optimized" if not should_update_reference(conanfile.ref, update): # First look all in the cache for package_id, compatible_package in compatibles.items(): node._package_id = package_id # Modifying package id under the hood, FIXME # Check that this same reference hasn't already been checked if self._evaluate_is_cached(node): # If we have already processed this compatible pref, # mark it as usable based on previous evaluation if node.binary in (BINARY_CACHE, BINARY_DOWNLOAD): self._compatible_found(conanfile, package_id, compatible_package) return cache_latest_prev = self._compatible_cache_latest_prev(node) # not check remotes if cache_latest_prev: # If we have binary info, it means that the package was already processed, # and we got a hit from the cache of compatibles self._binary_in_cache(node, cache_latest_prev) self._compatible_found(conanfile, package_id, compatible_package) return # If not found in the cache, then look for the first one in servers conanfile.output.info(f"Compatible configurations not found in cache, checking servers") if use_compatibility_optimization: compatible_packages = self._compatible_get_packages_from_remotes(node.ref, remotes) candidates = {pkg_id: pkg for pkg_id, pkg in compatibles.items() if pkg_id in compatible_packages} node.conanfile.output.info(f"Found {len(candidates)} compatible configurations " f"in remotes") else: candidates = compatibles compatible_packages = {} self._warn_about_new_compatibility = True for package_id, compatible_package in candidates.items(): conanfile.output.info(f"'{package_id}': " f"{conanfile.info.dump_diff(compatible_package)}") node._package_id = package_id # Modifying package id under the hood, FIXME # We already know which remotes have that package_id available_remotes = compatible_packages.get(package_id, remotes) self._evaluate_download(node, available_remotes, update=False) if node.binary == BINARY_DOWNLOAD: self._compatible_found(conanfile, package_id, compatible_package) return else: # Need to check in servers too for the latest thing if use_compatibility_optimization: compatible_packages = self._compatible_get_packages_from_remotes(node.ref, remotes) else: compatible_packages = {} self._warn_about_new_compatibility = True for package_id, compatible_package in compatibles.items(): conanfile.output.info(f"'{package_id}': " f"{conanfile.info.dump_diff(compatible_package)}") node._package_id = package_id # Modifying package id under the hood, FIXME if self._evaluate_is_cached(node): # If we have already processed this compatible pref, # mark it as usable based on previous evaluation if node.binary in (BINARY_CACHE, BINARY_DOWNLOAD, BINARY_UPDATE): self._compatible_found(conanfile, package_id, compatible_package) return cache_latest_prev = self._compatible_cache_latest_prev(node) # Not check remotes available_remotes = compatible_packages.get(package_id, [] if use_compatibility_optimization else remotes) if cache_latest_prev: self._evaluate_cache_update(cache_latest_prev, node, available_remotes, update) else: if available_remotes: self._evaluate_download(node, available_remotes, update) else: # If not in remotes, mark as missing, no need for further checks node.binary = BINARY_MISSING if node.binary in (BINARY_CACHE, BINARY_UPDATE, BINARY_DOWNLOAD): self._compatible_found(conanfile, package_id, compatible_package) return node.conanfile.output.info("No compatible configuration found", fg=Color.BRIGHT_CYAN) # If no compatible is found, restore original state node.binary = original_binary node._package_id = original_package_id def _compatible_cache_latest_prev(self, node): """ simplified checking of compatible_packages, that should be found existing, but will never be built, for example. They cannot be editable either at this point. """ # Obtain the cache_latest valid one, cleaning things if dirty while True: package_layout = self._cache.pkg_layout_latest(node.pref) cache_latest_prev = package_layout.reference if package_layout else None if cache_latest_prev is None: break if not self._evaluate_clean_pkg_folder_dirty(node, package_layout): break return cache_latest_prev @staticmethod def _binary_in_cache(node, cache_latest_prev): assert cache_latest_prev.revision node.binary = BINARY_CACHE node.binary_remote = None node.prev = cache_latest_prev.revision node.pref_timestamp = cache_latest_prev.timestamp def _compatible_get_packages_from_remotes(self, ref, remotes): """ Get available package ids in remotes for the given node reference """ results = {} for remote in remotes: try: remote_prefs = self._remote_manager.search_packages(remote, ref, list_only=True) if remote_prefs: for remote_pref in remote_prefs: results.setdefault(remote_pref.package_id, []).append(remote) except NotFoundException: # Not finding the reference in the remote is not an error, just continue pass except ConanConnectionError: ConanOutput().error(f"Failed finding for package ids '{ref}' in " f"remote '{remote.name}': remote not available") raise return results def _compatible_find_build_binary(self, node, compatibles): output = node.conanfile.output output.info(f"Requested binary package '{node.package_id}' invalid, can't be built") output.info(f"Checking {len(compatibles)} configurations, to build a compatible one, " f"as requested by '--build=compatible'") for pkg_id, compatible in compatibles.items(): if not compatible.cant_build: node._package_id = pkg_id # Modifying package id under the hood, FIXME self._compatible_found(node.conanfile, pkg_id, compatible) node.binary = BINARY_BUILD return def _evaluate_node(self, node, build_mode, remotes, update): assert node.binary is None, "Node.binary should be None" assert node.package_id is not None, "Node.package_id shouldn't be None" assert node.prev is None, "Node.prev should be None" # Check that this same reference hasn't already been checked if self._evaluate_is_cached(node): return self._process_node(node, build_mode, remotes, update) compatibles = None if node.binary == BINARY_MISSING \ and not build_mode.should_build_missing(node.conanfile) and not node.should_build: compatibles = self._compatible_get_packages(node) if compatibles: self._compatible_find_existing_binaries(node, compatibles, remotes, update) if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile): node.should_build = True node.build_allowed = True node.binary = BINARY_BUILD if not node.conanfile.info.cant_build else BINARY_INVALID if node.binary == BINARY_INVALID and build_mode.allowed_compatible(node.conanfile): if compatibles is None: compatibles = self._compatible_get_packages(node) if compatibles: # Before deciding to build a compatible binary, we check if it exists self._compatible_find_existing_binaries(node, compatibles, remotes, update) # And only if not found, we consider to build it if node.binary == BINARY_INVALID: self._compatible_find_build_binary(node, compatibles) if node.binary == BINARY_BUILD: conanfile = node.conanfile if conanfile.vendor and not conanfile.conf.get("tools.graph:vendor", choices=("build",)): node.conanfile.info.invalid = f"The package '{conanfile.ref}' is a vendoring one, " \ f"needs to be built from source, but it " \ "didn't enable 'tools.graph:vendor=build' to compute" \ " its dependencies" node.binary = BINARY_INVALID if any(n.node.binary in (BINARY_EDITABLE, BINARY_EDITABLE_BUILD) for n in node.transitive_deps.values()): conanfile.output.warning("Package is being built in the cache using editable " "dependencies, this is dangerous", warn_tag="risk") def _process_node(self, node, build_mode, remotes, update): if node.conanfile.info.invalid: node.binary = BINARY_INVALID return if node.recipe == RECIPE_PLATFORM: node.binary = BINARY_PLATFORM return if node.recipe == RECIPE_EDITABLE: # TODO: Check what happens when editable is passed an Invalid configuration if build_mode.editable or self._evaluate_build(node, build_mode) or \ build_mode.should_build_missing(node.conanfile): node.binary = BINARY_EDITABLE_BUILD else: node.binary = BINARY_EDITABLE # TODO: PREV? return # If the CLI says this package needs to be built, it doesn't make sense to mark # it as invalid if self._evaluate_build(node, build_mode): return # Obtain the cache_latest valid one, cleaning things if dirty while True: package_layout = self._cache.pkg_layout_latest(node.pref) cache_latest_prev = package_layout.reference if package_layout else None if cache_latest_prev is None: break if not self._evaluate_clean_pkg_folder_dirty(node, package_layout): break if node.conanfile.upload_policy == "skip": # Download/update shouldn't be checked in the servers if this is "skip-upload" # The binary can only be in cache or missing. if cache_latest_prev: self._binary_in_cache(node, cache_latest_prev) else: node.binary = BINARY_MISSING elif cache_latest_prev is None: # This binary does NOT exist in the cache self._evaluate_download(node, remotes, update) else: # This binary already exists in the cache, maybe can be updated self._evaluate_cache_update(cache_latest_prev, node, remotes, update) def _process_locked_node(self, node, build_mode, locked_prev): # Check that this same reference hasn't already been checked if self._evaluate_is_cached(node): return # If the CLI says this package needs to be built, it doesn't make sense to mark # it as invalid if self._evaluate_build(node, build_mode): # TODO: We migth want to rais if strict return if node.recipe == RECIPE_EDITABLE: # TODO: Raise if strict node.binary = BINARY_EDITABLE # TODO: PREV? return # in cache: node.prev = locked_prev if self._cache.exists_prev(node.pref): node.binary = BINARY_CACHE node.binary_remote = None # TODO: Dirty return # TODO: Check in remotes for download def _evaluate_download(self, node, remotes, update): try: self._get_package_from_remotes(node, remotes, update) except NotFoundException: node.binary = BINARY_MISSING else: node.binary = BINARY_DOWNLOAD def _evaluate_cache_update(self, cache_latest_prev, node, remotes, update): assert cache_latest_prev.revision if should_update_reference(node.ref, update): output = node.conanfile.output try: self._get_package_from_remotes(node, remotes, update) except NotFoundException: output.warning("Can't update, no package in remote") else: cache_time = cache_latest_prev.timestamp # TODO: cache 2.0 should we update the date if the prev is the same? if cache_time < node.pref_timestamp and cache_latest_prev != node.pref: node.binary = BINARY_UPDATE output.info("Current package revision is older than the remote one") return if cache_time > node.pref_timestamp: output.info("Current package revision is newer than the remote one") # The cache latest prev was there, if the server one didn't result as UPDATE # Then resolve to the cache latest prev self._binary_in_cache(node, cache_latest_prev) def _config_version(self): config_mode = self._global_conf.get("core.package_id:config_mode", default=None) if config_mode is None: return config_version_file = HomePaths(self._home_folder).config_version_path try: config_refs = loadconanconfig(config_version_file) result = {r: RequirementInfo(r, None, config_mode) for r in config_refs} except Exception as e: raise ConanException(f"core.package_id:config_mode defined, but error while loading " f"'{os.path.basename(config_version_file)}'" f" file in cache: {self._home_folder}: {e}") return RequirementsInfo(result) def _evaluate_package_id(self, node, config_version): compute_package_id(node, self._modes, config_version, self._hook_manager) # TODO: layout() execution don't need to be evaluated at GraphBuilder time. # it could even be delayed until installation time, but if we got enough info here for # package_id, we can run it conanfile = node.conanfile if hasattr(conanfile, "layout"): with conanfile_exception_formatter(conanfile, "layout"): conanfile.layout() def evaluate_graph(self, deps_graph, build_mode, lockfile, remotes, update, build_mode_test=None, tested_graph=None): if tested_graph is None: main_mode = BuildMode(build_mode) test_mode = None # Should not be used at all mainprefs = None else: main_mode = BuildMode(["never"]) test_mode = BuildMode(build_mode_test) mainprefs = [str(n.pref) for n in tested_graph.nodes if n.recipe not in (RECIPE_CONSUMER, RECIPE_VIRTUAL)] def _evaluate_single(n): mode = main_mode if mainprefs is None or str(n.pref) in mainprefs else test_mode if lockfile: locked_prev = lockfile.resolve_prev(n) # this is not public, should never happen if locked_prev: self._process_locked_node(n, mode, locked_prev) return self._evaluate_node(n, mode, remotes, update) levels = deps_graph.by_levels() # When creating a "conan config install-pkg" package, it should be independent of conf root_pkg_type = deps_graph.root.edges[0].dst.conanfile.package_type \ if deps_graph.root.edges else None config_version = self._config_version() if root_pkg_type is not PackageType.CONF else None for level in levels[:-1]: # all levels but the last one, which is the single consumer for node in level: self._evaluate_package_id(node, config_version) # group by pref to paralelize, so evaluation is done only 1 per pref nodes = {} for node in level: nodes.setdefault(node.pref, []).append(node) # PARALLEL, this is the slow part that can query servers for packages, and compatibility for pref, pref_nodes in nodes.items(): _evaluate_single(pref_nodes[0]) # END OF PARALLEL # Evaluate the possible nodes with repeated "prefs" that haven't been evaluated for pref, pref_nodes in nodes.items(): for n in pref_nodes[1:]: assert self._evaluate_is_cached(n) # The pref is the same, must exist cached if self._warn_about_new_compatibility: (ConanOutput().info("\nA new experimental approach for binary compatibility detection " "is available.\n" " Enable it by setting the ", newline=False) .info('core.graph:compatibility_mode=optimized', newline=False, fg=Color.BRIGHT_YELLOW) .info(" conf\n and get improved performance when querying multiple " "compatible binaries in remotes.\n")) # Last level is always necessarily a consumer or a virtual assert len(levels[-1]) == 1 node = levels[-1][0] assert node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL) if node.path is not None: if node.path.endswith(".py"): # For .py we keep evaluating the package_id, validate(), etc compute_package_id(node, self._modes, config_version, self._hook_manager) # To support the ``[layout]`` in conanfile.txt if hasattr(node.conanfile, "layout"): with conanfile_exception_formatter(node.conanfile, "layout"): node.conanfile.layout() self._skip_binaries(deps_graph) @staticmethod def _skip_binaries(graph): required_nodes = set() # Aggregate all necessary starting nodes required_nodes.add(graph.root) for node in graph.nodes: if node.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE): if node.skipped_build_requires: raise ConanException(f"Package {node.ref} skipped its test/tool requires with " f"tools.graph:skip_build, but was marked to be built ") can_skip = node.conanfile.conf.get("tools.graph:skip_binaries", check_type=bool, default=True) # Only those that are forced to build, not only "missing" if not node.build_allowed or not can_skip: required_nodes.add(node) root_nodes = required_nodes.copy() while root_nodes: new_root_nodes = set() for node in root_nodes: # The nodes that are directly required by this one to build correctly is_consumer = not (node.recipe != RECIPE_CONSUMER and node.binary not in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE)) deps_required = set() for req, t in node.transitive_deps.items(): if req.files or (req.direct and is_consumer): deps_required.add(t.node) deps_required.update(req.required_nodes) # Third pass, mark requires as skippeable for dep in node.transitive_deps.values(): dep.require.skip = dep.node not in deps_required # Finally accumulate all needed nodes for marking binaries as SKIP download news_req = [r for r in deps_required if (r.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE) or any(req.no_skip for req in r.transitive_deps)) if r not in required_nodes] # Avoid already expanded before new_root_nodes.update(news_req) # For expanding the next iteration required_nodes.update(deps_required) root_nodes = new_root_nodes for node in graph.nodes: if node not in required_nodes and node.conanfile.conf.get("tools.graph:skip_binaries", check_type=bool, default=True): node.binary = BINARY_SKIP ================================================ FILE: conan/internal/graph/graph_builder.py ================================================ import os from collections import deque from conan.internal.cache.conan_reference_layout import BasicLayout from conan.internal.methods import run_configure_method from conan.internal.model.recipe_ref import ref_matches from conan.internal.graph.graph import DepsGraph, Node, CONTEXT_HOST, \ CONTEXT_BUILD, TransitiveRequirement, RECIPE_VIRTUAL, RECIPE_EDITABLE, RECIPE_CONSUMER from conan.internal.graph.graph import RECIPE_PLATFORM from conan.internal.graph.graph_error import (GraphLoopError, GraphConflictError, GraphMissingError, GraphError) from conan.internal.graph.profile_node_definer import initialize_conanfile_profile from conan.internal.graph.provides import check_graph_provides from conan.errors import ConanException from conan.internal.model.conan_file import ConanFile from conan.internal.model.options import Options, _PackageOptions from conan.internal.model.pkg_type import PackageType from conan.api.model import RecipeReference from conan.internal.model.requires import Requirement from conan.internal.model.version_range import VersionRange class DepsGraphBuilder: ALLOW_ALIAS = False def __init__(self, proxy, loader, resolver, cache, remotes, update, check_update, global_conf): self._proxy = proxy self._loader = loader self._resolver = resolver self._cache = cache self._remotes = remotes # TODO: pass as arg to load_graph() self._update = update self._check_update = check_update self._resolve_prereleases = global_conf.get('core.version_ranges:resolve_prereleases') def load_graph(self, root_node, profile_host, profile_build, graph_lock=None): assert profile_host is not None assert profile_build is not None assert isinstance(profile_host.options, Options) assert isinstance(profile_build.options, Options) # print("Loading graph") dep_graph = DepsGraph() is_test_package = getattr(root_node.conanfile, "tested_reference_str", None) define_consumers = root_node.recipe == RECIPE_VIRTUAL or is_test_package self._prepare_node(root_node, profile_host, profile_build, Options(), define_consumers) rs = self._initialize_requires(root_node, dep_graph, graph_lock, profile_build, profile_host) dep_graph.add_node(root_node) open_requires = deque((r, root_node) for r in rs) try: while open_requires: # Fetch the first waiting to be expanded (depth-first) (require, node) = open_requires.popleft() if require.override: continue new_node = self._expand_require(require, node, dep_graph, profile_host, profile_build, graph_lock) if new_node and (not new_node.conanfile.vendor or new_node.recipe == RECIPE_EDITABLE or new_node.conanfile.conf.get("tools.graph:vendor", choices=("build",))): newr = self._initialize_requires(new_node, dep_graph, graph_lock, profile_build, profile_host) open_requires.extendleft((r, new_node) for r in reversed(newr)) self._remove_overrides(dep_graph) self._remove_orphans(dep_graph) check_graph_provides(dep_graph) except GraphError as e: dep_graph.error = e dep_graph.resolved_ranges = self._resolver.resolved_ranges refs = set(n.ref for n in dep_graph.nodes if n.recipe not in (RECIPE_VIRTUAL, RECIPE_EDITABLE, RECIPE_CONSUMER, RECIPE_PLATFORM)) self._cache.update_recipes_lru(refs) return dep_graph def _expand_require(self, require, node, graph, profile_host, profile_build, graph_lock): # Handle a requirement of a node. There are 2 possibilities # node -(require)-> new_node (creates a new node in the graph) # node -(require)-> previous (creates a diamond with a previously existing node) # TODO: allow bootstrapping, use references instead of names # print(" Expanding require ", node, "->", require) self._deduce_host_version(require, node) previous = node.check_downstream_exists(require) prev_node = None if previous is not None: prev_require, prev_node, base_previous = previous # print(" Existing previous requirements from ", base_previous, "=>", prev_require) if prev_require is None: raise GraphLoopError(node, require, prev_node) prev_ref = prev_node.ref if prev_node else prev_require.ref if prev_require.force or prev_require.override: # override if prev_require.defining_require is not require: require.overriden_ref = require.overriden_ref or require.ref.copy() # Old one # require.override_ref can be !=None if lockfile-overrides defined require.override_ref = (require.override_ref or prev_require.override_ref or prev_require.ref.copy()) # New one require.defining_require = prev_require.defining_require # The overrider require.ref = prev_ref # New one, maybe resolved with revision else: self._conflicting_version(require, node, prev_require, prev_node, prev_ref, base_previous, self._resolve_prereleases) if prev_node is None: # new node, must be added and expanded (node -> new_node) new_node = self._create_new_node(node, require, graph, profile_host, profile_build, graph_lock) return new_node else: # print("Closing a loop from ", node, "=>", prev_node) # Keep previous "test" status only if current is also test prev_node.test = prev_node.test and (node.test or require.test) self._save_options_conflicts(node, require, prev_node, graph) require.process_package_type(node, prev_node) graph.add_edge(node, prev_node, require) node.propagate_closing_loop(require, prev_node, graph.visibility_conflicts) def _save_options_conflicts(self, node, require, prev_node, graph): """ Store the discrepancies of options when closing a diamond, to later report them. This list is not exhaustive, only the diamond vertix, not other transitives """ down_options = self._compute_down_options(node, require, prev_node.ref) down_options = down_options._deps_package_options # noqa if not down_options: return down_pkg_options = _PackageOptions() for pattern, options in down_options.items(): if ref_matches(prev_node.ref, pattern, is_consumer=False): down_pkg_options.update_options(options) prev_options = {k: v for k, v in prev_node.conanfile.options.items()} for k, v in down_pkg_options.items(): prev_value = prev_options.get(k) if prev_value is not None and prev_value != v: d = graph.options_conflicts.setdefault(str(prev_node.ref), {}) conflicts = d.setdefault(k, {"value": prev_value}).setdefault("conflicts", []) conflicts.append((node.ref, v)) @staticmethod def _conflicting_version(require, node, prev_require, prev_node, prev_ref, base_previous, resolve_prereleases): # As we are closing a diamond, there can be conflicts. This will raise if so version_range = require.version_range prev_version_range = prev_require.version_range if prev_node is None else None if version_range: if require.ref.user != prev_require.ref.user or \ require.ref.channel != prev_require.ref.channel: raise GraphConflictError(node, require, prev_node, prev_require, base_previous) if prev_version_range is not None: # It it is not conflicting, but range can be incompatible, restrict range restricted_version_range = version_range.intersection(prev_version_range) if restricted_version_range is None: raise GraphConflictError(node, require, prev_node, prev_require, base_previous) require.ref.version = restricted_version_range.version() else: if version_range.contains(prev_ref.version, resolve_prereleases): require.ref = prev_ref else: raise GraphConflictError(node, require, prev_node, prev_require, base_previous) elif prev_version_range is not None: if require.ref.user != prev_require.ref.user or \ require.ref.channel != prev_require.ref.channel or \ not prev_version_range.contains(require.ref.version, resolve_prereleases): raise GraphConflictError(node, require, prev_node, prev_require, base_previous) else: if prev_ref != require.ref: raise GraphConflictError(node, require, prev_node, prev_require, base_previous) # If there is no conflict, then the incomplete require without revision can be updated # with the previous revision to avoid the later conflict if prev_ref.revision is not None and require.ref.revision is None: require.ref.revision = prev_ref.revision @staticmethod def _prepare_node(node, profile_host, profile_build, down_options, define_consumers=False): # basic node configuration: calling configure() and requirements() conanfile, ref = node.conanfile, node.ref profile_options = profile_host.options if node.context == CONTEXT_HOST \ else profile_build.options assert isinstance(profile_options, Options), type(profile_options) run_configure_method(conanfile, down_options, profile_options, ref) if define_consumers: # Mark this requirements as the "consumers" nodes tested_ref = getattr(conanfile, "tested_reference_str", None) tested_ref = RecipeReference.loads(tested_ref) if tested_ref else None for r in conanfile.requires.values(): if tested_ref is None or r.ref == tested_ref: r.is_consumer = True # Apply build_tools_requires from profile, overriding the declared ones profile = profile_host if node.context == CONTEXT_HOST else profile_build for pattern, tool_requires in profile.tool_requires.items(): if ref_matches(ref, pattern, is_consumer=conanfile._conan_is_consumer): # noqa for tool_require in tool_requires: # Do the override # Check if it is a self-loop of build-requires in build context and avoid it if ref and tool_require.name == ref.name and tool_require.user == ref.user and \ tool_require.channel == ref.channel: if tool_require.version == ref.version: continue # Also avoid it if the version is in the range version_range = repr(tool_require.version) if version_range[0] == "[" and version_range[-1] == "]": if ref.version.in_range(version_range[1:-1]): continue node.conanfile.requires.tool_require(tool_require.repr_notime(), raise_if_duplicated=False) def _initialize_requires(self, node, graph, graph_lock, profile_build, profile_host): result = [] skip_build = node.conanfile.conf.get("tools.graph:skip_build", check_type=bool) skip_test = node.conanfile.conf.get("tools.graph:skip_test", check_type=bool) for require in node.conanfile.requires.values(): if not require.visible and not require.package_id_mode: if skip_build and require.build: node.skipped_build_requires = True continue if skip_test and require.test: continue result.append(require) alias = require.alias # alias needs to be processed this early if alias is not None: if not DepsGraphBuilder.ALLOW_ALIAS and os.getenv("CONAN_ALLOW_ALIAS") != "will_break_next": raise ConanException(f"Alias requirements have been removed: '{node}' requiring: '{alias}'") resolved = False if graph_lock is not None: resolved = graph_lock.replace_alias(require, alias) # if partial, we might still need to resolve the alias if not resolved: self._resolve_alias(node, require, alias, graph) self._resolve_replace_requires(node, require, profile_build, profile_host, graph) if graph_lock: graph_lock.resolve_overrides(require, node.context) node.transitive_deps[require] = TransitiveRequirement(require, node=None) return result def _resolve_alias(self, node, require, alias, graph): # First try cached cached = graph.aliased.get(alias) if cached is not None: while True: new_cached = graph.aliased.get(cached) if new_cached is None: break else: cached = new_cached require.ref = cached return while alias is not None: # if not cached, then resolve try: result = self._proxy.get_recipe(alias, self._remotes, self._update, self._check_update) layout, recipe_status, remote = result except ConanException as e: raise GraphMissingError(node, require, str(e)) conanfile_path = layout.conanfile() dep_conanfile = self._loader.load_basic(conanfile_path) try: pointed_ref = RecipeReference.loads(dep_conanfile.alias) except Exception as e: raise ConanException(f"Alias definition error in {alias}: {str(e)}") # UPDATE THE REQUIREMENT! require.ref = pointed_ref graph.aliased[alias] = pointed_ref # Caching the alias new_req = Requirement(pointed_ref) # FIXME: Ugly temp creation just for alias check alias = new_req.alias node.conanfile.output.warning("Requirement 'alias' is provided in Conan 2 mainly for " "compatibility and upgrade from Conan 1, but it is an " "undocumented and legacy feature. Please update to use " "standard versioning mechanisms", warn_tag="legacy") def _resolve_recipe(self, ref, graph_lock): result = self._proxy.get_recipe(ref, self._remotes, self._update, self._check_update) layout, recipe_status, remote = result conanfile_path = layout.conanfile() dep_conanfile = self._loader.load_conanfile(conanfile_path, ref=ref, graph_lock=graph_lock, remotes=self._remotes, update=self._update, check_update=self._check_update) return layout, dep_conanfile, recipe_status, remote @staticmethod def _resolved_system(node, require, profile_build, profile_host, resolve_prereleases): profile = profile_build if node.context == CONTEXT_BUILD else profile_host if node.context == CONTEXT_BUILD: # If we are in the build context, the platform_tool_requires ALSO applies to the # regular requires. It is not possible in the build context to have tool-requires # and regular requires to the same package from Conan and from Platform system_reqs = profile.platform_tool_requires if not require.build: system_reqs = system_reqs + profile.platform_requires else: system_reqs = profile.platform_tool_requires if require.build \ else profile.platform_requires if system_reqs: version_range = require.version_range for d in system_reqs: if require.ref.name == d.name: if version_range: if version_range.contains(d.version, resolve_prereleases): require.ref.version = d.version # resolved range is replaced by exact require.ref.revision = d.revision or "platform" layout = BasicLayout(require.ref, None) return layout, ConanFile(str(d)), RECIPE_PLATFORM, None elif require.ref.version == d.version: if d.revision is None or require.ref.revision is None or \ d.revision == require.ref.revision: require.ref.revision = d.revision or "platform" layout = BasicLayout(require.ref, None) return layout, ConanFile(str(d)), RECIPE_PLATFORM, None def _resolve_replace_requires(self, node, require, profile_build, profile_host, graph): profile = profile_build if node.context == CONTEXT_BUILD else profile_host replacements = profile.replace_tool_requires if require.build else profile.replace_requires if not replacements: return for pattern, alternative_ref in replacements.items(): if pattern.name != require.ref.name: continue # no match in name if pattern.version != "*": # we need to check versions rrange = require.version_range # Is the version pattern a range itself? pversion = repr(pattern.version) if pversion[0] == "[" and pversion[-1] == "]": prange = VersionRange(pversion[1:-1]) if rrange: valid = prange.intersection(rrange) is not None else: valid = prange.contains(require.ref.version, self._resolve_prereleases) else: if rrange: valid = rrange.contains(pattern.version, self._resolve_prereleases) else: valid = require.ref.version == pattern.version if not valid: continue if pattern.user != "*" and pattern.user != require.ref.user: continue if pattern.channel != "*" and pattern.channel != require.ref.channel: continue require._required_ref = require.ref.copy() # Save the original ref before replacing original_require = repr(require.ref) if alternative_ref.version != "*": require.ref.version = alternative_ref.version if alternative_ref.user != "*": require.ref.user = alternative_ref.user if alternative_ref.channel != "*": require.ref.channel = alternative_ref.channel if alternative_ref.revision != "*": require.ref.revision = alternative_ref.revision if require.ref.name != alternative_ref.name: # This requires re-doing dict! node.conanfile.requires.reindex(require, alternative_ref.name) require.ref.name = alternative_ref.name graph.replaced_requires[original_require] = repr(require.ref) node.replaced_requires[original_require] = require break # First match executes the alternative and finishes checking others @staticmethod def _deduce_host_version(require, node): require_version = str(require.ref.version) if require_version.startswith(""): if not require.build or require.visible: raise ConanException(f"{node.ref} require '{require.ref}': 'host_version' can only " "be used for non-visible tool_requires") tracking_ref = require_version.split(':', 1) ref = require.ref if len(tracking_ref) > 1: ref = RecipeReference.loads(str(require.ref)) ref.name = tracking_ref[1][:-1] # Remove the trailing > req = Requirement(ref, headers=True, libs=True, visible=True) transitive = node.transitive_deps.get(req) if transitive is None: raise ConanException(f"{node.ref} require '{ref}': didn't find a matching " "host dependency") require.ref.version = transitive.require.ref.version def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock): resolved = self._resolved_system(node, require, profile_build, profile_host, self._resolve_prereleases) if graph_lock is not None: # Here is when the ranges and revisions are resolved graph_lock.resolve_locked(node, require, self._resolve_prereleases) if resolved is None: try: # TODO: If it is locked not resolve range # TODO: This range-resolve might resolve in a given remote or cache # Make sure next _resolve_recipe use it self._resolver.resolve(require, str(node.ref), self._remotes, self._update) resolved = self._resolve_recipe(require.ref, graph_lock) except ConanException as e: raise GraphMissingError(node, require, str(e)) layout, dep_conanfile, recipe_status, remote = resolved new_ref = layout.reference dep_conanfile.folders.set_base_recipe_metadata(layout.metadata()) # None for platform_xxx if getattr(require, "is_consumer", None): dep_conanfile._conan_is_consumer = True initialize_conanfile_profile(dep_conanfile, profile_build, profile_host, node.context, require.build, new_ref, parent=node.conanfile) context = CONTEXT_BUILD if require.build else node.context new_node = Node(new_ref, dep_conanfile, context=context, test=require.test or node.test) new_node.recipe = recipe_status new_node.remote = remote down_options = self._compute_down_options(node, require, new_ref) if recipe_status != RECIPE_PLATFORM: self._prepare_node(new_node, profile_host, profile_build, down_options) if dep_conanfile.package_type is PackageType.CONF and node.recipe != RECIPE_VIRTUAL: raise ConanException(f"Configuration package {dep_conanfile} cannot be used as " f"requirement, but {node.ref} is requiring it") require.process_package_type(node, new_node) graph.add_node(new_node) graph.add_edge(node, new_node, require) node.propagate_downstream(require, new_node, graph.visibility_conflicts) # This is necessary to prevent infinite loops even when visibility is False ancestor = node.check_loops(new_node) if ancestor is not None: raise GraphLoopError(new_node, require, ancestor) return new_node @staticmethod def _compute_down_options(node, require, new_ref): # The consumer "up_options" are the options that come from downstream to this node visible = require.visible and not node.conanfile.vendor if require.options is not None: # If the consumer has specified "requires(options=xxx)", we need to use it # It will have less priority than downstream consumers down_options = Options(options_values=require.options) down_options.scope(new_ref) # At the moment, the behavior is the most restrictive one: default_options and # options["dep"].opt=value only propagate to visible and host dependencies # we will evaluate if necessary a potential "build_options", but recall that it is # now possible to do "self.build_requires(..., options={k:v})" to specify it if visible: # Only visible requirements in the host context propagate options from downstream down_options.update_options(node.conanfile.up_options) else: if visible: down_options = node.conanfile.up_options elif not require.build: # for requires in "host", like test_requires, pass myoptions down_options = node.conanfile.private_up_options else: down_options = Options(options_values=node.conanfile.default_build_options) return down_options @staticmethod def _remove_overrides(dep_graph): for node in dep_graph.nodes: to_remove = [r for r in node.transitive_deps if r.override] for r in to_remove: node.transitive_deps.pop(r) @staticmethod def _remove_orphans(dep_graph): # when requires to the same thing with different visible=xxx converge, there can be orphans opened = {dep_graph.root} all_referenced = set() while opened: all_referenced.update(opened) next_open = set(edge.dst for node in opened for edge in node.edges if edge.dst not in all_referenced) opened = next_open # Keep order in previous list dep_graph.nodes = [n for n in dep_graph.nodes if n in all_referenced] ================================================ FILE: conan/internal/graph/graph_error.py ================================================ from conan.errors import ConanException class GraphError(ConanException): def serialize(self): return class GraphConflictError(GraphError): def __init__(self, node, require, prev_node, prev_require, base_previous): self.node = node self.require = require self.prev_node = prev_node self.prev_require = prev_require self.base_previous = base_previous def serialize(self): dst_id = self.prev_node.id if self.prev_node else None return {"type": "conflict", "name": self.require.ref.name, "branch1": {"src_id": self.base_previous.id, "src_ref": str(self.base_previous.ref), "dst_id": dst_id, "require": self.prev_require.serialize()}, "branch2": {"src_id": self.node.id, "src_ref": str(self.node.ref), "require": self.require.serialize()}} def __str__(self): conflicting_node = self.node.ref or self.base_previous.ref conflicting_node_msg = "" if conflicting_node is not None: conflicting_node_msg = f"\nConflict originates from {conflicting_node}\n" return f"Version conflict: " \ f"Conflict between {self.require.ref} and {self.prev_require.ref} in the graph." \ f"{conflicting_node_msg}" \ f"\nRun 'conan graph info ... --format=html > graph.html' " \ f"and open 'graph.html' to inspect the conflict graphically." class GraphLoopError(GraphError): def __init__(self, node, require, ancestor): self.node = node self.require = require self.ancestor = ancestor def serialize(self): return {"type": "loop", "require": {**self.require.serialize(), "name": str(self.require.ref).split("/")[0]}, "node": self.node.serialize(), "ancestor": self.ancestor.serialize() } def __str__(self): return "There is a cycle/loop in the graph:\n" \ f" Initial ancestor: {self.ancestor}\n" \ f" Require: {self.require.ref}\n" \ f" Dependency: {self.node}" class GraphMissingError(GraphError): def __init__(self, node, require, missing_error): self.node = node self.require = require self.missing_error = missing_error def serialize(self): return {"type": "missing", "node": {"id": self.node.id, "ref": str(self.node.ref)}, "require": self.require.serialize(), "error": self.missing_error} def __str__(self): return (f"Package '{self.require.ref}' not resolved: {self.missing_error}. " f"Required by '{self.node.ref or 'cli'}'") class GraphProvidesError(GraphError): def __init__(self, node, conflicting_node): self.node = node self.conflicting_node = conflicting_node node.error = conflicting_node.error def serialize(self): return {"type": "provide_conflict", "node": {"id": self.node.id, "ref": str(self.node.ref)}, "conflicting_node": {"id": self.conflicting_node.id, "ref": str(self.conflicting_node.ref)}, "provided": self.node.conanfile.provides or self.conflicting_node.conanfile.provides} def __str__(self): provides = self.node.conanfile.provides or self.conflicting_node.conanfile.provides return f"Provide Conflict: Both '{self.node.ref}' and '{self.conflicting_node.ref}' " \ f"provide '{provides}'." ================================================ FILE: conan/internal/graph/install_graph.py ================================================ import json import os import textwrap from conan.api.output import ConanOutput from conan.internal.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL, BINARY_SKIP, \ BINARY_MISSING, BINARY_INVALID, Overrides, BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_PLATFORM from conan.errors import ConanException, ConanInvalidConfiguration from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.files import load class _InstallPackageReference: """ Represents a single, unique PackageReference to be downloaded, built, etc. Same PREF should only be built or downloaded once, but it is possible to have multiple nodes in the DepsGraph that share the same PREF. PREF could have PREV if to be downloaded (must be the same for all), but won't if to be built """ def __init__(self): self.package_id = None self.prev = None self.nodes = [] # GraphNode self.binary = None # The action BINARY_DOWNLOAD, etc must be the same for all nodes self.context = None # Same PREF could be in both contexts, but only 1 context is enough to # be able to reproduce, typically host preferrably self.options = [] # to be able to fire a build, the options will be necessary self.filenames = [] # The build_order.json filenames e.g. "windows_build_order" # If some package, like ICU, requires itself, built for the "build" context architecture # to cross compile, there will be a dependency from the current "self" (host context) # to that "build" package_id. self.depends = [] # List of package_ids of dependencies to other binaries of the same ref self.overrides = Overrides() self.ref = None self.info = None @property def pref(self): return PkgReference(self.ref, self.package_id, self.prev) @property def conanfile(self): return self.nodes[0].conanfile @staticmethod def create(node): result = _InstallPackageReference() result.ref = node.ref result.package_id = node.pref.package_id result.prev = node.pref.revision result.binary = node.binary result.context = node.context # self_options are the minimum to reproduce state result.options = node.conanfile.self_options.dumps().splitlines() result.nodes.append(node) result.overrides = node.overrides() result.info = node.conanfile.info.serialize() # ConanInfo doesn't have deserialize return result def add(self, node): assert self.package_id == node.package_id assert self.binary == node.binary, f"Binary for {node}: {self.binary}!={node.binary}" assert self.prev == node.prev # The context might vary, but if same package_id, all fine # assert self.context == node.context self.nodes.append(node) def _build_args(self): if self.binary not in (BINARY_BUILD, BINARY_EDITABLE_BUILD): return None cmd = f"--requires={self.ref}" if self.context == "host" else f"--tool-requires={self.ref}" compatible = "compatible:" if self.info and self.info.get("compatibility_delta") else "" cmd += f" --build={compatible}{self.ref}" if self.options: scope = "" if self.context == "host" else ":b" cmd += " " + " ".join(f'-o{scope}="{o}"' for o in self.options) if self.overrides: cmd += f' --lockfile-overrides="{self.overrides}"' return cmd def serialize(self): return {"package_id": self.package_id, "prev": self.prev, "context": self.context, "binary": self.binary, "options": self.options, "filenames": self.filenames, "depends": self.depends, "overrides": self.overrides.serialize(), "build_args": self._build_args(), "info": self.info} @staticmethod def deserialize(data, filename, ref): result = _InstallPackageReference() result.ref = ref result.package_id = data["package_id"] result.prev = data["prev"] result.binary = data["binary"] result.context = data["context"] result.options = data["options"] result.filenames = data["filenames"] or [filename] result.depends = data["depends"] result.overrides = Overrides.deserialize(data["overrides"]) result.info = data.get("info") return result class _InstallRecipeReference: """ represents a single, unique Recipe reference (including revision, must have it) containing all the _InstallPackageReference that belongs to this RecipeReference. This approach is oriented towards a user-intuitive grouping specially in CI, in which all binary variants for the same recipe revision (repo+commit) are to be built grouped together""" def __init__(self): self.ref = None self._node = None self.packages = {} # {package_id: _InstallPackageReference} self.depends = [] # Other REFs, defines the graph topology and operation ordering def __str__(self): return f"{self.ref} ({self._node.binary}) -> {[str(d) for d in self.depends]}" @property def need_build(self): for package in self.packages.values(): if package.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD): return True return False def reduce(self): result = _InstallRecipeReference() result.ref = self.ref result._node = self._node result.depends = self.depends result.packages = {pid: pkg for pid, pkg in self.packages.items() if pkg.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD)} return result @property def node(self): return self._node @staticmethod def create(node): result = _InstallRecipeReference() result._node = node result.ref = node.ref result.add(node) return result def merge(self, other): assert self.ref == other.ref for d in other.depends: if d not in self.depends: self.depends.append(d) for other_pkgid, other_pkg in other.packages.items(): existing = self.packages.get(other_pkgid) if existing is None: self.packages[other_pkgid] = other_pkg else: assert existing.binary == other_pkg.binary for f in other_pkg.filenames: if f not in existing.filenames: existing.filenames.append(f) def add(self, node): install_pkg_ref = self.packages.get(node.package_id) if install_pkg_ref is not None: install_pkg_ref.add(node) else: install_pkg_ref = _InstallPackageReference.create(node) self.packages[node.package_id] = install_pkg_ref for dep in node.edges: if dep.dst.binary != BINARY_SKIP: if dep.dst.ref == node.ref: # If the node is itself, then it is internal dep install_pkg_ref.depends.append(dep.dst.pref.package_id) elif dep.dst.ref not in self.depends: self.depends.append(dep.dst.ref) def _install_order(self): # TODO: Repeated, refactor # a topological order by levels, returns a list of list, in order of processing levels = [] opened = self.packages while opened: current_level = [] closed = [] for o in opened.values(): requires = o.depends if not any(n in opened for n in requires): current_level.append(o) closed.append(o) if current_level: levels.append(current_level) # now initialize new level opened = {k: v for k, v in opened.items() if v not in closed} return levels def serialize(self): return {"ref": self.ref.repr_notime(), "depends": [ref.repr_notime() for ref in self.depends], "packages": [[v.serialize() for v in level] for level in self._install_order()] } @staticmethod def deserialize(data, filename): result = _InstallRecipeReference() result.ref = RecipeReference.loads(data["ref"]) for d in data["depends"]: result.depends.append(RecipeReference.loads(d)) for level in data["packages"]: for p in level: install_node = _InstallPackageReference.deserialize(p, filename, result.ref) result.packages[install_node.package_id] = install_node return result class _InstallConfiguration: """ Represents a single, unique PackageReference to be downloaded, built, etc. Same PREF should only be built or downloaded once, but it is possible to have multiple nodes in the DepsGraph that share the same PREF. PREF could have PREV if to be downloaded (must be the same for all), but won't if to be built """ def __init__(self): self.ref = None self.package_id = None self.prev = None self.nodes = [] # GraphNode self.binary = None # The action BINARY_DOWNLOAD, etc must be the same for all nodes self.context = None # Same PREF could be in both contexts, but only 1 context is enough to # be able to reproduce, typically host preferrably self.options = [] # to be able to fire a build, the options will be necessary self.filenames = [] # The build_order.json filenames e.g. "windows_build_order" self.depends = [] # List of full prefs self.overrides = Overrides() self.info = None def __str__(self): return f"{self.ref}:{self.package_id} ({self.binary}) -> {[str(d) for d in self.depends]}" @property def need_build(self): return self.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD) def reduce(self): return self @property def pref(self): return PkgReference(self.ref, self.package_id, self.prev) @property def conanfile(self): return self.nodes[0].conanfile @staticmethod def create(node): result = _InstallConfiguration() result.ref = node.ref result.package_id = node.pref.package_id result.prev = node.pref.revision result.binary = node.binary result.context = node.context # self_options are the minimum to reproduce state result.options = node.conanfile.self_options.dumps().splitlines() result.overrides = node.overrides() result.info = node.conanfile.info.serialize() result.nodes.append(node) for dep in node.edges: if dep.dst.binary != BINARY_SKIP: if dep.dst.pref not in result.depends: result.depends.append(dep.dst.pref) return result def add(self, node): assert self.package_id == node.package_id, f"{self.pref}!={node.pref}" assert self.binary == node.binary, f"Binary for {node}: {self.binary}!={node.binary}" assert self.prev == node.prev # The context might vary, but if same package_id, all fine # assert self.context == node.context self.nodes.append(node) for edge in node.edges: if edge.dst.binary != BINARY_SKIP: if edge.dst.pref not in self.depends: self.depends.append(edge.dst.pref) def _build_args(self): if self.binary not in (BINARY_BUILD, BINARY_EDITABLE_BUILD): return None cmd = f"--requires={self.ref}" if self.context == "host" else f"--tool-requires={self.ref}" compatible = "compatible:" if self.info and self.info.get("compatibility_delta") else "" cmd += f" --build={compatible}{self.ref}" if self.options: scope = "" if self.context == "host" else ":b" cmd += " " + " ".join(f'-o{scope}="{o}"' for o in self.options) if self.overrides: cmd += f' --lockfile-overrides="{self.overrides}"' return cmd def serialize(self): return {"ref": self.ref.repr_notime(), "pref": self.pref.repr_notime(), "package_id": self.pref.package_id, "prev": self.pref.revision, "context": self.context, "binary": self.binary, "options": self.options, "filenames": self.filenames, "depends": [d.repr_notime() for d in self.depends], "overrides": self.overrides.serialize(), "build_args": self._build_args(), "info": self.info } @staticmethod def deserialize(data, filename): result = _InstallConfiguration() result.ref = RecipeReference.loads(data["ref"]) result.package_id = data["package_id"] result.prev = data["prev"] result.binary = data["binary"] result.context = data["context"] result.options = data["options"] result.filenames = data["filenames"] or [filename] result.depends = [PkgReference.loads(p) for p in data["depends"]] result.overrides = Overrides.deserialize(data["overrides"]) result.info = data.get("info") return result def merge(self, other): assert self.binary == other.binary, f"Binary for {self.ref}: {self.binary}!={other.binary}" assert self.ref == other.ref for d in other.depends: if d not in self.depends: self.depends.append(d) for d in other.filenames: if d not in self.filenames: self.filenames.append(d) class ProfileArgs: def __init__(self, args): self._args = args @staticmethod def from_args(args): pr_args = [] for context in "host", "build": for f in "profile", "settings", "options", "conf": s = "pr" if f == "profile" else f[0] pr_args += [f'-{s}:{context[0]}="{v}"' for v in getattr(args, f"{f}_{context}") or []] return ProfileArgs(" ".join(pr_args)) def __str__(self): return self._args @staticmethod def deserialize(data): return ProfileArgs(data.get("args")) def serialize(self): return {"args": self._args} class InstallGraph: """ A graph containing the package references in order to be built/downloaded """ def __init__(self, deps_graph, order_by=None, profile_args=None): self._nodes = {} # ref with rev: _InstallGraphNode order_by = order_by or "recipe" self._order = order_by self._node_cls = _InstallRecipeReference if order_by == "recipe" else _InstallConfiguration self._is_test_package = False self.reduced = False self._profiles = {"self": profile_args} if profile_args is not None else {} self._filename = None if deps_graph is not None: self._initialize_deps_graph(deps_graph) self._is_test_package = deps_graph.root.conanfile.tested_reference_str is not None @staticmethod def load(filename): data = json.loads(load(filename)) filename = os.path.basename(filename) filename = os.path.splitext(filename)[0] install_graph = InstallGraph.deserialize(data, filename) return install_graph def merge(self, other): """ :type other: InstallGraph """ if self.reduced or other.reduced: raise ConanException("Reduced build-order files cannot be merged") if self._order != other._order: raise ConanException(f"Cannot merge build-orders of {self._order}!={other._order}") for ref, install_node in other._nodes.items(): existing = self._nodes.get(ref) if existing is None: self._nodes[ref] = install_node else: existing.merge(install_node) # Make sure that self is also updated current = self._profiles.pop("self", None) if current is not None: self._profiles[self._filename] = current new = other._profiles.get("self") if new is not None: self._profiles[other._filename] = new @staticmethod def deserialize(data, filename): legacy = isinstance(data, list) order, data, reduced, profiles = ("recipe", data, False, {}) if legacy else \ (data["order_by"], data["order"], data["reduced"], data.get("profiles", {})) result = InstallGraph(None, order_by=order) result.reduced = reduced result.legacy = legacy result._filename = filename result._profiles = {k: ProfileArgs.deserialize(v) for k, v in profiles.items()} for level in data: for item in level: elem = result._node_cls.deserialize(item, filename) key = elem.ref if order == "recipe" else elem.pref result._nodes[key] = elem return result def _initialize_deps_graph(self, deps_graph): for node in deps_graph.ordered_iterate(): if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL) \ or node.binary in (BINARY_SKIP, BINARY_PLATFORM): continue key = node.ref if self._order == "recipe" else node.pref existing = self._nodes.get(key) if existing is None: self._nodes[key] = self._node_cls.create(node) else: existing.add(node) def reduce(self): result = {} for k, node in self._nodes.items(): if node.need_build: result[k] = node.reduce() else: # Eliminate this element from the graph dependencies = node.depends # Find all consumers for n in self._nodes.values(): if k in n.depends: n.depends = [d for d in n.depends if d != k] # Discard the removed node # Add new edges, without repetition n.depends.extend(d for d in dependencies if d not in n.depends) self._nodes = result self.reduced = True def install_order(self, flat=False): # a topological order by levels, returns a list of list, in order of processing levels = [] opened = self._nodes while opened: current_level = [] closed = [] for o in opened.values(): requires = o.depends if not any(n in opened for n in requires): current_level.append(o) closed.append(o) if current_level: levels.append(current_level) else: self._raise_loop_detected(opened) # now initialize new level opened = {k: v for k, v in opened.items() if v not in closed} if flat: return [r for level in levels for r in level] return levels @staticmethod def _raise_loop_detected(nodes): """ We can exclude the nodes that have already been processed they do not content loops """ msg = [f"{n}" for n in nodes.values()] msg = "\n".join(msg) instructions = "This graph is ill-formed, and cannot be installed\n" \ "Most common cause is having dependencies both in build and host contexts\n"\ "forming a cycle, due to tool_requires having transitive requires.\n"\ "Check your profile [tool_requires] and recipe tool and regular requires\n"\ "You might inspect the dependency graph with:\n"\ " $ conan graph info . --format=html > graph.html" raise ConanException("There is a loop in the graph (some packages already ommitted):\n" f"{msg}\n\n{instructions}") def install_build_order(self): # TODO: Rename to serialize()? """ used for graph build-order and graph build-order-merge commands This is basically a serialization of the build-order """ install_order = self.install_order() result = {"order_by": self._order, "reduced": self.reduced, "order": [[n.serialize() for n in level] for level in install_order], "profiles": {k: v.serialize() for k, v in self._profiles.items()} } return result def _get_missing_invalid_packages(self): missing, invalid = [], [] def analyze_package(pkg): if pkg.binary == BINARY_MISSING: missing.append(pkg) elif pkg.binary == BINARY_INVALID: invalid.append(pkg) for _, install_node in self._nodes.items(): if self._order == "recipe": for package in install_node.packages.values(): analyze_package(package) elif self._order == "configuration": analyze_package(install_node) return missing, invalid def raise_errors(self): missing, invalid = self._get_missing_invalid_packages() if invalid: self._raise_invalid(invalid) if missing: self._raise_missing(missing) def get_errors(self): missing, invalid = self._get_missing_invalid_packages() errors = [] tab = " " if invalid or missing: errors.append("There are some error(s) in the graph:") if invalid: for package in invalid: errors.append(f"{tab}- {package.pref}: Invalid configuration") if missing: missing_prefs = set(n.pref for n in missing) # avoid duplicated for pref in list(sorted([str(pref) for pref in missing_prefs])): errors.append(f"{tab}- {pref}: Missing binary") if errors: return "\n".join(errors) return None @staticmethod def _raise_invalid(invalid): msg = ["There are invalid packages:"] for package in invalid: node = package.nodes[0] if node.conanfile.info.cant_build and node.should_build: binary, reason = "Cannot build for this configuration", node.conanfile.info.cant_build else: binary, reason = "Invalid", node.conanfile.info.invalid msg.append("{}: {}: {}".format(node.conanfile, binary, reason)) raise ConanInvalidConfiguration("\n".join(msg)) def _raise_missing(self, missing): # TODO: Remove out argument # TODO: A bit dirty access to .pref missing_prefs = set(n.nodes[0].pref for n in missing) # avoid duplicated missing_prefs_str = list(sorted([str(pref) for pref in missing_prefs])) out = ConanOutput() for pref_str in missing_prefs_str: out.error(f"Missing binary: {pref_str}", error_type="exception") out.writeln("") # Report details just the first one install_node = missing[0] node = install_node.nodes[0] package_id = node.package_id ref, conanfile = node.ref, node.conanfile msg = f"Can't find a '{ref}' package binary '{package_id}' for the configuration:\n"\ f"{conanfile.info.dumps()}" conanfile.output.warning(msg) missing_pkgs = "', '".join(list(sorted([str(pref.ref) for pref in missing_prefs]))) if self._is_test_package: build_msg = "This is a **test_package** missing binary. You can use --build (for " \ "all dependencies) or --build-test (exclusive for 'conan create' building " \ "test_package' dependencies) to define what can be built from sources" else: if len(missing_prefs) >= 5: build_str = "--build=missing" else: build_str = " ".join(list(sorted(["--build=%s" % str(pref.ref) for pref in missing_prefs]))) build_msg = f"Try to build locally from sources using the '{build_str}' argument" raise ConanException(textwrap.dedent(f'''\ Missing prebuilt package for '{missing_pkgs}'. You can try: - List all available packages using 'conan list "{ref}:*" -r=remote' - Explain missing binaries: replace 'conan install ...' with 'conan graph explain ...' - {build_msg} More Info at 'https://docs.conan.io/2/knowledge/faq.html#error-missing-prebuilt-package' ''')) ================================================ FILE: conan/internal/graph/installer.py ================================================ import os import shutil from multiprocessing.pool import ThreadPool from conan.api.output import ConanOutput, Color from conan.internal.methods import run_build_method, run_package_method from conan.internal.api.install.generators import write_generators from conan.internal.graph.graph import (BINARY_BUILD, BINARY_CACHE, BINARY_DOWNLOAD, BINARY_EDITABLE, BINARY_UPDATE, BINARY_EDITABLE_BUILD, BINARY_SKIP) from conan.internal.graph.install_graph import InstallGraph from conan.internal.source import retrieve_exports_sources, config_source from conan.internal.errors import conanfile_remove_attr, conanfile_exception_formatter from conan.errors import ConanException from conan.internal.model.cpp_info import CppInfo, MockInfoProperty from conan.api.model import PkgReference from conan.internal.paths import CONANINFO from conan.internal.util import cpu_count from conan.internal.util.files import clean_dirty, is_dirty, mkdir, rmdir, save, set_dirty, chdir def build_id(conan_file): if hasattr(conan_file, "build_id"): # construct new ConanInfo build_id_info = conan_file.info.clone() conan_file.info_build = build_id_info # effectively call the user function to change the package values with conanfile_exception_formatter(conan_file, "build_id"): conan_file.build_id() # compute modified ID return build_id_info.package_id() return None class _PackageBuilder: def __init__(self, cache, remote_manager, cache_folder, hook_manager): self._cache = cache self._hook_manager = hook_manager self._remote_manager = remote_manager self._home_folder = cache_folder def _get_build_folder(self, conanfile, package_layout): # Build folder can use a different package_ID if build_id() is defined. # This function decides if the build folder should be re-used (not build again) # and returns the build folder skip_build = False build_folder = package_layout.build() recipe_build_id = build_id(conanfile) pref = package_layout.reference if recipe_build_id is not None and pref.package_id != recipe_build_id: conanfile.output.info(f"build_id() computed {recipe_build_id}") # check if we already have a package with the calculated build_id recipe_ref = pref.ref build_prev = self._cache.get_matching_build_id(recipe_ref, recipe_build_id) if build_prev is None: # Only store build_id of the first one actually building it package_layout.build_id = recipe_build_id build_prev = build_prev or pref # We are trying to build a package id different from the one that has the # build_folder but belongs to the same recipe revision, so reuse the build_folder # from the one that is already build if build_prev.package_id != pref.package_id: other_pkg_layout = self._cache.pkg_layout(build_prev) build_folder = other_pkg_layout.build() skip_build = True if is_dirty(build_folder): conanfile.output.warning("Build folder is dirty, removing it: %s" % build_folder) rmdir(build_folder) clean_dirty(build_folder) skip_build = False if skip_build and os.path.exists(build_folder): conanfile.output.info("Won't be built, using previous build folder as defined " f"in build_id(): {build_folder}") return build_folder, skip_build @staticmethod def _copy_sources(conanfile, source_folder, build_folder): # Copies the sources to the build-folder, unless no_copy_source is defined rmdir(build_folder) if not getattr(conanfile, 'no_copy_source', False): conanfile.output.info('Copying sources to build folder') try: shutil.copytree(source_folder, build_folder, symlinks=True) except Exception as e: raise ConanException(f"{e}\nError copying sources to build folder") def _build(self, conanfile, pref): write_generators(conanfile, self._hook_manager, self._home_folder) try: run_build_method(conanfile, self._hook_manager) conanfile.output.success("Package '%s' built" % pref.package_id) conanfile.output.info("Build folder %s" % conanfile.build_folder) except Exception as exc: conanfile.output.error(f"\nPackage '{pref.package_id}' build failed", error_type="exception") conanfile.output.warning("Build folder %s" % conanfile.build_folder) if isinstance(exc, ConanException): raise exc raise ConanException(exc) def _package(self, conanfile, pref): # Creating ***info.txt files save(os.path.join(conanfile.folders.base_build, CONANINFO), conanfile.info.dumps()) package_id = pref.package_id # Do the actual copy, call the conanfile.package() method # While installing, the infos goes to build folder prev = run_package_method(conanfile, package_id, self._hook_manager, pref.ref) # FIXME: Conan 2.0 Clear the registry entry (package ref) return prev def build_package(self, node, recipe_layout, package_layout): conanfile = node.conanfile pref = node.pref base_source = recipe_layout.source() base_package = package_layout.package() base_build, skip_build = self._get_build_folder(conanfile, package_layout) # PREPARE SOURCES if not skip_build: # TODO: cache2.0 check locks # with package_layout.conanfile_write_lock(self._output): set_dirty(base_build) self._copy_sources(conanfile, base_source, base_build) mkdir(base_build) # BUILD & PACKAGE # TODO: cache2.0 check locks # with package_layout.conanfile_read_lock(self._output): with chdir(base_build): try: src = base_source if getattr(conanfile, 'no_copy_source', False) else base_build conanfile.folders.set_base_source(src) conanfile.folders.set_base_build(base_build) conanfile.folders.set_base_package(base_package) # In local cache, generators folder always in build_folder conanfile.folders.set_base_generators(base_build) conanfile.folders.set_base_pkg_metadata(package_layout.metadata()) if not skip_build: conanfile.output.info('Building your package in %s' % base_build) # In local cache, install folder always is build_folder self._build(conanfile, pref) clean_dirty(base_build) prev = self._package(conanfile, pref) assert prev node.prev = prev except ConanException as exc: # TODO: Remove this? unnecessary? raise exc return node.pref class BinaryInstaller: """ main responsible of retrieving binary packages or building them from source locally in case they are not found in remotes """ def __init__(self, api, global_conf, editable_packages, hook_manager): helpers = api._api_helpers # noqa self._editable_packages = editable_packages self._cache = helpers.cache self._remote_manager = helpers.remote_manager self._hook_manager = hook_manager self._global_conf = global_conf self._home_folder = api.home_folder def _install_source(self, node, remotes, need_conf=False): conanfile = node.conanfile download_source = conanfile.conf.get("tools.build:download_source", check_type=bool) if not download_source and (need_conf or node.binary != BINARY_BUILD): return conanfile = node.conanfile if node.binary in (BINARY_EDITABLE, BINARY_EDITABLE_BUILD): return recipe_layout = self._cache.recipe_layout(node.ref) export_source_folder = recipe_layout.export_sources() source_folder = recipe_layout.source() retrieve_exports_sources(self._remote_manager, recipe_layout, conanfile, node.ref, remotes) conanfile.folders.set_base_source(source_folder) conanfile.folders.set_base_export_sources(source_folder) conanfile.folders.set_base_recipe_metadata(recipe_layout.metadata()) config_source(export_source_folder, conanfile, self._hook_manager) return recipe_layout @staticmethod def install_system_requires(graph, only_info=False, install_order=None): if install_order is None: install_graph = InstallGraph(graph) install_order = install_graph.install_order() for level in install_order: for install_reference in level: for package in install_reference.packages.values(): if not only_info and package.binary == BINARY_SKIP: continue conanfile = package.nodes[0].conanfile # TODO: Refactor magic strings and use _SystemPackageManagerTool.mode_xxx ones mode = conanfile.conf.get("tools.system.package_manager:mode") if only_info and mode is None: continue if hasattr(conanfile, "system_requirements"): with conanfile_exception_formatter(conanfile, "system_requirements"): conanfile.system_requirements() for n in package.nodes: n.conanfile.system_requires = conanfile.system_requires conanfile = graph.root.conanfile mode = conanfile.conf.get("tools.system.package_manager:mode") if only_info and mode is None: return if hasattr(conanfile, "system_requirements"): with conanfile_exception_formatter(conanfile, "system_requirements"): conanfile.system_requirements() def install_sources(self, graph, remotes): install_graph = InstallGraph(graph) install_order = install_graph.install_order() for level in install_order: for install_reference in level: for package in install_reference.packages.values(): self._install_source(package.nodes[0], remotes, need_conf=True) def install(self, deps_graph, remotes, install_order=None): assert not deps_graph.error, "This graph cannot be installed: {}".format(deps_graph) if install_order is None: install_graph = InstallGraph(deps_graph) install_graph.raise_errors() install_order = install_graph.install_order() ConanOutput().title("Installing packages") package_count = sum([sum(len(install_reference.packages.values()) for level in install_order for install_reference in level)]) handled_count = 1 self._download_bulk(install_order) for level in install_order: for install_reference in level: for package in install_reference.packages.values(): recipe_layout = self._install_source(package.nodes[0], remotes) self._handle_package(recipe_layout, package, install_reference, handled_count, package_count) handled_count += 1 MockInfoProperty.message() def _download_bulk(self, install_order): """ executes the download of packages (both download and update), only once for a given PREF """ downloads = [] for level in install_order: for node in level: for package in node.packages.values(): if package.binary in (BINARY_UPDATE, BINARY_DOWNLOAD): downloads.append(package) if not downloads: return download_count = len(downloads) plural = 's' if download_count != 1 else '' ConanOutput().subtitle(f"Downloading {download_count} package{plural}") parallel = self._global_conf.get("core.download:parallel", check_type=int, default=cpu_count()) if parallel: # User can define core.download:parallel=0 to deactivate parallelism ConanOutput().info("Downloading binary packages in %s parallel threads" % parallel) thread_pool = ThreadPool(parallel) thread_pool.map(self._download_pkg, downloads) thread_pool.close() thread_pool.join() else: for node in downloads: self._download_pkg(node) def _download_pkg(self, package): node = package.nodes[0] assert node.pref.revision is not None assert node.pref.timestamp is not None self._remote_manager.get_package(node.pref, node.binary_remote) def _handle_package(self, recipe_layout, package, install_reference, handled_count, total_count): if package.binary in (BINARY_EDITABLE, BINARY_EDITABLE_BUILD): self._handle_node_editable(package) return assert package.binary in (BINARY_CACHE, BINARY_BUILD, BINARY_DOWNLOAD, BINARY_UPDATE) assert install_reference.ref.revision is not None, "Installer should receive RREV always" pref = PkgReference(install_reference.ref, package.package_id, package.prev) if package.binary == BINARY_BUILD: assert pref.revision is None ConanOutput()\ .subtitle(f"Installing package {pref.ref} ({handled_count} of {total_count})") ConanOutput(scope=str(pref.ref))\ .highlight("Building from source")\ .info(f"Package {pref}") compact_dumps = package.nodes[0].conanfile.info.summarize_compact() for line in compact_dumps: ConanOutput(scope=str(pref.ref)).info(line, fg=Color.BRIGHT_GREEN) package_layout = self._cache.create_build_pkg_layout(pref) self._handle_node_build(package, recipe_layout, package_layout) # Just in case it was recomputed package.package_id = package.nodes[0].pref.package_id # Just in case it was recomputed package.prev = package.nodes[0].pref.revision package.binary = package.nodes[0].binary pref = PkgReference(install_reference.ref, package.package_id, package.prev) else: assert pref.revision is not None package_layout = self._cache.pkg_layout(pref) if package.binary == BINARY_CACHE: node = package.nodes[0] pref = node.pref assert node.prev, "PREV for %s is None" % str(pref) msg = f'Already installed! ({handled_count} of {total_count})' node.conanfile.output.success(msg) os.utime(package_layout.base_folder, None) # Make sure that all nodes with same pref compute package_info() pkg_folder = package_layout.package() pkg_metadata = package_layout.metadata() assert os.path.isdir(pkg_folder), "Pkg '%s' folder must exist: %s" % (str(pref), pkg_folder) for n in package.nodes: n.prev = pref.revision # Make sure the prev is assigned conanfile = n.conanfile # Call the info method conanfile.folders.set_base_package(pkg_folder) conanfile.folders.set_base_pkg_metadata(pkg_metadata) self._call_finalize_method(conanfile, package_layout.finalize()) # Use package_folder which has been updated previously by install_method if necessary self._call_package_info(conanfile, conanfile.package_folder, is_editable=False) def _handle_node_editable(self, install_node): # It will only run generation node = install_node.nodes[0] conanfile = node.conanfile ref = node.ref editable = self._editable_packages.get(ref) conanfile_path = editable["path"] output_folder = editable.get("output_folder") base_path = os.path.dirname(conanfile_path) conanfile.folders.set_base_folders(base_path, output_folder) if node.binary == BINARY_EDITABLE_BUILD: output = conanfile.output output.info("Rewriting files of editable package " "'{}' at '{}'".format(conanfile.name, conanfile.generators_folder)) write_generators(conanfile, self._hook_manager, self._home_folder) run_build_method(conanfile, self._hook_manager) rooted_base_path = base_path if conanfile.folders.root is None else \ os.path.normpath(os.path.join(base_path, conanfile.folders.root)) for node in install_node.nodes: # Get source of information conanfile = node.conanfile # New editables mechanism based on Folders conanfile.folders.set_base_package(output_folder or rooted_base_path) conanfile.folders.set_base_folders(base_path, output_folder) conanfile.folders.set_base_pkg_metadata(os.path.join(conanfile.build_folder, "metadata")) # Need a temporary package revision for package_revision_mode # Cannot be PREV_UNKNOWN otherwise the consumers can't compute their packageID node.prev = "editable" # TODO: Check this base_path usage for editable when not defined self._call_package_info(conanfile, package_folder=rooted_base_path, is_editable=True) def _handle_node_build(self, package, recipe_layout, pkg_layout): node = package.nodes[0] pref = node.pref assert pref.package_id, "Package-ID without value" assert pkg_layout, "The pkg_layout should be declared here" assert node.binary == BINARY_BUILD assert node.prev is None, "PREV for %s to be built should be None" % str(pref) with pkg_layout.package_lock(): pkg_layout.package_remove() with pkg_layout.set_dirty_context_manager(): builder = _PackageBuilder(self._cache, self._remote_manager, self._home_folder, self._hook_manager) pref = builder.build_package(node, recipe_layout, pkg_layout) assert node.prev, "Node PREV shouldn't be empty" assert node.pref.revision, "Node PREF revision shouldn't be empty" assert pref.revision is not None, "PREV for %s to be built is None" % str(pref) # at this point the package reference should be complete pkg_layout.reference = pref self._cache.assign_prev(pkg_layout) # Make sure the current conanfile.folders is updated (it is later in package_info(), # but better make sure here, and be able to report the actual folder in case # something fails) node.conanfile.folders.set_base_package(pkg_layout.package()) node.conanfile.output.success("Package folder %s" % node.conanfile.package_folder) def _call_package_info(self, conanfile, package_folder, is_editable): with chdir(package_folder): with conanfile_exception_formatter(conanfile, "package_info"): self._hook_manager.execute("pre_package_info", conanfile=conanfile) if hasattr(conanfile, "package_info"): with conanfile_remove_attr(conanfile, ['info', "source_folder", "build_folder"], "package_info"): MockInfoProperty.package = str(conanfile) conanfile.package_info() # TODO: Check this package_folder usage for editable when not defined conanfile.cpp.package.set_relative_base_folder(package_folder) if is_editable: # Adjust the folders of the layout to consolidate the rootfolder of the # cppinfos inside # convert directory entries to be relative to the declared folders.build build_cppinfo = conanfile.cpp.build build_cppinfo.set_relative_base_folder(conanfile.build_folder) conanfile.layouts.build.set_relative_base_folder(conanfile.build_folder) # convert directory entries to be relative to the declared folders.source source_cppinfo = conanfile.cpp.source source_cppinfo.set_relative_base_folder(conanfile.source_folder) conanfile.layouts.source.set_relative_base_folder(conanfile.source_folder) full_editable_cppinfo = CppInfo() full_editable_cppinfo.merge(source_cppinfo) full_editable_cppinfo.merge(build_cppinfo) # In editables if we defined anything in the cpp infos we want to discard # the one defined in the conanfile cpp_info conanfile.cpp_info.merge(full_editable_cppinfo, overwrite=True) # Paste the editable cpp_info but prioritizing it, only if a # variable is not declared at build/source, the package will keep the value full_buildenv_info = conanfile.layouts.source.buildenv_info.copy() full_buildenv_info.compose_env(conanfile.layouts.build.buildenv_info) full_buildenv_info.compose_env(conanfile.buildenv_info) conanfile.buildenv_info = full_buildenv_info full_runenv_info = conanfile.layouts.source.runenv_info.copy() full_runenv_info.compose_env(conanfile.layouts.build.runenv_info) full_runenv_info.compose_env(conanfile.runenv_info) conanfile.runenv_info = full_runenv_info full_conf_info = conanfile.layouts.source.conf_info.copy() full_conf_info.compose_conf(conanfile.layouts.build.conf_info) full_conf_info.compose_conf(conanfile.conf_info) conanfile.conf_info = full_conf_info else: conanfile.layouts.package.set_relative_base_folder(conanfile.package_folder) conanfile.buildenv_info.compose_env(conanfile.layouts.package.buildenv_info) conanfile.runenv_info.compose_env(conanfile.layouts.package.runenv_info) conanfile.conf_info.compose_conf(conanfile.layouts.package.conf_info) self._hook_manager.execute("post_package_info", conanfile=conanfile) conanfile.cpp_info.check_component_requires(conanfile) try: conanfile.conf_info.validate() except ConanException as e: raise ConanException(f"{conanfile}: Error in package_info() method:\n\t{str(e)}") @staticmethod def _call_finalize_method(conanfile, finalize_folder): if hasattr(conanfile, "finalize"): conanfile.folders.set_finalize_folder(finalize_folder) if not os.path.exists(finalize_folder): mkdir(finalize_folder) conanfile.output.highlight("Calling finalize()") with conanfile_exception_formatter(conanfile, "finalize"): with conanfile_remove_attr(conanfile, ['cpp_info', 'settings', 'options'], 'finalize'): conanfile.finalize() conanfile.output.success(f"Finalized folder {finalize_folder}") ================================================ FILE: conan/internal/graph/profile_node_definer.py ================================================ from conan.internal.graph.graph import CONTEXT_BUILD from conan.errors import ConanException from conan.internal.model.recipe_ref import ref_matches def initialize_conanfile_profile(conanfile, profile_build, profile_host, base_context, is_build_require, ref=None, parent=None): """ this function fills conanfile information with the profile informaiton It is called for: - computing the root_node - GraphManager.load_consumer_conanfile, for "conan source" command - GraphManager._load_root_consumer for "conan install - GraphManager._load_root_test_package for "conan create ." with test_package folder - computing each graph node: GraphBuilder->create_new_node """ # NOTE: Need the context, as conanfile.context NOT defined yet # settings_build=profile_build ALWAYS # host -(r)-> host => settings_host=profile_host, settings_target=None # host -(br)-> build => settings_host=profile_build, settings_target=profile_host # build(gcc) -(r)-> build(openssl/zlib) => settings_host=profile_build, settings_target=None # build(gcc) -(br)-> build(gcc) => settings_host=profile_build, settings_target=profile_build # profile host settings_host = _per_package_settings(conanfile, profile_host, ref) settings_build = _per_package_settings(conanfile, profile_build, ref) if is_build_require or base_context == CONTEXT_BUILD: _initialize_conanfile(conanfile, profile_build, settings_build.copy(), ref) conanfile.buildenv_build = None conanfile.conf_build = None else: _initialize_conanfile(conanfile, profile_host, settings_host, ref) # Host profile with some build profile information is_consumer = conanfile._conan_is_consumer # noqa conanfile.buildenv_build = profile_build.buildenv.get_profile_env(ref, is_consumer) conanfile.conf_build = profile_build.conf.get_conanfile_conf(ref, is_consumer) conanfile.settings_build = settings_build conanfile.settings_target = None if is_build_require: if base_context == CONTEXT_BUILD: conanfile.settings_target = settings_build.copy() else: conanfile.settings_target = settings_host.copy() else: if base_context == CONTEXT_BUILD: # if parent is first level tool-requires, required by HOST context if parent is None or parent.settings_target is None: conanfile.settings_target = settings_host.copy() else: conanfile.settings_target = parent.settings_target.copy() def _per_package_settings(conanfile, profile, ref): # Prepare the settings for the loaded conanfile # Mixing the global settings with the specified for that name if exist tmp_settings = profile.processed_settings.copy() package_settings_values = profile.package_settings_values if package_settings_values: pkg_settings = [] for pattern, settings in package_settings_values.items(): if ref_matches(ref, pattern, conanfile._conan_is_consumer): # noqa pkg_settings.extend(settings) if pkg_settings: tmp_settings.update_values(pkg_settings) # if the global settings are composed with per-package settings, need to preprocess return tmp_settings def _initialize_conanfile(conanfile, profile, settings, ref): try: settings.constrained(conanfile.settings) except Exception as e: raise ConanException("The recipe %s is constraining settings. %s" % ( conanfile.display_name, str(e))) conanfile.settings = settings conanfile.settings._frozen = True conanfile._conan_buildenv = profile.buildenv conanfile._conan_runenv = profile.runenv # Maybe this can be done lazy too conanfile.conf = profile.conf.get_conanfile_conf(ref, conanfile._conan_is_consumer) # noqa def consumer_definer(conanfile, profile_host, profile_build): """ conanfile.txt does not declare settings, but it assumes it uses all the profile settings These settings are very necessary for helpers like generators to produce the right output """ tmp_settings = profile_host.processed_settings.copy() package_settings_values = profile_host.package_settings_values for pattern, settings in package_settings_values.items(): if ref_matches(ref=None, pattern=pattern, is_consumer=True): tmp_settings.update_values(settings) tmp_settings_build = profile_build.processed_settings.copy() package_settings_values_build = profile_build.package_settings_values for pattern, settings in package_settings_values_build.items(): if ref_matches(ref=None, pattern=pattern, is_consumer=True): tmp_settings_build.update_values(settings) conanfile.settings = tmp_settings conanfile.settings_build = tmp_settings_build conanfile.settings_target = None conanfile.settings._frozen = True conanfile._conan_buildenv = profile_host.buildenv conanfile._conan_runenv = profile_host.runenv conanfile.conf = profile_host.conf.get_conanfile_conf(None, is_consumer=True) conanfile.conf_build = profile_build.conf.get_conanfile_conf(None, is_consumer=True) ================================================ FILE: conan/internal/graph/provides.py ================================================ from conan.internal.graph.graph_error import GraphProvidesError from conan.api.model import RecipeReference def check_graph_provides(dep_graph): if dep_graph.error: return for node in dep_graph.nodes: provides = {} current_provides = node.conanfile.provides if isinstance(current_provides, str): # Just in case it is defined in configure() as str current_provides = [current_provides] for dep in node.transitive_deps.values(): dep_node = dep.node if node.ref is not None and dep_node.ref.name == node.ref.name: continue # avoid dependency to self (as tool-requires for cross-build) dep_provides = dep_node.conanfile.provides if dep_provides is None: continue if isinstance(dep_provides, str): dep_provides = [dep_provides] for provide in dep_provides: # First check if collides with current node if current_provides is not None and provide in current_provides: raise GraphProvidesError(node, dep_node) # Then, check if collides with other requirements new_req = dep.require.copy_requirement() new_req.ref = RecipeReference(provide, new_req.ref.version, new_req.ref.user, new_req.ref.channel) existing = node.transitive_deps.get(new_req) if existing is not None: raise GraphProvidesError(existing.node, dep_node) else: existing_provide = provides.get(new_req) if existing_provide is not None: raise GraphProvidesError(existing_provide, dep_node) else: provides[new_req] = dep_node ================================================ FILE: conan/internal/graph/proxy.py ================================================ from conan.api.output import ConanOutput from conan.internal.cache.conan_reference_layout import BasicLayout from conan.internal.graph.graph import (RECIPE_DOWNLOADED, RECIPE_INCACHE, RECIPE_NEWER, RECIPE_NOT_IN_REMOTE, RECIPE_UPDATED, RECIPE_EDITABLE, RECIPE_INCACHE_DATE_UPDATED, RECIPE_UPDATEABLE) from conan.internal.errors import NotFoundException, ConanReferenceAlreadyExistsInDB from conan.errors import ConanException class ConanProxy: def __init__(self, cache, remote_manager, editable_packages, legacy_update=None): # collaborators self._editable_packages = editable_packages self._cache = cache self._remote_manager = remote_manager self._resolved = {} # Cache of the requested recipes to optimize calls self._legacy_update = legacy_update def get_recipe(self, ref, remotes, update, check_update): """ :return: Tuple (layout, status, remote) """ # TODO: cache2.0 Check with new locks # with layout.conanfile_write_lock(self._out): resolved = self._resolved.get(ref) if resolved is None: resolved = self._get_recipe(ref, remotes, update, check_update) self._resolved[ref] = resolved return resolved # return the remote where the recipe was found or None if the recipe was not found def _get_recipe(self, reference, remotes, update, check_update): output = ConanOutput(scope=str(reference)) conanfile_path = self._editable_packages.get_path(reference) if conanfile_path is not None: return BasicLayout(reference, conanfile_path), RECIPE_EDITABLE, None # check if it there's any revision of this recipe in the local cache try: # Just do 1 call to the DB, not 2 if reference.revision is None: recipe_layout = self._cache.recipe_layout_latest(reference) else: recipe_layout = self._cache.recipe_layout(reference) ref = recipe_layout.reference # latest revision if it was not defined except ConanException: # NOT in disk, must be retrieved from remotes # we will only check all servers for latest revision if we did a --update layout, remote = self._download_recipe(reference, remotes, output, update, check_update) status = RECIPE_DOWNLOADED return layout, status, remote # TODO: cache2.0: check with new --update flows # TODO: If the revision is given, then we don't need to check for updates? if not (check_update or should_update_reference(reference, update)): status = RECIPE_INCACHE return recipe_layout, status, None # Need to check updates remote, remote_ref = self._find_newest_recipe_in_remotes(reference, remotes, update, check_update) if remote_ref is None: # Nothing found in remotes status = RECIPE_NOT_IN_REMOTE return recipe_layout, status, None # Something found in remotes, check if we already have the latest in local cache assert ref.timestamp cache_time = ref.timestamp if remote_ref.revision != ref.revision: if cache_time < remote_ref.timestamp: # the remote one is newer if should_update_reference(remote_ref, update): output.info(f"Updating to latest from remote '{remote.name}'...") try: new_recipe_layout = self._download(remote_ref, remote) except ConanReferenceAlreadyExistsInDB: # When updating to a newer revision in the server, but it already exists # in the cache with an older timestamp output.info(f"Latest from '{remote.name}' was found in " "the cache, using it and updating its timestamp") new_recipe_layout = self._cache.recipe_layout(remote_ref) self._cache.update_recipe_timestamp(remote_ref) # make it latest status = RECIPE_UPDATED return new_recipe_layout, status, remote else: status = RECIPE_UPDATEABLE else: status = RECIPE_NEWER # If your recipe in cache is newer it does not make sense to return a remote? remote = None else: # TODO: cache2.0 we are returning RECIPE_UPDATED just because we are updating # the date if cache_time >= remote_ref.timestamp: status = RECIPE_INCACHE else: self._cache.update_recipe_timestamp(remote_ref) status = RECIPE_INCACHE_DATE_UPDATED return recipe_layout, status, remote def _find_newest_recipe_in_remotes(self, reference, remotes, update, check_update): output = ConanOutput(scope=str(reference)) results = [] need_update = should_update_reference(reference, update) or check_update for remote in remotes: if remote.allowed_packages and not any(reference.matches(f, is_consumer=False) for f in remote.allowed_packages): output.debug(f"Excluding remote {remote.name} because recipe is filtered out") continue output.info(f"Checking remote: {remote.name}") try: if self._legacy_update and need_update and reference.revision is None: if not getattr(ConanProxy, "update_policy_legacy_warning", None): ConanProxy.update_policy_legacy_warning = True ConanOutput().warning("The 'core:update_policy' conf is deprecated and will " "be removed in future versions", warn_tag="deprecated") refs = self._remote_manager.get_recipe_revisions(reference, remote) results.extend([{'remote': remote, 'ref': ref} for ref in refs]) continue if not reference.revision: ref = self._remote_manager.get_latest_recipe_revision(reference, remote) else: ref = self._remote_manager.get_recipe_revision(reference, remote) if not need_update: return remote, ref results.append({'remote': remote, 'ref': ref}) except NotFoundException: pass if len(results) == 0: return None, None if self._legacy_update and need_update: # Use only the first occurence of each revision in the remotes filtered_results = [] revisions = set() for r in results: ref = r["ref"] if ref.revision not in revisions: revisions.add(ref.revision) filtered_results.append(r) results = filtered_results remotes_results = sorted(results, key=lambda k: k['ref'].timestamp, reverse=True) # get the latest revision from all remotes found_rrev = remotes_results[0] return found_rrev.get("remote"), found_rrev.get("ref") def _download_recipe(self, ref, remotes, scoped_output, update, check_update): # When a recipe doesn't existin local cache, it is retrieved from servers scoped_output.info("Not found in local cache, looking in remotes...") if not remotes: raise ConanException("No remote defined") remote, latest_rref = self._find_newest_recipe_in_remotes(ref, remotes, update, check_update) if not latest_rref: msg = "Unable to find '%s' in remotes" % repr(ref) raise NotFoundException(msg) recipe_layout = self._download(latest_rref, remote) return recipe_layout, remote def _download(self, ref, remote): assert ref.revision assert ref.timestamp recipe_layout = self._remote_manager.get_recipe(ref, remote) output = ConanOutput(scope=str(ref)) output.info("Downloaded recipe revision %s" % ref.revision) return recipe_layout def should_update_reference(reference, update): if update is None: return False # Old API usages only ever passed a bool if isinstance(update, bool): return update # Legacy syntax had --update without pattern, it manifests as a "*" pattern return any(name == "*" or reference.name == name for name in update) ================================================ FILE: conan/internal/graph/python_requires.py ================================================ import os from conan.errors import ConanException from conan.api.model import RecipeReference from conan.internal.model.requires import Requirement class PyRequire: def __init__(self, module, conanfile, ref, path, recipe_status, remote): self.module = module self.conanfile = conanfile self.ref = ref self.path = path self.recipe = recipe_status self.remote = remote def serialize(self): return {"remote": self.remote.name if self.remote is not None else None, "recipe": self.recipe, "path": self.path} class PyRequires: """ this is the object that replaces the declared conanfile.py_requires""" def __init__(self): self._pyrequires = {} # {pkg-name: PythonRequire} def serialize(self): return {r.ref.repr_notime(): r.serialize() for r in self._pyrequires.values()} def all_refs(self): return [r.ref for r in self._pyrequires.values()] def info_requires(self): return {pyreq.ref: getattr(pyreq.conanfile, "package_id_python_mode", None) for pyreq in self._pyrequires.values()} def items(self): return self._pyrequires.items() def __getitem__(self, item): try: return self._pyrequires[item] except KeyError: raise ConanException("'%s' is not a python_require" % item) def add_pyrequire(self, py_require): key = py_require.ref.name # single item assignment, direct existing = self._pyrequires.get(key) if existing and existing is not py_require: # if is the same one, can be added. # TODO: Better test python_requires conflicts raise ConanException("The python_require '%s' already exists" % key) self._pyrequires[key] = py_require transitive = getattr(py_require.conanfile, "python_requires", None) if transitive is None: return for name, transitive_py_require in transitive.items(): existing = self._pyrequires.get(name) if existing and existing.ref != transitive_py_require.ref: raise ConanException("Conflict in py_requires %s - %s" % (existing.ref, transitive_py_require.ref)) self._pyrequires[name] = transitive_py_require class PyRequireLoader: def __init__(self, proxy, range_resolver, global_conf): self._proxy = proxy self._range_resolver = range_resolver self._cached_py_requires = {} self._resolve_prereleases = global_conf.get("core.version_ranges:resolve_prereleases") def load_py_requires(self, conanfile, loader, graph_lock, remotes, update, check_update): py_requires_refs = getattr(conanfile, "python_requires", None) if py_requires_refs is None: return if isinstance(py_requires_refs, str): py_requires_refs = [py_requires_refs, ] py_requires = self._resolve_py_requires(py_requires_refs, graph_lock, loader, remotes, update, check_update) if hasattr(conanfile, "python_requires_extend"): py_requires_extend = conanfile.python_requires_extend if isinstance(py_requires_extend, str): py_requires_extend = [py_requires_extend, ] for p in py_requires_extend: pkg_name, base_class_name = p.rsplit(".", 1) base_class = getattr(py_requires[pkg_name].module, base_class_name) conanfile.__bases__ = (base_class,) + conanfile.__bases__ conanfile.python_requires = py_requires def _resolve_py_requires(self, py_requires_refs, graph_lock, loader, remotes, update, check_update): result = PyRequires() for py_requires_ref in py_requires_refs: py_requires_ref = RecipeReference.loads(py_requires_ref) requirement = Requirement(py_requires_ref) resolved_ref = self._resolve_ref(requirement, graph_lock, remotes, update) try: py_require = self._cached_py_requires[resolved_ref] except KeyError: pyreq_conanfile = self._load_pyreq_conanfile(loader, graph_lock, resolved_ref, remotes, update, check_update) conanfile, module, new_ref, path, recipe_status, remote = pyreq_conanfile py_require = PyRequire(module, conanfile, new_ref, path, recipe_status, remote) self._cached_py_requires[resolved_ref] = py_require result.add_pyrequire(py_require) return result def _resolve_ref(self, requirement, graph_lock, remotes, update): if requirement.alias: raise ConanException("python-requires 'alias' are not supported in Conan 2.0. " "Please use version ranges instead") if graph_lock: graph_lock.resolve_locked_pyrequires(requirement, self._resolve_prereleases) # If the lock hasn't resolved the range, and it hasn't failed (it is partial), resolve it self._range_resolver.resolve(requirement, "python_requires", remotes, update) ref = requirement.ref return ref def _load_pyreq_conanfile(self, loader, graph_lock, ref, remotes, update, check_update): try: recipe = self._proxy.get_recipe(ref, remotes, update, check_update) except ConanException as e: raise ConanException(f"Cannot resolve python_requires '{ref}': {str(e)}") layout, recipe_status, remote = recipe path = layout.conanfile() new_ref = layout.reference conanfile, module = loader.load_basic_module(path, graph_lock, remotes=remotes, update=update, check_update=check_update) conanfile.name = new_ref.name conanfile.version = str(new_ref.version) conanfile.user = new_ref.user # TODO: Is this really necessary? conanfile.channel = new_ref.channel if getattr(conanfile, "alias", None): raise ConanException("python-requires 'alias' are not supported in Conan 2.0. " "Please use version ranges instead") return conanfile, module, new_ref, os.path.dirname(path), recipe_status, remote ================================================ FILE: conan/internal/graph/range_resolver.py ================================================ from conan.api.output import ConanOutput from conan.internal.graph.proxy import should_update_reference from conan.errors import ConanException from conan.api.model import RecipeReference from conan.internal.model.version_range import VersionRange class RangeResolver: def __init__(self, cache, remote_manager, global_conf, editable_packages): self._cache = cache self._editable_packages = editable_packages self._remote_manager = remote_manager self._cached_cache = {} # Cache caching of search result, so invariant wrt installations self._cached_remote_found = {} # dict {ref (pkg/*): {remote_name: results (pkg/1, pkg/2)}} self.resolved_ranges = {} self._resolve_prereleases = global_conf.get('core.version_ranges:resolve_prereleases') def resolve(self, require, base_conanref, remotes, update): try: version_range = require.version_range except Exception as e: base = base_conanref or "conanfile" raise ConanException(f"\n Recipe '{base}' requires '{require.ref}' " f"version-range definition error:\n {e}") if version_range is None: return assert isinstance(version_range, VersionRange) if require.ref.revision is not None: ConanOutput(base_conanref).warning("Specifying a revision for requirement " f"'{require.ref.repr_notime()}' together with " "a version range has no effect. " "The revision will be ignored.", warn_tag="risk") # Check if this ref with version range was already solved previous_ref = self.resolved_ranges.get(require.ref) if previous_ref is not None: require.ref = previous_ref return ref = require.ref search_ref = RecipeReference(ref.name, "*", ref.user, ref.channel) resolved_ref = self._resolve_local(search_ref, version_range) if resolved_ref is None or should_update_reference(search_ref, update): remote_resolved_ref = self._resolve_remote(search_ref, version_range, remotes, update) if resolved_ref is None or (remote_resolved_ref is not None and resolved_ref.version < remote_resolved_ref.version): resolved_ref = remote_resolved_ref if resolved_ref is None: raise ConanException(f"Version range '{version_range}' from requirement '{require.ref}' " f"required by '{base_conanref}' could not be resolved") # To fix Cache behavior, we remove the revision information resolved_ref.revision = None # FIXME: Wasting information already obtained from server? self.resolved_ranges[require.ref] = resolved_ref require.ref = resolved_ref def _resolve_local(self, search_ref, version_range): pattern = str(search_ref) local_found = self._cached_cache.get(pattern) if local_found is None: # This local_found is weird, it contains multiple revisions, not just latest local_found = self._cache.search_recipes(pattern) # TODO: This is still necessary to filter user/channel, until search_recipes is fixed local_found = [ref for ref in local_found if ref.user == search_ref.user and ref.channel == search_ref.channel] local_found.extend(r for r in self._editable_packages.edited_refs if r.name == search_ref.name and r.user == search_ref.user and r.channel == search_ref.channel) self._cached_cache[pattern] = local_found if local_found: return self._resolve_version(version_range, local_found, self._resolve_prereleases) def _search_remote_recipes(self, remote, search_ref): if remote.allowed_packages and not any(search_ref.matches(f, is_consumer=False) for f in remote.allowed_packages): return [] pattern = str(search_ref) pattern_cached = self._cached_remote_found.setdefault(pattern, {}) results = pattern_cached.get(remote.name) if results is None: results = self._remote_manager.search_recipes(remote, pattern) # TODO: This is still necessary to filter user/channel, until search_recipes is fixed results = [ref for ref in results if ref.user == search_ref.user and ref.channel == search_ref.channel] pattern_cached.update({remote.name: results}) return results def _resolve_remote(self, search_ref, version_range, remotes, update): update_candidates = [] for remote in remotes: remote_results = self._search_remote_recipes(remote, search_ref) resolved_version = self._resolve_version(version_range, remote_results, self._resolve_prereleases) if resolved_version: if not should_update_reference(search_ref, update): return resolved_version # Return first valid occurrence in first remote else: update_candidates.append(resolved_version) if len(update_candidates) > 0: # pick latest from already resolved candidates resolved_version = self._resolve_version(version_range, update_candidates, self._resolve_prereleases) return resolved_version @staticmethod def _resolve_version(version_range, refs_found, resolve_prereleases): for ref in reversed(sorted(refs_found)): if version_range.contains(ref.version, resolve_prereleases): return ref ================================================ FILE: conan/internal/hook_manager.py ================================================ import os from conan.internal.loader import load_python_file from conan.errors import ConanException, ConanInvalidConfiguration valid_hook_methods = ["pre_export", "post_export", "pre_validate", "post_validate", "pre_source", "post_source", "pre_generate", "post_generate", "pre_build", "post_build", "post_build_fail", "pre_package", "post_package", "pre_package_info", "post_package_info", "post_package_id"] # package_id is called a lot more than others, only post class HookManager: def __init__(self, hooks_folder): self._hooks_folder = hooks_folder self.hooks = {} self.validate_hook = False # Quick check for performance self.post_package_id_hook = False # Quick check for performance self._load_hooks() # A bit dirty, but avoid breaking tests def execute(self, method_name, conanfile): assert method_name in valid_hook_methods, \ "Method '{}' not in valid hooks methods".format(method_name) hooks = self.hooks.get(method_name) if hooks is None: return for name, method in hooks: # TODO: This display_name is ugly, improve it display_name = conanfile.display_name try: conanfile.display_name = "%s: [HOOK - %s] %s()" % (conanfile.display_name, name, method_name) method(conanfile) except ConanInvalidConfiguration: raise except Exception as e: raise ConanException("[HOOK - %s] %s(): %s" % (name, method_name, str(e))) finally: conanfile.display_name = display_name def _load_hooks(self): hooks = {} for root, dirs, files in os.walk(self._hooks_folder): for f in files: if f.startswith("hook_") and f.endswith(".py"): hook_path = os.path.join(root, f) name = os.path.relpath(hook_path, self._hooks_folder).replace("\\", "/") hooks[name] = hook_path # Load in alphabetical order, just in case the order is important there is a criteria # This is difficult to test, apparently in most cases os.walk is alphabetical for name, hook_path in sorted(hooks.items()): self._load_hook(hook_path, name) self.validate_hook = bool(self.hooks.get("pre_validate") or self.hooks.get("post_validate")) self.post_package_id_hook = bool(self.hooks.get("post_package_id")) def _load_hook(self, hook_path, hook_name): try: hook, _ = load_python_file(hook_path) for method in valid_hook_methods: hook_method = getattr(hook, method, None) if hook_method: self.hooks.setdefault(method, []).append((hook_name, hook_method)) except Exception as e: raise ConanException("Error loading hook '%s': %s" % (hook_path, str(e))) def reinit(self): self.hooks.clear() self._load_hooks() ================================================ FILE: conan/internal/internal_tools.py ================================================ from conan.errors import ConanException universal_arch_separator = '|' def is_universal_arch(settings_value, valid_definitions): if (settings_value is None or valid_definitions is None or universal_arch_separator not in settings_value): return False parts = settings_value.split(universal_arch_separator) if parts != sorted(parts): raise ConanException(f"Architectures must be in alphabetical order separated by " f"{universal_arch_separator}") valid_macos_values = [val for val in valid_definitions if ("arm" in val or "x86" in val)] return all(part in valid_macos_values for part in parts) def raise_on_universal_arch(conanfile): if is_universal_arch(conanfile.settings.get_safe("arch"), conanfile.settings.possible_values().get("arch")): raise ConanException("Universal binaries not supported by toolchain.") ================================================ FILE: conan/internal/loader.py ================================================ import traceback from importlib import invalidate_caches, util as imp_util import inspect import os import re import sys import types import uuid from threading import Lock import yaml from pathlib import Path from conan.api.output import ConanOutput from conan.tools.cmake import cmake_layout from conan.tools.google import bazel_layout from conan.tools.microsoft import vs_layout from conan.internal.errors import conanfile_exception_formatter, NotFoundException from conan.errors import ConanException from conan.internal.model.conan_file import ConanFile from conan.internal.model.options import Options from conan.api.model import RecipeReference from conan.internal.paths import DATA_YML from conan.internal.model.version_range import validate_conan_version from conan.internal.util.config_parser import TextINIParse from conan.internal.util.files import load, chdir, load_user_encoded class ConanFileLoader: def __init__(self, pyreq_loader=None, conanfile_helpers=None): self._pyreq_loader = pyreq_loader self._cached_conanfile_classes = {} self._conanfile_helpers = conanfile_helpers invalidate_caches() def load_basic(self, conanfile_path, graph_lock=None, display="", remotes=None, update=None, check_update=None): """ loads a conanfile basic object without evaluating anything """ return self.load_basic_module(conanfile_path, graph_lock, display, remotes, update, check_update)[0] def load_basic_module(self, conanfile_path, graph_lock=None, display="", remotes=None, update=None, check_update=None, tested_python_requires=None): """ loads a conanfile basic object without evaluating anything, returns the module too """ cached = self._cached_conanfile_classes.get(conanfile_path) if cached: conanfile = cached[0](display) conanfile._conan_helpers = self._conanfile_helpers if hasattr(conanfile, "init") and callable(conanfile.init): with conanfile_exception_formatter(conanfile, "init"): conanfile.init() return conanfile, cached[1] try: module, conanfile = _parse_conanfile(conanfile_path) if isinstance(tested_python_requires, RecipeReference): if getattr(conanfile, "python_requires", None) == "tested_reference_str": conanfile.python_requires = tested_python_requires.repr_notime() elif tested_python_requires: if getattr(conanfile, "python_requires", None) != "tested_reference_str": ConanOutput().warning("test_package/conanfile.py should declare 'python_requires" " = \"tested_reference_str\"'", warn_tag="deprecated") conanfile.python_requires = tested_python_requires if self._pyreq_loader: self._pyreq_loader.load_py_requires(conanfile, self, graph_lock, remotes, update, check_update) conanfile.recipe_folder = os.path.dirname(conanfile_path) conanfile.recipe_path = Path(conanfile.recipe_folder) # Load and populate dynamic fields from the data file conan_data = self._load_data(conanfile_path) conanfile.conan_data = conan_data self._cached_conanfile_classes[conanfile_path] = (conanfile, module) result = conanfile(display) result._conan_helpers = self._conanfile_helpers if hasattr(result, "init") and callable(result.init): with conanfile_exception_formatter(result, "init"): result.init() return result, module except ConanException as e: raise ConanException("Error loading conanfile at '{}': {}".format(conanfile_path, e)) @staticmethod def _load_data(conanfile_path): data_path = os.path.join(os.path.dirname(conanfile_path), DATA_YML) if not os.path.exists(data_path): return None try: data = yaml.safe_load(load(data_path)) except Exception as e: raise ConanException("Invalid yml format at {}: {}".format(DATA_YML, e)) return data or {} def load_named(self, conanfile_path, name, version, user, channel, graph_lock=None, remotes=None, update=None, check_update=None, tested_python_requires=None): """ loads the basic conanfile object and evaluates its name and version """ conanfile, _ = self.load_basic_module(conanfile_path, graph_lock, remotes=remotes, update=update, check_update=check_update, tested_python_requires=tested_python_requires) # Export does a check on existing name & version if name: if conanfile.name and name != conanfile.name: raise ConanException("Package recipe with name %s!=%s" % (name, conanfile.name)) conanfile.name = name if version: if conanfile.version and version != conanfile.version: raise ConanException("Package recipe with version %s!=%s" % (version, conanfile.version)) conanfile.version = version if user: if conanfile.user and user != conanfile.user: raise ConanException("Package recipe with user %s!=%s" % (user, conanfile.user)) conanfile.user = user if channel: if conanfile.channel and channel != conanfile.channel: raise ConanException("Package recipe with channel %s!=%s" % (channel, conanfile.channel)) conanfile.channel = channel if conanfile.channel and not conanfile.user: raise ConanException(f"{conanfile_path}: Can't specify channel '{conanfile.channel}' without user") if hasattr(conanfile, "set_name"): with conanfile_exception_formatter("conanfile.py", "set_name"): conanfile.set_name() if hasattr(conanfile, "set_version"): with conanfile_exception_formatter("conanfile.py", "set_version"): conanfile.set_version() return conanfile def load_export(self, conanfile_path, name, version, user, channel, graph_lock=None, remotes=None): """ loads the conanfile and evaluates its name, version, and enforce its existence """ conanfile = self.load_named(conanfile_path, name, version, user, channel, graph_lock, remotes=remotes) if not conanfile.name: raise ConanException("conanfile didn't specify name") if not conanfile.version: raise ConanException("conanfile didn't specify version") ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) conanfile.display_name = str(ref) conanfile.output.scope = conanfile.display_name return conanfile def load_consumer(self, conanfile_path, name=None, version=None, user=None, channel=None, graph_lock=None, remotes=None, update=None, check_update=None, tested_python_requires=None): """ loads a conanfile.py in user space. Might have name/version or not """ conanfile = self.load_named(conanfile_path, name, version, user, channel, graph_lock, remotes, update, check_update, tested_python_requires=tested_python_requires) ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) if str(ref): conanfile.display_name = "%s (%s)" % (os.path.basename(conanfile_path), str(ref)) else: conanfile.display_name = os.path.basename(conanfile_path) conanfile.output.scope = conanfile.display_name conanfile._conan_is_consumer = True return conanfile def load_conanfile(self, conanfile_path, ref, graph_lock=None, remotes=None, update=None, check_update=None): """ load a conanfile with a full reference, name, version, user and channel are obtained from the reference, not evaluated. Main way to load from the cache """ try: conanfile, _ = self.load_basic_module(conanfile_path, graph_lock, str(ref), remotes, update=update, check_update=check_update) except Exception as e: raise ConanException("%s: Cannot load recipe.\n%s" % (str(ref), str(e))) conanfile.name = ref.name conanfile.version = str(ref.version) conanfile.user = ref.user conanfile.channel = ref.channel return conanfile def load_conanfile_txt(self, conan_txt_path): if not os.path.exists(conan_txt_path): raise NotFoundException("Conanfile not found!") try: contents = load_user_encoded(conan_txt_path) except Exception as e: raise ConanException(f"Cannot load conanfile.txt:\n{e}") path, basename = os.path.split(conan_txt_path) display_name = basename conanfile = self._parse_conan_txt(contents, path, display_name) conanfile._conan_helpers = self._conanfile_helpers conanfile._conan_is_consumer = True return conanfile @staticmethod def _parse_conan_txt(contents, path, display_name): conanfile = ConanFile(display_name) try: parser = ConanFileTextLoader(contents) except Exception as e: raise ConanException("%s:\n%s" % (path, str(e))) for reference in parser.requirements: conanfile.requires(reference) for build_reference in parser.tool_requirements: # TODO: Improve this interface conanfile.requires.tool_require(build_reference) for ref in parser.test_requirements: # TODO: Improve this interface conanfile.requires.test_require(ref) if parser.layout: layout_method = {"cmake_layout": cmake_layout, "vs_layout": vs_layout, "bazel_layout": bazel_layout}.get(parser.layout) if not layout_method: raise ConanException("Unknown predefined layout '{}' declared in " "conanfile.txt".format(parser.layout)) def layout(_self): layout_method(_self) conanfile.layout = types.MethodType(layout, conanfile) conanfile.generators = parser.generators try: conanfile.options = Options.loads(parser.options) except Exception: raise ConanException("Error while parsing [options] in conanfile.txt\n" "Options should be specified as 'pkg/*:option=value'") return conanfile def load_virtual(self, requires=None, tool_requires=None, python_requires=None, graph_lock=None, remotes=None, update=None, check_updates=None): # If user don't specify namespace in options, assume that it is # for the reference (keep compatibility) conanfile = ConanFile(display_name="cli") conanfile._conan_helpers = self._conanfile_helpers if tool_requires: for reference in tool_requires: conanfile.requires.tool_require(repr(reference)) if requires: for reference in requires: conanfile.requires(repr(reference)) if python_requires: conanfile.python_requires = [pr.repr_notime() for pr in python_requires] if self._pyreq_loader: self._pyreq_loader.load_py_requires(conanfile, self, graph_lock, remotes, update, check_updates) conanfile._conan_is_consumer = True conanfile.generators = [] # remove the default txt generator return conanfile def _parse_module(conanfile_module, module_id): """ Parses a python in-memory module, to extract the classes, mainly the main class defining the Recipe, but also process possible existing generators @param conanfile_module: the module to be processed @return: the main ConanFile class from the module """ result = None for name, attr in conanfile_module.__dict__.items(): if (name.startswith("_") or not inspect.isclass(attr) or attr.__dict__.get("__module__") != module_id): continue if issubclass(attr, ConanFile) and attr != ConanFile: if result is None: result = attr else: raise ConanException("More than 1 conanfile in the file") if result is None: raise ConanException("No subclass of ConanFile") return result _load_python_lock = Lock() # Loading our Python files is not thread-safe (modifies sys) def _parse_conanfile(conanfile_path): with _load_python_lock: module, module_id = _load_python_file(conanfile_path) try: conanfile = _parse_module(module, module_id) return module, conanfile except Exception as e: # re-raise with file name raise ConanException("%s: %s" % (conanfile_path, str(e))) def load_python_file(conan_file_path): """ From a given path, obtain the in memory python import module """ with _load_python_lock: module, module_id = _load_python_file(conan_file_path) return module, module_id def _load_python_file(conan_file_path): """ From a given path, obtain the in memory python import module """ if not os.path.exists(conan_file_path): raise NotFoundException("%s not found!" % conan_file_path) def new_print(*args, **kwargs): # Make sure that all user python files print() goes to stderr kwargs.setdefault("file", sys.stderr) print(*args, **kwargs) module_id = str(uuid.uuid1()) current_dir = os.path.dirname(conan_file_path) sys.path.insert(0, current_dir) try: old_modules = list(sys.modules.keys()) with chdir(current_dir): old_dont_write_bytecode = sys.dont_write_bytecode try: sys.dont_write_bytecode = True spec = imp_util.spec_from_file_location(module_id, conan_file_path) loaded = imp_util.module_from_spec(spec) spec.loader.exec_module(loaded) sys.dont_write_bytecode = old_dont_write_bytecode except ImportError: version_txt = _get_required_conan_version_without_loading(conan_file_path) if version_txt: validate_conan_version(version_txt) raise required_conan_version = getattr(loaded, "required_conan_version", None) if required_conan_version: validate_conan_version(required_conan_version) # These lines are necessary, otherwise local conanfile imports with same name # collide, but no error, and overwrite other packages imports!! added_modules = set(sys.modules).difference(old_modules) for added in added_modules: module = sys.modules[added] if module: try: try: # Most modules will have __file__ != None folder = os.path.dirname(module.__file__) except (AttributeError, TypeError): # But __file__ might not exist or equal None # Like some builtins and Namespace packages py3 folder = module.__path__._path[0] except AttributeError: # In case the module.__path__ doesn't exist pass else: if folder.startswith(current_dir): module = sys.modules.pop(added) module.print = new_print sys.modules["%s.%s" % (module_id, added)] = module except ConanException: raise except Exception: trace = traceback.format_exc().split('\n') raise ConanException("Unable to load conanfile in %s\n%s" % (conan_file_path, '\n'.join(trace[3:]))) finally: sys.path.pop(0) loaded.print = new_print return loaded, module_id def _get_required_conan_version_without_loading(conan_file_path): # First, try to detect the required_conan_version in "text" mode # https://github.com/conan-io/conan/issues/11239 contents = load(conan_file_path) txt_version = None try: found = re.search(r"(.*)required_conan_version\s*=\s*[\"'](.*)[\"']", contents) if found and "#" not in found.group(1): txt_version = found.group(2) except: # noqa this should be solid, cannot fail pass return txt_version class ConanFileTextLoader: """Parse a conanfile.txt file""" def __init__(self, input_text): # Prefer composition over inheritance, the __getattr__ was breaking things self._config_parser = TextINIParse(input_text, ["requires", "generators", "options", "imports", "tool_requires", "test_requires", "layout"], strip_comments=True) @property def layout(self): """returns the declared layout""" tmp = [r.strip() for r in self._config_parser.layout.splitlines()] if len(tmp) > 1: raise ConanException("Only one layout can be declared in the [layout] section of " "the conanfile.txt") return tmp[0] if tmp else None @property def requirements(self): """returns a list of requires EX: "OpenCV/2.4.10@phil/stable" """ return [r.strip() for r in self._config_parser.requires.splitlines()] @property def tool_requirements(self): """returns a list of tool_requires EX: "OpenCV/2.4.10@phil/stable" """ return [r.strip() for r in self._config_parser.tool_requires.splitlines()] @property def test_requirements(self): """returns a list of test_requires EX: "gtest/2.4.10@phil/stable" """ return [r.strip() for r in self._config_parser.test_requires.splitlines()] @property def options(self): return self._config_parser.options @property def generators(self): return self._config_parser.generators.splitlines() ================================================ FILE: conan/internal/methods.py ================================================ import os from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.errors import conanfile_exception_formatter, conanfile_remove_attr from conan.internal.paths import CONANINFO from conan.internal.model.manifest import FileTreeManifest from conan.api.model import PkgReference from conan.internal.model.pkg_type import PackageType from conan.internal.model.requires import BuildRequirements, TestRequirements, ToolRequirements from conan.internal.util.files import mkdir, chdir, save def run_source_method(conanfile, hook_manager): mkdir(conanfile.source_folder) with chdir(conanfile.source_folder): hook_manager.execute("pre_source", conanfile=conanfile) if hasattr(conanfile, "source"): conanfile.output.highlight("Calling source() in {}".format(conanfile.source_folder)) with conanfile_exception_formatter(conanfile, "source"): with conanfile_remove_attr(conanfile, ['info', 'settings', "options"], "source"): conanfile.source() hook_manager.execute("post_source", conanfile=conanfile) def run_build_method(conanfile, hook_manager): if os.path.isfile(conanfile.build_folder): raise ConanException(f"{conanfile}: Failed to create build folder, there is already a file " f"named: {conanfile.build_folder}") mkdir(conanfile.build_folder) mkdir(conanfile.package_metadata_folder) with chdir(conanfile.build_folder): hook_manager.execute("pre_build", conanfile=conanfile) if hasattr(conanfile, "build"): conanfile.output.highlight("Calling build()") with conanfile_exception_formatter(conanfile, "build"): try: conanfile.build() except Exception: hook_manager.execute("post_build_fail", conanfile=conanfile) raise hook_manager.execute("post_build", conanfile=conanfile) def run_package_method(conanfile, package_id, hook_manager, ref): """ calls the recipe "package()" method - Assigns folders to conanfile.package_folder, source_folder, install_folder, build_folder - Calls pre-post package hook """ if conanfile.package_folder == conanfile.build_folder: raise ConanException("Cannot 'conan package' to the build folder. " "--build-folder and package folder can't be the same") mkdir(conanfile.package_folder) scoped_output = conanfile.output # Make the copy of all the patterns scoped_output.info("Generating the package") scoped_output.info("Packaging in folder %s" % conanfile.package_folder) hook_manager.execute("pre_package", conanfile=conanfile) if hasattr(conanfile, "package"): scoped_output.highlight("Calling package()") with conanfile_exception_formatter(conanfile, "package"): with chdir(conanfile.build_folder): with conanfile_remove_attr(conanfile, ['info'], "package"): conanfile.package() hook_manager.execute("post_package", conanfile=conanfile) save(os.path.join(conanfile.package_folder, CONANINFO), conanfile.info.dumps()) manifest = FileTreeManifest.create(conanfile.package_folder) manifest.save(conanfile.package_folder) package_output = ConanOutput(scope="%s: package()" % scoped_output.scope) manifest.report_summary(package_output, "Packaged") prev = manifest.summary_hash scoped_output.info("Created package revision %s" % prev) pref = PkgReference(ref, package_id) pref.revision = prev scoped_output.success("Package '%s' created" % package_id) scoped_output.success("Full package reference: {}".format(pref.repr_notime())) return prev def run_configure_method(conanfile, down_options, profile_options, ref): """ Run all the config-related functions for the given conanfile object """ initial_requires_count = len(conanfile.requires) if hasattr(conanfile, "config_options"): with conanfile_exception_formatter(conanfile, "config_options"): conanfile.config_options() elif "auto_shared_fpic" in conanfile.implements: auto_shared_fpic_config_options(conanfile) auto_language(conanfile) # default implementation removes `compiler.cstd` # Assign only the current package options values, but none of the dependencies is_consumer = conanfile._conan_is_consumer # noqa conanfile.options.apply_downstream(down_options, profile_options, ref, is_consumer) if hasattr(conanfile, "configure"): with conanfile_exception_formatter(conanfile, "configure"): conanfile.configure() elif "auto_shared_fpic" in conanfile.implements: auto_shared_fpic_configure(conanfile) if initial_requires_count != len(conanfile.requires): conanfile.output.warning("Requirements should only be added in the requirements()/" "build_requirements() methods, not configure()/config_options(), " "which might raise errors in the future.", warn_tag="deprecated") result = conanfile.options.get_upstream_options(down_options, ref, is_consumer) self_options, up_options, private_up_options = result # self_options are the minimum to reproduce state, as defined from downstream (not profile) conanfile.self_options = self_options # up_options are the minimal options that should be propagated to dependencies conanfile.up_options = up_options conanfile.private_up_options = private_up_options PackageType.compute_package_type(conanfile) conanfile.build_requires = BuildRequirements(conanfile.requires) conanfile.test_requires = TestRequirements(conanfile.requires) conanfile.tool_requires = ToolRequirements(conanfile.requires) if hasattr(conanfile, "requirements"): with conanfile_exception_formatter(conanfile, "requirements"): conanfile.requirements() if hasattr(conanfile, "build_requirements"): with conanfile_exception_formatter(conanfile, "build_requirements"): conanfile.build_requirements() def auto_shared_fpic_config_options(conanfile): if conanfile.settings.get_safe("os") == "Windows": conanfile.options.rm_safe("fPIC") def auto_shared_fpic_configure(conanfile): if conanfile.options.get_safe("header_only"): conanfile.options.rm_safe("fPIC") conanfile.options.rm_safe("shared") elif conanfile.options.get_safe("shared"): conanfile.options.rm_safe("fPIC") def auto_header_only_package_id(conanfile): if conanfile.options.get_safe("header_only") or conanfile.package_type is PackageType.HEADER: conanfile.info.clear() def auto_language(conanfile): if not conanfile.languages: conanfile.settings.rm_safe("compiler.cstd") return if "C" not in conanfile.languages: conanfile.settings.rm_safe("compiler.cstd") if "C++" not in conanfile.languages: conanfile.settings.rm_safe("compiler.cppstd") conanfile.settings.rm_safe("compiler.libcxx") ================================================ FILE: conan/internal/model/__init__.py ================================================ ================================================ FILE: conan/internal/model/conan_file.py ================================================ import os import subprocess from pathlib import Path from conan.api.output import ConanOutput, Color, LEVEL_QUIET from conan.internal.subsystems import command_env_wrapper from conan.errors import ConanException from conan.internal.model.cpp_info import MockInfoProperty from conan.internal.model.conf import Conf from conan.internal.model.dependencies import ConanFileDependencies from conan.internal.model.layout import Folders, Infos, Layouts from conan.internal.model.options import Options from conan.internal.model.requires import Requirements from conan.internal.model.settings import Settings class ConanFile: """ The base class for all package recipes """ # Reference name = None version = None # Any str, can be "1.1" or whatever user = None channel = None # Metadata url = None # The URL where this File is located, as github, to collaborate in package license = None author = None description = None topics = None homepage = None build_policy = None upload_policy = None exports = None exports_sources = None generators = [] revision_mode = "hash" # Binary model: Settings and Options settings = None options = None default_options = None default_build_options = None package_type = None vendor = False languages = [] implements = [] provides = None deprecated = None win_bash = None win_bash_run = None # For run scope _conan_is_consumer = False # #### Requirements requires = None tool_requires = None build_requires = None test_requires = None tested_reference_str = None no_copy_source = False recipe_folder = None # Package information cpp = None buildenv_info = None runenv_info = None conf_info = None generator_info = None conan_data = None def __init__(self, display_name=""): self.display_name = display_name # something that can run commands, as os.sytem self._conan_helpers = None from conan.tools.env import Environment self.buildenv_info = Environment() self.runenv_info = Environment() # At the moment only for build_requires, others will be ignored self.conf_info = Conf() self.info = None self._conan_buildenv = None # The profile buildenv, will be assigned initialize() self._conan_runenv = None self._conan_node = None # access to container Node object, to access info, context, deps... if isinstance(self.generators, str): self.generators = [self.generators] if isinstance(self.languages, str): self.languages = [self.languages] if not all(lang == "C" or lang == "C++" for lang in self.languages): raise ConanException("Only 'C' and 'C++' languages are allowed in 'languages' attribute") if isinstance(self.settings, str): self.settings = [self.settings] self.requires = Requirements(self.requires, self.build_requires, self.test_requires, self.tool_requires) self.options = Options(self.options or {}, self.default_options) if isinstance(self.topics, str): self.topics = [self.topics] if isinstance(self.provides, str): self.provides = [self.provides] # user declared variables self.user_info = MockInfoProperty("user_info") self.env_info = MockInfoProperty("env_info") self._conan_dependencies = None if not hasattr(self, "virtualbuildenv"): # Allow the user to override it with True or False self.virtualbuildenv = True if not hasattr(self, "virtualrunenv"): # Allow the user to override it with True or False self.virtualrunenv = True self.env_scripts = {} # Accumulate the env scripts generated in order self.system_requires = {} # Read only, internal {"apt": []} # layout() method related variables: self.folders = Folders() self.cpp = Infos() self.layouts = Layouts() def serialize(self): result = {} for a in ("name", "user", "channel", "url", "license", "author", "description", "homepage", "build_policy", "upload_policy", "revision_mode", "provides", "deprecated", "win_bash", "win_bash_run", "default_options", "options_description"): v = getattr(self, a, None) result[a] = v result["version"] = str(self.version) if self.version is not None else None result["topics"] = list(self.topics) if self.topics is not None else None result["package_type"] = str(self.package_type) result["languages"] = self.languages settings = self.settings if settings is not None: result["settings"] = settings.serialize() if isinstance(settings, Settings) else list(settings) result["options"] = self.options.serialize() result["options_definitions"] = self.options.possible_values if self.generators is not None: result["generators"] = list(s.__name__ if isinstance(s, type) else s for s in self.generators) if self.license is not None: result["license"] = list(self.license) if not isinstance(self.license, str) else self.license result["requires"] = self.requires.serialize() if hasattr(self, "python_requires"): result["python_requires"] = self.python_requires.serialize() else: result["python_requires"] = None result["system_requires"] = self.system_requires result["recipe_folder"] = self.recipe_folder result["source_folder"] = self.source_folder result["build_folder"] = self.build_folder result["generators_folder"] = self.generators_folder result["package_folder"] = self.package_folder result["immutable_package_folder"] = self.immutable_package_folder result["cpp_info"] = self.cpp_info.serialize() result["conf_info"] = self.conf_info.serialize() result["label"] = self.display_name if self.info is not None: result["info"] = self.info.serialize() result["vendor"] = self.vendor if self.conan_data: result["conandata"] = self.conan_data return result @property def output(self): # an output stream (writeln, info, warn error) scope = self.display_name if not scope: scope = self.ref if self._conan_node else "" return ConanOutput(scope=scope) @property def context(self): return self._conan_node.context @property def subgraph(self): return self._conan_node.subgraph() @property def dependencies(self): # Caching it, this object is requested many times if self._conan_dependencies is None: self._conan_dependencies = ConanFileDependencies.from_node(self._conan_node) return self._conan_dependencies @property def ref(self): return self._conan_node.ref @property def pref(self): return self._conan_node.pref @property def buildenv(self): # Lazy computation of the package buildenv based on the profileone from conan.tools.env import Environment if not isinstance(self._conan_buildenv, Environment): self._conan_buildenv = self._conan_buildenv.get_profile_env(self.ref, self._conan_is_consumer) return self._conan_buildenv @property def runenv(self): # Lazy computation of the package runenv based on the profile one from conan.tools.env import Environment if not isinstance(self._conan_runenv, Environment): self._conan_runenv = self._conan_runenv.get_profile_env(self.ref, self._conan_is_consumer) return self._conan_runenv @property def cpp_info(self): """ Same as using ``self.cpp.package`` in the ``layout()`` method. Use it if you need to read the ``package_folder`` to locate the already located artifacts. """ return self.cpp.package @cpp_info.setter def cpp_info(self, value): self.cpp.package = value @property def source_folder(self): """ The folder in which the source code lives. The path is built joining the base directory (a cache directory when running in the cache or the ``output folder`` when running locally) with the value of ``folders.source`` if declared in the ``layout()`` method. :return: A string with the path to the source folder. """ return self.folders.source_folder @property def source_path(self) -> Path: self.output.warning(f"Use of 'source_path' is deprecated, please use 'source_folder' instead", warn_tag="deprecated") assert self.source_folder is not None, "`source_folder` is `None`" return Path(self.source_folder) @property def export_sources_folder(self): """ The value depends on the method you access it: - At ``source(self)``: Points to the base source folder (that means self.source_folder but without taking into account the ``folders.source`` declared in the ``layout()`` method). The declared `exports_sources` are copied to that base source folder always. - At ``exports_sources(self)``: Points to the folder in the cache where the export sources have to be copied. :return: A string with the mentioned path. """ return self.folders.base_export_sources @property def export_sources_path(self) -> Path: self.output.warning(f"Use of 'export_sources_path' is deprecated, please use " f"'export_sources_folder' instead", warn_tag="deprecated") assert self.export_sources_folder is not None, "`export_sources_folder` is `None`" return Path(self.export_sources_folder) @property def export_folder(self): return self.folders.base_export @property def export_path(self) -> Path: self.output.warning(f"Use of 'export_path' is deprecated, please use 'export_folder' instead", warn_tag="deprecated") assert self.export_folder is not None, "`export_folder` is `None`" return Path(self.export_folder) @property def build_folder(self): """ The folder used to build the source code. The path is built joining the base directory (a cache directory when running in the cache or the ``output folder`` when running locally) with the value of ``folders.build`` if declared in the ``layout()`` method. :return: A string with the path to the build folder. """ return self.folders.build_folder @property def recipe_metadata_folder(self): return self.folders.recipe_metadata_folder @property def package_metadata_folder(self): return self.folders.package_metadata_folder @property def build_path(self) -> Path: self.output.warning(f"Use of 'build_path' is deprecated, please use 'build_folder' instead", warn_tag="deprecated") assert self.build_folder is not None, "`build_folder` is `None`" return Path(self.build_folder) @property def package_folder(self): """ The folder to copy the final artifacts for the binary package. In the local cache a package folder is created for every different package ID. :return: A string with the path to the package folder. """ return self.folders.base_package @property def immutable_package_folder(self): return self.folders.immutable_package_folder @property def generators_folder(self): return self.folders.generators_folder @property def package_path(self) -> Path: self.output.warning(f"Use of 'package_path' is deprecated, please use 'package_folder' instead", warn_tag="deprecated") assert self.package_folder is not None, "`package_folder` is `None`" return Path(self.package_folder) @property def generators_path(self) -> Path: self.output.warning(f"Use of 'generators_path' is deprecated, please use " f"'generators_folder' instead", warn_tag="deprecated") assert self.generators_folder is not None, "`generators_folder` is `None`" return Path(self.generators_folder) def run(self, command: str, stdout=None, cwd=None, ignore_errors=False, env="", quiet=False, shell=True, scope="build", stderr=None): """ Run a command in the current package context. :parameter command: The command to run. :parameter stdout: The output stream to write the command output. If ``None``, it defaults to the standard output stream. :parameter stderr: The error output stream to write the command error output. If ``None``, it defaults to the standard error stream. :parameter cwd: The current working directory to run the command in. :parameter ignore_errors: If ``True``, do not raise an error if the command returns a non-zero exit code. :parameter env: The environment file to use. If empty, it defaults to ``"conanbuild"`` for when ``scope`` is ``build`` or ``"conanrun"`` for ``run``. If set to ``None`` explicitly, no environment file will be applied, which is useful for commands that do not require any environment. :parameter quiet: If ``True``, suppress the output of the command. :parameter shell: If ``True``, run the command in a shell. This is passed to the underlying ``Popen`` function. :parameter scope: The scope of the command, either ``"build"`` or ``"run"``. """ # NOTE: "self.win_bash" is the new parameter "win_bash" for Conan 2.0 command = self._conan_helpers.cmd_wrapper.wrap(command, conanfile=self) if env == "": # This default allows not breaking for users with ``env=None`` indicating # they don't want any env-file applied env = "conanbuild" if scope == "build" else "conanrun" env = [env] if env and isinstance(env, str) else (env or []) assert isinstance(env, list), "env argument to ConanFile.run() should be a list" envfiles_folder = self.generators_folder or os.getcwd() wrapped_cmd = command_env_wrapper(self, command, env, envfiles_folder=envfiles_folder, scope=scope) from conan.internal.util.runners import conan_run if not quiet: ConanOutput().info(f"{self.display_name}: RUN: {command}", fg=Color.BRIGHT_BLUE) ConanOutput().debug(f"{self.display_name}: Full command: {wrapped_cmd}") if quiet or ConanOutput.get_output_level() == LEVEL_QUIET: stdout = subprocess.DEVNULL if stdout is None else stdout stderr = subprocess.DEVNULL if stderr is None else stderr retcode = conan_run(wrapped_cmd, cwd=cwd, stdout=stdout, stderr=stderr, shell=shell) if not quiet: ConanOutput().writeln("") if not ignore_errors and retcode != 0: raise ConanException("Error %d while executing" % retcode) return retcode def __repr__(self): return self.display_name def set_deploy_folder(self, deploy_folder): self.cpp_info.deploy_base_folder(self.package_folder, deploy_folder) self.buildenv_info.deploy_base_folder(self.package_folder, deploy_folder) self.runenv_info.deploy_base_folder(self.package_folder, deploy_folder) self.folders.set_base_package(deploy_folder) ================================================ FILE: conan/internal/model/conanconfig.py ================================================ import json import yaml from conan.api.model import RecipeReference from conan.errors import ConanException from conan.internal.util.files import load, save def loadconanconfig(filename): try: contents = json.loads(load(filename)) config_versions = contents["config_version"] config_versions = [RecipeReference.loads(r) for r in config_versions] except Exception as e: raise ConanException(f"Error while loading config file {filename}: {str(e)}") return config_versions def loadconanconfig_yml(filename): try: contents = yaml.safe_load(load(filename)) config_versions = contents["packages"] config_versions = [RecipeReference.loads(r) for r in config_versions] urls = contents.get("urls") except Exception as e: raise ConanException(f"Error while loading config file {filename}: {str(e)}") return config_versions, urls def saveconanconfig(filename, config_versions): try: config_versions = [r.repr_notime() for r in config_versions] save(filename, json.dumps({"config_version": config_versions}, indent=4)) except Exception as e: raise ConanException(f"Error while saving config file {filename}: {str(e)}") ================================================ FILE: conan/internal/model/conanfile_interface.py ================================================ from pathlib import Path from conan.internal.graph.graph import CONTEXT_BUILD class ConanFileInterface: """ This is just a protective wrapper to give consumers a limited view of conanfile dependencies, "read" only, and only to some attributes, not methods """ def __str__(self): return str(self._conanfile) def __init__(self, conanfile): self._conanfile = conanfile def __eq__(self, other): """ The conanfile is a different entity per node, and conanfile equality is identity :type other: ConanFileInterface """ return self._conanfile == other._conanfile def __hash__(self): return hash(self._conanfile) @property def options(self): return self._conanfile.options @property def recipe_folder(self): return self._conanfile.recipe_folder @property def recipe_metadata_folder(self): return self._conanfile.recipe_metadata_folder @property def package_folder(self): return self._conanfile.package_folder @property def immutable_package_folder(self): return self._conanfile.immutable_package_folder @property def package_metadata_folder(self): return self._conanfile.package_metadata_folder @property def package_path(self) -> Path: assert self.package_folder is not None, "`package_folder` is `None`" return Path(self.package_folder) @property def ref(self): return self._conanfile.ref @property def pref(self): return self._conanfile.pref @property def buildenv_info(self): return self._conanfile.buildenv_info @property def runenv_info(self): return self._conanfile.runenv_info @property def cpp_info(self): return self._conanfile.cpp_info @property def settings(self): return self._conanfile.settings @property def settings_build(self): return self._conanfile.settings_build @property def context(self): return self._conanfile.context @property def conf_info(self): return self._conanfile.conf_info @property def generator_info(self): return self._conanfile.generator_info @property def dependencies(self): return self._conanfile.dependencies @property def folders(self): return self._conanfile.folders @property def is_build_context(self): return self._conanfile.context == CONTEXT_BUILD @property def package_type(self): return self._conanfile.package_type @property def languages(self): return self._conanfile.languages @property def info(self): return self._conanfile.info def set_deploy_folder(self, deploy_folder): self._conanfile.set_deploy_folder(deploy_folder) @property def conan_data(self): return self._conanfile.conan_data @property def license(self): return self._conanfile.license @property def description(self): return self._conanfile.description @property def author(self): return self._conanfile.author @property def homepage(self): return self._conanfile.homepage @property def url(self): return self._conanfile.url @property def extension_properties(self): return getattr(self._conanfile, "extension_properties", {}) @property def recipe(self) -> str: # IMPORTANT: this should be used only for "informational" purposes, see GH#18996. return self._conanfile._conan_node.recipe @property def conf(self): return self._conanfile.conf ================================================ FILE: conan/internal/model/conf.py ================================================ import copy import hashlib import numbers import platform import re import os import fnmatch import textwrap from collections import OrderedDict from jinja2 import Environment, FileSystemLoader from conan.errors import ConanException from conan.internal.api.detect import detect_api from conan.internal.cache.home_paths import HomePaths from conan.internal.model.options import _PackageOption from conan.internal.model.recipe_ref import ref_matches from conan.internal.model.settings import SettingsItem from conan.internal.util.files import load, save BUILT_IN_CONFS = { "core:required_conan_version": "Raise if current version does not match the defined range.", "core:non_interactive": "Disable interactive user input, raises error if input necessary", "core:warnings_as_errors": "Treat warnings matching any of the patterns in this list as errors and then raise an exception. " "Current warning tags are 'network', 'deprecated'", "core:skip_warnings": "Do not show warnings matching any of the patterns in this list. " "Current warning tags are 'network', 'deprecated', 'experimental'", "core:default_profile": "Defines the default host profile ('default' by default)", "core:default_build_profile": "Defines the default build profile ('default' by default)", "core:allow_uppercase_pkg_names": "Temporarily (will be removed in 2.X) allow uppercase names", "core.version_ranges:resolve_prereleases": "Whether version ranges can resolve to pre-releases or not", "core.upload:retry": "Number of retries in case of failure when uploading to Conan server", "core.upload:retry_wait": "Seconds to wait between upload attempts to Conan server", "core.upload:parallel": "Number of concurrent threads to upload packages", "core.download:parallel": "Number of concurrent threads to download packages", "core.download:retry": "Number of retries in case of failure when downloading from Conan server", "core.download:retry_wait": "Seconds to wait between download attempts from Conan server", "core.download:download_cache": "Define path to a file download cache", "core.cache:storage_path": "Absolute path where the packages and database are stored", "core:update_policy": "(Legacy). If equal 'legacy' when multiple remotes, update based on order of remotes, only the timestamp of the first occurrence of each revision counts.", # Sources backup "core.sources:download_cache": "Folder to store the sources backup", "core.sources:download_urls": "List of URLs to download backup sources from", "core.sources:upload_url": "Remote URL to upload backup sources to", "core.sources:exclude_urls": "URLs which will not be backed up", "core.sources.patch:extra_path": "Extra path to search for patch files for conan create", # Package ID "core.package_id:default_unknown_mode": "By default, 'semver_mode'", "core.package_id:default_non_embed_mode": "By default, 'minor_mode'", "core.package_id:default_embed_mode": "By default, 'full_mode'", "core.package_id:default_python_mode": "By default, 'minor_mode'", "core.package_id:default_build_mode": "By default, 'None'", "core.package_id:config_mode": "How the 'config_version' affects binaries. By default 'None'", # General HTTP(python-requests) configuration "core.net.http:max_retries": "Maximum number of connection retries (requests library)", "core.net.http:timeout": "Number of seconds without response to timeout (requests library)", "core.net.http:no_proxy_match": "List of urls to skip from proxies configuration", "core.net.http:proxies": "Dictionary containing the proxy configuration", "core.net.http:cacert_path": "Path containing a custom Cacert file", "core.net.http:client_cert": "Path or tuple of files containing a client cert (and key)", "core.net.http:clean_system_proxy": "If defined, the proxies system env-vars will be discarded", # Compression for `conan upload` "core.upload:compression_format": "The compression format used when uploading Conan packages. " "Possible values: 'zst', 'xz', 'gz' (default=gz)", "core.gzip:compresslevel": "The Gzip compression level for Conan artifacts (default=9)", "core:compresslevel": "The compression level for Conan artifacts (default zstd=3, gz=9)", # Excluded from revision_mode = "scm" dirty and Git().is_dirty() checks "core.scm:excluded": "List of excluded patterns for builtin git dirty checks", "core.scm:local_url": "By default allows to store local folders as remote url, but not upload them. Use 'allow' for allowing upload and 'block' to completely forbid it", # Compatibility opt-in, to be removed in future versions as optimized behavior becomes default "core.graph:compatibility_mode": "(Experimental) Set this to 'optimized' to enable the improved compatibility behaviour when querying multiple compatible binaries in remotes", # Tools "tools.android:ndk_path": "Argument for the CMAKE_ANDROID_NDK", "tools.android:cmake_legacy_toolchain": "Define to explicitly pass ANDROID_USE_LEGACY_TOOLCHAIN_FILE in CMake toolchain", "tools.build:skip_test": "Do not execute CMake.test() and Meson.test() when enabled", "tools.build:download_source": "Force download of sources for every package", "tools.build:jobs": "Default compile jobs number -jX Ninja, Make, /MP VS (default: max CPUs)", "tools.build:sysroot": "Pass the --sysroot= flag if available. (None by default)", "tools.build:add_rpath_link": "Add -Wl,-rpath-link flags pointing to all lib directories for host dependencies (CMake and Meson toolchains)", "tools.build.cross_building:can_run": "(boolean) Indicates whether is possible to run a non-native app on the same architecture. It's used by 'can_run' tool", "tools.build.cross_building:cross_build": "(boolean) Decides whether cross-building or not regardless of arch/OS settings. Used by 'cross_building' tool", "tools.build:verbosity": "Verbosity of build systems if set. Possible values are 'quiet' and 'verbose'", "tools.compilation:verbosity": "Verbosity of compilation tools if set. Possible values are 'quiet' and 'verbose'", "tools.cmake.cmaketoolchain:generator": "User defined CMake generator to use instead of default", "tools.cmake.cmaketoolchain:find_package_prefer_config": "Argument for the CMAKE_FIND_PACKAGE_PREFER_CONFIG", "tools.cmake.cmaketoolchain:toolchain_file": "Use other existing file rather than conan_toolchain.cmake one", "tools.cmake.cmaketoolchain:user_toolchain": "Inject existing user toolchains at the beginning of conan_toolchain.cmake", "tools.cmake.cmaketoolchain:system_name": "Define CMAKE_SYSTEM_NAME in CMakeToolchain", "tools.cmake.cmaketoolchain:system_version": "Define CMAKE_SYSTEM_VERSION in CMakeToolchain", "tools.cmake.cmaketoolchain:system_processor": "Define CMAKE_SYSTEM_PROCESSOR in CMakeToolchain", "tools.cmake.cmaketoolchain:toolset_arch": "Toolset architecture to be used as part of CMAKE_GENERATOR_TOOLSET in CMakeToolchain", "tools.cmake.cmaketoolchain:toolset_cuda": "(Experimental) Path to a CUDA toolset to use, or version if installed at the system level", "tools.cmake.cmaketoolchain:presets_environment": "String to define wether to add or not the environment section to the CMake presets. Empty by default, will generate the environment section in CMakePresets. Can take values: 'disabled'.", "tools.cmake.cmaketoolchain:extra_variables": "Dictionary with variables to be injected in CMakeToolchain (potential override of CMakeToolchain defined variables)", "tools.cmake.cmaketoolchain:enabled_blocks": "Select the specific blocks to use in the conan_toolchain.cmake", "tools.cmake.cmaketoolchain:user_presets": "(Experimental) Select a different name instead of CMakeUserPresets.json, empty to disable", "tools.cmake.cmake_layout:build_folder_vars": "Settings and Options that will produce a different build folder and different CMake presets names", "tools.cmake.cmake_layout:build_folder": "(Experimental) Allow configuring the base folder of the build for local builds", "tools.cmake.cmake_layout:test_folder": "(Experimental) Allow configuring the base folder of the build for test_package", "tools.cmake:cmake_program": "Path to CMake executable", "tools.cmake.cmakedeps:new": "Use the new CMakeDeps generator", "tools.cmake:ctest_args": "To inject list of arguments to CMake.ctest() runner", "tools.cmake:install_strip": "(Deprecated) Add --strip to cmake.install(). Use tools.build:install_strip instead", "tools.deployer:symlinks": "Set to False to disable deployers copying symlinks", "tools.files.download:retry": "Number of retries in case of failure when downloading", "tools.files.download:retry_wait": "Seconds to wait between download attempts", "tools.files.download:verify": "If set, overrides recipes on whether to perform SSL verification for their downloaded files. Only recommended to be set while testing", "tools.files.unzip:filter": "Define tar extraction filter: 'fully_trusted', 'tar', 'data'", "tools.graph:vendor": "(Experimental) If 'build', enables the computation of dependencies of vendoring packages to build them", "tools.graph:skip_binaries": "Allow the graph to skip binaries not needed in the current configuration (True by default)", "tools.graph:skip_build": "(Experimental) Do not expand build/tool_requires", "tools.graph:skip_test": "(Experimental) Do not expand test_requires. If building it might need 'tools.build:skip_test=True'", "tools.gnu:make_program": "Indicate path to make program", "tools.gnu:disable_flags": "Disable the automatic addition of flags to some build systems. List of possible values: ['arch', 'arch_link', 'libcxx', 'build_type', 'build_type_link', 'threads','cppstd', 'cstd']", "tools.gnu:define_libcxx11_abi": "Force definition of GLIBCXX_USE_CXX11_ABI=1 for libstdc++11", "tools.gnu:pkg_config": "Path to pkg-config executable used by PkgConfig build helper", "tools.gnu:build_triplet": "Custom build triplet to pass to Autotools scripts", "tools.gnu:host_triplet": "Custom host triplet to pass to Autotools scripts", "tools.gnu:extra_configure_args": "List of extra arguments to pass to configure when using AutotoolsToolchain and GnuToolchain", "tools.google.bazel:configs": "List of Bazel configurations to be used as 'bazel build --config=config1 ...'", "tools.google.bazel:bazelrc_path": "List of paths to bazelrc files to be used as 'bazel --bazelrc=rcpath1 ... build'", "tools.meson.mesontoolchain:backend": "Any Meson backend: ninja, vs, vs2010, vs2012, vs2013, vs2015, vs2017, vs2019, xcode", "tools.meson.mesontoolchain:extra_machine_files": "List of paths for any additional native/cross file references to be appended to the existing Conan ones", "tools.microsoft:winsdk_version": "Use this winsdk_version in vcvars", "tools.microsoft:msvc_update": "Force the specific update irrespective of compiler.update (CMakeToolchain and VCVars)", "tools.microsoft.msbuild:vs_version": "Defines the IDE version (15, 16, 17) when using the msvc compiler. Necessary if compiler.version specifies a toolset that is not the IDE default", "tools.microsoft.msbuild:max_cpu_count": "Argument for the /m when running msvc to build parallel projects", "tools.microsoft.msbuild:installation_path": "VS install path, to avoid auto-detect via vswhere, like C:/Program Files (x86)/Microsoft Visual Studio/2019/Community. Use empty string to disable", "tools.microsoft.msbuilddeps:exclude_code_analysis": "Suppress MSBuild code analysis for patterns", "tools.microsoft.msbuildtoolchain:compile_options": "Dictionary with MSBuild compiler options", "tools.microsoft.bash:subsystem": "The subsystem to be used when conanfile.win_bash==True. Possible values: msys2, msys, cygwin, wsl, sfu", "tools.microsoft.bash:path": "The path to the shell to run when conanfile.win_bash==True", "tools.microsoft.bash:active": "Set True only when Conan runs in a POSIX Bash (MSYS2/Cygwin) where Python's subprocess (shell=True) uses a POSIX-compatible shell (e.g., /bin/sh). Do not set when using Conan from cmd/PowerShell or with native Windows Python ('win32').", "tools.intel:installation_path": "Defines the Intel oneAPI installation root path", "tools.intel:setvars_args": "Custom arguments to be passed onto the setvars.sh|bat script from Intel oneAPI", "tools.system.package_manager:tool": "Default package manager tool: 'apk', 'apt-get', 'yum', 'dnf', 'brew', 'pacman', 'choco', 'zypper', 'pkg' or 'pkgutil'", "tools.system.package_manager:mode": "Mode for package_manager tools: 'check', 'report', 'report-installed' or 'install'", "tools.system.package_manager:sudo": "Use 'sudo' when invoking the package manager tools in Linux (False by default)", "tools.system.package_manager:sudo_askpass": "Use the '-A' argument if using sudo in Linux to invoke the system package manager (False by default)", "tools.system.pyenv:python_interpreter": "(Experimental) Path to the Python interpreter to be used to create the virtualenv", "tools.system.pipenv:python_interpreter": "(Deprecated) Use 'tools.system.pyenv:python_interpreter' instead. Path to the Python interpreter to be used to create the virtualenv", "tools.apple:sdk_path": "Path to the SDK to be used", "tools.apple:enable_bitcode": "(boolean) Enable/Disable Bitcode Apple Clang flags", "tools.apple:enable_arc": "(boolean) Enable/Disable ARC Apple Clang flags", "tools.apple:enable_visibility": "(boolean) Enable/Disable Visibility Apple Clang flags", "tools.env.virtualenv:powershell": "If specified, it generates PowerShell launchers (.ps1). Use this configuration setting the PowerShell executable you want to use (e.g., 'powershell.exe' or 'pwsh'). Setting it to True or False is deprecated as of Conan 2.11.0.", "tools.env:dotenv": "(Experimental) Generate dotenv environment files", "tools.env:deactivation_mode": "(Experimental) If 'function', generate a deactivate function instead of a script to unset the environment variables", # Compilers/Flags configurations "tools.build:compiler_executables": "Defines a Python dict-like with the compilers path to be used. Allowed keys {'c', 'cpp', 'cuda', 'objc', 'objcxx', 'rc', 'fortran', 'asm', 'hip', 'ispc'}", "tools.build:cxxflags": "List of extra CXX flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", "tools.build:cflags": "List of extra C flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", "tools.build:defines": "List of extra definition flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", "tools.build:sharedlinkflags": "List of extra flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", "tools.build:exelinkflags": "List of extra flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", "tools.build:rcflags": "List of extra RC (resource compiler) flags used by different toolchains like CMakeToolchain, MSBuildToolchain and MesonToolchain", "tools.build:linker_scripts": "List of linker script files to pass to the linker used by different toolchains like CMakeToolchain, AutotoolsToolchain, and MesonToolchain", # Toolchain installation "tools.build:install_strip": "(boolean) Strip the binaries when installing them with CMake, Meson and Autotools", # Package ID composition "tools.info.package_id:confs": "List of existing configuration to be part of the package ID", } BUILT_IN_CONFS = {key: value for key, value in sorted(BUILT_IN_CONFS.items())} _BUILT_IN_CONFS_TYPES = { "core:required_conan_version": str, "tools.microsoft:msvc_update": str } CORE_CONF_PATTERN = re.compile(r"^(core\..+|core):.*") TOOLS_CONF_PATTERN = re.compile(r"^(tools\..+|tools):.*") USER_CONF_PATTERN = re.compile(r"^(user\..+|user):.*") def _is_profile_module(module_name): # These are the modules that are propagated to profiles and user recipes return TOOLS_CONF_PATTERN.match(module_name) or USER_CONF_PATTERN.match(module_name) # FIXME: Refactor all the next classes because they are mostly the same as # conan.tools.env.environment ones class _ConfVarPlaceHolder: pass class _ConfValue: def __init__(self, name, value, path=False, update=None, important=False): self.name = name self._important = important self._value = value self._value_type = type(value) self._path = path self._update = update @staticmethod def parse(name, value, path=False, update=None): if name != name.lower(): raise ConanException("Conf '{}' must be lowercase".format(name)) name, important = (name[:-1], True) if name[-1] == "!" else (name, False) if isinstance(value, (_PackageOption, SettingsItem)): raise ConanException(f"Invalid 'conf' type, please use Python types (int, str, ...)") return _ConfValue(name, value, path=path, update=update, important=important) def __repr__(self): return repr(self._value) @property def value(self): if self._value_type is list and _ConfVarPlaceHolder in self._value: v = self._value[:] v.remove(_ConfVarPlaceHolder) return v return self._value def copy(self): # Using copy for when self._value is a mutable list return _ConfValue(self.name, copy.copy(self._value), self._path, self._update, self._important) def dumps(self): name = f"{self.name}!" if self._important else self.name if self._value is None: return "{}=!".format(name) # unset elif self._value_type is list and _ConfVarPlaceHolder in self._value: v = self._value[:] v.remove(_ConfVarPlaceHolder) return "{}={}".format(name, v) else: return "{}={}".format(name, self._value) def serialize(self): name = f"{self.name}!" if self._important else self.name if self._value is None: _value = "!" # unset elif self._value_type is list and _ConfVarPlaceHolder in self._value: v = self._value[:] v.remove(_ConfVarPlaceHolder) _value = v else: _value = self._value return {name: _value} def update(self, value): assert self._value_type is dict, "Only dicts can be updated" assert isinstance(value, dict), "Only dicts can update" self._value.update(value) def remove(self, value): if self._value_type is list: self._value.remove(value) elif self._value_type is dict: self._value.pop(value, None) def append(self, value): if self._value_type is not list: raise ConanException("Only list-like values can append other values.") if isinstance(value, list): self._value.extend(value) else: if isinstance(value, (_PackageOption, SettingsItem)): raise ConanException(f"Invalid 'conf' type, please use Python types (int, str, ...)") self._value.append(value) def prepend(self, value): if self._value_type is not list: raise ConanException("Only list-like values can prepend other values.") if isinstance(value, list): self._value = value + self._value else: if isinstance(value, (_PackageOption, SettingsItem)): raise ConanException(f"Invalid 'conf' type, please use Python types (int, str, ...)") self._value.insert(0, value) def compose_conf_value(self, other): """ self has precedence, the "other" will add/append if possible and not conflicting, but self mandates what to do. If self has define(), without placeholder, that will remain. :type other: _ConfValue """ v_type = self._value_type o_type = other._value_type important = other._important and not self._important if v_type is list and o_type is list: # If important, we swap values to prioritize the other v1, v2 = (other._value, self._value) if important else (self._value, other._value) try: index = v1.index(_ConfVarPlaceHolder) except ValueError: # It doesn't have placeholder if important: self._value = other._value else: new_value = v1[:] # do a copy new_value[index:index + 1] = v2 # replace the placeholder self._value = new_value elif v_type is dict and o_type is dict: if self._update: # only if the current one is marked as "*=" update, otherwise it remains # as this is a "compose" operation, self has priority, it is the one updating # If important, we swap values to prioritize the other v1, v2 = (other._value, self._value) if important else (self._value, other._value) new_value = v2.copy() new_value.update(v1) self._value = new_value elif important: self._value = other._value elif ((issubclass(v_type, numbers.Number) and issubclass(o_type, numbers.Number)) or # They might be different kind of numbers, so skip the check below self._value is None or other._value is None): # It means any of those values were an "unset" so doing nothing because we don't # really know the original value type if important: self._value = other._value self._value_type = other._value_type elif o_type != v_type: raise ConanException("It's not possible to compose {} values " "and {} ones.".format(v_type.__name__, o_type.__name__)) # TODO: In case of any other object types? elif important: # equal type, but just string self._value = other._value def set_relative_base_folder(self, folder): if not self._path: return if isinstance(self._value, list): self._value = [os.path.join(folder, v) if v != _ConfVarPlaceHolder else v for v in self._value] if isinstance(self._value, dict): self._value = {k: os.path.join(folder, v) for k, v in self._value.items()} elif isinstance(self._value, str): self._value = os.path.join(folder, self._value) class Conf: # Putting some default expressions to check that any value could be false boolean_false_expressions = ("0", '"0"', "false", '"false"', "off") boolean_true_expressions = ("1", '"1"', "true", '"true"', "on") def __init__(self): # It being ordered allows for Windows case-insensitive composition self._values = OrderedDict() # {var_name: [] of values, including separators} def __bool__(self): return bool(self._values) def __repr__(self): return "Conf: " + repr(self._values) def __eq__(self, other): """ :type other: Conf """ return other._values == self._values def clear(self): self._values.clear() def validate(self): for conf in self._values: self._check_conf_name(conf) def items(self): # FIXME: Keeping backward compatibility for k, v in self._values.items(): yield k, v.value def get(self, conf_name, default=None, check_type=None, choices=None): """ Get all the values of the given configuration name. :param conf_name: Name of the configuration. :param default: Default value in case of conf does not have the conf_name key. :param check_type: Check the conf type(value) is the same as the given by this param. There are two default smart conversions for bool and str types. :param choices: list of possible values this conf can have, if value not in it, errors. """ # Skipping this check only the user.* configurations self._check_conf_name(conf_name) conf_value = self._values.get(conf_name) if conf_value: v = conf_value.value if choices is not None and v not in choices and v is not None: raise ConanException(f"Unknown value '{v}' for '{conf_name}'") # Some smart conversions if check_type is bool and not isinstance(v, bool): if str(v).lower() in Conf.boolean_false_expressions: return False if str(v).lower() in Conf.boolean_true_expressions: return True raise ConanException(f"[conf] {conf_name} must be a boolean-like object " f"(true/false, 1/0, on/off) and value '{v}' does not match it.") elif check_type is str and not isinstance(v, str): return str(v) elif v is None: # value was unset return default elif (check_type is not None and not isinstance(v, check_type) or check_type is int and isinstance(v, bool)): raise ConanException(f"[conf] {conf_name} must be a " f"{check_type.__name__}-like object. The value '{v}' " f"introduced is a {type(v).__name__} object") return v else: return default def pop(self, conf_name, default=None): """ Remove the given configuration, returning its value. :param conf_name: Name of the configuration. :param default: Default value to return in case the configuration doesn't exist. :return: """ value = self.get(conf_name, default=default) self._values.pop(conf_name, None) return value def show(self, fnpattern, pattern=""): return {key: self.get(key) for key in self._values.keys() if fnmatch.fnmatch(pattern + key, fnpattern)} def copy(self): c = Conf() c._values = OrderedDict((k, v.copy()) for k, v in self._values.items()) return c def filter_core(self): c = Conf() c._values = OrderedDict((k, v.copy()) for k, v in self._values.items() if not CORE_CONF_PATTERN.match(k)) return c def dumps(self): """ Returns a string with the format ``name=conf-value`` """ return "\n".join([v.dumps() for v in sorted(self._values.values(), key=lambda x: x.name)]) def serialize(self): """ Returns a dict-like object, e.g., ``{"tools.xxxx": "value1"}`` """ ret = {} for v in self._values.values(): ret.update(v.serialize()) return ret def define(self, name, value): """ Define a value for the given configuration name. :param name: Name of the configuration. :param value: Value of the configuration. """ v = _ConfValue.parse(name, value) self._values[v.name] = v def define_path(self, name, value): v = _ConfValue.parse(name, value, path=True) self._values[v.name] = v def unset(self, name): """ Clears the variable, equivalent to a unset or set XXX= :param name: Name of the configuration. """ v = _ConfValue.parse(name, None) self._values[v.name] = v def update(self, name, value): """ Update the value to the given configuration name. :param name: Name of the configuration. :param value: Value of the configuration. """ # Placeholder trick is not good for dict update, so we need to explicitly update=True conf_value = _ConfValue.parse(name, {}, update=True) self._values.setdefault(conf_value.name, conf_value).update(value) def update_path(self, name, value): conf_value = _ConfValue.parse(name, {}, path=True, update=True) self._values.setdefault(conf_value.name, conf_value).update(value) def append(self, name, value): """ Append a value to the given configuration name. :param name: Name of the configuration. :param value: Value to append. """ conf_value = _ConfValue.parse(name, [_ConfVarPlaceHolder]) self._values.setdefault(conf_value.name, conf_value).append(value) def append_path(self, name, value): conf_value = _ConfValue.parse(name, [_ConfVarPlaceHolder], path=True) self._values.setdefault(conf_value.name, conf_value).append(value) def prepend(self, name, value): """ Prepend a value to the given configuration name. :param name: Name of the configuration. :param value: Value to prepend. """ conf_value = _ConfValue.parse(name, [_ConfVarPlaceHolder]) self._values.setdefault(conf_value.name, conf_value).prepend(value) def prepend_path(self, name, value): conf_value = _ConfValue.parse(name, [_ConfVarPlaceHolder], path=True) self._values.setdefault(conf_value.name, conf_value).prepend(value) def remove(self, name, value): """ Remove a value from the given configuration name. :param name: Name of the configuration. :param value: Value to remove. """ conf_value = self._values.get(name) if conf_value: conf_value.remove(value) else: raise ConanException("Conf {} does not exist.".format(name)) def compose_conf(self, other): """ :param other: other has less priority than current one :type other: Conf """ for k, v in other._values.items(): existing = self._values.get(k) if existing is None: self._values[k] = v.copy() else: existing.compose_conf_value(v) return self def copy_conaninfo_conf(self): """ Get a new `Conf()` object with all the configurations required by the consumer to be included in the final `ConanInfo().package_id()` computation. For instance, let's suppose that we have this Conan `profile`: ``` ... [conf] tools.info.package_id:confs=["tools.build:cxxflags", "tools.build:cflags"] tools.build:cxxflags=["flag1xx"] tools.build:cflags=["flag1"] tools.build:defines=["DEF1"] ... Then, the resulting `Conf()` will have only these configuration lines: tools.build:cxxflags=["flag1xx"] tools.build:cflags=["flag1"] ``` :return: a new `< Conf object >` with the configuration selected by `tools.info.package_id:confs`. """ result = Conf() # Reading the list of all the configurations selected by the user to use for the package_id package_id_confs = self.get("tools.info.package_id:confs", default=[], check_type=list) for conf_name in package_id_confs: matching_confs = [c for c in self._values if re.match(conf_name, c)] for name in matching_confs: value = self.get(name) # Pruning any empty values, those should not affect package ID if value: result.define(name, value) return result def set_relative_base_folder(self, folder): for v in self._values.values(): v.set_relative_base_folder(folder) @staticmethod def _check_conf_name(conf): if conf.startswith("user"): if USER_CONF_PATTERN.match(conf) is None: raise ConanException(f"User conf '{conf}' invalid format, not 'user.org.group:conf'") elif conf not in BUILT_IN_CONFS: raise ConanException(f"[conf] '{conf}' does not exist in configuration list. " "Run 'conan config list' to see all the available confs.") class ConfDefinition: # Order is important, "define" must be latest actions = (("+=", "append"), ("=+", "prepend"), ("=!", "unset"), ("*=", "update"), ("=", "define")) def __init__(self): self._pattern_confs = OrderedDict() def __repr__(self): return "ConfDefinition: " + repr(self._pattern_confs) def __bool__(self): return bool(self._pattern_confs) def get(self, conf_name, default=None, check_type=None, choices=None): """ Get the value of the conf name requested and convert it to the [type]-like passed. """ pattern, name = self._split_pattern_name(conf_name) return self._pattern_confs.get(pattern, Conf()).get(name, default=default, check_type=check_type, choices=choices) def show(self, fnpattern): """ Get the value of the confs that match the requested pattern """ result = {} for patter_key, patter_conf in self._pattern_confs.items(): if patter_key is None: patter_key = "" else: patter_key += ":" pattern_values = patter_conf.show(fnpattern, patter_key) result.update({patter_key + pattern_subkey: pattern_subvalue for pattern_subkey, pattern_subvalue in pattern_values.items()}) return result def pop(self, conf_name, default=None): """ Remove the conf name passed. """ pattern, name = self._split_pattern_name(conf_name) return self._pattern_confs.get(pattern, Conf()).pop(name, default=default) @staticmethod def _split_pattern_name(pattern_name): if pattern_name.count(":") >= 2: pattern, name = pattern_name.split(":", 1) else: pattern, name = None, pattern_name return pattern, name def get_conanfile_conf(self, ref, is_consumer=False): """ computes package-specific Conf it is only called when conanfile.buildenv is called the last one found in the profile file has top priority """ result = Conf() for pattern, conf in self._pattern_confs.items(): if pattern is None or ref_matches(ref, pattern, is_consumer): # Latest declared has priority, copy() necessary to not destroy data result = conf.copy().compose_conf(result) return result def update_conf_definition(self, other): """ :type other: ConfDefinition :param other: The argument profile has priority/precedence over the current one. """ for pattern, conf in other._pattern_confs.items(): self._update_conf_definition(pattern, conf) def _update_conf_definition(self, pattern, conf): existing = self._pattern_confs.get(pattern) if existing: self._pattern_confs[pattern] = conf.compose_conf(existing) else: self._pattern_confs[pattern] = conf def rebase_conf_definition(self, global_conf): """ for taking the new global.conf and composing with the profile [conf] :type global_conf: ConfDefinition """ result = ConfDefinition() # Do not add ``core.xxx`` configuration to profiles for k, v in global_conf._pattern_confs.items(): result._pattern_confs[k] = v.filter_core() result.update_conf_definition(self) self._pattern_confs = result._pattern_confs return def update(self, key, value, profile=False, method="define"): """ Define/append/prepend/unset any Conf line >> update("tools.build:verbosity", "verbose") """ pattern, name = self._split_pattern_name(key) if not _is_profile_module(name): if profile: raise ConanException("[conf] '{}' not allowed in profiles".format(key)) if pattern is not None: raise ConanException("Conf '{}' cannot have a package pattern".format(key)) # strip whitespaces before/after = # values are not strip() unless they are a path, to preserve potential whitespaces name = name.strip() # When loading from profile file, latest line has priority conf = Conf() if method == "unset": conf.unset(name) else: getattr(conf, method)(name, value) # Update self._update_conf_definition(pattern, conf) def dumps(self): result = [] for pattern, conf in self._pattern_confs.items(): if pattern is None: result.append(conf.dumps()) else: result.append("\n".join("{}:{}".format(pattern, line) if line else "" for line in conf.dumps().splitlines())) if result: result.append("") return "\n".join(result) def serialize(self): result = {} for pattern, conf in self._pattern_confs.items(): if pattern is None: result.update(conf.serialize()) else: for k, v in conf.serialize().items(): result[f"{pattern}:{k}"] = v return result @staticmethod def _get_evaluated_value(_v): """ Function to avoid eval() catching local variables """ try: value = eval(_v) # This destroys Windows path strings with backslash except (Exception,): # It means eval() failed because of a string without quotes value = _v.strip() else: if not isinstance(value, (numbers.Number, bool, dict, list, set, tuple)) \ and value is not None: # If it is quoted string we respect it as-is value = _v.strip() return value def loads(self, text, profile=False): self._pattern_confs = {} for line in text.splitlines(): line = line.strip() if not line or line.startswith("#"): continue for op, method in ConfDefinition.actions: tokens = line.split(op, 1) if len(tokens) != 2: continue pattern_name, value = tokens _, name = self._split_pattern_name(pattern_name) # We only implement str type at the moment isstr = _BUILT_IN_CONFS_TYPES.get(name) is str parsed_value = value.strip() if isstr else ConfDefinition._get_evaluated_value(value) self.update(pattern_name, parsed_value, profile=profile, method=method) break else: raise ConanException("Bad conf definition: {}".format(line)) def validate(self): for conf in self._pattern_confs.values(): conf.validate() def clear(self): self._pattern_confs.clear() def load_global_conf(home_folder): home_paths = HomePaths(home_folder) global_conf_path = home_paths.global_conf_path new_config = ConfDefinition() if os.path.exists(global_conf_path): text = load(global_conf_path) distro = None if platform.system() in ["Linux", "FreeBSD"]: import distro template = Environment(loader=FileSystemLoader(home_folder)).from_string(text) home_folder = home_folder.replace("\\", "/") from conan import conan_version content = template.render({"platform": platform, "os": os, "distro": distro, "conan_version": conan_version, "conan_home_folder": home_folder, "detect_api": detect_api, "hashlib": hashlib}) new_config.loads(content) else: # creation of a blank global.conf file for user convenience default_global_conf = textwrap.dedent("""\ # Core configuration (type 'conan config list' to list possible values) # e.g, for CI systems, to raise if user input would block # core:non_interactive = True # some tools.xxx config also possible, though generally better in profiles # tools.android:ndk_path = my/path/to/android/ndk """) save(global_conf_path, default_global_conf) return new_config ================================================ FILE: conan/internal/model/cpp_info.py ================================================ import copy import glob import json import os import re from collections import OrderedDict, defaultdict from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.model.pkg_type import PackageType from conan.internal.util.files import load, save _DIRS_VAR_NAMES = ["_includedirs", "_srcdirs", "_libdirs", "_resdirs", "_bindirs", "_builddirs", "_frameworkdirs", "_objects"] _FIELD_VAR_NAMES = ["_system_libs", "_package_framework", "_frameworks", "_libs", "_defines", "_cflags", "_cxxflags", "_sharedlinkflags", "_exelinkflags", "_sources"] _ALL_NAMES = _DIRS_VAR_NAMES + _FIELD_VAR_NAMES _SINGLE_VALUE_VARS = "_type", "_exe", "_location", "_link_location", "_languages" class MockInfoProperty: """ # TODO: Remove in 2.X to mock user_info and env_info """ counter = {} package = None def __init__(self, name): self._name = name @staticmethod def message(): if not MockInfoProperty.counter: return ConanOutput().warning("Usage of deprecated Conan 1.X features that will be removed in " "Conan 2.X:", warn_tag="deprecated") for k, v in MockInfoProperty.counter.items(): ConanOutput().warning(f" '{k}' used in: {', '.join(v)}", warn_tag="deprecated") MockInfoProperty.counter = {} def __getitem__(self, key): MockInfoProperty.counter.setdefault(self._name, set()).add(self.package) return [] def __setitem__(self, key, value): MockInfoProperty.counter.setdefault(self._name, set()).add(self.package) def __getattr__(self, attr): MockInfoProperty.counter.setdefault(self._name, set()).add(self.package) return [] def __setattr__(self, attr, value): if attr != "_name": MockInfoProperty.counter.setdefault(self._name, set()).add(self.package) return super(MockInfoProperty, self).__setattr__(attr, value) class _Component: def __init__(self, set_defaults=False): # ###### PROPERTIES self._properties = None # ###### DIRECTORIES self._includedirs = None # Ordered list of include paths self._srcdirs = None # Ordered list of source paths self._libdirs = None # Directories to find libraries self._resdirs = None # Directories to find resources, data, etc self._bindirs = None # Directories to find executables and shared libs self._builddirs = None self._frameworkdirs = None # ##### FIELDS self._system_libs = None # Ordered list of system libraries self._frameworks = None # system Apple OS frameworks self._package_framework = None # any other frameworks self._libs = None # The libs to link against self._defines = None # preprocessor definitions self._cflags = None # pure C flags self._cxxflags = None # C++ compilation flags self._sharedlinkflags = None # linker flags self._exelinkflags = None # linker flags self._objects = None # linker flags self._sources = None # source files self._exe = None # application executable, only 1 allowed, following CPS self._languages = None self._sysroot = None self._requires = None # LEGACY 1.X fields, can be removed in 2.X self.names = MockInfoProperty("cpp_info.names") self.filenames = MockInfoProperty("cpp_info.filenames") self.build_modules = MockInfoProperty("cpp_info.build_modules") if set_defaults: self.includedirs = ["include"] self.libdirs = ["lib"] self.bindirs = ["bin"] # CPS self._type = None self._location = None self._link_location = None def serialize(self): return { "includedirs": self._includedirs, "srcdirs": self._srcdirs, "libdirs": self._libdirs, "resdirs": self._resdirs, "bindirs": self._bindirs, "builddirs": self._builddirs, "frameworkdirs": self._frameworkdirs, "system_libs": self._system_libs, "frameworks": self._frameworks, "libs": self._libs, "defines": self._defines, "cflags": self._cflags, "cxxflags": self._cxxflags, "sharedlinkflags": self._sharedlinkflags, "exelinkflags": self._exelinkflags, "objects": self._objects, "sources": self._sources, "sysroot": self._sysroot, "requires": self._requires, "properties": self._properties, "exe": self._exe, # single exe, incompatible with libs "type": str(self._type) if self._type else None, "location": self._location, "link_location": self._link_location, "languages": self._languages } @staticmethod def deserialize(contents): result = _Component() for field, value in contents.items(): if hasattr(result, field): setattr(result, field, value) else: # If there's on setter, use the internal field, e.g, _properties which has # set_property method, but not a setter setattr(result, f"_{field}", value) return result def clone(self): # Necessary below for exploding a cpp_info.libs = [lib1, lib2] into components result = _Component() for k, v in vars(self).items(): if k.startswith("_"): setattr(result, k, copy.copy(v)) return result @property def includedirs(self): if self._includedirs is None: self._includedirs = [] return self._includedirs @includedirs.setter def includedirs(self, value): self._includedirs = value @property def srcdirs(self): if self._srcdirs is None: self._srcdirs = [] return self._srcdirs @srcdirs.setter def srcdirs(self, value): self._srcdirs = value @property def libdirs(self): if self._libdirs is None: self._libdirs = [] return self._libdirs @libdirs.setter def libdirs(self, value): self._libdirs = value @property def resdirs(self): if self._resdirs is None: self._resdirs = [] return self._resdirs @resdirs.setter def resdirs(self, value): self._resdirs = value @property def bindirs(self): if self._bindirs is None: self._bindirs = [] return self._bindirs @bindirs.setter def bindirs(self, value): self._bindirs = value @property def builddirs(self): if self._builddirs is None: self._builddirs = [] return self._builddirs @builddirs.setter def builddirs(self, value): self._builddirs = value @property def bindir(self): bindirs = self.bindirs if not bindirs or len(bindirs) != 1: raise ConanException(f"The bindir property is undefined because bindirs " f"{'is empty' if not bindirs else 'has more than one element'}." f" Consider using the bindirs property.") return bindirs[0] @property def libdir(self): libdirs = self.libdirs if not libdirs or len(libdirs) != 1: raise ConanException(f"The libdir property is undefined because libdirs " f"{'is empty' if not libdirs else 'has more than one element'}." f" Consider using the libdirs property.") return libdirs[0] @property def includedir(self): includedirs = self.includedirs if not includedirs or len(includedirs) != 1: raise ConanException(f"The includedir property is undefined because includedirs " f"{'is empty' if not includedirs else 'has more than one element'}." f" Consider using the includedirs property.") return includedirs[0] @property def system_libs(self): if self._system_libs is None: self._system_libs = [] return self._system_libs @system_libs.setter def system_libs(self, value): self._system_libs = value @property def package_framework(self): return self._package_framework @package_framework.setter def package_framework(self, value): self._package_framework = value @property def frameworks(self): if self._frameworks is None: self._frameworks = [] return self._frameworks @frameworks.setter def frameworks(self, value): self._frameworks = value @property def frameworkdirs(self): if self._frameworkdirs is None: self._frameworkdirs = [] return self._frameworkdirs @frameworkdirs.setter def frameworkdirs(self, value): self._frameworkdirs = value @property def libs(self): if self._libs is None: self._libs = [] return self._libs @libs.setter def libs(self, value): self._libs = value @property def exe(self): return self._exe @exe.setter def exe(self, value): self._exe = value @property def type(self): return self._type @type.setter def type(self, value): self._type = PackageType(value) if value is not None else None @property def location(self): return self._location @location.setter def location(self, value): self._location = value @property def link_location(self): return self._link_location @link_location.setter def link_location(self, value): self._link_location = value @property def languages(self): return self._languages @languages.setter def languages(self, value): self._languages = value @property def defines(self): if self._defines is None: self._defines = [] return self._defines @defines.setter def defines(self, value): self._defines = value @property def cflags(self): if self._cflags is None: self._cflags = [] return self._cflags @cflags.setter def cflags(self, value): self._cflags = value @property def cxxflags(self): if self._cxxflags is None: self._cxxflags = [] return self._cxxflags @cxxflags.setter def cxxflags(self, value): self._cxxflags = value @property def sharedlinkflags(self): if self._sharedlinkflags is None: self._sharedlinkflags = [] return self._sharedlinkflags @sharedlinkflags.setter def sharedlinkflags(self, value): self._sharedlinkflags = value @property def exelinkflags(self): if self._exelinkflags is None: self._exelinkflags = [] return self._exelinkflags @exelinkflags.setter def exelinkflags(self, value): self._exelinkflags = value @property def objects(self): if self._objects is None: self._objects = [] return self._objects @objects.setter def objects(self, value): self._objects = value @property def sources(self): if self._sources is None: self._sources = [] return self._sources @sources.setter def sources(self, value): self._sources = value @property def sysroot(self): if self._sysroot is None: self._sysroot = "" return self._sysroot @sysroot.setter def sysroot(self, value): self._sysroot = value @property def requires(self): if self._requires is None: self._requires = [] return self._requires @requires.setter def requires(self, value): self._requires = value @property def required_component_names(self): """ Names of the required INTERNAL components of the same package (not scoped with ::)""" if self.requires is None: return [] return [r for r in self.requires if "::" not in r] def set_property(self, property_name, value): if self._properties is None: self._properties = {} self._properties[property_name] = value def get_property(self, property_name, check_type=None): if self._properties is None: return None try: value = self._properties[property_name] if check_type is not None and not isinstance(value, check_type): raise ConanException( f'The expected type for {property_name} is "{check_type.__name__}", but "{type(value).__name__}" was found') return value except KeyError: pass def get_init(self, attribute, default): # Similar to dict.setdefault item = getattr(self, attribute) if item is not None: return item setattr(self, attribute, default) return default def merge(self, other, overwrite=False): """ @param overwrite: @type other: _Component """ def merge_list(o, d): d.extend(e for e in o if e not in d) for varname in _ALL_NAMES: other_values = getattr(other, varname) if other_values is not None: if not overwrite: current_values = self.get_init(varname, []) merge_list(other_values, current_values) else: setattr(self, varname, other_values) for varname in _SINGLE_VALUE_VARS: # To allow editable of .exe/.location other_values = getattr(other, varname) if other_values is not None: # Just overwrite the existing value, not possible to append setattr(self, varname, other_values) if other.requires: current_values = self.get_init("requires", []) merge_list(other.requires, current_values) if other._properties: current_values = self.get_init("_properties", {}) for k, v in other._properties.items(): existing = current_values.get(k) if existing is not None and isinstance(existing, list) and not overwrite: existing.extend(v) else: current_values[k] = copy.copy(v) def set_relative_base_folder(self, folder): for varname in _DIRS_VAR_NAMES: origin = getattr(self, varname) if origin is not None: origin[:] = [os.path.join(folder, el) for el in origin] properties = self._properties if properties is not None: modules = properties.get("cmake_build_modules") # Only this prop at this moment if modules is not None: assert isinstance(modules, list), "cmake_build_modules must be a list" properties["cmake_build_modules"] = [os.path.join(folder, v) for v in modules] def deploy_base_folder(self, package_folder, deploy_folder): def relocate(el): rel_path = os.path.relpath(el, package_folder) if rel_path.startswith(".."): # If it is pointing to a folder outside of the package, then do not relocate return el return os.path.join(deploy_folder, rel_path) for varname in _DIRS_VAR_NAMES: origin = getattr(self, varname) if origin is not None: origin[:] = [relocate(f) for f in origin] properties = self._properties if properties is not None: modules = properties.get("cmake_build_modules") # Only this prop at this moment if modules is not None: assert isinstance(modules, list), "cmake_build_modules must be a list" properties["cmake_build_modules"] = [relocate(f) for f in modules] def parsed_requires(self): return [r.split("::", 1) if "::" in r else (None, r) for r in self.requires] def _auto_deduce_locations(self, conanfile, library_name): def _lib_match_by_glob(dir_, filename): # Run a glob.glob function to find the file given by the filename matches = glob.glob(f"{dir_}/{filename}") if matches: return matches def _lib_match_by_regex(dir_, pattern): ret = set() # pattern is a regex compiled pattern, so let's iterate each file to find the library files = os.listdir(dir_) for file_name in files: full_path = os.path.join(dir_, file_name) if os.path.isfile(full_path) and pattern.match(file_name): # Issue: https://github.com/conan-io/conan/issues/17721 (stop resolving symlinks) ret.add(full_path) return list(ret) def _find_matching(dirs, pattern): for d in dirs: if not os.path.exists(d): continue # If pattern is an exact match if isinstance(pattern, str): # pattern == filename lib_found = _lib_match_by_glob(d, pattern) else: lib_found = _lib_match_by_regex(d, pattern) if lib_found: if len(lib_found) > 1: lib_found.sort() found, _ = os.path.splitext(os.path.basename(lib_found[0])) if found != libname and found != f"lib{libname}": out.warning(f"There were several matches for Lib {libname}: {lib_found}") return lib_found[0].replace("\\", "/") out = ConanOutput(scope=str(conanfile)) pkg_type = conanfile.package_type libdirs = self.libdirs bindirs = self.bindirs libname = self.libs[0] static_location = None shared_location = None dll_location = None deduced_type = None # libname is exactly the pattern, e.g., ["mylib.a"] instead of ["mylib"] _, ext = os.path.splitext(libname) if ext in (".lib", ".a", ".dll", ".so", ".dylib"): if ext in (".lib", ".a"): static_location = _find_matching(libdirs, libname) elif ext in (".so", ".dylib"): shared_location = _find_matching(libdirs, libname) elif ext == ".dll": dll_location = _find_matching(bindirs, libname) else: lib_sanitized = re.escape(libname) component_sanitized = re.escape(library_name) # At first, exact match regex_static = re.compile(rf"(?:lib)?{lib_sanitized}\.(?:a|lib)") regex_shared = re.compile(rf"(?:lib)?{lib_sanitized}\.(?:so|dylib)") regex_dll = re.compile(rf".*(?:{lib_sanitized}|{component_sanitized}).*\.dll") static_location = _find_matching(libdirs, regex_static) shared_location = _find_matching(libdirs, regex_shared) if not any([static_location, shared_location]): # Let's extend a little bit the pattern search regex_wider_static = re.compile(rf"(?:lib)?{lib_sanitized}(?:[._-].+)?\.(?:a|lib)") regex_wider_shared = re.compile(rf"(?:lib)?{lib_sanitized}(?:[._-].+)?\.(?:so|dylib)") static_location = _find_matching(libdirs, regex_wider_static) shared_location = _find_matching(libdirs, regex_wider_shared) if static_location or not shared_location: dll_location = _find_matching(bindirs, regex_dll) if static_location: if shared_location: out.warning(f"Lib {libname} has both static {static_location} and " f"shared {shared_location} in the same package") if self._type is PackageType.STATIC or pkg_type is PackageType.STATIC: self._location = static_location deduced_type = PackageType.STATIC else: self._location = shared_location deduced_type = PackageType.SHARED elif dll_location: self._location = dll_location self._link_location = static_location deduced_type = PackageType.SHARED else: self._location = static_location deduced_type = PackageType.STATIC elif shared_location: self._location = shared_location deduced_type = PackageType.SHARED elif dll_location: # Only .dll but no link library self._location = dll_location deduced_type = PackageType.SHARED if not self._location: raise ConanException(f"{conanfile}: Cannot obtain 'location' for library '{libname}' " f"in {libdirs}. You can specify 'cpp_info.location' directly " f"or report in github.com/conan-io/conan/issues if you think it " f"should have been deduced correctly.") if self._type is not None and self._type != deduced_type: ConanException(f"{conanfile}: Incorrect deduced type '{deduced_type}' for library" f" '{libname}' that declared .type='{self._type}'") self._type = deduced_type if self._type != pkg_type: out.warning(f"Lib {libname} deduced as '{self._type}, but 'package_type={pkg_type}'") def deduce_locations(self, conanfile, component_name=""): name = f'{conanfile} cpp_info.components["{component_name}"]' if component_name \ else f'{conanfile} cpp_info' # executable if self._exe: # exe is a new field, it should have the correct location if self._type is None: self._type = PackageType.APP if self._type is not PackageType.APP: raise ConanException(f"{name} incorrect .type {self._type} for .exe {self._exe}") if self.libs: raise ConanException(f"{name} has both .exe and .libs") if not self.location: raise ConanException(f"{name} has .exe and no .location") return if self._type is PackageType.APP: # old school Conan application packages without defining an exe, not an error return # libraries if len(self.libs) > 1: # it could be 0, as the libs itself is not necessary raise ConanException(f"{name} has more than 1 library in .libs: {self.libs}, " "cannot deduce locations") # fully defined by user in conanfile, nothing to do. if self._location or self._link_location: if self._type is None or self._type not in [PackageType.SHARED, PackageType.STATIC]: raise ConanException(f"{name} location defined without defined library type") return # possible header only, which allows also an empty header-only only for common flags if len(self.libs) == 0: if self._type is None: self._type = PackageType.HEADER return # automatic location deduction from a single .lib=["lib"] if self._type is not None and self._type not in [PackageType.SHARED, PackageType.STATIC]: raise ConanException(f"{name} has a library but .type {self._type} is not static/shared") # If no location is defined, it's time to guess the location self._auto_deduce_locations(conanfile, library_name=component_name or conanfile.ref.name) class CppInfo: def __init__(self, set_defaults=False): self.components = defaultdict(lambda: _Component(set_defaults)) self.default_components = None self._package = _Component(set_defaults) def __getattr__(self, attr): # all cpp_info.xxx of not defined things will go to the global package return getattr(self._package, attr) def __setattr__(self, attr, value): if attr in ("components", "default_components", "_package", "_aggregated", "required_components"): super(CppInfo, self).__setattr__(attr, value) else: setattr(self._package, attr, value) def serialize(self): ret = {"root": self._package.serialize()} if self.default_components: ret["default_components"] = self.default_components for component_name, info in self.components.items(): ret[component_name] = info.serialize() return ret def deserialize(self, content): self._package = _Component.deserialize(content.pop("root")) self.default_components = content.get("default_components") for component_name, info in content.items(): self.components[component_name] = _Component.deserialize(info) return self def save(self, path): save(path, json.dumps(self.serialize())) def load(self, path): content = json.loads(load(path)) return self.deserialize(content) @property def has_components(self): return len(self.components) > 0 def merge(self, other, overwrite=False): """Merge 'other' into self. 'other' can be an old cpp_info object Used to merge Layout source + build cpp objects info (editables) @type other: CppInfo @param other: The other CppInfo to merge @param overwrite: New values from other overwrite the existing ones """ # Global merge self._package.merge(other._package, overwrite) # sysroot only of package, not components, first defined wins self._package.sysroot = self._package.sysroot or other._package.sysroot # COMPONENTS for cname, c in other.components.items(): # Make sure each component created on the fly does not bring new defaults self.components.setdefault(cname, _Component(set_defaults=False)).merge(c, overwrite) def set_relative_base_folder(self, folder): """Prepend the folder to all the directories definitions, that are relative""" self._package.set_relative_base_folder(folder) for component in self.components.values(): component.set_relative_base_folder(folder) def deploy_base_folder(self, package_folder, deploy_folder): """Prepend the folder to all the directories""" self._package.deploy_base_folder(package_folder, deploy_folder) for component in self.components.values(): component.deploy_base_folder(package_folder, deploy_folder) def get_sorted_components(self): """ Order the components taking into account if they depend on another component in the same package (not scoped with ::). First less dependant. :return: ``OrderedDict`` {component_name: component} """ result = OrderedDict() opened = self.components.copy() while opened: new_open = OrderedDict() for name, c in opened.items(): if not any(n in opened for n in c.required_component_names): result[name] = c else: new_open[name] = c if len(opened) == len(new_open): msg = ["There is a dependency loop in 'self.cpp_info.components' requires:"] for name, c in opened.items(): loop_reqs = ", ".join(n for n in c.required_component_names if n in opened) msg.append(f" {name} requires {loop_reqs}") raise ConanException("\n".join(msg)) opened = new_open return result def aggregated_components(self): """Aggregates all the components as global values, returning a new CppInfo Used by many generators to obtain a unified, aggregated view of all components """ # This method had caching before, but after a ``--deployer``, the package changes # location, and this caching was invalid, still pointing to the Conan cache instead of # the deployed if self.has_components: result = _Component() # Reversed to make more dependant first for component in reversed(self.get_sorted_components().values()): result.merge(component) # NOTE: The properties are not aggregated because they might refer only to the # component like "cmake_target_name" describing the target name FOR THE component # not the namespace. # FIXME: What to do about sysroot? result._properties = copy.copy(self._package._properties) else: result = copy.copy(self._package) aggregated = CppInfo() aggregated._package = result return aggregated def check_component_requires(self, conanfile): """ quality check for component requires, called by BinaryInstaller after package_info() - Check that all recipe ``requires`` are used if consumer recipe explicit opt-in to use component requires - Check that component external dep::comp dependency "dep" is a recipe "requires" - Check that every internal component require actually exist It doesn't check that external components do exist """ if not self.has_components and not self._package.requires: return # Accumulate all external requires comps = self.required_components missing_internal = [c[1] for c in comps if c[0] is None and c[1] not in self.components] if missing_internal: msg = f"{conanfile}: package_info(): There are '(cpp_info/components).requires' to " \ f"other internal components that are not defined: {missing_internal}" raise ConanException(msg) external = [c[0] for c in comps if c[0] is not None] if not external: return # Only direct host (not test) dependencies can define required components # We use conanfile.dependencies to use the already replaced ones by "replace_requires" # So consumers can keep their ``self.cpp_info.requires = ["pkg_name::comp"]`` direct_dependencies = [r.ref.name for r, d in conanfile.dependencies.items() if r.direct and not r.build and not r.is_test and r.visible and not r.override] for e in external: if e not in direct_dependencies: msg = f"{conanfile}: package_info(): There are '(cpp_info/components).requires' " \ f"that includes package '{e}::', but such package is not a a direct " \ f"requirement of the recipe" raise ConanException(msg) # TODO: discuss if there are cases that something is required but not transitive for e in direct_dependencies: if e not in external: msg = f"{conanfile}: package_info(): The direct dependency '{e}' is not used by " \ f"any '(cpp_info/components).requires'." raise ConanException(msg) @property def required_components(self): """Returns a list of tuples with (require, component_name) required by the package If the require is internal (to another component), the require will be None""" # FIXME: Cache the value # First aggregate without repetition, respecting the order ret = [r for r in self._package.requires] for comp in self.components.values(): for r in comp.requires: if r not in ret: ret.append(r) # Then split the names ret = [r.split("::", 1) if "::" in r else (None, r) for r in ret] return ret def deduce_full_cpp_info(self, conanfile): if conanfile.cpp_info.has_components and (conanfile.cpp_info.exe or conanfile.cpp_info.libs): raise ConanException(f"{conanfile}: 'cpp_info' contains components and .exe or .libs") result = CppInfo() # clone it if self.libs and len(self.libs) > 1: # expand in multiple components ConanOutput(scope=str(conanfile)).warning( "The 'cpp_info.libs' contain more than 1 library. " "Define 'cpp_info.components' instead.", warn_tag="deprecated") assert not self.components, f"{conanfile} cpp_info shouldn't have .libs and .components" common = self._package.clone() common.libs = [] common.type = str(PackageType.HEADER) # the type of components is a string! result.components["_common"] = common for lib in self.libs: c = _Component() # Do not do a full clone, we don't need the properties c.type = self.type c.includedirs = self.includedirs c.libdirs = self.libdirs c.bindirs = self.bindirs c.libs = [lib] c.requires = ["_common"] result.components[f"_{lib}"] = c else: result._package = self._package.clone() result.default_components = self.default_components new_components = {} for k, v in self.components.items(): if v.libs and len(v.libs) > 1: ConanOutput(scope=str(conanfile)).warning( f"The 'cpp_info.components[{k}] contains more than 1 library. " "Define 1 component for each library instead.", warn_tag="deprecated") # Now the root, empty one common = v.clone() common.libs = [] common.type = str(PackageType.HEADER) # the type of components is a string! new_components[k] = common for lib in v.libs: c = _Component() c.type = v.type c.includedirs = v.includedirs c.libdirs = v.libdirs c.bindirs = v.bindirs c.libs = [lib] new_components[f"_{k}_{lib}"] = c common.requires.append(f"_{k}_{lib}") else: new_components[k] = v.clone() result.components = new_components result._package.deduce_locations(conanfile) for comp_name, comp in result.components.items(): comp.deduce_locations(conanfile, component_name=comp_name) return result ================================================ FILE: conan/internal/model/dependencies.py ================================================ from collections import OrderedDict from conan.api.model import RecipeReference from conan.errors import ConanException from conan.internal.graph.graph import RECIPE_PLATFORM from conan.internal.model.conanfile_interface import ConanFileInterface class UserRequirementsDict: """ user facing dict to allow access of dependencies by name """ def __init__(self, data, require_filter=None): self._data = data # dict-like self._require_filter = require_filter # dict {trait: value} for requirements def filter(self, require_filter): def filter_fn(require): for k, v in require_filter.items(): if getattr(require, k) != v: return False return True data = OrderedDict((k, v) for k, v in self._data.items() if filter_fn(k)) return UserRequirementsDict(data, require_filter) def __bool__(self): return bool(self._data) def get(self, ref, build=None, **kwargs): return self._get(ref, build, **kwargs)[1] def _get(self, ref, build=None, **kwargs): if build is None: current_filters = self._require_filter or {} if "build" not in current_filters: # By default we search in the "host" context kwargs["build"] = False else: kwargs["build"] = build data = self.filter(kwargs) ret = [] if "/" in ref: # FIXME: Validate reference ref = RecipeReference.loads(ref) for require, value in data.items(): if require.ref == ref: ret.append((require, value)) else: name = ref for require, value in data.items(): if require.ref.name == name: ret.append((require, value)) if len(ret) > 1: current_filters = data._require_filter or "{}" requires = "\n".join(["- {}".format(require) for require, _ in ret]) raise ConanException("There are more than one requires matching the specified filters:" " {}\n{}".format(current_filters, requires)) if not ret: raise KeyError("'{}' not found in the dependency set".format(ref)) key, value = ret[0] return key, value def __getitem__(self, name): return self.get(name) def __delitem__(self, name): r, _ = self._get(name) del self._data[r] def items(self): return self._data.items() def values(self): return self._data.values() def __contains__(self, item): try: self.get(item) return True except KeyError: return False except ConanException: # ConanException is raised when there are more than one matching the filters # so it's definitely in the dict return True class ConanFileDependencies(UserRequirementsDict): @staticmethod def from_node(node): d = OrderedDict((require, ConanFileInterface(transitive.node.conanfile)) for require, transitive in node.transitive_deps.items()) if node.replaced_requires: cant_be_removed = set() for old_req, new_req in node.replaced_requires.items(): # Two different replaced_requires can point to the same real requirement existing = d[new_req] added_req = new_req.copy_requirement() added_req.ref = RecipeReference.loads(old_req) d[added_req] = existing if new_req.ref.name == added_req.ref.name: cant_be_removed.add(new_req) # Now remove the replaced from dependencies dict for new_req in node.replaced_requires.values(): if new_req not in cant_be_removed: d.pop(new_req, None) return ConanFileDependencies(d) def filter(self, require_filter, remove_system=True): # FIXME: Copy of hte above, to return ConanFileDependencies class object def filter_fn(require): for k, v in require_filter.items(): if getattr(require, k) != v: return False return True data = OrderedDict((k, v) for k, v in self._data.items() if filter_fn(k)) if remove_system: data = OrderedDict((k, v) for k, v in data.items() if v.recipe != RECIPE_PLATFORM) return ConanFileDependencies(data, require_filter) def transitive_requires(self, other): """ :type other: ConanFileDependencies """ data = OrderedDict() for k, v in self._data.items(): for otherk, otherv in other._data.items(): if v == otherv: data[otherk] = v # Use otherk to respect original replace_requires return ConanFileDependencies(data) @property def topological_sort(self): # Return first independent nodes, final ones are the more direct deps result = OrderedDict() opened = self._data.copy() while opened: opened_values = set(opened.values()) new_opened = OrderedDict() for req, conanfile in opened.items(): deps_in_opened = any(d in opened_values for d in conanfile.dependencies.values()) if deps_in_opened: new_opened[req] = conanfile # keep it for next iteration else: result[req] = conanfile # No dependencies in open set! opened = new_opened return ConanFileDependencies(result) @property def direct_host(self): return self.filter({"build": False, "direct": True, "test": False, "skip": False}) @property def direct_build(self): return self.filter({"build": True, "direct": True}) @property def host(self): return self.filter({"build": False, "test": False, "skip": False}) @property def test(self): # Not needed a direct_test because they are visible=False so only the direct consumer # will have them in the graph return self.filter({"build": False, "test": True, "skip": False}) @property def build(self): return self.filter({"build": True}) def get_transitive_requires(consumer, dependency): """ the transitive requires that we need are the consumer ones, not the current dependencey ones, so we get the current ones, then look for them in the consumer, and return those """ # The build dependencies cannot be transitive in generators like CMakeDeps, # even if users make them visible pkg_deps = dependency.dependencies.filter({"direct": True, "build": False}) # First we filter the skipped dependencies result = consumer.dependencies.filter({"skip": False}) # and we keep those that are really dependencies of the current package result = result.transitive_requires(pkg_deps) return result ================================================ FILE: conan/internal/model/info.py ================================================ import hashlib from conan.errors import ConanException from conan.internal.model.dependencies import UserRequirementsDict from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.config_parser import TextINIParse class _VersionRepr: """Class to return strings like 1.Y.Z from a Version object""" def __init__(self, version): self._version = version def stable(self): if self._version.major == 0: return str(self._version) else: return self.major() def major(self): # This check is to avoid breaking non-integer major versions # for legacy reasons. Users are warned against using them if not isinstance(self._version.major.value, int): return str(self._version.major) return ".".join([str(self._version.major), 'Y', 'Z']) def minor(self): # This check is to avoid breaking non-integer major versions # for legacy reasons. Users are warned against using them if not isinstance(self._version.major.value, int): return str(self._version.major) v0 = str(self._version.major) v1 = str(self._version.minor) if self._version.minor is not None else "0" return ".".join([v0, v1, 'Z']) def patch(self): # This check is to avoid breaking non-integer major versions # for legacy reasons. Users are warned against using them if not isinstance(self._version.major.value, int): return str(self._version.major) v0 = str(self._version.major) v1 = str(self._version.minor) if self._version.minor is not None else "0" v2 = str(self._version.patch) if self._version.patch is not None else "0" return ".".join([v0, v1, v2]) def pre(self): # This check is to avoid breaking non-integer major versions # for legacy reasons. Users are warned against using them if not isinstance(self._version.major.value, int): return str(self._version.major) v0 = str(self._version.major) v1 = str(self._version.minor) if self._version.minor is not None else "0" v2 = str(self._version.patch) if self._version.patch is not None else "0" v = ".".join([v0, v1, v2]) if self._version.pre is not None: v += "-%s" % self._version.pre return v @property def build(self): return self._version.build if self._version.build is not None else "" class RequirementInfo: def __init__(self, ref, package_id, default_package_id_mode): self._ref = ref self._package_id = package_id self.name = self.version = self.user = self.channel = self.package_id = None self.recipe_revision = None self.package_id_mode = default_package_id_mode try: func_package_id_mode = getattr(self, default_package_id_mode) except AttributeError: raise ConanException(f"require {self._ref} package_id_mode='{default_package_id_mode}' " "is not a known package_id_mode") else: func_package_id_mode() def copy(self): # Useful for build_id() result = RequirementInfo(self._ref, self._package_id, "unrelated_mode") for f in ("name", "version", "user", "channel", "recipe_revision", "package_id"): setattr(result, f, getattr(self, f)) return result def pref(self): ref = RecipeReference(self.name, self.version, self.user, self.channel, self.recipe_revision) return PkgReference(ref, self.package_id) def dumps(self): return repr(self.pref()) def unrelated_mode(self): self.name = self.version = self.user = self.channel = self.package_id = None self.recipe_revision = None def semver_mode(self): self.name = self._ref.name self.version = _VersionRepr(self._ref.version).stable() self.user = self._ref.user self.channel = self._ref.channel self.package_id = None self.recipe_revision = None def full_version_mode(self): self.name = self._ref.name self.version = self._ref.version self.user = self._ref.user self.channel = self._ref.channel self.package_id = None self.recipe_revision = None def patch_mode(self): self.name = self._ref.name self.version = _VersionRepr(self._ref.version).patch() self.user = self._ref.user self.channel = self._ref.channel self.package_id = None self.recipe_revision = None def minor_mode(self): self.name = self._ref.name self.version = _VersionRepr(self._ref.version).minor() self.user = self._ref.user self.channel = self._ref.channel self.package_id = None self.recipe_revision = None def major_mode(self): self.name = self._ref.name self.version = _VersionRepr(self._ref.version).major() self.user = self._ref.user self.channel = self._ref.channel self.package_id = None self.recipe_revision = None def full_package_mode(self): self.name = self._ref.name self.version = self._ref.version self.user = self._ref.user self.channel = self._ref.channel self.package_id = self._package_id self.recipe_revision = None def revision_mode(self): self.name = self._ref.name self.version = self._ref.version self.user = self._ref.user self.channel = self._ref.channel self.package_id = None self.recipe_revision = self._ref.revision if self._ref.revision != "platform" else None def full_mode(self): self.name = self._ref.name self.version = self._ref.version self.user = self._ref.user self.channel = self._ref.channel self.package_id = self._package_id self.recipe_revision = self._ref.revision if self._ref.revision != "platform" else None full_recipe_mode = full_version_mode recipe_revision_mode = full_mode # to not break everything and help in upgrade class RequirementsInfo(UserRequirementsDict): def copy(self): # For build_id() implementation data = {pref: req_info.copy() for pref, req_info in self._data.items()} return RequirementsInfo(data) def serialize(self): return [r.dumps() for r in self._data.values()] def __bool__(self): return bool(self._data) def clear(self): self._data = {} def remove(self, *args): for name in args: del self[name] @property def pkg_names(self): return [r.ref.name for r in self._data.keys()] def dumps(self): result = [] for req_info in self._data.values(): dumped = req_info.dumps() if dumped: result.append(dumped) return "\n".join(sorted(result)) def unrelated_mode(self): self.clear() def semver_mode(self): for r in self._data.values(): r.semver_mode() def patch_mode(self): for r in self._data.values(): r.patch_mode() def minor_mode(self): for r in self._data.values(): r.minor_mode() def major_mode(self): for r in self._data.values(): r.major_mode() def full_version_mode(self): for r in self._data.values(): r.full_version_mode() def full_recipe_mode(self): for r in self._data.values(): r.full_recipe_mode() def full_package_mode(self): for r in self._data.values(): r.full_package_mode() def revision_mode(self): for r in self._data.values(): r.revision_mode() def full_mode(self): for r in self._data.values(): r.full_mode() recipe_revision_mode = full_mode # to not break everything and help in upgrade class PythonRequiresInfo: def __init__(self, refs, default_package_id_mode): self._default_package_id_mode = default_package_id_mode if refs: self._refs = [RequirementInfo(r, None, default_package_id_mode=mode or default_package_id_mode) for r, mode in sorted(refs.items())] else: self._refs = None def copy(self): # For build_id() implementation refs = {r._ref: r.package_id_mode for r in self._refs} if self._refs else None return PythonRequiresInfo(refs, self._default_package_id_mode) def serialize(self): return [r.dumps() for r in self._refs or []] def __bool__(self): return bool(self._refs) def clear(self): self._refs = None def dumps(self): return '\n'.join(r.dumps() for r in self._refs) def unrelated_mode(self): self._refs = None def semver_mode(self): for r in self._refs: r.semver_mode() def patch_mode(self): for r in self._refs: r.patch_mode() def minor_mode(self): for r in self._refs: r.minor_mode() def major_mode(self): for r in self._refs: r.major_mode() def full_version_mode(self): for r in self._refs: r.full_version_mode() def full_recipe_mode(self): for r in self._refs: r.full_recipe_mode() def revision_mode(self): for r in self._refs: r.revision_mode() def full_mode(self): for r in self._refs: r.full_mode() recipe_revision_mode = full_mode def load_binary_info(text): # This is used for search functionality, search prints info from this file parser = TextINIParse(text) conan_info_json = {} for section, lines in parser.line_items(): try: items = [line.split("=", 1) for line in lines] conan_info_json[section] = {item[0].strip(): item[1].strip() for item in items} except IndexError: conan_info_json[section] = lines return conan_info_json class ConanInfo: def __init__(self, settings=None, options=None, reqs_info=None, build_requires_info=None, python_requires=None, conf=None, config_version=None): self.invalid = None self.cant_build = False # It will set to a str with a reason if the validate_build() fails self.settings = settings self.settings_target = None # needs to be explicitly defined by recipe package_id() self.options = options self.requires = reqs_info self.build_requires = build_requires_info self.python_requires = python_requires self.conf = conf self.config_version = config_version self.compatibility_delta = None def clone(self): """ Useful for build_id implementation and for compatibility() """ result = ConanInfo() result.invalid = self.invalid result.settings = self.settings.copy() result.options = self.options.copy_conaninfo_options() result.requires = self.requires.copy() result.build_requires = self.build_requires.copy() result.python_requires = self.python_requires.copy() result.conf = self.conf.copy() result.settings_target = self.settings_target.copy() if self.settings_target else None result.config_version = self.config_version.copy() if self.config_version else None return result def serialize(self): result = {} settings_dumps = self.settings.serialize() if settings_dumps: result["settings"] = settings_dumps if self.settings_target is not None: settings_target_dumps = self.settings_target.serialize() if settings_target_dumps: result["settings_target"] = settings_target_dumps options_dumps = self.options.serialize() if options_dumps: result["options"] = options_dumps requires_dumps = self.requires.serialize() if requires_dumps: result["requires"] = requires_dumps python_requires_dumps = self.python_requires.serialize() if python_requires_dumps: result["python_requires"] = python_requires_dumps build_requires_dumps = self.build_requires.serialize() if build_requires_dumps: result["build_requires"] = build_requires_dumps conf_dumps = self.conf.serialize() if conf_dumps: result["conf"] = conf_dumps config_version_dumps = self.config_version.serialize() if self.config_version else None if config_version_dumps: result["config_version"] = config_version_dumps if self.compatibility_delta: result["compatibility_delta"] = self.compatibility_delta return result def dumps(self): """ Get all the information contained in settings, options, requires, python_requires, build_requires and conf. :return: `str` with the result of joining all the information, e.g., `"[settings]\nos=Windows\n[options]\nuse_Qt=True"` """ result = [] settings_dumps = self.settings.dumps() if settings_dumps: result.append("[settings]") result.append(settings_dumps) if self.settings_target: settings_target_dumps = self.settings_target.dumps() if settings_target_dumps: result.append("[settings_target]") result.append(settings_target_dumps) options_dumps = self.options.dumps() if options_dumps: result.append("[options]") result.append(options_dumps) requires_dumps = self.requires.dumps() if requires_dumps: result.append("[requires]") result.append(requires_dumps) if self.python_requires: python_reqs_dumps = self.python_requires.dumps() if python_reqs_dumps: result.append("[python_requires]") result.append(python_reqs_dumps) if self.build_requires: build_requires_dumps = self.build_requires.dumps() if build_requires_dumps: result.append("[build_requires]") result.append(build_requires_dumps) if self.conf: # TODO: Think about the serialization of Conf, not 100% sure if dumps() is the best result.append("[conf]") result.append(self.conf.dumps()) config_version_dumps = self.config_version.dumps() if self.config_version else None if config_version_dumps: result.append("[config_version]") result.append(config_version_dumps) result.append("") # Append endline so file ends with LF return '\n'.join(result) def summarize_compact(self): result = [] serialized = self.serialize() for key, values in serialized.items(): if isinstance(values, dict): result.append(f"{key}: " + " ".join(f"{k}={v}" for k, v in values.items())) elif isinstance(values, type([])): result.append(f"{key}: {' '.join(values)}") return result def dump_diff(self, compatible): self_dump = self.dumps() compatible_dump = compatible.dumps() result = [] for line in compatible_dump.splitlines(): if line not in self_dump: result.append(line) return ', '.join(result) def package_id(self): """ Get the `package_id` that is the result of applying the has function SHA-1 to the `self.dumps()` return. :return: `str` the `package_id`, e.g., `"040ce2bd0189e377b2d15eb7246a4274d1c63317"` """ text = self.dumps() md = hashlib.sha1() md.update(text.encode()) package_id = md.hexdigest() return package_id def clear(self): self.settings.clear() self.options.clear() self.requires.clear() self.conf.clear() self.build_requires.clear() self.python_requires.clear() if self.config_version is not None: self.config_version.clear() def validate(self): # If the options are not fully defined, this is also an invalid case try: self.options.validate() except ConanException as e: self.invalid = str(e) try: self.settings.validate() except ConanException as e: self.invalid = str(e) ================================================ FILE: conan/internal/model/layout.py ================================================ import os from conan.internal.model.cpp_info import CppInfo from conan.internal.model.conf import Conf class Infos: def __init__(self): self.source = CppInfo() self.build = CppInfo() self.package = CppInfo(set_defaults=True) class PartialLayout: def __init__(self): from conan.tools.env import Environment self.buildenv_info = Environment() self.runenv_info = Environment() self.conf_info = Conf() def set_relative_base_folder(self, folder): self.buildenv_info.set_relative_base_folder(folder) self.runenv_info.set_relative_base_folder(folder) self.conf_info.set_relative_base_folder(folder) class Layouts: def __init__(self): self.source = PartialLayout() self.build = PartialLayout() self.package = PartialLayout() class Folders: def __init__(self): self._base_source = None self._base_build = None self._base_package = None self._base_generators = None self._base_export = None self._base_export_sources = None self._base_recipe_metadata = None self._base_pkg_metadata = None self._immutable_package_folder = None self.source = "" self.build = "" self.package = "" self.generators = "" # Relative location of the project root, if the conanfile is not in that project root, but # in a subfolder: e.g: If the conanfile is in a subfolder then self.root = ".." self.root = None # The relative location with respect to the project root of the subproject containing the # conanfile.py, that makes most of the output folders defined in layouts (cmake_layout, etc) # start from the subproject again self.subproject = None self.build_folder_vars = None def set_base_folders(self, conanfile_folder, output_folder): """ this methods can be used for defining all the base folders in the local flow (conan install, source, build), where only the current conanfile location and the potential --output-folder user argument are the folders to take into account If the "layout()" method defines a self.folders.root = "xxx" it will be used to compute the base folder @param conanfile_folder: the location where the current consumer conanfile is @param output_folder: Can potentially be None (for export-pkg: TODO), in that case the conanfile location is used """ # This must be called only after ``layout()`` has been called base_folder = conanfile_folder if self.root is None else \ os.path.normpath(os.path.join(conanfile_folder, self.root)) self._base_source = base_folder self._base_build = output_folder or base_folder self._base_generators = output_folder or base_folder self._base_export_sources = output_folder or base_folder self._base_recipe_metadata = os.path.join(base_folder, "metadata") # TODO: It is likely that this base_pkg_metadata is not really used with this value self._base_pkg_metadata = output_folder or base_folder @property def source_folder(self): if self._base_source is None: return None if not self.source: return os.path.normpath(self._base_source) return os.path.normpath(os.path.join(self._base_source, self.source)) @property def base_source(self): return self._base_source def set_base_source(self, folder): self._base_source = folder @property def build_folder(self): if self._base_build is None: return None if not self.build: return os.path.normpath(self._base_build) return os.path.normpath(os.path.join(self._base_build, self.build)) @property def recipe_metadata_folder(self): return self._base_recipe_metadata def set_base_recipe_metadata(self, folder): self._base_recipe_metadata = folder @property def package_metadata_folder(self): return self._base_pkg_metadata def set_base_pkg_metadata(self, folder): self._base_pkg_metadata = folder @property def base_build(self): return self._base_build def set_base_build(self, folder): self._base_build = folder @property def base_package(self): return self._base_package def set_base_package(self, folder): self._base_package = folder @property def package_folder(self): """For the cache, the package folder is only the base""" return self._base_package def set_finalize_folder(self, folder): self._immutable_package_folder = self.package_folder self.set_base_package(folder) @property def immutable_package_folder(self): return self._immutable_package_folder or self.package_folder @property def generators_folder(self): if self._base_generators is None: return None if not self.generators: return os.path.normpath(self._base_generators) return os.path.normpath(os.path.join(self._base_generators, self.generators)) def set_base_generators(self, folder): self._base_generators = folder @property def base_export(self): return self._base_export def set_base_export(self, folder): self._base_export = folder @property def base_export_sources(self): return self._base_export_sources def set_base_export_sources(self, folder): self._base_export_sources = folder ================================================ FILE: conan/internal/model/lockfile.py ================================================ import fnmatch import json import os from collections import OrderedDict from conan.api.output import ConanOutput from conan.internal.graph.graph import RECIPE_VIRTUAL, RECIPE_CONSUMER, CONTEXT_BUILD, Overrides from conan.errors import ConanException from conan.api.model import RecipeReference from conan.internal.model.version_range import VersionRange from conan.internal.util.files import load, save LOCKFILE = "conan.lock" LOCKFILE_VERSION = "0.5" class _LockRequires: """ This is an ordered set of locked references. It is implemented this way to allow adding package_id:prev information later, otherwise it could be a bare list """ def __init__(self): self._requires = OrderedDict() # {require: package_ids} def refs(self): return self._requires.keys() def get(self, item): return self._requires.get(item) def serialize(self): result = [] for k, v in self._requires.items(): if v is None: result.append(repr(k)) else: result.append((repr(k), v)) return result @staticmethod def deserialize(data): result = _LockRequires() for d in data: if isinstance(d, str): result._requires[RecipeReference.loads(d)] = None else: result._requires[RecipeReference.loads(d[0])] = d[1] return result def add(self, ref, package_ids=None): if ref.revision is not None: old_package_ids = self._requires.pop(ref, None) # Get existing one if old_package_ids is not None: if package_ids is not None: package_ids = old_package_ids.update(package_ids) else: package_ids = old_package_ids self._requires[ref] = package_ids else: # Manual addition of something without revision existing = {r: r for r in self._requires}.get(ref) if existing and existing.revision is not None: raise ConanException(f"Cannot add {ref} to lockfile, already exists") self._requires[ref] = package_ids def remove(self, pattern): ref = RecipeReference.loads(pattern) version = str(ref.version) remove = [] if version.startswith("[") and version.endswith("]"): version_range = VersionRange(version[1:-1]) for k, v in self._requires.items(): if fnmatch.fnmatch(k.name, ref.name) and version_range.contains(k.version, None): new_pattern = f"{k.name}/*@{ref.user or ''}" new_pattern += f"/{ref.channel}" if ref.channel else "" if k.matches(new_pattern, False): remove.append(k) else: remove = [k for k in self._requires if k.matches(pattern, False)] self._requires = OrderedDict((k, v) for k, v in self._requires.items() if k not in remove) return remove def update(self, refs, name): if not refs: return for r in refs: r = RecipeReference.loads(r) new_reqs = {} for k, v in self._requires.items(): if r.name == k.name: ConanOutput().info(f"Replacing {name}: {k.repr_notime()} -> {repr(r)}") else: new_reqs[k] = v self._requires = new_reqs self._requires[r] = None # No package-id at the moment self.sort() def sort(self): self._requires = OrderedDict(reversed(sorted(self._requires.items()))) def merge(self, other): """ :type other: _LockRequires """ # TODO: What happens when merging incomplete refs? Probably str(ref) should be used for k, v in other._requires.items(): if k in self._requires: if v is not None: self._requires.setdefault(k, {}).update(v) else: self._requires[k] = v self.sort() class Lockfile: def __init__(self, deps_graph=None, lock_packages=False): self._requires = _LockRequires() self._python_requires = _LockRequires() self._build_requires = _LockRequires() self._conf_requires = _LockRequires() self._alias = {} self._overrides = Overrides() self.partial = False if deps_graph is None: return self.update_lock(deps_graph, lock_packages) def update_lock(self, deps_graph, lock_packages=False): for graph_node in deps_graph.nodes: try: for r in graph_node.conanfile.python_requires.all_refs(): self._python_requires.add(r) except AttributeError: pass if graph_node.recipe in (RECIPE_VIRTUAL, RECIPE_CONSUMER) or graph_node.ref is None: continue assert graph_node.conanfile is not None pids = {graph_node.package_id: graph_node.prev} if lock_packages else None if graph_node.context == CONTEXT_BUILD: self._build_requires.add(graph_node.ref, pids) else: self._requires.add(graph_node.ref, pids) self._alias.update(deps_graph.aliased) self._overrides.update(deps_graph.overrides()) self._requires.sort() self._build_requires.sort() self._python_requires.sort() self._conf_requires.sort() @staticmethod def load(path): if not path: raise IOError("Invalid path") if not os.path.isfile(path): raise ConanException("Missing lockfile in: %s" % path) content = load(path) try: return Lockfile.loads(content) except Exception as e: raise ConanException("Error parsing lockfile '{}': {}".format(path, e)) @staticmethod def loads(content): return Lockfile.deserialize(json.loads(content)) def dumps(self): return json.dumps(self.serialize(), indent=4) def save(self, path): save(path, self.dumps() + "\n") def merge(self, other): """ :type other: Lockfile """ self._requires.merge(other._requires) self._build_requires.merge(other._build_requires) self._python_requires.merge(other._python_requires) self._conf_requires.merge(other._conf_requires) self._alias.update(other._alias) self._overrides.update(other._overrides) def add(self, requires=None, build_requires=None, python_requires=None, config_requires=None): """ adding new things manually will trigger the sort() of the locked list, so lockfiles alwasys keep the ordered lists. This means that for some especial edge cases it might be necessary to allow removing from a lockfile, for example to test an older version than the one locked (in general adding works better for moving forward to newer versions) """ if requires: for r in requires: self._requires.add(r) self._requires.sort() if build_requires: for r in build_requires: self._build_requires.add(r) self._build_requires.sort() if python_requires: for r in python_requires: self._python_requires.add(r) self._python_requires.sort() if config_requires: for r in config_requires: self._conf_requires.add(r) self._conf_requires.sort() def remove(self, requires=None, build_requires=None, python_requires=None, config_requires=None): def _remove(reqs, self_reqs, name): if reqs: removed = [] for r in reqs: removed.extend(self_reqs.remove(r)) for d in removed: ConanOutput().info(f"Removed locked {name}: {d.repr_notime()}") _remove(requires, self._requires, "require") _remove(build_requires, self._build_requires, "build_require") _remove(python_requires, self._python_requires, "python_require") _remove(config_requires, self._conf_requires, "config_requires") def update(self, requires=None, build_requires=None, python_requires=None, config_requires=None): self._requires.update(requires, "require") self._build_requires.update(build_requires, "build_requires") self._python_requires.update(python_requires, "python_requires") self._conf_requires.update(config_requires, "config_requires") @staticmethod def deserialize(data): """ constructs a GraphLock from a json like dict """ graph_lock = Lockfile() version = data.get("version") if version and version != LOCKFILE_VERSION: raise ConanException("This lockfile was created with an incompatible " "version. Please regenerate the lockfile") if "requires" in data: graph_lock._requires = _LockRequires.deserialize(data["requires"]) if "build_requires" in data: graph_lock._build_requires = _LockRequires.deserialize(data["build_requires"]) if "python_requires" in data: graph_lock._python_requires = _LockRequires.deserialize(data["python_requires"]) if "alias" in data: graph_lock._alias = {RecipeReference.loads(k): RecipeReference.loads(v) for k, v in data["alias"].items()} if "overrides" in data: graph_lock._overrides = Overrides.deserialize(data["overrides"]) if "config_requires" in data: graph_lock._conf_requires = _LockRequires.deserialize(data["config_requires"]) return graph_lock def serialize(self): """ returns the object serialized as a dict of plain python types that can be converted to json """ result = {"version": LOCKFILE_VERSION} if self._requires: result["requires"] = self._requires.serialize() if self._build_requires: result["build_requires"] = self._build_requires.serialize() if self._python_requires: result["python_requires"] = self._python_requires.serialize() if self._alias: result["alias"] = {repr(k): repr(v) for k, v in self._alias.items()} if self._overrides: result["overrides"] = self._overrides.serialize() if self._conf_requires: result["config_requires"] = self._conf_requires.serialize() return result def resolve_locked(self, node, require, resolve_prereleases): if require.build or node.context == CONTEXT_BUILD: locked_refs = self._build_requires.refs() kind = "build_requires" elif node.is_conf: locked_refs = self._conf_requires.refs() kind = "config_requires" else: locked_refs = self._requires.refs() kind = "requires" try: self._resolve(require, locked_refs, resolve_prereleases, kind) except ConanException: overrides = self._overrides.get(require.ref) if overrides is not None and len(overrides) > 1: msg = f"Override defined for {require.ref}, but multiple possible overrides" \ f" {overrides}. You might need to apply the 'conan graph build-order'" \ f" overrides for correctly building this package with this lockfile" ConanOutput().error(msg, error_type="exception") raise def resolve_overrides(self, require, context): """ The lockfile contains the overrides to be able to inject them when the lockfile is applied to upstream dependencies, that have the overrides downstream """ if not self._overrides: return overriden = self._overrides.get(require.ref) if overriden and len(overriden) == 1: override_ref = next(iter(overriden)) locked_refs = self._build_requires.refs() if context == "build" else self._requires.refs() if override_ref not in locked_refs: return # The override came from the other context require.overriden_ref = require.overriden_ref or require.ref.copy() require.override_ref = override_ref require.ref = override_ref def resolve_prev(self, node): if node.context == CONTEXT_BUILD: prevs = self._build_requires.get(node.ref) else: prevs = self._requires.get(node.ref) if prevs: return prevs.get(node.package_id) def _resolve(self, require, locked_refs, resolve_prereleases, kind): version_range = require.version_range ref = require.ref matches = [r for r in locked_refs if r.name == ref.name and r.user == ref.user and r.channel == ref.channel] if version_range: for m in matches: if version_range.contains(m.version, resolve_prereleases): require.ref = m break else: if not self.partial: raise ConanException(f"Requirement '{ref}' not in lockfile '{kind}'") else: ref = require.ref if ref.revision is None: for m in matches: if m.version == ref.version: require.ref = m break else: if not self.partial: raise ConanException(f"Requirement '{ref}' not in lockfile '{kind}'") else: if ref not in matches and not self.partial: raise ConanException(f"Requirement '{repr(ref)}' not in lockfile '{kind}'") def replace_alias(self, require, alias): locked_alias = self._alias.get(alias) if locked_alias is not None: require.ref = locked_alias return True elif not self.partial: raise ConanException(f"Requirement alias '{alias}' not in lockfile") def resolve_locked_pyrequires(self, require, resolve_prereleases=None): locked_refs = self._python_requires.refs() # CHANGE self._resolve(require, locked_refs, resolve_prereleases, "python_requires") ================================================ FILE: conan/internal/model/manifest.py ================================================ import os from collections import defaultdict from conan.internal.paths import CONAN_MANIFEST, COMPRESSIONS, PACKAGE_FILE_NAME, EXPORT_FILE_NAME, \ EXPORT_SOURCES_FILE_NAME from conan.internal.util.dates import timestamp_now, timestamp_to_str from conan.internal.util.files import load, md5, md5sum, save, gather_files class FileTreeManifest: def __init__(self, the_time, file_sums): """file_sums is a dict with filepaths and md5's: {filepath/to/file.txt: md5}""" self.time = the_time self.file_sums = file_sums def files(self): return self.file_sums.keys() @property def summary_hash(self): s = ["%s: %s" % (f, fmd5) for f, fmd5 in sorted(self.file_sums.items())] s.append("") return md5("\n".join(s)) @staticmethod def loads(text): """ parses a string representation, generated with __repr__ """ tokens = text.split("\n") the_time = int(tokens[0]) file_sums = {} for md5line in tokens[1:]: if md5line: filename, file_md5 = md5line.rsplit(": ", 1) file_sums[filename] = file_md5 return FileTreeManifest(the_time, file_sums) @staticmethod def load(folder): text = load(os.path.join(folder, CONAN_MANIFEST)) return FileTreeManifest.loads(text) def __repr__(self): # Used for serialization and saving it to disk ret = ["%s" % self.time] for file_path, file_md5 in sorted(self.file_sums.items()): ret.append("%s: %s" % (file_path, file_md5)) ret.append("") content = "\n".join(ret) return content def __str__(self): """ Used for displaying the manifest in user readable format in Uploader, when the server manifest is newer than the cache one (and not force) """ ret = ["Time: %s" % timestamp_to_str(self.time)] for file_path, file_md5 in sorted(self.file_sums.items()): ret.append("%s, MD5: %s" % (file_path, file_md5)) ret.append("") content = "\n".join(ret) return content def save(self, folder, filename=CONAN_MANIFEST): path = os.path.join(folder, filename) save(path, repr(self)) def report_summary(self, output, suffix="Copied"): ext_files = defaultdict(list) for f in self.file_sums: if f == "conaninfo.txt": continue _, ext = os.path.splitext(f) ext_files[ext].append(os.path.basename(f)) if not ext_files: if suffix != "Copied": output.warning("No files in this package!") return for ext, files in ext_files.items(): files_str = (": " + ", ".join(files)) if len(files) < 5 else "" file_or_files = "file" if len(files) == 1 else "files" if not ext: output.info("%s %d %s%s" % (suffix, len(files), file_or_files, files_str)) else: output.info("%s %d '%s' %s%s" % (suffix, len(files), ext, file_or_files, files_str)) @classmethod def create(cls, folder, exports_sources_folder=None): """ Walks a folder and create a FileTreeManifest for it, reading file contents from disk, and capturing current time """ files, _ = gather_files(folder) # The folders symlinks are discarded for the manifest for f in (PACKAGE_FILE_NAME, EXPORT_FILE_NAME, EXPORT_SOURCES_FILE_NAME): for e in COMPRESSIONS: files.pop(f + e, None) files.pop(CONAN_MANIFEST, None) file_dict = {} for name, filepath in files.items(): # For a symlink: md5 of the pointing path, no matter if broken, relative or absolute. value = md5(os.readlink(filepath)) if os.path.islink(filepath) else md5sum(filepath) file_dict[name] = value if exports_sources_folder: export_files, _ = gather_files(exports_sources_folder) # The folders symlinks are discarded for the manifest for name, filepath in export_files.items(): # For a symlink: md5 of the pointing path, no matter if broken, relative or absolute. value = md5(os.readlink(filepath)) if os.path.islink(filepath) else md5sum(filepath) file_dict["export_source/%s" % name] = value date = timestamp_now() return cls(date, file_dict) def __eq__(self, other): """ Two manifests are equal if file_sums """ return self.file_sums == other.file_sums def difference(self, other): result = {} for f, h in self.file_sums.items(): h2 = other.file_sums.get(f) if h != h2: result[f] = h, h2 for f, h in other.file_sums.items(): h2 = self.file_sums.get(f) if h != h2: result[f] = h2, h return result ================================================ FILE: conan/internal/model/options.py ================================================ from conan.errors import ConanException from conan.internal.model.recipe_ref import ref_matches _falsey_options = ["false", "none", "0", "off", ""] def option_not_exist_msg(option_name, existing_options): """ Someone is referencing an option that is not available in the current package options """ result = ["option '%s' doesn't exist" % option_name, "Possible options are %s" % existing_options or "none"] return "\n".join(result) class _PackageOption: def __init__(self, name, value, possible_values=None): self._name = name self._value = value # Value None = not defined self.important = False # possible_values only possible origin is recipes if possible_values is None: self._possible_values = None else: # This can contain "ANY" self._possible_values = [str(v) if v is not None else None for v in possible_values] def dumps(self, scope=None): if self._value is None: return None important = "!" if self.important else "" if scope: return "%s:%s%s=%s" % (scope, self._name, important, self._value) else: return "%s%s=%s" % (self._name, important, self._value) def copy_conaninfo_option(self): # To generate a copy without validation, for package_id info.options value assert self._possible_values is not None # this should always come from recipe, with [] return _PackageOption(self._name, self._value, self._possible_values + ["ANY"]) def __bool__(self): if self._value is None: return False return self._value.lower() not in _falsey_options def __str__(self): return str(self._value) def __int__(self): return int(self._value) def _check_valid_value(self, value): """ checks that the provided value is allowed by current restrictions """ if self._possible_values is None: # validation not defined (profile) return if value in self._possible_values: return if value is not None and "ANY" in self._possible_values: return msg = ("'%s' is not a valid 'options.%s' value.\nPossible values are %s" % (value, self._name, self._possible_values)) raise ConanException(msg) def __eq__(self, other): # To promote the other to string, and always compare as strings # if self.options.myoption == 1 => will convert 1 to "1" if other is None: return self._value is None other = str(other) self._check_valid_value(other) if self._value is None: return False # Other is not None here return other == self.__str__() @property def name(self): return self._name @property def value(self): return self._value @value.setter def value(self, v): v = str(v) if v is not None else None self._check_valid_value(v) self._value = v def validate(self): # check that this has a valid option value defined if self._value is not None: return if None not in self._possible_values: raise ConanException("'options.%s' value not defined" % self._name) class _PackageOptions: def __init__(self, recipe_options_definition=None): if recipe_options_definition is None: self._constrained = False self._data = {} else: self._constrained = True self._data = {str(option): _PackageOption(str(option), None, possible_values) for option, possible_values in recipe_options_definition.items()} self._freeze = False def dumps(self, scope=None): result = [] for _, package_option in sorted(list(self._data.items())): dump = package_option.dumps(scope) if dump: result.append(dump) return "\n".join(result) @property def possible_values(self): return {k: v._possible_values for k, v in self._data.items()} def update(self, options): """ @type options: _PackageOptions """ # Necessary for init() extending of options for python_requires_extend for k, v in options._data.items(): self._data[k] = v def clear(self): # for header_only() clearing self._data.clear() def freeze(self): self._freeze = True def __contains__(self, option): return str(option) in self._data def get_safe(self, field, default=None): return self._data.get(field, default) def rm_safe(self, field): # This should never raise any exception, in any case self._data.pop(field, None) def validate(self): for child in self._data.values(): child.validate() def copy_conaninfo_options(self): # To generate a copy without validation, for package_id info.options value result = _PackageOptions() for k, v in self._data.items(): result._data[k] = v.copy_conaninfo_option() return result def _ensure_exists(self, field): if self._constrained and field not in self._data: raise ConanException(option_not_exist_msg(field, list(self._data.keys()))) def __getattr__(self, field): assert field[0] != "_", "ERROR %s" % field try: return self._data[field] except KeyError: raise ConanException(option_not_exist_msg(field, list(self._data.keys()))) def __delattr__(self, field): assert field[0] != "_", "ERROR %s" % field # It is always possible to remove an option, even if it is frozen (freeze=True), # and it got a value, because it is the only way an option could be removed # conditionally to other option value (like fPIC if shared) self._ensure_exists(field) del self._data[field] def __setattr__(self, field, value): if field[0] == "_": return super(_PackageOptions, self).__setattr__(field, value) self._set(field, value) def __setitem__(self, item, value): self._set(item, value) def _set(self, item, value): # programmatic way to define values, for Conan codebase important = item[-1] == "!" item = item[:-1] if important else item current_value = self._data.get(item) if self._freeze and current_value.value is not None and current_value != value: raise ConanException(f"Incorrect attempt to modify option '{item}' " f"from '{current_value}' to '{value}'") self._ensure_exists(item) v = self._data.setdefault(item, _PackageOption(item, None)) new_value_important = important or (isinstance(value, _PackageOption) and value.important) if new_value_important or not v.important: v.value = value v.important = new_value_important def items(self): result = [] for field, package_option in sorted(list(self._data.items())): result.append((field, package_option.value)) return result def update_options(self, other, is_pattern=False): """ @param is_pattern: if True, then the value might not exist and won't be updated @type other: _PackageOptions """ for k, v in other._data.items(): if is_pattern and k not in self._data: continue self._set(k, v) class Options: def __init__(self, options=None, options_values=None): # options=None means an unconstrained/profile definition try: self._package_options = _PackageOptions(options) # Addressed only by name, as only 1 configuration is allowed # if more than 1 is present, 1 should be "private" requirement and its options # are not public, not overridable self._deps_package_options = {} # {name("Boost": PackageOptions} if options_values: for k, v in options_values.items(): if v is None: continue # defining a None value means same as not giving value k = str(k).strip() v = str(v).strip() tokens = k.split(":", 1) if len(tokens) == 2: package, option = tokens if not package: raise ConanException("Invalid empty package name in options. " f"Use a pattern like `mypkg/*:{option}`") if "/" not in package and "*" not in package and "&" not in package: msg = "The usage of package names `{}` in options is " \ "deprecated, use a pattern like `{}/*:{}` " \ "instead".format(k, package, option) raise ConanException(msg) if "[" in package: msg = (f"Options pattern {package} contains a version range, which has no effect. " f"Only '&' for consumer and '*' as wildcard are supported in this context.") from conan.api.output import ConanOutput ConanOutput().warning(msg, warn_tag="risk") self._deps_package_options.setdefault(package, _PackageOptions())[option] = v else: self._package_options[k] = v except Exception as e: raise ConanException("Error while initializing options. %s" % str(e)) def __repr__(self): return self.dumps() @property def possible_values(self): return self._package_options.possible_values def dumps(self): """ produces a multiline text representation of all values, first self then others. In alphabetical order, skipping real None (not string "None") values: option1=value1 other_option=3 OtherPack:opt3=12.1 """ result = [] pkg_options_dumps = self._package_options.dumps() if pkg_options_dumps: result.append(pkg_options_dumps) for pkg_pattern, pkg_option in sorted(self._deps_package_options.items()): dep_pkg_option = pkg_option.dumps(scope=pkg_pattern) if dep_pkg_option: result.append(dep_pkg_option) return "\n".join(result) @staticmethod def loads(text): """ parses a multiline text in the form produced by dumps(), NO validation here """ values = {} for line in text.splitlines(): line = line.strip() if not line or line.startswith("#"): continue try: name, value = line.split("=", 1) values[name] = value except ValueError: raise ConanException(f"Error while parsing option '{line}'. " f"Options should be specified as 'pkg/*:option=value'") return Options(options_values=values) def serialize(self): # used by ConanInfo serialization, involved in "list package-ids" output # we need to maintain the "options" and "req_options" first level or servers will break # This happens always after reading from conaninfo.txt => all str and not None result = {k: v for k, v in self._package_options.items()} # Include the dependencies ones, in case they have been explicitly added in package_id() # to the conaninfo.txt, we want to report them for pkg_pattern, pkg_option in sorted(self._deps_package_options.items()): for key, value in pkg_option.items(): result["%s:%s" % (pkg_pattern, key)] = value return result def clear(self): # for header_only() clearing self._package_options.clear() self._deps_package_options.clear() def __contains__(self, option): return option in self._package_options def __getattr__(self, attr): return getattr(self._package_options, attr) def __setattr__(self, attr, value): if attr[0] == "_" or attr == "values": return super(Options, self).__setattr__(attr, value) return setattr(self._package_options, attr, value) def __delattr__(self, field): self._package_options.__delattr__(field) def __getitem__(self, item): if isinstance(item, str): if "/" not in item and "*" not in item: # FIXME: To allow patterns like "*" or "foo*" item += "/*" return self._deps_package_options.setdefault(item, _PackageOptions()) def scope(self, ref): """ when there are free options like "shared=True", they apply to the "consumer" package Once we know the name of such consumer package, it can be defined in the data, so it will be later correctly apply when processing options """ package_options = self._deps_package_options.setdefault(str(ref), _PackageOptions()) package_options.update_options(self._package_options) self._package_options = _PackageOptions() def copy_conaninfo_options(self): # To generate the package_id info.options copy, that can destroy, change and remove things result = Options() result._package_options = self._package_options.copy_conaninfo_options() # In most scenarios this should be empty at this stage, because it was cleared if self._deps_package_options: raise ConanException("Dependencies options were defined incorrectly. Maybe you" " tried to define options values in 'requirements()' or other" " invalid place") return result def update(self, options=None, options_values=None): # Necessary for init() extending of options for python_requires_extend new_options = Options(options, options_values) self._package_options.update(new_options._package_options) for pkg, pkg_option in new_options._deps_package_options.items(): self._deps_package_options.setdefault(pkg, _PackageOptions()).update(pkg_option) def update_options(self, other): """ dict-like update of options, "other" has priority, overwrite existing @type other: Options """ self._package_options.update_options(other._package_options) for pkg, pkg_option in other._deps_package_options.items(): self._deps_package_options.setdefault(pkg, _PackageOptions()).update_options(pkg_option) def apply_downstream(self, down_options, profile_options, own_ref, is_consumer): """ compute the current package options, starting from the self defined ones and applying the options defined by the downstrream consumers and the profile Only modifies the current package_options, not the dependencies ones """ assert isinstance(down_options, Options) assert isinstance(profile_options, Options) for defined_options in down_options, profile_options: if own_ref is None or own_ref.name is None: # If the current package doesn't have a name defined, is a pure consumer without name # Get the non-scoped options, plus the "all-matching=*" pattern self._package_options.update_options(defined_options._package_options) for pattern, options in defined_options._deps_package_options.items(): if ref_matches(None, pattern, is_consumer=is_consumer): self._package_options.update_options(options, is_pattern=True) else: # If the current package has a name, there should be a match, either exact name # match, or a fnmatch approximate one for pattern, options in defined_options._deps_package_options.items(): if ref_matches(own_ref, pattern, is_consumer=is_consumer): self._package_options.update_options(options, is_pattern="*" in pattern) self._package_options.freeze() def get_upstream_options(self, down_options, own_ref, is_consumer): """ compute which options should be propagated to the dependencies, a combination of the downstream defined default_options with the current default_options ones. This happens at "configure()" time, while building the graph. Also compute the minimum "self_options" which is the state that a package should define in order to reproduce """ assert isinstance(down_options, Options) # We need to store a copy for internal propagation for test_requires and tool_requires private_deps_options = Options() private_deps_options._deps_package_options = self._deps_package_options.copy() # self_options are the minimal necessary for a build-order # TODO: check this, isn't this just a copy? self_options = Options() self_options._deps_package_options = down_options._deps_package_options.copy() # compute now the necessary to propagate all down - self + self deps upstream_options = Options() for pattern, options in down_options._deps_package_options.items(): if ref_matches(own_ref, pattern, is_consumer=is_consumer): # Remove the exact match-name to this package, don't further propagate up pattern_name = pattern.split("/", 1)[0] if "*" not in pattern_name: continue self._deps_package_options.setdefault(pattern, _PackageOptions()).update_options(options) upstream_options._deps_package_options = self._deps_package_options # When the upstream is computed, the current dependencies are invalidated, so users will # not be able to do ``self.options["mydep"]`` because it will be empty. self.dependencies # is the way to access dependencies (in other methods) self._deps_package_options = {} return self_options, upstream_options, private_deps_options ================================================ FILE: conan/internal/model/pkg_type.py ================================================ from enum import Enum from conan.errors import ConanException class PackageType(Enum): LIBRARY = "library" # abstract type, should contain shared option to define STATIC = "static-library" SHARED = "shared-library" HEADER = "header-library" BUILD_SCRIPTS = "build-scripts" APP = "application" PYTHON = "python-require" CONF = "configuration" UNKNOWN = "unknown" def __str__(self): return self.value def __eq__(self, other): # This is useful for comparing with string type at user code, like ``package_type == "xxx"`` return super().__eq__(PackageType(other)) @staticmethod def compute_package_type(conanfile): # This doesnt implement the header_only option without shared one. Users should define # their package_type as they wish in the configure() method def deduce_from_options(): try: header = conanfile.options.header_only except ConanException: pass else: if header: return PackageType.HEADER try: shared = conanfile.options.shared except ConanException: pass else: if shared: return PackageType.SHARED else: return PackageType.STATIC return PackageType.UNKNOWN conanfile_type = conanfile.package_type if conanfile_type is not None: # Explicit definition in recipe try: conanfile_type = PackageType(conanfile_type) except ValueError: raise ConanException(f"{conanfile}: Invalid package type '{conanfile_type}'. " f"Valid types: {[i.value for i in PackageType]}") if conanfile_type is PackageType.LIBRARY: conanfile_type = deduce_from_options() if conanfile_type is PackageType.UNKNOWN: raise ConanException(f"{conanfile}: Package type is 'library'," " but no 'shared' option declared") elif any(option in conanfile.options for option in ["shared", "header_only"]): conanfile.output.warning(f"{conanfile}: package_type '{conanfile_type}' is defined, " "but 'shared' and/or 'header_only' options are present. " "The package_type will have precedence over the options " "regardless of their value.") conanfile.package_type = conanfile_type else: # automatic default detection with option shared/header-only conanfile.package_type = deduce_from_options() ================================================ FILE: conan/internal/model/profile.py ================================================ import copy from collections import OrderedDict, defaultdict from conan.errors import ConanException from conan.tools.env.environment import ProfileEnvironment from conan.internal.model.conf import ConfDefinition from conan.internal.model.options import Options from conan.api.model import RecipeReference class Profile: """A profile contains a set of setting (with values), environment variables """ def __init__(self): # Input sections, as defined by user profile files and command line self.settings = OrderedDict() self.package_settings = defaultdict(OrderedDict) self.options = Options() self.tool_requires = OrderedDict() # ref pattern: list of ref self.replace_requires = {} self.replace_tool_requires = {} self.platform_tool_requires = [] self.platform_requires = [] self.conf = ConfDefinition() self.buildenv = ProfileEnvironment() self.runenv = ProfileEnvironment() self.runner = {} # Cached processed values self.processed_settings = None # Settings with values, and smart completion self._package_settings_values = None def __repr__(self): return self.dumps() def serialize(self): def _serialize_tool_requires(): return {pattern: [repr(ref) for ref in refs] for pattern, refs in self.tool_requires.items()} result = { "settings": self.settings, "package_settings": self.package_settings, "options": self.options.serialize(), "tool_requires": _serialize_tool_requires(), "conf": self.conf.serialize(), # FIXME: Perform a serialize method for ProfileEnvironment "build_env": self.buildenv.dumps() } if self.replace_requires: result["replace_requires"] = {str(pattern): str(replace) for pattern, replace in self.replace_requires.items()} if self.replace_tool_requires: result["replace_tool_requires"] = {str(pattern): str(replace) for pattern, replace in self.replace_tool_requires.items()} if self.platform_tool_requires: result["platform_tool_requires"] = [str(t) for t in self.platform_tool_requires] if self.platform_requires: result["platform_requires"] = [str(t) for t in self.platform_requires] return result @property def package_settings_values(self): if self._package_settings_values is None: self._package_settings_values = {} for pkg, settings in self.package_settings.items(): self._package_settings_values[pkg] = list(settings.items()) return self._package_settings_values def process_settings(self, cache_settings): assert self.processed_settings is None, "processed settings must be None" self.processed_settings = cache_settings.copy() self.processed_settings.update_values(list(self.settings.items())) def dumps(self): result = ["[settings]"] for name, value in sorted(self.settings.items()): result.append("%s=%s" % (name, value)) for package, values in self.package_settings.items(): for name, value in sorted(values.items()): result.append("%s:%s=%s" % (package, name, value)) options_str = self.options.dumps() if options_str: result.append("[options]") result.append(options_str) if self.tool_requires: result.append("[tool_requires]") for pattern, req_list in self.tool_requires.items(): result.append("%s: %s" % (pattern, ", ".join(str(r) for r in req_list))) if self.platform_tool_requires: result.append("[platform_tool_requires]") result.extend(str(t) for t in self.platform_tool_requires) if self.platform_requires: result.append("[platform_requires]") result.extend(str(t) for t in self.platform_requires) if self.replace_requires: result.append("[replace_requires]") for pattern, ref in self.replace_requires.items(): result.append(f"{pattern}: {ref}") if self.replace_tool_requires: result.append("[replace_tool_requires]") for pattern, ref in self.replace_tool_requires.items(): result.append(f"{pattern}: {ref}") if self.conf: result.append("[conf]") result.append(self.conf.dumps()) if self.buildenv: result.append("[buildenv]") result.append(self.buildenv.dumps()) if self.runenv: result.append("[runenv]") result.append(self.runenv.dumps()) if result and result[-1] != "": result.append("") return "\n".join(result).replace("\n\n", "\n") def compose_profile(self, other): self.update_settings(other.settings) self.update_package_settings(other.package_settings) self.options.update_options(other.options) # It is possible that build_requires are repeated, or same package but different versions for pattern, req_list in other.tool_requires.items(): existing_build_requires = self.tool_requires.get(pattern) existing = OrderedDict() if existing_build_requires is not None: for br in existing_build_requires: # TODO: Understand why sometimes they are str and other are RecipeReference r = RecipeReference.loads(br) \ if not isinstance(br, RecipeReference) else br existing[r.name] = br for req in req_list: r = RecipeReference.loads(req) \ if not isinstance(req, RecipeReference) else req existing[r.name] = req self.tool_requires[pattern] = list(existing.values()) self.replace_requires.update(other.replace_requires) self.replace_tool_requires.update(other.replace_tool_requires) runner_type = self.runner.get("type") other_runner_type = other.runner.get("type") if runner_type and other_runner_type and runner_type != other_runner_type: raise ConanException(f"Found different runner types in profile composition " f"({runner_type} and {other_runner_type})") self.runner.update(other.runner) current_platform_tool_requires = {r.name: r for r in self.platform_tool_requires} current_platform_tool_requires.update({r.name: r for r in other.platform_tool_requires}) self.platform_tool_requires = list(current_platform_tool_requires.values()) current_platform_requires = {r.name: r for r in self.platform_requires} current_platform_requires.update({r.name: r for r in other.platform_requires}) self.platform_requires = list(current_platform_requires.values()) self.conf.update_conf_definition(other.conf) self.buildenv.update_profile_env(other.buildenv) # Profile composition, last has priority self.runenv.update_profile_env(other.runenv) def update_settings(self, new_settings): """Mix the specified settings with the current profile. Specified settings are prioritized to profile""" assert isinstance(new_settings, OrderedDict) # apply the current profile res = copy.copy(self.settings) if new_settings: # Invalidate the current subsettings if the parent setting changes # Example: new_settings declare a different "compiler", # so invalidate the current "compiler.XXX" for name, value in new_settings.items(): if "." not in name: if name in self.settings and self.settings[name] != value: for cur_name, _ in self.settings.items(): if cur_name.startswith("%s." % name): del res[cur_name] # Now merge the new values res.update(new_settings) self.settings = res def update_package_settings(self, package_settings): """Mix the specified package settings with the specified profile. Specified package settings are prioritized to profile""" for package_name, settings in package_settings.items(): self.package_settings[package_name].update(settings) ================================================ FILE: conan/internal/model/recipe_ref.py ================================================ from conan.api.model import RecipeReference def ref_matches(ref, pattern, is_consumer): if not ref or not str(ref): assert is_consumer ref = RecipeReference.loads("*/*") # FIXME: ugly return ref.matches(pattern, is_consumer=is_consumer) ================================================ FILE: conan/internal/model/requires.py ================================================ from conan.errors import ConanException from conan.internal.model.pkg_type import PackageType from conan.api.model import RecipeReference from conan.internal.model.version_range import VersionRange class Requirement: """ A user definition of a requires in a conanfile """ def __init__(self, ref, *, headers=None, libs=None, build=False, run=None, visible=None, transitive_headers=None, transitive_libs=None, test=None, package_id_mode=None, force=None, override=None, direct=None, options=None, no_skip=False): # * prevents the usage of more positional parameters, always ref + **kwargs # By default this is a generic library requirement self.ref = ref self._required_ref = ref # Store the original reference self._headers = headers # This dependent node has headers that must be -I self._libs = libs self._build = build # This dependent node is a build tool that runs at build time only self._run = run # node contains executables, shared libs or data necessary at host run time self._visible = visible # Even if not libsed or visible, the node is unique, can conflict self._transitive_headers = transitive_headers self._transitive_libs = transitive_libs self._test = test self._package_id_mode = package_id_mode self._force = force self._override = override self._direct = direct self.options = options # Meta and auxiliary information # The "defining_require" is the require that defines the current value. If this require is # overriden/forced, this attribute will point to the overriding/forcing requirement. self.defining_require = self # if not overriden, it points to itself self.overriden_ref = None # to store if the requirement has been overriden (store old ref) self.override_ref = None # to store if the requirement has been overriden (store new ref) self.is_test = test # to store that it was a test, even if used as regular requires too self.skip = False self.required_nodes = set() # store which intermediate nodes are required, to compute "Skip" self.no_skip = no_skip @property def files(self): # require needs some files in dependency package return self.headers or self.libs or self.run or self.build @staticmethod def _default_if_none(field, default_value): return field if field is not None else default_value @property def headers(self): return self._default_if_none(self._headers, True) @headers.setter def headers(self, value): self._headers = value @property def libs(self): return self._default_if_none(self._libs, True) @libs.setter def libs(self, value): self._libs = value @property def visible(self): return self._default_if_none(self._visible, True) @visible.setter def visible(self, value): self._visible = value @property def test(self): return self._default_if_none(self._test, False) @test.setter def test(self, value): self._test = value @property def force(self): return self._default_if_none(self._force, False) @force.setter def force(self, value): self._force = value @property def override(self): return self._default_if_none(self._override, False) @override.setter def override(self, value): self._override = value @property def direct(self): return self._default_if_none(self._direct, True) @direct.setter def direct(self, value): self._direct = value @property def build(self): return self._build @build.setter def build(self, value): self._build = value @property def run(self): return self._default_if_none(self._run, False) @run.setter def run(self, value): self._run = value @property def transitive_headers(self): return self._transitive_headers @transitive_headers.setter def transitive_headers(self, value): self._transitive_headers = value @property def transitive_libs(self): return self._transitive_libs @transitive_libs.setter def transitive_libs(self, value): self._transitive_libs = value @property def package_id_mode(self): return self._package_id_mode @package_id_mode.setter def package_id_mode(self, value): self._package_id_mode = value def __repr__(self): return repr(self.__dict__) def __str__(self): traits = 'build={}, headers={}, libs={}, ' \ 'run={}, visible={}'.format(self.build, self.headers, self.libs, self.run, self.visible) return "{}, Traits: {}".format(self.ref, traits) def serialize(self): result = {"ref": str(self.ref), "require": str(self._required_ref)} serializable = ("run", "libs", "skip", "test", "force", "direct", "build", "transitive_headers", "transitive_libs", "headers", "package_id_mode", "visible") for attribute in serializable: result[attribute] = getattr(self, attribute) return result def copy_requirement(self): return Requirement(self.ref, headers=self.headers, libs=self.libs, build=self.build, run=self.run, visible=self.visible, transitive_headers=self.transitive_headers, transitive_libs=self.transitive_libs) @property def version_range(self): """ returns the version range expression, without brackets [] or None if it is not an expression """ version = repr(self.ref.version) if version[0] == "[" and version[-1] == "]": return VersionRange(version[1:-1]) @property def alias(self): version = repr(self.ref.version) if version.startswith("(") and version.endswith(")"): return RecipeReference(self.ref.name, version[1:-1], self.ref.user, self.ref.channel, self.ref.revision) def process_package_type(self, src_node, node): """If the requirement traits have not been adjusted, then complete them with package type definition""" pkg_type = node.conanfile.package_type def set_if_none(field, value): if getattr(self, field) is None: setattr(self, field, value) if pkg_type is PackageType.APP: # Change the default requires headers&libs to False for APPS set_if_none("_headers", False) set_if_none("_libs", False) set_if_none("_run", True) elif pkg_type is PackageType.SHARED: set_if_none("_run", True) elif pkg_type is PackageType.STATIC: set_if_none("_run", False) elif pkg_type is PackageType.HEADER: set_if_none("_run", False) set_if_none("_libs", False) set_if_none("_headers", True) elif pkg_type is PackageType.BUILD_SCRIPTS: set_if_none("_run", True) set_if_none("_libs", False) set_if_none("_headers", False) set_if_none("_visible", False) # Conflicts might be allowed for this kind of package src_pkg_type = src_node.conanfile.package_type if src_pkg_type is PackageType.HEADER: set_if_none("_transitive_headers", True) set_if_none("_transitive_libs", True) def __hash__(self): return hash((self.ref.name, self.build)) def __eq__(self, other): """If the name is the same and they are in the same context, and if both of them are propagating includes or libs or run info or both are visible or the reference is the same, we consider the requires equal, so they can conflict""" return (self.ref.name == other.ref.name and self.build == other.build and (self.override or # an override with same name and context, always match (self.headers and other.headers) or (self.libs and other.libs) or (self.run and other.run) or ((self.visible or self.test) and (other.visible or other.test)) or (self.ref == other.ref and self.options == other.options))) def aggregate(self, other): """ when closing loop and finding the same dependency on a node, the information needs to be aggregated :param other: is the existing Require that the current node has, which information has to be appended to "self", which is the requires that is being propagated to the current node from upstream """ assert self.build == other.build if other.override: # If the other aggregated is an override, it shouldn't add information # it already did override upstream, and the actual information used in this node is # the propagated one. self.force = True return self.headers |= other.headers self.libs |= other.libs self.run = self.run or other.run self.visible |= other.visible self.force |= other.force self.direct |= other.direct self.transitive_headers = self.transitive_headers or other.transitive_headers self.transitive_libs = self.transitive_libs or other.transitive_libs if not other.test: self.test = False # it it was previously a test, but also required by non-test # necessary even if no propagation, order of requires matter self.is_test = self.is_test or other.is_test # package_id_mode is not being propagated downstream. So it is enough to check if the # current require already defined it or not if self.package_id_mode is None: self.package_id_mode = other.package_id_mode self.required_nodes.update(other.required_nodes) def transform_downstream(self, pkg_type, require, dep_pkg_type): """ consumer ---self---> foo ---require---> bar \\ -------------------????-------------------- / Compute new Requirement to be applied to "consumer" translating the effect of the dependency to such "consumer". Result can be None if nothing is to be propagated """ if require.visible is False: # TODO: We could implement checks in case private is violated (e.g shared libs) return if require.build: # public! # TODO: To discuss if this way of conflicting build_requires is actually useful or not # Build-requires will propagate its main trait for running exes/shared to downstream # consumers so run=require.run, irrespective of the 'self.run' trait downstream_require = Requirement(require.ref, headers=False, libs=False, build=True, run=require.run, visible=self.visible, direct=False) return downstream_require if self.build: # Build-requires # If the above is shared or the requirement is explicit run=True # visible=self.visible will further propagate it downstream if dep_pkg_type is PackageType.SHARED or require.run: downstream_require = Requirement(require.ref, headers=False, libs=False, build=True, run=True, visible=self.visible, direct=False) return downstream_require return # Regular and test requires if dep_pkg_type is PackageType.SHARED or dep_pkg_type is PackageType.STATIC: if pkg_type is PackageType.SHARED: downstream_require = Requirement(require.ref, headers=False, libs=False, run=require.run) elif pkg_type is PackageType.STATIC: downstream_require = Requirement(require.ref, headers=False, libs=require.libs, run=require.run) elif pkg_type is PackageType.APP: downstream_require = Requirement(require.ref, headers=False, libs=False, run=require.run) elif pkg_type is PackageType.HEADER: downstream_require = Requirement(require.ref, headers=require.headers, libs=require.libs, run=require.run) else: if pkg_type != PackageType.UNKNOWN: raise ConanException(f"Package '{self.ref}' with type '{pkg_type}' cannot have " f"a '{dep_pkg_type}' dependency to '{require.ref}'") # TODO: This is undertested, changing it did not break tests downstream_require = require.copy_requirement() elif dep_pkg_type is PackageType.HEADER: downstream_require = Requirement(require.ref, headers=False, libs=False, run=require.run) else: # Unknown, default. This happens all the time while check_downstream as shared is unknown # FIXME downstream_require = require.copy_requirement() # This is faster than pkg_type in (pkg.shared, pkg.static, ....) if pkg_type is PackageType.SHARED or pkg_type is PackageType.APP: downstream_require.libs = False downstream_require.headers = False elif pkg_type is PackageType.STATIC: downstream_require.headers = False assert require.visible, "at this point require should be visible" if require.transitive_headers is not None: downstream_require.headers = require.headers and require.transitive_headers if self.transitive_headers is not None: downstream_require.transitive_headers = self.transitive_headers if require.transitive_libs is not None: downstream_require.libs = require.libs and require.transitive_libs if self.transitive_libs is not None: downstream_require.transitive_libs = self.transitive_libs if self.visible is False: downstream_require.visible = False if pkg_type is not PackageType.HEADER: # These rules are not valid for header-only # If non-default, then the consumer requires has priority if self.headers is False: downstream_require.headers = False if self.libs is False: downstream_require.libs = False # TODO: Automatic assignment invalidates user possibility of overriding default # if required.run is not None: # downstream_require.run = required.run if self.test: downstream_require.test = True # If the current one is resolving conflicts, the downstream one will be too downstream_require.force = require.force downstream_require.direct = False return downstream_require def deduce_package_id_mode(self, pkg_type, dep_node, non_embed_mode, embed_mode, build_mode, unknown_mode): # If defined by the ``require(package_id_mode=xxx)`` trait, that is higher priority # The "conf" values are defaults, no hard overrides if self.package_id_mode: return if self.test: return # test_requires never affect the binary_id dep_conanfile = dep_node.conanfile dep_pkg_type = dep_conanfile.package_type if self.build: build_mode = getattr(dep_conanfile, "build_mode", build_mode) if build_mode and self.direct: self.package_id_mode = build_mode return if pkg_type is PackageType.HEADER: self.package_id_mode = "unrelated_mode" return # If the dependency defines the mode, that has priority over default embed_mode = getattr(dep_conanfile, "package_id_embed_mode", embed_mode) non_embed_mode = getattr(dep_conanfile, "package_id_non_embed_mode", non_embed_mode) unknown_mode = getattr(dep_conanfile, "package_id_unknown_mode", unknown_mode) if self.headers or self.libs: # only if linked if pkg_type is PackageType.SHARED or pkg_type is PackageType.APP: if dep_pkg_type is PackageType.SHARED: self.package_id_mode = non_embed_mode else: self.package_id_mode = embed_mode elif pkg_type is PackageType.STATIC: if dep_pkg_type is PackageType.HEADER: self.package_id_mode = embed_mode else: self.package_id_mode = non_embed_mode if self.package_id_mode is None: self.package_id_mode = unknown_mode # For cases like Application->Application, without headers or libs, package_id_mode=None # It will be independent by default class BuildRequirements: # Just a wrapper around requires for backwards compatibility with self.build_requires() syntax def __init__(self, requires): self._requires = requires def __call__(self, ref, package_id_mode=None, visible=False, run=None, options=None, override=None): # TODO: Check which arguments could be user-defined self._requires.build_require(ref, package_id_mode=package_id_mode, visible=visible, run=run, options=options, override=override) class ToolRequirements: # Just a wrapper around requires for backwards compatibility with self.build_requires() syntax def __init__(self, requires): self._requires = requires def __call__(self, ref, package_id_mode=None, visible=False, run=True, options=None, override=None): # TODO: Check which arguments could be user-defined self._requires.tool_require(ref, package_id_mode=package_id_mode, visible=visible, run=run, options=options, override=override) class TestRequirements: # Just a wrapper around requires for backwards compatibility with self.build_requires() syntax def __init__(self, requires): self._requires = requires def __call__(self, ref, run=None, options=None, force=None): self._requires.test_require(ref, run=run, options=options, force=force) class Requirements: """ User definitions of all requires in a conanfile """ def __init__(self, declared=None, declared_build=None, declared_test=None, declared_build_tool=None): self._requires = {} # Construct from the class definitions if declared is not None: if isinstance(declared, str): self.__call__(declared) else: try: for item in declared: if not isinstance(item, str): # TODO (2.X): Remove protection after transition from 1.X raise ConanException(f"Incompatible 1.X requires declaration '{item}'") self.__call__(item) except TypeError: raise ConanException("Wrong 'requires' definition, " "did you mean 'requirements()'?") if declared_build is not None: if isinstance(declared_build, str): self.build_require(declared_build) else: try: for item in declared_build: self.build_require(item) except TypeError: raise ConanException("Wrong 'build_requires' definition, " "did you mean 'build_requirements()'?") if declared_test is not None: if isinstance(declared_test, str): self.test_require(declared_test) else: try: for item in declared_test: self.test_require(item) except TypeError: raise ConanException("Wrong 'test_requires' definition, " "did you mean 'build_requirements()'?") if declared_build_tool is not None: if isinstance(declared_build_tool, str): self.build_require(declared_build_tool, run=True) else: try: for item in declared_build_tool: self.build_require(item, run=True) except TypeError: raise ConanException("Wrong 'tool_requires' definition, " "did you mean 'build_requirements()'?") def reindex(self, require, new_name): """ This operation is necessary when the reference name of a package is changed as a result of an "alternative" replacement of the package name, otherwise the dictionary gets broken by modified key """ result = {} for k, v in self._requires.items(): if k is require: k.ref.name = new_name result[k] = v self._requires = result def values(self): return self._requires.values() # TODO: Plan the interface for smooth transition from 1.X def __call__(self, str_ref, **kwargs): if str_ref is None: return assert isinstance(str_ref, str) ref = RecipeReference.loads(str_ref) req = Requirement(ref, **kwargs) if self._requires.get(req): raise ConanException("Duplicated requirement: {}".format(ref)) self._requires[req] = req def build_require(self, ref, raise_if_duplicated=True, package_id_mode=None, visible=False, run=None, options=None, override=None): """ Represent a generic build require, could be a tool, like "cmake" or a bundle of build scripts. visible = False => Only the direct consumer can see it, won't conflict build = True => They run in the build machine (e.g cmake) libs = False => We won't link with it, is a tool, no propagate the libs. headers = False => We won't include headers, is a tool, no propagate the includes. run = None => It will be determined by the package_type of the ref """ if ref is None: return # FIXME: This raise_if_duplicated is ugly, possibly remove ref = RecipeReference.loads(ref) req = Requirement(ref, headers=False, libs=False, build=True, run=run, visible=visible, package_id_mode=package_id_mode, options=options, override=override) if raise_if_duplicated and self._requires.get(req): raise ConanException("Duplicated requirement: {}".format(ref)) self._requires[req] = req def test_require(self, ref, run=None, options=None, force=None): """ Represent a testing framework like gtest visible = False => Only the direct consumer can see it, won't conflict build = False => The test are linked in the host context to run in the host machine libs = True => We need to link with gtest headers = True => We need to include gtest. run = None => It will be determined by the package_type of ref, maybe is gtest shared """ ref = RecipeReference.loads(ref) # visible = False => Only the direct consumer can see it, won't conflict # build = False => They run in host context, e.g the gtest application is a host app # libs = True => We need to link with it # headers = True => We need to include it req = Requirement(ref, headers=True, libs=True, build=False, run=run, visible=False, test=True, package_id_mode=None, options=options, force=force) if self._requires.get(req): raise ConanException("Duplicated requirement: {}".format(ref)) self._requires[req] = req def tool_require(self, ref, raise_if_duplicated=True, package_id_mode=None, visible=False, run=True, options=None, override=None): """ Represent a build tool like "cmake". visible = False => Only the direct consumer can see it, won't conflict build = True => They run in the build machine (e.g cmake) libs = False => We won't link with it, is a tool, no propagate the libs. headers = False => We won't include headers, is a tool, no propagate the includes. """ if ref is None: return # FIXME: This raise_if_duplicated is ugly, possibly remove ref = RecipeReference.loads(ref) req = Requirement(ref, headers=False, libs=False, build=True, run=run, visible=visible, package_id_mode=package_id_mode, options=options, override=override) if raise_if_duplicated and self._requires.get(req): raise ConanException("Duplicated requirement: {}".format(ref)) self._requires[req] = req def __repr__(self): return repr(self._requires.values()) def serialize(self): return [v.serialize() for v in self._requires.values()] def __len__(self): return len(self._requires) ================================================ FILE: conan/internal/model/settings.py ================================================ import os import yaml from conan.internal.cache.home_paths import HomePaths from conan.internal.default_settings import default_settings_yml from conan.internal.internal_tools import is_universal_arch from conan.errors import ConanException from conan.internal.util.files import save, load def bad_value_msg(name, value, value_range): return ("Invalid setting '%s' is not a valid '%s' value.\nPossible values are %s\n" 'Read "http://docs.conan.io/2/knowledge/faq.html#error-invalid-setting"' # value range can be either a list or a dict, we only want to list the keys % (value, name, [v for v in value_range if v is not None])) def undefined_field(name, field, fields=None, value=None): value_str = " for '%s'" % value if value else "" result = ["'%s.%s' doesn't exist%s" % (name, field, value_str), "'%s' possible configurations are %s" % (name, fields or "none")] return ConanException("\n".join(result)) class SettingsItem: """ represents a setting value and its child info, which could be: - A range of valid values: [Debug, Release] (for settings.compiler.runtime of VS) - List [None, "ANY"] to accept None or any value - A dict {subsetting: definition}, e.g. {version: [], runtime: []} for VS """ def __init__(self, definition, name, value): self._definition = definition # range of possible values self._name = name # settings.compiler self._value = value # gcc @staticmethod def new(definition, name): if definition is None: raise ConanException(f"Definition of settings.yml '{name}' cannot be null") if isinstance(definition, dict): parsed_definitions = {} # recursive for k, v in definition.items(): # None string from yaml definition maps to python None, means not-defined value k = str(k) if k is not None else None parsed_definitions[k] = Settings(v, name, k) else: # list or tuple of possible values, it can include "ANY" parsed_definitions = [str(v) if v is not None else None for v in definition] return SettingsItem(parsed_definitions, name, None) def __contains__(self, value): return value in (self._value or "") def copy(self): """ deepcopy, recursive """ if not isinstance(self._definition, dict): definition = self._definition # Not necessary to copy this, not mutable else: definition = {k: v.copy() for k, v in self._definition.items()} return SettingsItem(definition, self._name, self._value) def copy_conaninfo_settings(self): """ deepcopy, recursive This function adds "ANY" to lists, to allow the ``package_id()`` method to modify some of values, but not all, just the "final" values without subsettings. We cannot let users manipulate to random strings things that contain subsettings like ``compiler``, because that would leave the thing in an undefined state, with some now inconsistent subsettings, that cannot be accessed anymore. So with this change the options are: - If you need more "binary-compatible" descriptions of a compiler, lets say like "gcc_or_clang", then you need to add that string to settings.yml. And add the subsettings that you want for it. - Settings that are "final" (lists), like build_type, or arch or compiler.version they can get any value without issues. """ if not isinstance(self._definition, dict): definition = self._definition[:] + ["ANY"] else: definition = {k: v.copy_conaninfo_settings() for k, v in self._definition.items()} definition["ANY"] = Settings() return SettingsItem(definition, self._name, self._value) def __bool__(self): if not self._value: return False return self._value.lower() not in ["false", "none", "0", "off"] def __str__(self): return str(self._value) def __eq__(self, other): if other is None: return self._value is None other = self._validate(other) return other == self._value def __delattr__(self, item): """ This is necessary to remove libcxx subsetting from compiler in config() del self.settings.compiler.stdlib """ child_setting = self._get_child(self._value) delattr(child_setting, item) def _validate(self, value): value = str(value) if value is not None else None is_universal = is_universal_arch(value, self._definition) if self._name == "settings.arch" else False if "ANY" not in self._definition and value not in self._definition and not is_universal: raise ConanException(bad_value_msg(self._name, value, self._definition)) return value def _get_child(self, item): if not isinstance(self._definition, dict): raise undefined_field(self._name, item, None, self._value) if self._value is None: raise ConanException("'%s' value not defined" % self._name) return self._get_definition() def _get_definition(self): if self._value not in self._definition and "ANY" in self._definition: return self._definition["ANY"] return self._definition[self._value] def __getattr__(self, item): item = str(item) sub_config_dict = self._get_child(item) return getattr(sub_config_dict, item) def __setattr__(self, item, value): if item[0] == "_" or item.startswith("value"): return super(SettingsItem, self).__setattr__(item, value) item = str(item) sub_config_dict = self._get_child(item) return setattr(sub_config_dict, item, value) @property def value(self): return self._value @value.setter def value(self, v): self._value = self._validate(v) @property def values_range(self): # This needs to support 2 operations: "in" and iteration. Beware it can return "ANY" return self._definition @property def values_list(self): if self._value is None: return [] result = [] partial_name = ".".join(self._name.split(".")[1:]) result.append((partial_name, self._value)) if isinstance(self._definition, dict): sub_config_dict = self._get_definition() result.extend(sub_config_dict.values_list) return result def validate(self): if self._value is None and None not in self._definition: raise ConanException("'%s' value not defined" % self._name) if isinstance(self._definition, dict): self._get_definition().validate() def possible_values(self): if isinstance(self._definition, list): return self.values_range.copy() ret = {} for key, value in self._definition.items(): ret[key] = value.possible_values() return ret def rm_safe(self, name): """ Iterates all possible subsettings, calling rm_safe() for all of them. If removing "compiler.cppstd", this will iterate msvc, gcc, clang, etc, calling rm_safe(cppstd) for all of them""" if isinstance(self._definition, list): return for subsetting in self._definition.values(): subsetting.rm_safe(name) class Settings: def __init__(self, definition=None, name="settings", parent_value="settings"): if parent_value is None and definition: raise ConanException("settings.yml: null setting can't have subsettings") definition = definition or {} if not isinstance(definition, dict): val = "" if parent_value == "settings" else f"={parent_value}" raise ConanException(f"Invalid settings.yml format: '{name}{val}' is not a dictionary") self._name = name # settings, settings.compiler self._parent_value = parent_value # gcc, x86 self._data = {k: SettingsItem.new(v, f"{name}.{k}") for k, v in definition.items()} self._frozen = False def serialize(self): """ Returns a dictionary with all the settings (and sub-settings) as ``field: value`` """ ret = [] for _, s in self._data.items(): # TODO: Refactor it and use s.serialize() ret.extend(s.values_list) return dict(ret) def get_safe(self, name, default=None): """ Get the setting value avoiding throwing if it does not exist or has been removed :param name: :param default: :return: """ try: tmp = self for prop in name.split("."): tmp = getattr(tmp, prop, None) except ConanException: return default if tmp is not None and tmp.value is not None: # In case of subsettings is None return tmp.value return default def rm_safe(self, name): """ Removes the setting or subsetting from the definition. For example, rm_safe("compiler.cppstd") remove all "cppstd" subsetting from all compilers, irrespective of the current value of the "compiler" """ if "." in name: setting, remainder = name.split(".", 1) # setting=compiler, remainder = cppstd try: self._data[setting].rm_safe(remainder) # call rm_safe("cppstd") for the "compiler" except KeyError: pass else: if name == "*": self.clear() else: self._data.pop(name, None) def copy(self): """ deepcopy, recursive """ result = Settings({}, name=self._name, parent_value=self._parent_value) result._data = {k: v.copy() for k, v in self._data.items()} return result def copy_conaninfo_settings(self): result = Settings({}, name=self._name, parent_value=self._parent_value) result._data = {k: v.copy_conaninfo_settings() for k, v in self._data.items()} return result @staticmethod def loads(text): try: return Settings(yaml.safe_load(text) or {}) except (yaml.YAMLError, AttributeError) as ye: raise ConanException("Invalid settings.yml format: {}".format(ye)) def validate(self): for child in self._data.values(): child.validate() @property def fields(self): return sorted(list(self._data.keys())) def clear(self): self._data = {} def _check_field(self, field): if field not in self._data: raise undefined_field(self._name, field, self.fields, self._parent_value) def __getattr__(self, field): assert field[0] != "_", "ERROR %s" % field self._check_field(field) return self._data[field] def __delattr__(self, field): assert field[0] != "_", "ERROR %s" % field self._check_field(field) del self._data[field] def __setattr__(self, field, value): if field[0] == "_": return super(Settings, self).__setattr__(field, value) self._check_field(field) if self._frozen: raise ConanException(f"Tried to define '{field}' setting inside recipe") self._data[field].value = value @property def values_list(self): # TODO: make it private, leave .items accessor only result = [] for field in self.fields: config_item = self._data[field] result.extend(config_item.values_list) return result def items(self): return self.values_list def update_values(self, values, raise_undefined=True): """ Receives a list of tuples (compiler.version, value) This is more an updater than a setter. """ self._frozen = False # Could be restored at the end, but not really necessary assert isinstance(values, (list, tuple)), values for (name, value) in values: list_settings = name.split(".") attr = self try: for setting in list_settings[:-1]: attr = getattr(attr, setting) value = str(value) if value is not None else None setattr(attr, list_settings[-1], value) except ConanException: # fails if receiving settings doesn't have it defined if raise_undefined: raise def constrained(self, constraint_def): """ allows to restrict a given Settings object with the input of another Settings object 1. The other Settings object MUST be exclusively a subset of the former. No additions allowed 2. If the other defines {"compiler": None} means to keep the full specification """ constraint_def = constraint_def or [] if not isinstance(constraint_def, (list, tuple, set)): raise ConanException("Please defines settings as a list or tuple") for field in constraint_def: self._check_field(field) to_remove = [k for k in self._data if k not in constraint_def] for k in to_remove: del self._data[k] def dumps(self): """ produces a text string with lines containing a flattened version: compiler.arch = XX compiler.arch.speed = YY """ result = [] for (name, value) in self.values_list: # It is important to discard None values, so migrations in settings can be done # without breaking all existing packages SHAs, by adding a first None option # that doesn't change the final sha if value is not None: result.append("%s=%s" % (name, value)) return '\n'.join(result) def possible_values(self): """Check the range of values of the definition of a setting """ ret = {} for key, element in self._data.items(): ret[key] = element.possible_values() return ret def load_settings_yml(home_folder): """Returns {setting: [value, ...]} defining all the possible settings without values""" _home_paths = HomePaths(home_folder) settings_path = _home_paths.settings_path if not os.path.exists(settings_path): save(settings_path, default_settings_yml) save(settings_path + ".orig", default_settings_yml) # stores a copy, to check migrations def _load_settings(path): try: return yaml.safe_load(load(path)) or {} except yaml.YAMLError as ye: raise ConanException("Invalid settings.yml format: {}".format(ye)) settings = _load_settings(settings_path) user_settings_file = _home_paths.settings_path_user if os.path.exists(user_settings_file): settings_user = _load_settings(user_settings_file) def appending_recursive_dict_update(d, u): # Not the same behavior as conandata_update, because this append lists for k, v in u.items(): if isinstance(v, list): current = d.get(k) or [] d[k] = current + [value for value in v if value not in current] elif isinstance(v, dict): current = d.get(k) or {} if isinstance(current, list): # convert to dict lists current = {k: None for k in current} d[k] = appending_recursive_dict_update(current, v) else: d[k] = v return d appending_recursive_dict_update(settings, settings_user) return Settings(settings) ================================================ FILE: conan/internal/model/version.py ================================================ from functools import total_ordering from typing import Optional from conan.errors import ConanException @total_ordering class _VersionItem: """ a single "digit" in a version, like X.Y.Z all X and Y and Z are VersionItems They can be int or strings """ def __init__(self, item): try: self._v = int(item) except ValueError: self._v = item @property def value(self): return self._v def __str__(self): return str(self._v) def __add__(self, other): # necessary for the "bump()" functionality. Other aritmetic operations are missing return self._v + other def __eq__(self, other): if not isinstance(other, _VersionItem): other = _VersionItem(other) return self._v == other._v def __hash__(self): return hash(self._v) def __lt__(self, other): """ @type other: _VersionItem """ if not isinstance(other, _VersionItem): other = _VersionItem(other) try: return self._v < other._v except TypeError: return str(self._v) < str(other._v) @total_ordering class Version: """ This is NOT an implementation of semver, as users may use any pattern in their versions. It is just a helper to parse "." or "-" and compare taking into account integers when possible """ def __init__(self, value, qualifier=False): value = str(value) self._value = value self._build = None self._pre = None self._qualifier = qualifier # it is a prerelease or build qualifier, not a main version if not qualifier: items = value.rsplit("+", 1) # split for build if len(items) == 2: value, build = items self._build = Version(build, qualifier=True) # This is a nested version by itself # split for pre-release, from the left, semver allows hyphens in identifiers :( items = value.split("-", 1) if len(items) == 2: value, pre = items self._pre = Version(pre, qualifier=True) # This is a nested version by itself items = value.split(".") items = [_VersionItem(item) for item in items] self._items = tuple(items) while items and items[-1].value == 0: del items[-1] self._nonzero_items = tuple(items) def bump(self, index): """ :meta private: Bump the version Increments by 1 the version field at the specified index, setting to 0 the fields on the right. 2.5 => bump(1) => 2.6 1.5.7 => bump(0) => 2.0.0 :param index: """ # this method is used to compute version ranges from tilde ~1.2 and caret ^1.2.1 ranges # TODO: at this moment it only works for digits, cannot increment pre-release or builds # better not make it public yet, keep it internal items = list(self._items[:index]) try: items.append(self._items[index]+1) except TypeError: raise ConanException(f"Cannot bump '{self._value} version index {index}, not an int") items.extend([0] * (len(items) - index - 1)) v = ".".join(str(i) for i in items) # prerelease and build are dropped while bumping digits return Version(v) def upper_bound(self, index): items = list(self._items[:index]) try: items.append(self._items[index] + 1) except TypeError: raise ConanException(f"Cannot bump '{self._value} version index {index}, not an int") items.extend([0] * (len(items) - index - 1)) v = ".".join(str(i) for i in items) v += "-" # Exclude prereleases return Version(v) @property def pre(self): return self._pre @property def build(self): return self._build @property def main(self): return self._items @property def major(self): try: return self.main[0] except IndexError: return None @property def minor(self): try: return self.main[1] except IndexError: return None @property def patch(self): try: return self.main[2] except IndexError: return None @property def micro(self): try: return self.main[3] except IndexError: return None def __str__(self): return self._value def __repr__(self): return self._value def __eq__(self, other): if other is None: return False if not isinstance(other, Version): other = Version(other, self._qualifier) return (self._nonzero_items, self._pre, self._build) ==\ (other._nonzero_items, other._pre, other._build) def __hash__(self): return hash((self._nonzero_items, self._pre, self._build)) def __lt__(self, other): if other is None: return False if not isinstance(other, Version): other = Version(other) if self._pre: if other._pre: # both are pre-releases return (self._nonzero_items, self._pre, self._build) < \ (other._nonzero_items, other._pre, other._build) else: # Left hand is pre-release, right side is regular if self._nonzero_items == other._nonzero_items: # Problem only happens if both equal return True else: return self._nonzero_items < other._nonzero_items else: if other._pre: # Left hand is regular, right side is pre-release if self._nonzero_items == other._nonzero_items: # Problem only happens if both equal return False else: return self._nonzero_items < other._nonzero_items else: # None of them is pre-release return (self._nonzero_items, self._build) < (other._nonzero_items, other._build) def in_range(self, version_range: str, resolve_prerelease: Optional[bool] = None): """ Check if the version is in the specified range """ from conan.internal.model.version_range import VersionRange return VersionRange(version_range).contains(self, resolve_prerelease=resolve_prerelease) ================================================ FILE: conan/internal/model/version_range.py ================================================ from functools import total_ordering from typing import Optional from conan.internal.model.version import Version from conan.errors import ConanException @total_ordering class _Condition: def __init__(self, operator, version): self.operator = operator self.display_version = version value = str(version) if (operator == ">=" or operator == "<") and "-" not in value and version.build is None: value += "-" self.version = Version(value) def __str__(self): return f"{self.operator}{self.display_version}" def __repr__(self): return self.__str__() def __hash__(self): return hash((self.operator, self.version)) def __lt__(self, other): # Notice that this is done on the modified version, might contain extra prereleases if self.version < other.version: return True elif self.version == other.version: if self.operator == "<": if other.operator == "<": return self.display_version.pre is not None else: return True elif self.operator == "<=": if other.operator == "<": return False else: return self.display_version.pre is None elif self.operator == ">": if other.operator == ">": return self.display_version.pre is None else: return False else: if other.operator == ">": return True # There's a possibility of getting here while validating if a range is non-void # by comparing >= & <= for lower limit <= upper limit elif other.operator == "<=": return True else: return self.display_version.pre is not None return False def __eq__(self, other): return (self.display_version == other.display_version and self.operator == other.operator) class _ConditionSet: def __init__(self, expression, prerelease): expressions = expression.split() if not expressions: # Guarantee at least one expression expressions = [""] self.prerelease = prerelease self.conditions = [] for e in expressions: e = e.strip() self.conditions.extend(self._parse_expression(e)) @staticmethod def _parse_expression(expression): if expression in ("", "*"): return [_Condition(">=", Version("0.0.0"))] elif len(expression) == 1: raise ConanException(f'Error parsing version range "{expression}"') operator = expression[0] if operator not in (">", "<", "^", "~", "="): if expression[-1] == "*": # Handle patterns like "1.2.*" operator = "*" expression = expression[:-1] else: operator = "=" index = 0 else: index = 1 if operator in (">", "<"): if expression[1] == "=": operator += "=" index = 2 elif expression[1] == "=": raise ConanException(f"Invalid version range operator '{operator}=' in {expression}, you should probably use {operator} instead.") version = expression[index:] if version == "": raise ConanException(f'Error parsing version range "{expression}"') if operator == "~": # tilde minor if "-" not in version: version += "-" v = Version(version) index = 1 if len(v.main) > 1 else 0 return [_Condition(">=", v), _Condition("<", v.upper_bound(index))] elif operator == "^": # caret major v = Version(version) def first_non_zero(main): for i, m in enumerate(main): if m != 0: return i return len(main) initial_index = first_non_zero(v.main) return [_Condition(">=", v), _Condition("<", v.upper_bound(initial_index))] else: return [_Condition(operator, Version(version))] def valid(self, version, conf_resolve_prepreleases): if version.pre: # Follow the expression desires only if core.version_ranges:resolve_prereleases is None, # else force to the conf's value if conf_resolve_prepreleases is None: if not self.prerelease: return False elif conf_resolve_prepreleases is False: return False for condition in self.conditions: if condition.operator == ">": if not version > condition.version: return False elif condition.operator == "<": if not version < condition.version: return False elif condition.operator == ">=": if not version >= condition.version: return False elif condition.operator == "<=": if not version <= condition.version: return False elif condition.operator == "=": if not version == condition.version: return False elif condition.operator == "*": if not str(version).startswith(str(condition.version)): return False return True class VersionRange: def __init__(self, expression): self._expression = expression tokens = expression.split(",") prereleases = False for t in tokens[1:]: if "include_prerelease" in t: if "include_prerelease=" in t: from conan.api.output import ConanOutput ConanOutput().warning( f'include_prerelease version range option in "{expression}" does not take an attribute, ' 'its presence unconditionally enables prereleases') prereleases = True break else: t = t.strip() if len(t) > 0 and t[0].isalpha(): from conan.api.output import ConanOutput ConanOutput().warning(f'Unrecognized version range option "{t}" in "{expression}"') else: raise ConanException(f'"{t}" in version range "{expression}" is not a valid option') version_expr = tokens[0] self.condition_sets = [] for alternative in version_expr.split("||"): self.condition_sets.append(_ConditionSet(alternative, prereleases)) def __str__(self): return self._expression def contains(self, version: Version, resolve_prerelease: Optional[bool]): """ Whether is inside the version range :param version: Version to check against :param resolve_prerelease: If ``True``, ensure prereleases can be resolved in this range If ``False``, prerelases can NOT be resolved in this range If ``None``, prereleases are resolved only if this version range expression says so :return: Whether the version is inside the range """ assert isinstance(version, Version), type(version) for condition_set in self.condition_sets: if condition_set.valid(version, resolve_prerelease): return True return False def intersection(self, other): conditions = [] def _calculate_limits(operator, lhs, rhs): limits = ([c for c in lhs.conditions if operator in c.operator] + [c for c in rhs.conditions if operator in c.operator]) if limits: return sorted(limits, reverse=operator == ">")[0] prerelease = True for lhs_conditions in self.condition_sets: for rhs_conditions in other.condition_sets: internal_conditions = [] lower_limit = _calculate_limits(">", lhs_conditions, rhs_conditions) upper_limit = _calculate_limits("<", lhs_conditions, rhs_conditions) if lower_limit: internal_conditions.append(lower_limit) if upper_limit: internal_conditions.append(upper_limit) if internal_conditions and (not lower_limit or not upper_limit or lower_limit <= upper_limit): conditions.append(internal_conditions) # conservative approach: if any of the conditions forbid prereleases, forbid them in the result if not lhs_conditions.prerelease or not rhs_conditions.prerelease: prerelease = False if not conditions: return None expression = ' || '.join(' '.join(str(c) for c in cs) for cs in conditions) + (', include_prerelease' if prerelease else '') result = VersionRange(expression) # TODO: Direct definition of conditions not reparsing # result.condition_sets = self.condition_sets + other.condition_sets return result def version(self): return Version(f"[{self._expression}]") def validate_conan_version(required_range): from conan import __version__ # To avoid circular imports clientver = Version(__version__) version_range = VersionRange(required_range) for conditions in version_range.condition_sets: conditions.prerelease = True if not version_range.contains(clientver, resolve_prerelease=None): raise ConanException("Current Conan version ({}) does not satisfy " "the defined one ({}).".format(clientver, required_range)) ================================================ FILE: conan/internal/model/workspace.py ================================================ import os import shutil import yaml from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.errors import scoped_traceback from conan.internal.util.files import load, save # Related folder WORKSPACE_FOLDER = "conanws" # Related files WORKSPACE_YML = "conanws.yml" WORKSPACE_PY = "conanws.py" class Workspace: """ The base class for all workspaces """ def __init__(self, folder, conan_api): self.folder = folder self.conan_data = self._conan_load_data() self._conan_api = conan_api self.output = ConanOutput(scope=f"Workspace '{self.name()}'") def __getattribute__(self, item): # Return a protected wrapper around workspace overridable callables in order to # be able to have clean errors if user errors in conanws.py code myattr = object.__getattribute__(self, item) if item not in ("name", "packages", "add", "remove", "clean", "build_order"): return myattr def wrapper(*args, **kwargs): try: return myattr(*args, **kwargs) except ConanException: raise except Exception as e: m = scoped_traceback(f"Error in {item}() method", e, scope="conanws.py") raise ConanException(f"Workspace conanws.py file: {m}") return wrapper def name(self): return self.conan_data.get("name") or os.path.basename(self.folder) def _conan_load_data(self): data_path = os.path.join(self.folder, WORKSPACE_YML) if not os.path.exists(data_path): return {} try: data = yaml.safe_load(load(data_path)) except Exception as e: raise ConanException("Invalid yml format at {}: {}".format(WORKSPACE_YML, e)) return data or {} def add(self, ref, path, output_folder): if not path or not os.path.isfile(path): raise ConanException(f"Cannot add to workspace. File not found: {path}") path = self._conan_rel_path(os.path.dirname(path)) editable = { "path": path, "ref": str(ref) } if output_folder: editable["output_folder"] = self._conan_rel_path(output_folder) packages = self.conan_data.setdefault("packages", []) for p in packages: if p["path"] == path: self.output.warning(f"Package {path} already exists, updating its reference") p["ref"] = editable["ref"] break else: packages.append(editable) save(os.path.join(self.folder, WORKSPACE_YML), yaml.dump(self.conan_data)) def remove(self, path): path = self._conan_rel_path(path) package_found = next((package_info for package_info in self.conan_data.get("packages", []) if package_info["path"].replace("\\", "/") == path), None) if not package_found: raise ConanException(f"No editable package to remove from this path: {path}") self.conan_data["packages"].remove(package_found) save(os.path.join(self.folder, WORKSPACE_YML), yaml.dump(self.conan_data)) return path def clean(self): self.output.info("Default workspace clean: Removing the output-folder of each editable") for package_info in self.conan_data.get("packages", []): editable_label = package_info.get("ref", "") or package_info['path'] if not package_info.get("output_folder"): self.output.info(f"Editable {editable_label} doesn't have an output_folder defined") continue of = os.path.join(self.folder, package_info["output_folder"]) try: self.output.info(f"Removing {editable_label} output folder: {of}") shutil.rmtree(of) except OSError as e: self.output.warning(f"Error removing {editable_label} output folder: {str(e)}") def _conan_rel_path(self, path): if path is None: return None if not os.path.isabs(path): raise ConanException(f"Editable path must be absolute: {path}") path = os.path.relpath(path, self.folder) return path.replace("\\", "/") # Normalize to unix path def packages(self): return self.conan_data.get("packages", []) def load_conanfile(self, conanfile_path): conanfile_path = os.path.join(self.folder, conanfile_path, "conanfile.py") from conan.internal.loader import ConanFileLoader from conan.internal.cache.home_paths import HomePaths from conan.internal.conan_app import ConanFileHelpers, CmdWrapper cmd_wrap = CmdWrapper(HomePaths(self._conan_api.home_folder).wrapper_path) helpers = ConanFileHelpers(None, cmd_wrap, self._conan_api._api_helpers.global_conf, cache=None, home_folder=self._conan_api.home_folder, conan_api=self._conan_api) loader = ConanFileLoader(pyreq_loader=None, conanfile_helpers=helpers) conanfile = loader.load_named(conanfile_path, name=None, version=None, user=None, channel=None, remotes=None, graph_lock=None) return conanfile def root_conanfile(self): # noqa return None def build_order(self, order): # noqa msg = ["Packages build order:"] for level in order: for item in level: msg.append(f" {item['ref']}: {item['folder']}") self.output.info("\n".join(msg)) ================================================ FILE: conan/internal/paths.py ================================================ import os import platform from pathlib import Path from conan.errors import ConanException if platform.system() == "Windows": def _conan_expand_user(path): """ wrapper to the original expanduser function, to workaround python returning verbatim %USERPROFILE% when some other app (git for windows) sets HOME envvar """ path = str(path) if path[0] != '~': return path # In win these variables should exist and point to user directory, which # must exist. home = os.environ.get("HOME") try: # Problematic cases of wrong HOME variable # - HOME = %USERPROFILE% verbatim, as messed by some other tools # - MSYS console, that defines a different user home in /c/mingw/msys/users/xxx # In these cases, it is safe to remove it and rely on USERPROFILE directly if home and (not os.path.exists(home) or (os.getenv("MSYSTEM") and os.getenv("USERPROFILE"))): del os.environ["HOME"] result = os.path.expanduser(path) finally: if home is not None: os.environ["HOME"] = home return result else: _conan_expand_user = os.path.expanduser DEFAULT_CONAN_HOME = ".conan2" def find_file_walk_up(start, filename, end=None): path = Path(start) end = Path(end) if end else None while True: file = path / filename if file.is_file(): return file if len(path.parts) == 1: # finish at '/' break if end and path == end: break path = path.parent return None def get_conan_user_home(): def _user_home_from_conanrc_file(): try: conanrc_path = find_file_walk_up(os.getcwd(), ".conanrc") with open(conanrc_path) as conanrc_file: values = {k: str(v) for k, v in (line.split('=') for line in conanrc_file.read().splitlines() if not line.startswith("#"))} conan_home = values["conan_home"] # check if it's a local folder if conan_home[:2] in ("./", ".\\") or conan_home.startswith(".."): conan_home = conanrc_path.parent.absolute() / conan_home return conan_home except (OSError, KeyError, TypeError): return None user_home = _user_home_from_conanrc_file() or os.getenv("CONAN_HOME") if user_home is None: # the default, in the user home user_home = os.path.join(_conan_expand_user("~"), DEFAULT_CONAN_HOME) else: # Do an expansion, just in case the user is using ~/something/here user_home = _conan_expand_user(user_home) if not os.path.isabs(user_home): raise ConanException("Invalid CONAN_HOME value '%s', " "please specify an absolute or path starting with ~/ " "(relative to user home)" % user_home) return user_home # Files CONANFILE = 'conanfile.py' CONANFILE_TXT = "conanfile.txt" CONAN_MANIFEST = "conanmanifest.txt" CONANINFO = "conaninfo.txt" PACKAGE_FILE_NAME = "conan_package.t" EXPORT_FILE_NAME = "conan_export.t" EXPORT_SOURCES_FILE_NAME = "conan_sources.t" COMPRESSIONS = "gz", "xz", "zst" DATA_YML = "conandata.yml" ================================================ FILE: conan/internal/rest/__init__.py ================================================ import json def response_to_str(response): content = response.content try: # A bytes message, decode it as str if isinstance(content, bytes): content = content.decode() content_type = response.headers.get("content-type") if content_type == "application/json": # Errors from Artifactory looks like: # {"errors" : [ {"status" : 400, "message" : "Bla bla bla"}]} try: data = json.loads(content)["errors"][0] content = "{}: {}".format(data["status"], data["message"]) except Exception: pass elif "text/html" in content_type: content = "{}: {}".format(response.status_code, response.reason) return content except Exception: return response.content ================================================ FILE: conan/internal/rest/auth_manager.py ================================================ """ Collaborate with RestApiClient to make remote anonymous and authenticated calls. Uses user_input to request user's login and password and obtain a token for calling authenticated methods if receives AuthenticationException from RestApiClient. Flow: Directly invoke a REST method in RestApiClient, example: get_conan. if receives AuthenticationException (not open method) will ask user for login and password (with LOGIN_RETRIES retries) and retry to call with the new token. """ from conan.api.output import ConanOutput from conan.internal.rest.remote_credentials import RemoteCredentials from conan.internal.rest.rest_client import RestApiClient from conan.internal.errors import AuthenticationException, ForbiddenException from conan.errors import ConanException LOGIN_RETRIES = 3 class _RemoteCreds: def __init__(self, localdb): self._localdb = localdb def get(self, remote, msg=True): creds = getattr(remote, "_creds", None) if creds is None: user, token, _ = self._localdb.get_login(remote.url) creds = user, token if msg: usermsg = f"with user '{user}'" if user else "anonymously" ConanOutput().info(f"Connecting to remote '{remote.name}' {usermsg}") setattr(remote, "_creds", creds) return creds def set(self, remote, user, token): setattr(remote, "_creds", (user, token)) ConanOutput().success(f"Authenticated in remote '{remote.name}' with user '{user}'") self._localdb.store(user, token, None, remote.url) class ConanApiAuthManager: def __init__(self, requester, cache_folder, localdb, global_conf): self._requester = requester self._creds = _RemoteCreds(localdb) self._global_conf = global_conf self._cache_folder = cache_folder def call_rest_api_method(self, remote, method_name, *args, **kwargs): """Handles AuthenticationException and request user to input a user and a password""" user, token = self._creds.get(remote, msg=(method_name != "authenticate")) rest_client = RestApiClient(remote, token, self._requester, self._global_conf) if method_name == "authenticate": return self._authenticate(rest_client, remote, *args, **kwargs) try: ret = getattr(rest_client, method_name)(*args, **kwargs) return ret except ForbiddenException as e: raise ForbiddenException(f"Permission denied for user: '{user}': {e}") except AuthenticationException: # User valid but not enough permissions # token is None when you change user with user command # Anonymous is not enough, ask for a user ConanOutput().info(f"Remote '{remote.name}' needs authentication, obtaining credentials") if self._get_credentials_and_authenticate(rest_client, user, remote): return self.call_rest_api_method(remote, method_name, *args, **kwargs) def _get_credentials_and_authenticate(self, rest_client, user, remote): """Try LOGIN_RETRIES to obtain a password from user input for which we can get a valid token from api_client. If a token is returned, credentials are stored in localdb and rest method is called""" creds = RemoteCredentials(self._cache_folder, self._global_conf) for _ in range(LOGIN_RETRIES): input_user, input_password, interactive = creds.auth(remote) try: self._authenticate(rest_client, remote, input_user, input_password) except AuthenticationException: out = ConanOutput() if user is None: out.error('Wrong user or password', error_type="exception") else: out.error(f'Wrong password for user "{user}"', error_type="exception") if not interactive: raise AuthenticationException(f"Authentication error in remote '{remote.name}'") else: return True raise AuthenticationException("Too many failed login attempts, bye!") def _authenticate(self, rest_client, remote, user, password): try: token = rest_client.authenticate(user, password) except UnicodeDecodeError: raise ConanException("Password contains not allowed symbols") # Store result in DB self._creds.set(remote, user, token) ================================================ FILE: conan/internal/rest/caching_file_downloader.py ================================================ import os import shutil from urllib.parse import urlparse from urllib.request import url2pathname from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan.internal.rest.file_downloader import FileDownloader from conan.internal.rest.download_cache import DownloadCache from conan.internal.errors import AuthenticationException, ForbiddenException, NotFoundException from conan.errors import ConanException from conan.internal.util.files import mkdir, set_dirty_context_manager, remove_if_dirty, human_size class SourcesCachingDownloader: """ Class for downloading recipe download() urls if the config is active, it can use caching/backup-sources """ def __init__(self, conanfile): helpers = getattr(conanfile, "_conan_helpers") self._global_conf = helpers.global_conf self._file_downloader = FileDownloader(helpers.requester, scope=conanfile.display_name, source_credentials=True) self._home_folder = helpers.home_folder self._output = conanfile.output self._conanfile = conanfile def download(self, urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256): download_cache_folder = self._global_conf.get("core.sources:download_cache") backups_urls = self._global_conf.get("core.sources:download_urls", check_type=list) if not (backups_urls or download_cache_folder) or not sha256: # regular, non backup/caching download if backups_urls or download_cache_folder: self._output.warning("Cannot cache download() without sha256 checksum") self._download_from_urls(urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256) else: self._caching_download(urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256, download_cache_folder, backups_urls) def _caching_download(self, urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256, download_cache_folder, backups_urls): """ this download will first check in the local cache, if not there, it will go to the list of backup_urls defined by user conf (by default ["origin"]), and iterate it until something is found. """ # We are going to use the download_urls definition for backups download_cache_folder = download_cache_folder or HomePaths(self._home_folder).default_sources_backup_folder # regular local shared download cache, not using Conan backup sources servers backups_urls = backups_urls or ["origin"] if download_cache_folder and not os.path.isabs(download_cache_folder): raise ConanException("core.download:download_cache must be an absolute path") download_cache = DownloadCache(download_cache_folder) cached_path = download_cache.source_path(sha256) with download_cache.lock(sha256): remove_if_dirty(cached_path) if os.path.exists(cached_path): self._output.info(f"Source {urls} retrieved from local download cache") else: with set_dirty_context_manager(cached_path): if None in backups_urls: raise ConanException("Trying to download sources from None backup remote." f" Remotes were: {backups_urls}") for backup_url in backups_urls: is_last = backup_url is backups_urls[-1] if backup_url == "origin": # recipe defined URLs if self._origin_download(urls, cached_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256, is_last): break else: if self._backup_download(backup_url, backups_urls, sha256, cached_path, urls, is_last): break download_cache.update_backup_sources_json(cached_path, self._conanfile, urls) # Everything good, file in the cache, just copy it to final destination mkdir(os.path.dirname(file_path)) shutil.copy2(cached_path, file_path) def _origin_download(self, urls, cached_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256, is_last): """ download from the internet, the urls provided by the recipe (mirrors). """ try: self._download_from_urls(urls, cached_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256) except ConanException as e: if is_last: raise else: # TODO: Improve printing of AuthenticationException self._output.warning(f"Sources for {urls} failed in 'origin': {e}") self._output.warning("Checking backups") else: if not is_last: self._output.info(f"Sources for {urls} found in origin") return True def _backup_download(self, backup_url, backups_urls, sha256, cached_path, urls, is_last): """ download from a Conan backup sources file server, like an Artifactory generic repo All failures are bad, except NotFound. The server must be live, working and auth, we don't want silently skipping a backup because it is down. """ try: backup_url = backup_url if backup_url.endswith("/") else backup_url + "/" self._file_downloader.download(backup_url + sha256, cached_path, sha256=sha256) self._file_downloader.download(backup_url + sha256 + ".json", cached_path + ".json") self._output.info(f"Sources for {urls} found in remote backup {backup_url}") return True except NotFoundException: if is_last: raise NotFoundException(f"File {urls} not found in {backups_urls}") else: self._output.warning(f"File {urls} not found in {backup_url}") except (AuthenticationException, ForbiddenException) as e: raise ConanException(f"Authentication to source backup server '{backup_url}' " f"failed: {e}. " f"Please check your 'source_credentials.json'") def _download_from_urls(self, urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256): """ iterate the recipe provided list of urls (mirrors, all with same checksum) until one succeed """ os.makedirs(os.path.dirname(file_path), exist_ok=True) # filename in subfolder must exist if not isinstance(urls, (list, tuple)): urls = [urls] for url in urls: try: if url.startswith("file:"): # plain copy from local disk, no real download file_origin = url2pathname(urlparse(url).path) shutil.copyfile(file_origin, file_path) self._file_downloader.check_checksum(file_path, md5, sha1, sha256) else: self._file_downloader.download(url, file_path, retry, retry_wait, verify_ssl, auth, True, headers, md5, sha1, sha256) return # Success! Return to caller except Exception as error: if url != urls[-1]: # If it is not the last one, do not raise, warn and move to next msg = f"Could not download from the URL {url}: {error}." self._output.warning(msg) self._output.info("Trying another mirror.") else: raise class ConanInternalCacheDownloader: """ This is used for the download of Conan packages from server, not for sources/backup sources """ def __init__(self, requester, config, scope=None): self._download_cache = config.get("core.download:download_cache") if self._download_cache and not os.path.isabs(self._download_cache): raise ConanException("core.download:download_cache must be an absolute path") self._file_downloader = FileDownloader(requester, scope=scope) self._scope = scope def download(self, url, file_path, auth, verify_ssl, retry, retry_wait, metadata=False): if not self._download_cache or metadata: # Metadata not cached and can be overwritten self._file_downloader.download(url, file_path, retry=retry, retry_wait=retry_wait, verify_ssl=verify_ssl, auth=auth, overwrite=metadata) return download_cache = DownloadCache(self._download_cache) cached_path, h = download_cache.cached_path(url) with download_cache.lock(h): remove_if_dirty(cached_path) if not os.path.exists(cached_path): with set_dirty_context_manager(cached_path): self._file_downloader.download(url, cached_path, retry=retry, retry_wait=retry_wait, verify_ssl=verify_ssl, auth=auth, overwrite=False) else: # Found in cache! total_length = os.path.getsize(cached_path) is_large_file = total_length > 10000000 # 10 MB if is_large_file: base_name = os.path.basename(file_path) hs = human_size(total_length) ConanOutput(scope=self._scope).info(f"Copying {hs} {base_name} from download " f"cache, instead of downloading it") # Everything good, file in the cache, just copy it to final destination mkdir(os.path.dirname(file_path)) shutil.copy2(cached_path, file_path) ================================================ FILE: conan/internal/rest/client_routes.py ================================================ from urllib.parse import urlencode from conan.api.model import RecipeReference from conan.internal.rest.rest_routes import RestRoutes def _format_ref(url, ref): url = url.format(name=ref.name, version=ref.version, username=ref.user or "_", channel=ref.channel or "_", revision=ref.revision) return url def _format_pref(url, pref): ref = pref.ref url = url.format(name=ref.name, version=ref.version, username=ref.user or "_", channel=ref.channel or "_", revision=ref.revision, package_id=pref.package_id, p_revision=pref.revision) return url class ClientV2Router: """Builds urls for v2""" def __init__(self, root_url): self.root_url = root_url self.base_url = "{}/v2/".format(root_url) self.routes = RestRoutes() def ping(self): # FIXME: The v2 ping is not returning capabilities return "{}/v1/".format(self.root_url) + self.routes.ping def search(self, pattern, ignorecase): """URL search recipes""" query = '' if pattern: if isinstance(pattern, RecipeReference): pattern = repr(pattern) params = {"q": pattern} if not ignorecase: params["ignorecase"] = "False" query = "?%s" % urlencode(params) return self.base_url + "%s%s" % (self.routes.common_search, query) def search_packages(self, ref, list_only): """URL search packages for a recipe""" route = self.routes.common_search_packages_revision \ if ref.revision else self.routes.common_search_packages url = _format_ref(route, ref) params = {"list_only": list_only} query = "?%s" % urlencode(params) return self.base_url + url + query def common_authenticate(self): return self.base_url + self.routes.common_authenticate def common_check_credentials(self): return self.base_url + self.routes.common_check_credentials def recipe_file(self, ref, path): """Recipe file url""" return self.base_url + self._for_recipe_file(ref, path) def package_file(self, pref, path): """Package file url""" return self.base_url + self._for_package_file(pref, path) def remove_recipe(self, ref): """Remove recipe url""" return self.base_url + self._for_recipe(ref) def recipe_revisions(self, ref): """Get revisions for a recipe url""" return self.base_url + _format_ref(self.routes.recipe_revisions, ref) def remove_package(self, pref): """Remove package url""" assert pref.revision is not None, "remove_package v2 needs PREV" return self.base_url + self._for_package(pref) def remove_all_packages(self, ref): """Remove package url""" return self.base_url + self._for_packages(ref) def recipe_snapshot(self, ref): """get recipe manifest url""" return self.base_url + self._for_recipe_files(ref) def package_snapshot(self, pref): """get recipe manifest url""" return self.base_url + self._for_package_files(pref) def package_revisions(self, pref): """get revisions for a package url""" return self.base_url + _format_pref(self.routes.package_revisions, pref) def package_latest(self, pref): """Get the latest of a package""" assert pref.ref.revision is not None, "Cannot get the latest package without RREV" return self.base_url + _format_pref(self.routes.package_revision_latest, pref) def recipe_latest(self, ref): """Get the latest of a recipe""" assert ref.revision is None, "for_recipe_latest shouldn't receive RREV" return self.base_url + _format_ref(self.routes.recipe_latest, ref) def _for_package_file(self, pref, path): """url for getting a file from a package, with revisions""" assert pref.ref.revision is not None, "_for_package_file needs RREV" assert pref.revision is not None, "_for_package_file needs PREV" return ClientV2Router._format_pref_path(self.routes.package_revision_file, pref, path) def _for_package_files(self, pref): """url for getting the recipe list""" assert pref.revision is not None, "_for_package_files needs PREV" assert pref.ref.revision is not None, "_for_package_files needs RREV" return _format_pref(self.routes.package_revision_files, pref) def _for_recipe_file(self, ref, path): """url for a recipe file, with or without revisions""" assert ref.revision is not None, "for_recipe_file needs RREV" return ClientV2Router._format_ref_path(self.routes.recipe_revision_file, ref, path) def _for_recipe_files(self, ref): """url for getting the recipe list""" assert ref.revision is not None, "for_recipe_files needs RREV" return _format_ref(self.routes.recipe_revision_files, ref) def _for_recipe(self, ref): """url for a recipe with or without revisions (without rev, only for delete the root recipe, or v1)""" return _format_ref(self.routes.recipe_revision, ref) def _for_packages(self, ref): """url for a recipe with or without revisions""" return _format_ref(self.routes.packages_revision, ref) def _for_package(self, pref): """url for the package with or without revisions""" return _format_pref(self.routes.package_revision, pref) @staticmethod def _format_ref_path(url, ref, path): ret = url.format(name=ref.name, version=ref.version, username=ref.user or "_", channel=ref.channel or "_", revision=ref.revision, path=path) return ret @staticmethod def _format_pref_path(url, pref, path): ref = pref.ref return url.format(name=ref.name, version=ref.version, username=ref.user or "_", channel=ref.channel or "_", revision=ref.revision, package_id=pref.package_id, p_revision=pref.revision, path=path) ================================================ FILE: conan/internal/rest/conan_requester.py ================================================ import fnmatch import json import logging import os import platform import requests import urllib3 from jinja2 import Template from requests.adapters import HTTPAdapter from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan import __version__ from conan.internal.loader import load_python_file from conan.internal.errors import scoped_traceback from conan.errors import ConanException # Capture SSL warnings as pointed out here: # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning # TODO: Fix this security warning from conan.internal.util.files import load logging.captureWarnings(True) DEFAULT_TIMEOUT = (30, 60) # connect, read timeouts INFINITE_TIMEOUT = -1 class _SourceURLCredentials: """ Only for sources download (get(), download(), conan config install """ def __init__(self, cache_folder): self._urls = {} self._auth_source_plugin = None if not cache_folder: return auth_source_plugin_path = HomePaths(cache_folder).auth_source_plugin_path self._auth_source_plugin = _load_auth_source_plugin(auth_source_plugin_path) creds_path = os.path.join(cache_folder, "source_credentials.json") if not os.path.exists(creds_path): return def _get_auth(credentials): if "token" in credentials or ("user" in credentials and "password" in credentials): return credentials raise ConanException(f"Unknown credentials method for '{credentials['url']}'") try: template = Template(load(creds_path)) content = template.render({"platform": platform, "os": os}) content = json.loads(content) self._urls = {credentials["url"]: _get_auth(credentials) for credentials in content["credentials"]} except Exception as e: raise ConanException(f"Error loading 'source_credentials.json' {creds_path}: {repr(e)}") def add_auth(self, url, kwargs): # First, try to use "auth_source_plugin" if self._auth_source_plugin: try: c = self._auth_source_plugin(url) except Exception as e: msg = f"Error while processing 'auth_source_remote.py' plugin" msg = scoped_traceback(msg, e, scope="/extensions/plugins") raise ConanException(msg) if c: if c.get("token"): kwargs["headers"]["Authorization"] = f"Bearer {c.get('token')}" if c.get("user") and c.get("password"): kwargs["auth"] = (c.get("user"), c.get("password")) if c.get("headers"): kwargs.setdefault("headers", {}).update(c["headers"]) return # Then, try to find the credentials in "_urls" for u, creds in self._urls.items(): if url.startswith(u): token = creds.get("token") if token: kwargs["headers"]["Authorization"] = f"Bearer {token}" user = creds.get("user") password = creds.get("password") if user and password: kwargs["auth"] = (user, password) headers = creds.get("headers") if headers: kwargs.setdefault("headers", {}).update(headers) break class ConanRequester: def __init__(self, config, cache_folder=None): self._url_creds = _SourceURLCredentials(cache_folder) _max_retries = config.get("core.net.http:max_retries", default=2, check_type=int) self._http_requester = requests.Session() _adapter = HTTPAdapter(max_retries=self._get_retries(_max_retries)) self._http_requester.mount("http://", _adapter) self._http_requester.mount("https://", _adapter) self._timeout = config.get("core.net.http:timeout", default=DEFAULT_TIMEOUT) self._no_proxy_match = config.get("core.net.http:no_proxy_match", check_type=list) self._proxies = config.get("core.net.http:proxies") self._cacert_path = config.get("core.net.http:cacert_path", check_type=str) self._client_certificates = config.get("core.net.http:client_cert") self._clean_system_proxy = config.get("core.net.http:clean_system_proxy", default=False, check_type=bool) platform_info = "; ".join([" ".join([platform.system(), platform.release()]), "Python " + platform.python_version(), platform.machine()]) self._user_agent = "Conan/%s (%s)" % (__version__, platform_info) @staticmethod def _get_retries(max_retries): retry = max_retries if retry == 0: return 0 retry_status_code_set = { requests.codes.internal_server_error, requests.codes.bad_gateway, requests.codes.service_unavailable, requests.codes.gateway_timeout, requests.codes.variant_also_negotiates, requests.codes.insufficient_storage, requests.codes.bandwidth_limit_exceeded } return urllib3.Retry( total=retry, backoff_factor=0.05, status_forcelist=retry_status_code_set ) def _should_skip_proxy(self, url): if self._no_proxy_match: for entry in self._no_proxy_match: if fnmatch.fnmatch(url, entry): return True return False def _add_kwargs(self, url, kwargs): # verify is the kwargs that comes from caller, RestAPI, it is defined in # Conan remote "verify_ssl" source_credentials = kwargs.pop("source_credentials", None) if kwargs.get("verify", None) is not False: # False means de-activate if self._cacert_path is not None: kwargs["verify"] = self._cacert_path kwargs["cert"] = self._client_certificates if self._proxies: if not self._should_skip_proxy(url): kwargs["proxies"] = self._proxies if self._timeout and self._timeout != INFINITE_TIMEOUT: kwargs["timeout"] = self._timeout if not kwargs.get("headers"): kwargs["headers"] = {} if source_credentials: self._url_creds.add_auth(url, kwargs) # Only set User-Agent if none was provided if not kwargs["headers"].get("User-Agent"): kwargs["headers"]["User-Agent"] = self._user_agent return kwargs def get(self, url, **kwargs): return self._call_method("get", url, **kwargs) def head(self, url, **kwargs): return self._call_method("head", url, **kwargs) def put(self, url, **kwargs): return self._call_method("put", url, **kwargs) def delete(self, url, **kwargs): return self._call_method("delete", url, **kwargs) def post(self, url, **kwargs): return self._call_method("post", url, **kwargs) def _call_method(self, method, url, **kwargs): popped = False if self._clean_system_proxy: old_env = dict(os.environ) # Clean the proxies from the environ and use the conan specified proxies for var_name in ("http_proxy", "https_proxy", "ftp_proxy", "all_proxy", "no_proxy"): popped = True if os.environ.pop(var_name, None) else popped popped = True if os.environ.pop(var_name.upper(), None) else popped ConanOutput(scope="HttpRequest").trace(f"{method}: {url}") try: all_kwargs = self._add_kwargs(url, kwargs) tmp = getattr(self._http_requester, method)(url, **all_kwargs) return tmp finally: if popped: os.environ.clear() os.environ.update(old_env) def _load_auth_source_plugin(auth_source_plugin_path): if os.path.exists(auth_source_plugin_path): mod, _ = load_python_file(auth_source_plugin_path) return getattr(mod, "auth_source_plugin", None) ================================================ FILE: conan/internal/rest/download_cache.py ================================================ import hashlib import json import os from contextlib import contextmanager from threading import Lock import fasteners from conan.errors import ConanException from conan.internal.util.dates import timestamp_now from conan.internal.util.files import load, save, remove_if_dirty class DownloadCache: """ The download cache has 3 folders - "s": SOURCE_BACKUP for the files.download(internet_url) backup sources feature - "c": CONAN_CACHE: for caching Conan packages artifacts - "locks": The LOCKS folder containing the file locks for concurrent access to the cache """ _LOCKS = "locks" _SOURCE_BACKUP = "s" _CONAN_CACHE = "c" def __init__(self, path: str): self._path: str = path def source_path(self, sha256): return os.path.join(self._path, self._SOURCE_BACKUP, sha256) def cached_path(self, url): md = hashlib.sha256() md.update(url.encode()) h = md.hexdigest() return os.path.join(self._path, self._CONAN_CACHE, h), h _thread_locks = {} # Needs to be shared among all instances @contextmanager def lock(self, lock_id): lock = os.path.join(self._path, self._LOCKS, lock_id) with fasteners.InterProcessLock(lock): # TODO: Abstract away when necessary for concurrency # Once the process has access, make sure multithread is locked too # as SimpleLock doesn't work multithread thread_lock = self._thread_locks.setdefault(lock, Lock()) thread_lock.acquire() try: yield finally: thread_lock.release() def get_backup_sources_files(self, excluded_urls, package_list=None, only_upload=True): """Get list of backup source files currently present in the cache, either all of them if no package_list is give, or filtered by those belonging to the references in the package_list Will exclude the sources that come from URLs present in excluded_urls @param excluded_urls: a list of URLs to exclude backup sources files if they come from any of these URLs @param package_list: a PackagesList object to filter backup files from (The files should have been downloaded form any of the references in the package_list) @param only_upload: if True, only return the files for packages that are set to be uploaded""" path_backups = os.path.join(self._path, self._SOURCE_BACKUP) if not os.path.exists(path_backups): return [] if excluded_urls is None: excluded_urls = [] def has_excluded_urls(backup_urls): return all(any(url.startswith(excluded_url) for excluded_url in excluded_urls) for url in backup_urls) all_refs = set() if package_list is not None: for ref, packages in package_list.items(): ref_info = package_list.recipe_dict(ref) if (not only_upload or ref_info.get("upload") or any(package_list.package_dict(p).get("upload") for p in packages)): all_refs.add(str(ref)) path_backups_contents = [] dirty_ext = ".dirty" for path in os.listdir(path_backups): if remove_if_dirty(os.path.join(path_backups, path)): continue if path.endswith(dirty_ext): if not os.path.exists(os.path.join(path_backups, os.path.splitext(path)[0])): if os.path.exists(os.path.join(path_backups, path)): os.remove(os.path.join(path_backups, path)) continue if not path.endswith(".json"): path_backups_contents.append(path) files_to_upload = [] for path in path_backups_contents: blob_path = os.path.join(path_backups, path) metadata_path = os.path.join(blob_path + ".json") if not os.path.exists(metadata_path): raise ConanException(f"Missing metadata file for backup source {blob_path}") metadata = json.loads(load(metadata_path)) refs = metadata["references"] for ref, urls in refs.items(): if not has_excluded_urls(urls) and (not only_upload or package_list is None or ref in all_refs): files_to_upload.append(metadata_path) files_to_upload.append(blob_path) break return files_to_upload @staticmethod def update_backup_sources_json(cached_path, conanfile, urls): """ create or update the sha256.json file with the references and new urls used """ summary_path = cached_path + ".json" if os.path.exists(summary_path): summary = json.loads(load(summary_path)) else: summary = {"references": {}, "timestamp": timestamp_now()} try: summary_key = str(conanfile.ref) except AttributeError: # If there's no node associated with the conanfile, # try to construct a reference from the conanfile itself. # We accept it if we have a name and a version at least. if conanfile.name and conanfile.version: user = f"@{conanfile.user}" if conanfile.user else "" channel = f"/{conanfile.channel}" if conanfile.channel else "" summary_key = f"{conanfile.name}/{conanfile.version}{user}{channel}" else: # The recipe path would be different between machines # So best we can do is to set this as unknown summary_key = "unknown" if not isinstance(urls, (list, tuple)): urls = [urls] existing_urls = summary["references"].setdefault(summary_key, []) existing_urls.extend(url for url in urls if url not in existing_urls) conanfile.output.verbose(f"Updating ${summary_path} summary file") summary_dump = json.dumps(summary) conanfile.output.debug(f"New summary: ${summary_dump}") save(summary_path, json.dumps(summary)) ================================================ FILE: conan/internal/rest/file_downloader.py ================================================ import os import re import time from conan.api.output import ConanOutput, TimedOutput from conan.internal.rest import response_to_str from conan.internal.errors import (ConanConnectionError, RequestErrorException, AuthenticationException, ForbiddenException, NotFoundException) from conan.errors import ConanException from conan.internal.util.files import human_size, check_with_algorithm_sum class FileDownloader: def __init__(self, requester, scope=None, source_credentials=None): self._output = ConanOutput(scope=scope) self._requester = requester self._source_credentials = source_credentials def download(self, url, file_path, retry=2, retry_wait=0, verify_ssl=True, auth=None, overwrite=False, headers=None, md5=None, sha1=None, sha256=None): """ in order to make the download concurrent, the folder for file_path MUST exist """ assert file_path, "Conan 2.0 always downloads files to disk, not to memory" assert os.path.isabs(file_path), "Target file_path must be absolute" if os.path.exists(file_path): if overwrite: self._output.warning("file '%s' already exists, overwriting" % file_path) else: # Should not happen, better to raise, probably we had to remove # the dest folder before raise ConanException("Error, the file to download already exists: '%s'" % file_path) try: for counter in range(retry + 1): try: self._download_file(url, auth, headers, file_path, verify_ssl) break except (NotFoundException, ForbiddenException, AuthenticationException, RequestErrorException): raise except ConanException as exc: if counter == retry: raise else: self._output.warning(exc, warn_tag="network") self._output.info(f"Waiting {retry_wait} seconds to retry...") time.sleep(retry_wait) self.check_checksum(file_path, md5, sha1, sha256) self._output.debug(f"Downloaded {file_path} from {url}") except Exception: if os.path.exists(file_path): os.remove(file_path) raise @staticmethod def check_checksum(file_path, md5, sha1, sha256): if md5 is not None: check_with_algorithm_sum("md5", file_path, md5) if sha1 is not None: check_with_algorithm_sum("sha1", file_path, sha1) if sha256 is not None: check_with_algorithm_sum("sha256", file_path, sha256) def _download_file(self, url, auth, headers, file_path, verify_ssl, try_resume=False): if try_resume and os.path.exists(file_path): range_start = os.path.getsize(file_path) headers = headers.copy() if headers else {} headers["range"] = "bytes={}-".format(range_start) else: range_start = 0 try: response = self._requester.get(url, stream=True, verify=verify_ssl, auth=auth, headers=headers, source_credentials=self._source_credentials) except Exception as exc: raise ConanException("Error downloading file %s: '%s'" % (url, exc)) if not response.ok: if response.status_code == 404: raise NotFoundException("Not found: %s" % url) elif response.status_code == 403: if auth is None or (hasattr(auth, "bearer") and auth.bearer is None): # TODO: This is a bit weird, why this conversion? Need to investigate raise AuthenticationException(response_to_str(response)) raise ForbiddenException(response_to_str(response)) elif response.status_code == 401: raise AuthenticationException(response_to_str(response)) raise ConanException("Error %d downloading file %s" % (response.status_code, url)) def get_total_length(): if range_start: content_range = response.headers.get("Content-Range", "") match = re.match(r"^bytes (\d+)-(\d+)/(\d+)", content_range) if not match or range_start != int(match.group(1)): raise ConanException("Error in resumed download from %s\n" "Incorrect Content-Range header %s" % (url, content_range)) return int(match.group(3)) else: total_size = response.headers.get('Content-Length') or len(response.content) return int(total_size) try: total_length = get_total_length() is_large_file = total_length > 10000000 # 10 MB base_name = os.path.basename(file_path) def msg_format(msg, downloaded): perc = int(total_downloaded_size * 100 / total_length) return msg + f" {human_size(downloaded)} {perc}% {base_name}" timed_output = TimedOutput(10, out=self._output, msg_format=msg_format) if is_large_file: hs = human_size(total_length) action = "Downloading" if range_start == 0 else "Continuing download of" self._output.info(f"{action} {hs} {base_name}") chunk_size = 1024 * 100 total_downloaded_size = range_start mode = "ab" if range_start else "wb" with open(file_path, mode) as file_handler: for chunk in response.iter_content(chunk_size): file_handler.write(chunk) total_downloaded_size += len(chunk) if is_large_file: timed_output.info("Downloaded", total_downloaded_size) gzip = (response.headers.get("content-encoding") == "gzip") response.close() # it seems that if gzip we don't know the size, cannot resume and shouldn't raise if total_downloaded_size != total_length and not gzip: if (total_length > total_downloaded_size > range_start and response.headers.get("Accept-Ranges") == "bytes"): self._download_file(url, auth, headers, file_path, verify_ssl, try_resume=True) else: raise ConanException("Transfer interrupted before complete: %s < %s" % (total_downloaded_size, total_length)) except Exception as e: # If this part failed, it means problems with the connection to server raise ConanConnectionError("Download failed, check server, possibly try again\n%s" % str(e)) ================================================ FILE: conan/internal/rest/file_uploader.py ================================================ import io import os import time from conan.api.output import ConanOutput, TimedOutput from conan.internal.rest import response_to_str from conan.internal.errors import InternalErrorException, RequestErrorException, AuthenticationException, \ ForbiddenException, NotFoundException from conan.errors import ConanException from conan.internal.util.files import sha1sum class FileUploader: def __init__(self, requester, verify, config, source_credentials=None): self._requester = requester self._config = config self._verify_ssl = verify self._source_credentials = source_credentials @staticmethod def _handle_400_response(response, auth): if response.status_code == 400: raise RequestErrorException(response_to_str(response)) if response.status_code == 401: raise AuthenticationException(response_to_str(response)) if response.status_code == 403: if auth is None or auth.bearer is None: raise AuthenticationException(response_to_str(response)) raise ForbiddenException(response_to_str(response)) def _dedup(self, url, headers, auth): """ send the headers to see if it is possible to skip uploading the file, because it is already in the server. Artifactory support file deduplication """ dedup_headers = {"X-Checksum-Deploy": "true"} if headers: dedup_headers.update(headers) response = self._requester.put(url, data="", verify=self._verify_ssl, headers=dedup_headers, auth=auth, source_credentials=self._source_credentials) if response.status_code == 500: raise InternalErrorException(response_to_str(response)) self._handle_400_response(response, auth) if response.status_code == 201: # Artifactory returns 201 if the file is there return response def exists(self, url, auth): response = self._requester.head(url, verify=self._verify_ssl, auth=auth, source_credentials=self._source_credentials) return bool(response.ok) def upload(self, url, abs_path, auth=None, dedup=False, retry=None, retry_wait=None, ref=None): retry = retry if retry is not None else self._config.get("core.upload:retry", default=1, check_type=int) retry_wait = retry_wait if retry_wait is not None else \ self._config.get("core.upload:retry_wait", default=5, check_type=int) # Send always the header with the Sha1 headers = {"X-Checksum-Sha1": sha1sum(abs_path)} if dedup: response = self._dedup(url, headers, auth) if response: return response for counter in range(retry + 1): try: return self._upload_file(url, abs_path, headers, auth, ref) except (NotFoundException, ForbiddenException, AuthenticationException, RequestErrorException): raise except ConanException as exc: if counter == retry: raise else: ConanOutput().warning(exc, warn_tag="network") ConanOutput().info("Waiting %d seconds to retry..." % retry_wait) time.sleep(retry_wait) def _upload_file(self, url, abs_path, headers, auth, ref): with FileProgress(abs_path, mode='rb', msg=f"{ref}: Uploading") as file_handler: try: response = self._requester.put(url, data=file_handler, verify=self._verify_ssl, headers=headers, auth=auth, source_credentials=self._source_credentials) self._handle_400_response(response, auth) response.raise_for_status() # Raise HTTPError for bad http response status return response except ConanException: raise except Exception as exc: raise ConanException(exc) class FileProgress(io.FileIO): def __init__(self, path: str, msg: str = "Uploading", interval: float = 10, *args, **kwargs): super().__init__(path, *args, **kwargs) self._size = os.path.getsize(path) self._filename = os.path.basename(path) # Report only on big sizes (>100MB) self._reporter = TimedOutput(interval=interval) if self._size > 100_000_000 else None self._bytes_read = 0 self.msg = msg def read(self, size: int = -1) -> bytes: block = super().read(size) self._bytes_read += len(block) if self._reporter: current_percentage = int(self._bytes_read * 100.0 / self._size) if self._size != 0 else 0 self._reporter.info(f"{self.msg} {self._filename}: {current_percentage}%") return block ================================================ FILE: conan/internal/rest/pkg_sign.py ================================================ import os import json from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.cache.conan_reference_layout import METADATA from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file from conan.internal.util.files import load, mkdir, save, sha256sum PKGSIGN_MANIFEST = "pkgsign-manifest.json" PKGSIGN_SIGNATURES = "pkgsign-signatures.json" def _save_manifest(artifacts_folder, signature_folder): """ Creates the summary content as a dictionary for manipulation. Returns a structure like: { "files": [ {"file": "conan_package.tgz", "sha256": "abc123"}, {"file": "other_file.bin", "sha256": "fff999"}, ... ] } """ files_list = [] # Sort files by filename to ensure consistent order for fname in sorted(os.listdir(artifacts_folder)): file_path = os.path.join(artifacts_folder, fname) if os.path.isfile(file_path): entry = { "file": fname, "sha256": sha256sum(file_path) } files_list.append(entry) save(os.path.join(signature_folder, PKGSIGN_MANIFEST), json.dumps({"files": files_list}, indent=2)) def _save_signatures(signature_folder, signatures): """ Saves the content of signatures file in the signature folder :param signature_folder: Signature folder path :param signatures: dict of {filename: signature_value} """ for signature in signatures: for key in ["method", "provider", "sign_artifacts"]: if not signature.get(key): raise ConanException(f"[Package sign] Signature '{key}' is missing in signature data") if not isinstance(signature.get("sign_artifacts"), dict): raise ConanException("[Package sign] Signature 'sign_artifacts' must be a dict of " "{name: filename}") content = { "signatures": signatures } save(os.path.join(signature_folder, PKGSIGN_SIGNATURES), json.dumps(content, indent=2)) def _verify_files_checksums(signature_folder, files): """ Verifies that the files' checksums match those stored in the summary. :param signature_folder: Signature folder path :param files: dict of {filename: filepath} of files in artifact folder to verify """ if not os.path.isfile(os.path.join(signature_folder, PKGSIGN_MANIFEST)): ConanOutput().warning(f"[Package sign] Manifest file '{PKGSIGN_MANIFEST}' does not exist in " f"signature folder '{signature_folder}'. Please update your plugin " f"according to the docs at https://docs.conan.io/2/reference/extensions/package_signing.html" f". Skipping checksum verification!", warn_tag="deprecated") return manifest_content = load(os.path.join(signature_folder, PKGSIGN_MANIFEST)) expected_list = json.loads(manifest_content).get("files", []) expected_files = {item["file"]: item["sha256"] for item in expected_list} # This is checking that the files of the package exist in the manifest instead of the opposite # because some files might be missing such as conan_sources.tgz for filename, file_path in files.items(): expected_checksum = expected_files.get(filename) actual_checksum = sha256sum(file_path) if actual_checksum != expected_checksum: raise ConanException( f"[Package sign] Checksum mismatch for file {filename}: " f"expected {expected_checksum}, got {actual_checksum}." ) else: ConanOutput().info(f"[Package sign] Checksum verified for file {filename} ({actual_checksum}).") class PkgSignaturesPlugin: def __init__(self, cache, home_folder): self._cache = cache signer = HomePaths(home_folder).sign_plugin_path if os.path.isfile(signer): mod, _ = load_python_file(signer) self._plugin_sign_function = getattr(mod, "sign", None) self._plugin_verify_function = getattr(mod, "verify", None) else: self._plugin_sign_function = self._plugin_verify_function = None @property def is_sign_configured(self): return self._plugin_sign_function is not None @property def is_verify_configured(self): return self._plugin_verify_function is not None def sign_pkg(self, ref, files, folder): metadata_sign = os.path.join(folder, METADATA, "sign") mkdir(metadata_sign) # Generate the package sign manifest before calling the plugin _save_manifest(folder, metadata_sign) signatures = self._plugin_sign_function(ref, artifacts_folder=folder, signature_folder=metadata_sign) if isinstance(signatures, list): # Save signatures file with the plugin's returned signatures data if signatures: _save_signatures(metadata_sign, signatures) else: # Fallback to old behavior (plugin sign() returns None) ConanOutput().warning("[Package sign] The signature plugin sign() function must return " "a list of signature dicts. See the documentation at " "https://docs.conan.io/2/reference/extensions/package_signing.html", warn_tag="deprecated") def sign(self, upload_data): if not self.is_sign_configured: return for rref, packages in upload_data.items(): recipe_bundle = upload_data.recipe_dict(rref) if recipe_bundle["upload"]: self.sign_pkg(rref, recipe_bundle["files"], self._cache.recipe_layout(rref).download_export()) for pref in packages: pkg_bundle = upload_data.package_dict(pref) if pkg_bundle["upload"]: self.sign_pkg(pref, pkg_bundle["files"], self._cache.pkg_layout(pref).download_package()) def verify(self, ref, folder, metadata_folder, files): if self._plugin_verify_function is None: return metadata_sign = os.path.join(metadata_folder, "sign") _verify_files_checksums(metadata_sign, files) # Verify package files checksums before calling the plugin self._plugin_verify_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, files=files) ================================================ FILE: conan/internal/rest/remote_credentials.py ================================================ import json import os import platform from jinja2 import Template from conan.api.input import UserInput from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file from conan.api.output import ConanOutput from conan.internal.errors import scoped_traceback from conan.errors import ConanException from conan.internal.util.files import load class RemoteCredentials: def __init__(self, cache_folder, global_conf): self._global_conf = global_conf self._urls = {} auth_plugin_path = HomePaths(cache_folder).auth_remote_plugin_path self._auth_remote_plugin = _load_auth_remote_plugin(auth_plugin_path) creds_path = os.path.join(cache_folder, "credentials.json") if not os.path.exists(creds_path): return try: template = Template(load(creds_path)) content = template.render({"platform": platform, "os": os}) content = json.loads(content) self._urls = {credentials["remote"]: {"user": credentials["user"], "password": credentials["password"]} for credentials in content["credentials"]} except Exception as e: raise ConanException(f"Error loading 'credentials.json' {creds_path}: {repr(e)}") def auth(self, remote, user=None): # First get the auth_remote_plugin if self._auth_remote_plugin is not None: try: plugin_user, plugin_password = self._auth_remote_plugin(remote, user=user) except Exception as e: msg = f"Error while processing 'auth_remote.py' plugin" msg = scoped_traceback(msg, e, scope="/extensions/plugins") raise ConanException(msg) if plugin_user and plugin_password: return plugin_user, plugin_password, False # Then prioritize the cache "credentials.json" file creds = self._urls.get(remote.name) if creds is not None: try: return creds["user"], creds["password"], False except KeyError as e: raise ConanException(f"Authentication error, wrong credentials.json: {e}") # Then, check environment definition env_user, env_passwd = self._get_env(remote.name, user) if env_passwd is not None: if env_user is None: raise ConanException("Found password in env-var, but not defined user") return env_user, env_passwd, False # If not found, then interactive prompt ui = UserInput(self._global_conf.get("core:non_interactive", check_type=bool)) input_user, input_password = ui.request_login(remote.name, user) return input_user, input_password, True @staticmethod def _get_env(remote, user): """ Try get creds from env-vars """ remote = remote.replace("-", "_").upper() if user is None: user = os.getenv(f"CONAN_LOGIN_USERNAME_{remote}") or os.getenv("CONAN_LOGIN_USERNAME") if user: ConanOutput().info("Got username '%s' from environment" % user) passwd = os.getenv(f"CONAN_PASSWORD_{remote}") or os.getenv("CONAN_PASSWORD") if passwd: ConanOutput().info("Got password '******' from environment") return user, passwd def _load_auth_remote_plugin(auth_remote_plugin_path): if os.path.exists(auth_remote_plugin_path): mod, _ = load_python_file(auth_remote_plugin_path) return getattr(mod, "auth_remote_plugin", None) ================================================ FILE: conan/internal/rest/remote_manager.py ================================================ import os import shutil import sys from collections import namedtuple from typing import List from requests.exceptions import ConnectionError from conan.api.model import LOCAL_RECIPES_INDEX from conan.internal.paths import CONANINFO, CONAN_MANIFEST, PACKAGE_FILE_NAME, EXPORT_FILE_NAME from conan.internal.rest.rest_client_local_recipe_index import RestApiClientLocalRecipesIndex from conan.api.model import Remote from conan.api.output import ConanOutput from conan.internal.cache.conan_reference_layout import METADATA from conan.internal.rest.pkg_sign import PkgSignaturesPlugin from conan.internal.errors import ConanConnectionError, NotFoundException, PackageNotFoundException from conan.errors import ConanException from conan.internal.model.info import load_binary_info from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.files import rmdir, human_size from conan.internal.util.files import mkdir, tar_extract class RemoteManager: """ Will handle the remotes to get recipes, packages etc """ _ErrorMsg = namedtuple("ErrorMsg", ["message"]) def __init__(self, cache, auth_manager, home_folder): self._cache = cache self._auth_manager = auth_manager self._signer = PkgSignaturesPlugin(cache, home_folder) self._home_folder = home_folder def _local_folder_remote(self, remote): if remote.remote_type == LOCAL_RECIPES_INDEX: return RestApiClientLocalRecipesIndex(remote, self._home_folder) def check_credentials(self, remote, force_auth=False): self._call_remote(remote, "check_credentials", force_auth) def upload_recipe(self, ref, files_to_upload, remote): assert isinstance(ref, RecipeReference) assert ref.revision, "upload_recipe requires RREV" remote.invalidate_cache() self._call_remote(remote, "upload_recipe", ref, files_to_upload) def upload_package(self, pref, files_to_upload, remote): assert pref.ref.revision, "upload_package requires RREV" assert pref.revision, "upload_package requires PREV" remote.invalidate_cache() self._call_remote(remote, "upload_package", pref, files_to_upload) def get_recipe(self, ref, remote, metadata=None): assert ref.revision, "get_recipe without revision specified" assert ref.timestamp, "get_recipe without ref.timestamp specified" # This fails if there is a DB entry for this ref layout = self._cache.create_ref_layout(ref) export_folder = layout.export() local_folder_remote = self._local_folder_remote(remote) if local_folder_remote is not None: local_folder_remote.get_recipe(ref, export_folder) else: self._download_recipe(layout, ref, remote, metadata) # Make sure that the source dir is deleted, but it seems unnecessary rmdir(layout.source()) mkdir(layout.metadata()) return layout def _download_recipe(self, layout, ref, remote, metadata): download_export = layout.download_export() try: zipped_files = self._call_remote(remote, "get_recipe", ref, download_export, metadata, only_metadata=False) # The timestamp of the ``ref`` from the server has been already obtained by ConanProxy # or it will be obtained explicitly by the ``conan download`` # filter metadata files # This could be also optimized in download, avoiding downloading them, for performance zipped_files = {k: v for k, v in zipped_files.items() if not k.startswith(METADATA)} # quick server package integrity check: if "conanfile.py" not in zipped_files: raise ConanException(f"Corrupted {ref} in '{remote.name}' remote: no conanfile.py") if "conanmanifest.txt" not in zipped_files: raise ConanException(f"Corrupted {ref} in '{remote.name}' remote: " f"no conanmanifest.txt") self._signer.verify(ref, download_export, layout.metadata(), files=zipped_files) except BaseException: # So KeyboardInterrupt also cleans things ConanOutput(scope=str(ref)).error(f"Error downloading from remote '{remote.name}'", error_type="exception") self._cache.remove_recipe_layout(layout) raise export_folder = layout.export() export_file = next((f for f in zipped_files if f.startswith(EXPORT_FILE_NAME)), None) tgz_file = zipped_files.pop(export_file, None) if tgz_file: uncompress_file(tgz_file, export_folder, scope=str(ref)) mkdir(export_folder) for file_name, file_path in zipped_files.items(): # copy CONANFILE shutil.move(file_path, os.path.join(export_folder, file_name)) def get_recipe_metadata(self, recipe_layout, ref, remote, metadata): """ Get only the metadata for a locally existing recipe in Cache """ assert ref.revision, "get_recipe without revision specified" output = ConanOutput(scope=str(ref)) output.info("Retrieving recipe metadata from remote '%s' " % remote.name) download_export = recipe_layout.download_export() try: self._call_remote(remote, "get_recipe", ref, download_export, metadata, only_metadata=True) except BaseException: # So KeyboardInterrupt also cleans things output.error(f"Error downloading metadata from remote '{remote.name}'", error_type="exception") raise def get_recipe_sources(self, ref, layout, remote): assert ref.revision, "get_recipe_sources requires RREV" download_folder = layout.download_export() export_sources_folder = layout.export_sources() local_folder_remote = self._local_folder_remote(remote) if local_folder_remote is not None: local_folder_remote.get_recipe_sources(ref, export_sources_folder) else: zipped_files = self._call_remote(remote, "get_recipe_sources", ref, download_folder) if not zipped_files: mkdir(export_sources_folder) # create the folder even if no source files else: self._signer.verify(ref, download_folder, layout.metadata(), files=zipped_files) # Only 1 file is guaranteed tgz_file = next(iter(zipped_files.values())) uncompress_file(tgz_file, export_sources_folder, scope=str(ref)) def get_package(self, pref, remote, metadata=None): output = ConanOutput(scope=str(pref.ref)) output.info("Retrieving package %s from remote '%s' " % (pref.package_id, remote.name)) assert pref.revision is not None pkg_layout = self._cache.create_pkg_layout(pref) with pkg_layout.set_dirty_context_manager(): mkdir(pkg_layout.metadata()) self._get_package(pkg_layout, pref, remote, output, metadata) def get_package_metadata(self, pref, remote, metadata): """ only download the metadata, not the packge itself """ output = ConanOutput(scope=str(pref.ref)) output.info("Retrieving package metadata %s from remote '%s' " % (pref.package_id, remote.name)) assert pref.revision is not None pkg_layout = self._cache.pkg_layout(pref) try: download_pkg_folder = pkg_layout.download_package() self._call_remote(remote, "get_package", pref, download_pkg_folder, metadata, only_metadata=True) except BaseException as e: # So KeyboardInterrupt also cleans things output.error(f"Exception while getting package metadata: {str(pref.package_id)}", error_type="exception") output.error(f"Exception: {type(e)} {str(e)}", error_type="exception") raise def _get_package(self, layout, pref, remote, scoped_output, metadata): try: assert pref.revision is not None if remote.recipes_only: raise NotFoundException(f"Remote '{remote.name}' doesn't allow binary downloads") download_pkg_folder = layout.download_package() # Download files to the pkg_tgz folder, not to the final one zipped_files = self._call_remote(remote, "get_package", pref, download_pkg_folder, metadata, only_metadata=False) zipped_files = {k: v for k, v in zipped_files.items() if not k.startswith(METADATA)} # quick server package integrity check: for f in (CONANINFO, CONAN_MANIFEST): if f not in zipped_files: raise ConanException(f"Corrupted {pref} in '{remote.name}' remote: no {f}") # This is guaranteed to exists, otherwise RestClient would have raised already package_file = next(f for f in zipped_files if PACKAGE_FILE_NAME in f) self._signer.verify(pref, download_pkg_folder, layout.metadata(), zipped_files) tgz_file = zipped_files.pop(package_file) package_folder = layout.package() uncompress_file(tgz_file, package_folder, scope=str(pref.ref)) mkdir(package_folder) # Just in case it doesn't exist, because uncompress did nothing for file_name, file_path in zipped_files.items(): # copy CONANINFO and CONANMANIFEST shutil.move(file_path, os.path.join(package_folder, file_name)) scoped_output.success('Package installed %s' % pref.package_id) scoped_output.info("Downloaded package revision %s" % pref.revision) except NotFoundException: raise PackageNotFoundException(pref) except BaseException as e: # So KeyboardInterrupt also cleans things self._cache.remove_package_layout(layout) scoped_output.error(f"Exception while getting package: {str(pref.package_id)}", error_type="exception") scoped_output.error(f"Exception: {type(e)} {str(e)}", error_type="exception") raise def search_recipes(self, remote, pattern): # Used by ListAPI to "conan list *" recipes, and by RangeResolver to resolve version-ranges cached_method = remote._caching.setdefault("search_recipes", {}) try: return cached_method[pattern] except KeyError: result = self._call_remote(remote, "search", pattern) cached_method[pattern] = result return result def search_packages(self, remote, ref, list_only=False): # Used only by ListAPI to list the different package_ids for a reference if remote.recipes_only: return {} packages = self._call_remote(remote, "search_packages", ref, list_only) if list_only: packages = {PkgReference(ref, pid): None for pid, data in packages.items()} else: # Avoid serializing conaninfo in server side packages = {PkgReference(ref, pid): load_binary_info(data["content"]) if "content" in data else data for pid, data in packages.items() if not data.get("recipe_hash")} return packages def remove_recipe(self, ref, remote): remote.invalidate_cache() return self._call_remote(remote, "remove_recipe", ref) def remove_packages(self, prefs, remote): remote.invalidate_cache() return self._call_remote(remote, "remove_packages", prefs) def remove_all_packages(self, ref, remote): remote.invalidate_cache() return self._call_remote(remote, "remove_all_packages", ref) def authenticate(self, remote, name, password): return self._call_remote(remote, 'authenticate', name, password, enforce_disabled=False) def get_recipe_revisions(self, ref: RecipeReference, remote: Remote) -> List[RecipeReference]: # Used by ListAPI to list recipe revisions for a ref without revision # and by ConanProxy resolving legacy_update Conan 1 logic assert ref.revision is None, "get_recipe_revisions_references of a reference with revision" return self._call_remote(remote, "get_recipe_revisions_references", ref) def get_recipe_revision(self, ref: RecipeReference, remote: Remote) -> RecipeReference: # Used by UploadUpstreamChecker to see if the revision exist in the server # Used by Download, to get timestamp from server and respect it # Used by ConanProxy to confirm existence of specific revision assert ref.revision is not None, "recipe_exists needs a revision" return self._call_remote(remote, "get_recipe_revision_reference", ref) def get_latest_recipe_revision(self, ref: RecipeReference, remote: Remote) -> RecipeReference: # Used by ListAPI to retrieve the latest revision # Used by ConanProxy to resolve to the latest revision assert ref.revision is None, "get_latest_recipe_reference of a reference with revision" return self._call_remote(remote, "get_latest_recipe_reference", ref) def get_package_revisions(self, pref: PkgReference, remote: Remote) -> List[PkgReference]: # Used by ListAPI to retrieve multiple package revisions assert pref.revision is None, "get_package_revisions_references of a reference with revision" if remote.recipes_only: return [] return self._call_remote(remote, "get_package_revisions_references", pref) def get_package_revision(self, pref: PkgReference, remote: Remote) -> PkgReference: # Used by UploadUpstreamChecker to see if the revision exist in the server # Used by Download, to get timestamp from server and respect it assert pref.revision is not None, "get_package_revision_reference needs a revision" return self._call_remote(remote, "get_package_revision_reference", pref) def get_latest_package_revision(self, pref: PkgReference, remote: Remote, info=None) -> PkgReference: # Used by List to resolve the latest package revision # Used by GraphBinariesAnalyzer to resolve to latest package revision assert pref.revision is None, "get_latest_package_reference of a reference with revision" if remote.recipes_only: raise NotFoundException(f"Remote '{remote.name}' doesn't allow binary downloads") # These headers are useful to know what configurations are being requested in the server headers = None if info: headers = {} settings = [f'{k}={v}' for k, v in info.settings.items()] if settings: headers['Conan-PkgID-Settings'] = ';'.join(settings) options = [f'{k}={v}' for k, v in info.options.serialize().items() if k in ("shared", "fPIC", "header_only")] if options: headers['Conan-PkgID-Options'] = ';'.join(options) cached_method = remote._caching.setdefault("get_latest_package_reference", {}) try: result = cached_method[pref] except KeyError: try: result = self._call_remote(remote, "get_latest_package_reference", pref, headers=headers) cached_method[pref] = result return result except NotFoundException as e: # Let's avoid leaking memory by saving all the exception objects, # which translates to a ~2x memory increase. Now, it only saves the type and the # final message. For now, let's cache only the NotFoundException one. cached_method[pref] = self._ErrorMsg(str(e)) raise e else: if isinstance(result, self._ErrorMsg): # Let's raise it raise NotFoundException(result.message) return result def _call_remote(self, remote, method, *args, **kwargs): assert (isinstance(remote, Remote)) enforce_disabled = kwargs.pop("enforce_disabled", True) if remote.disabled and enforce_disabled: raise ConanException("Remote '%s' is disabled" % remote.name) local_folder_remote = self._local_folder_remote(remote) try: if local_folder_remote is not None: return local_folder_remote.call_method(method, *args, **kwargs) return self._auth_manager.call_rest_api_method(remote, method, *args, **kwargs) except ConnectionError as exc: raise ConanConnectionError(("%s\n\nUnable to connect to remote %s=%s\n" "1. Make sure the remote is reachable or,\n" "2. Disable it with 'conan remote disable ' or,\n" "3. Use the '-nr/--no-remote' argument\n" "Then try again." ) % (str(exc), remote.name, remote.url)) except ConanException as exc: exc.remote = remote raise except Exception as exc: raise ConanException(exc, remote=remote) def uncompress_file(src_path, dest_folder, scope=None): if sys.version_info.minor < 14 and src_path.endswith("zst"): raise ConanException(f"File {os.path.basename(src_path)} compressed with 'zst', " f"unsupported for Python<3.14 ") try: filesize = os.path.getsize(src_path) big_file = filesize > 10000000 # 10 MB if big_file: hs = human_size(filesize) ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}") with open(src_path, mode='rb') as file_handler: tar_extract(file_handler, dest_folder) except Exception as e: error_msg = "Error while extracting downloaded file '%s' to %s\n%s\n"\ % (src_path, dest_folder, str(e)) # try to remove the files try: if os.path.exists(dest_folder): shutil.rmtree(dest_folder) error_msg += "Folder removed" except Exception: error_msg += "Folder not removed, files/package might be damaged, remove manually" raise ConanException(error_msg) ================================================ FILE: conan/internal/rest/rest_client.py ================================================ from conan.internal import REVISIONS from conan.internal.rest.rest_client_v2 import RestV2Methods from conan.errors import ConanException CHECKSUM_DEPLOY = "checksum_deploy" # capability class RestApiClient: """ Rest Api Client for handle remote. """ def __init__(self, remote, token, requester, config): self._token = token self._remote_url = remote.url self._requester = requester self._verify_ssl = remote.verify_ssl self._config = config self._remote = remote def _capable(self, capability): # Caching of capabilities per-remote capabilities = getattr(self._remote, "_capabilities", None) if capabilities is None: tmp = RestV2Methods(self._remote_url, self._token, self._requester, self._config, self._verify_ssl) capabilities = tmp.server_capabilities() setattr(self._remote, "_capabilities", capabilities) return capability in capabilities def _get_api(self): revisions = self._capable(REVISIONS) if not revisions: raise ConanException("The remote doesn't support revisions. " "Conan 2.0 is no longer compatible with " "remotes that don't accept revisions.") checksum_deploy = self._capable(CHECKSUM_DEPLOY) return RestV2Methods(self._remote_url, self._token, self._requester, self._config, self._verify_ssl, checksum_deploy) def get_recipe(self, ref, dest_folder, metadata, only_metadata): return self._get_api().get_recipe(ref, dest_folder, metadata, only_metadata) def get_recipe_sources(self, ref, dest_folder): return self._get_api().get_recipe_sources(ref, dest_folder) def get_package(self, pref, dest_folder, metadata, only_metadata): return self._get_api().get_package(pref, dest_folder, metadata, only_metadata) def upload_recipe(self, ref, files_to_upload): return self._get_api().upload_recipe(ref, files_to_upload) def upload_package(self, pref, files_to_upload): return self._get_api().upload_package(pref, files_to_upload) def authenticate(self, user, password): # BYPASS capabilities, in case v1/ping is protected api_v2 = RestV2Methods(self._remote_url, self._token, self._requester, self._config, self._verify_ssl) token = api_v2.authenticate(user, password) return token def check_credentials(self, force_auth=False): return self._get_api().check_credentials(force_auth) def search(self, pattern=None, ignorecase=True): return self._get_api().search(pattern, ignorecase) def search_packages(self, reference, list_only=False): return self._get_api().search_packages(reference, list_only) def remove_recipe(self, ref): return self._get_api().remove_recipe(ref) def remove_all_packages(self, ref): return self._get_api().remove_all_packages(ref) def remove_packages(self, prefs): return self._get_api().remove_packages(prefs) def get_recipe_revisions_references(self, ref): return self._get_api().get_recipe_revisions_references(ref) def get_package_revisions_references(self, pref): return self._get_api().get_package_revisions_references(pref) def get_latest_recipe_reference(self, ref): return self._get_api().get_latest_recipe_reference(ref) def get_latest_package_reference(self, pref, headers): return self._get_api().get_latest_package_reference(pref, headers=headers) def get_recipe_revision_reference(self, ref): return self._get_api().get_recipe_revision_reference(ref) def get_package_revision_reference(self, pref): return self._get_api().get_package_revision_reference(pref) ================================================ FILE: conan/internal/rest/rest_client_local_recipe_index.py ================================================ import os import sys import textwrap from fnmatch import fnmatch from io import StringIO import yaml from conan.api.model import LOCAL_RECIPES_INDEX from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan.internal.api.export import cmd_export from conan.internal.hook_manager import HookManager from conan.internal.loader import ConanFileLoader from conan.internal.errors import ConanReferenceDoesNotExistInDB, RecipeNotFoundException, \ PackageNotFoundException from conan.errors import ConanException from conan.internal.model.conf import ConfDefinition from conan.api.model import RecipeReference from conan.internal.util.files import load, save, rmdir, copytree_compat def add_local_recipes_index_remote(home_folder, remote): if remote.remote_type != LOCAL_RECIPES_INDEX: return local_recipes_index_path = HomePaths(home_folder).local_recipes_index_path repo_folder = os.path.join(local_recipes_index_path, remote.name) output = ConanOutput() if os.path.exists(repo_folder): output.warning(f"The cache folder for remote {remote.name} existed, removing it") rmdir(repo_folder) cache_path = os.path.join(repo_folder, ".conan") hook_folder = HomePaths(cache_path).hooks_path trim_hook = os.path.join(hook_folder, "hook_trim_conandata.py") hook_content = textwrap.dedent("""\ from conan.tools.files import trim_conandata def post_export(conanfile): if conanfile.conan_data: trim_conandata(conanfile) """) save(trim_hook, hook_content) def remove_local_recipes_index_remote(home_folder, remote): if remote.remote_type == LOCAL_RECIPES_INDEX: local_recipes_index_path = HomePaths(home_folder).local_recipes_index_path local_recipes_index_path = os.path.join(local_recipes_index_path, remote.name) ConanOutput().info(f"Removing temporary files for '{remote.name}' " f"local-recipes-index remote") rmdir(local_recipes_index_path) class RestApiClientLocalRecipesIndex: """ Implements the RestAPI but instead of over HTTP for a remote server, using just a local folder assuming the conan-center-index repo layout """ def __init__(self, remote, home_folder): self._remote = remote local_recipes_index_path = HomePaths(home_folder).local_recipes_index_path local_recipes_index_path = os.path.join(local_recipes_index_path, remote.name, ".conan") repo_folder = self._remote.url from conan.internal.conan_app import LocalRecipesIndexApp self._app = LocalRecipesIndexApp(local_recipes_index_path) self._hook_manager = HookManager(HomePaths(local_recipes_index_path).hooks_path) self._layout = _LocalRecipesIndexLayout(repo_folder) def call_method(self, method_name, *args, **kwargs): return getattr(self, method_name)(*args, **kwargs) def get_recipe(self, ref, dest_folder): export_folder = self._app.cache.recipe_layout(ref).export() return self._copy_files(export_folder, dest_folder) def get_recipe_sources(self, ref, dest_folder): try: export_sources = self._app.cache.recipe_layout(ref).export_sources() except ConanReferenceDoesNotExistInDB: # This can happen when there a local-recipes-index is being queried for sources it # doesn't contain # If that is the case, check if they are in the repo, try to export exported_ref = self._export_recipe(ref) # This will raise NotFound if non existing export_sources = self._app.cache.recipe_layout(exported_ref).export_sources() return self._copy_files(export_sources, dest_folder) def get_package(self, pref, dest_folder, metadata, only_metadata): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support " "binary packages") def upload_recipe(self, ref, files_to_upload): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support upload") def upload_package(self, pref, files_to_upload): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support upload") def authenticate(self, user, password): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support " "authentication") def check_credentials(self, force_auth=False): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support upload") def search(self, pattern=None): return self._layout.get_recipes_references(pattern) def search_packages(self, reference, _=False): assert self and reference return {} def remove_recipe(self, ref): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support remove") def remove_all_packages(self, ref): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support remove") def remove_packages(self, prefs): raise ConanException(f"Remote local-recipes-index '{self._remote.name}' doesn't support remove") def get_recipe_revisions_references(self, ref): ref = self._export_recipe(ref) return [ref] def get_package_revisions_references(self, pref): raise PackageNotFoundException(pref) def get_latest_recipe_reference(self, ref): ref = self._export_recipe(ref) return ref def get_latest_package_reference(self, pref, headers): raise PackageNotFoundException(pref) def get_recipe_revision_reference(self, ref): new_ref = self._export_recipe(ref) return new_ref def get_package_revision_reference(self, pref): raise PackageNotFoundException(pref) # Helper methods to implement the interface def _export_recipe(self, ref): folder = self._layout.get_recipe_folder(ref) conanfile_path = os.path.join(folder, "conanfile.py") original_stderr = sys.stderr sys.stderr = StringIO() try: global_conf = ConfDefinition() new_ref, _ = cmd_export(self._app.loader, self._app.cache, self._hook_manager, global_conf, conanfile_path, ref.name, str(ref.version), None, None, remotes=[self._remote]) except Exception as e: raise ConanException(f"Error while exporting recipe from remote: {self._remote.name}\n" f"{str(e)}") finally: export_err = sys.stderr.getvalue() sys.stderr = original_stderr ConanOutput(scope="local-recipes-index").debug(f"Internal export for {ref}:\n" f"{textwrap.indent(export_err, ' ')}") if new_ref.user != ref.user or new_ref.channel != ref.channel: raise RecipeNotFoundException(ref) if ref.revision is not None and new_ref.revision != ref.revision: ConanOutput().warning(f"A specific revision '{ref.repr_notime()}' was requested, but it " "doesn't match the current available revision in source. The " "local-recipes-index can't provide a specific revision") raise RecipeNotFoundException(ref) return new_ref @staticmethod def _copy_files(source_folder, dest_folder): if not os.path.exists(source_folder): return {} copytree_compat(source_folder, dest_folder) ret = {} for root, _, _files in os.walk(dest_folder): for _f in _files: rel = os.path.relpath(os.path.join(root, _f), dest_folder) ret[rel] = os.path.join(dest_folder, root, _f) return ret class _LocalRecipesIndexLayout: def __init__(self, base_folder): self._base_folder = base_folder def _get_base_folder(self, recipe_name): return os.path.join(self._base_folder, "recipes", recipe_name) @staticmethod def _load_config_yml(folder): config = os.path.join(folder, "config.yml") if not os.path.isfile(config): return None return yaml.safe_load(load(config)) def get_recipes_references(self, pattern): name_pattern = pattern.split("/", 1)[0] recipes_dir = os.path.join(self._base_folder, "recipes") if not os.path.isdir(recipes_dir): raise ConnectionError("Cannot connect to 'local-recipes-index' repository, missing " f"'recipes' folder: {recipes_dir}") recipes = os.listdir(recipes_dir) recipes.sort() ret = [] excluded = set() original_pattern = pattern try: pattern_ref = RecipeReference.loads(pattern) # We don't care about user/channel for now, we'll check for those below, # just add all candidates for now pattern = f"{pattern_ref.name}/{pattern_ref.version}" except: # pattern = pattern pattern = f"{name_pattern}/*" original_pattern = pattern pass loader = ConanFileLoader(None) for r in recipes: if r.startswith("."): # Skip hidden folders, no recipes should start with a dot continue if not fnmatch(r, name_pattern): continue folder = self._get_base_folder(r) config_yml = self._load_config_yml(folder) if config_yml is None: raise ConanException(f"Corrupted repo, folder {r} without 'config.yml'") versions = config_yml["versions"] for v in versions: # TODO: Check the search pattern is the same as remotes and cache ref = f"{r}/{v}" if not fnmatch(ref, pattern): continue subfolder = versions[v]["folder"] # This check can be removed after compatibility with 2.0 conanfile = os.path.join(recipes_dir, r, subfolder, "conanfile.py") conanfile_content = load(conanfile) if "from conans" in conanfile_content or "import conans" in conanfile_content: excluded.add(r) continue ref = RecipeReference.loads(ref) try: recipe = loader.load_basic(conanfile) ref.user = recipe.user ref.channel = recipe.channel except Exception as e: ConanOutput().warning(f"Couldn't load recipe {conanfile}: {e}") if ref.matches(original_pattern, None): # Check again the pattern with the user/channel ret.append(ref) if excluded: ConanOutput().warning(f"Excluding recipes not Conan 2.0 ready: {', '.join(excluded)}") return ret def get_recipe_folder(self, ref): folder = self._get_base_folder(ref.name) data = self._load_config_yml(folder) if data is None: raise RecipeNotFoundException(ref) versions = data["versions"] if str(ref.version) not in versions: raise RecipeNotFoundException(ref) subfolder = versions[str(ref.version)]["folder"] return os.path.join(folder, subfolder) ================================================ FILE: conan/internal/rest/rest_client_v2.py ================================================ import copy import fnmatch import hashlib import json import os from threading import Thread from requests.auth import AuthBase, HTTPBasicAuth from uuid import getnode as get_mac from conan.api.output import ConanOutput from conan.internal.paths import EXPORT_SOURCES_FILE_NAME, CONANINFO, CONAN_MANIFEST, \ EXPORT_FILE_NAME, PACKAGE_FILE_NAME from conan.internal.rest.caching_file_downloader import ConanInternalCacheDownloader from conan.internal.rest import response_to_str from conan.internal.rest.client_routes import ClientV2Router from conan.internal.rest.file_uploader import FileUploader from conan.internal.errors import AuthenticationException, ForbiddenException, NotFoundException, \ RecipeNotFoundException, PackageNotFoundException, EXCEPTION_CODE_MAPPING from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.dates import from_iso8601_to_timestamp # TODO: We might want to replace this raw Thread for a ThreadPool, to align with other code usages class ExceptionThread(Thread): def run(self): self._exc = None try: super().run() except Exception as e: self._exc = e def join(self, timeout=None): super().join(timeout=timeout) def raise_errors(self): if self._exc: raise self._exc class JWTAuth(AuthBase): """Attaches JWT Authentication to the given Request object.""" def __init__(self, token): self.bearer = "Bearer %s" % str(token) if token else None def __call__(self, request): if self.bearer: request.headers['Authorization'] = self.bearer return request def _raise_exception_from_error(error_code, text): tmp = {v: k for k, v in EXCEPTION_CODE_MAPPING.items() # All except NotFound if k not in (RecipeNotFoundException, PackageNotFoundException)} try: raise tmp[error_code](text) except KeyError: raise ConanException(f"Server exception {error_code}: {text}") def _get_mac_digest(): # To avoid re-hashing all the time the same mac cached = getattr(_get_mac_digest, "_cached_value", None) if cached is not None: return cached sha1 = hashlib.sha1() sha1.update(str(get_mac()).encode()) cached = str(sha1.hexdigest()) _get_mac_digest._cached_value = cached return cached class RestV2Methods: def __init__(self, remote_url, token, requester, config, verify_ssl, checksum_deploy=False): self.remote_url = remote_url self.custom_headers = {'X-Client-Anonymous-Id': _get_mac_digest()} self.requester = requester self._config = config self.verify_ssl = verify_ssl self._checksum_deploy = checksum_deploy self.router = ClientV2Router(self.remote_url.rstrip("/")) self.auth = JWTAuth(token) @staticmethod def _check_error_response(ret): if ret.status_code == 401: raise AuthenticationException("Wrong user or password") # Cannot check content-type=text/html, conan server is doing it wrong if not ret.ok or "html>" in str(ret.content): raise ConanException("%s\n\nInvalid server response, check remote URL and " "try again" % str(ret.content)) def authenticate(self, user, password): """Sends user + password to get: - A plain response with a regular token (not supported refresh in the remote) and None """ auth = HTTPBasicAuth(user, password) url = self.router.common_authenticate() # logger.debug("REST: Authenticate to get access_token: %s" % url) ret = self.requester.get(url, auth=auth, headers=self.custom_headers, verify=self.verify_ssl) self._check_error_response(ret) return ret.content.decode() def check_credentials(self, force_auth=False): """If token is not valid will raise AuthenticationException. User will be asked for new user/pass""" url = self.router.common_check_credentials() auth = self.auth if force_auth and auth.bearer is None: auth = JWTAuth("unset") # logger.debug("REST: Check credentials: %s" % url) ret = self.requester.get(url, auth=auth, headers=self.custom_headers, verify=self.verify_ssl) if ret.status_code != 200: ret.charset = "utf-8" # To be able to access ret.text (ret.content are bytes) text = ret.text if ret.status_code != 404 else "404 Not found" _raise_exception_from_error(ret.status_code, text) return ret.content.decode() def server_capabilities(self): """Get information about the server: status, version, type and capabilities""" url = self.router.ping() auth = self.auth ret = self.requester.get(url, auth=auth, headers=self.custom_headers, verify=self.verify_ssl) server_capabilities = ret.headers.get('X-Conan-Server-Capabilities') if not server_capabilities and not ret.ok: # Old Artifactory might return 401/403 without capabilities, we don't want # to cache them #5687, so raise the exception and force authentication _raise_exception_from_error(ret.status_code, response_to_str(ret)) if server_capabilities is None: # Some servers returning 200-ok, even if not valid repo raise ConanException(f"Remote {self.remote_url} doesn't seem like a valid Conan remote") return [cap.strip() for cap in server_capabilities.split(",") if cap] def _get_json(self, url, headers=None): req_headers = self.custom_headers.copy() req_headers.update(headers or {}) response = self.requester.get(url, auth=self.auth, headers=req_headers, verify=self.verify_ssl, stream=True) if response.status_code != 200: # Error message is text response.charset = "utf-8" # To be able to access ret.text (ret.content are bytes) _raise_exception_from_error(response.status_code, response_to_str(response)) content = response.content.decode() content_type = response.headers.get("Content-Type") if content_type != 'application/json' and content_type != 'application/json; charset=utf-8': raise ConanException("%s\n\nResponse from remote is not json, but '%s'" % (content, content_type)) try: # This can fail, if some proxy returns 200 and an html message result = json.loads(content) except Exception: raise ConanException("Remote responded with broken json: %s" % content) if not isinstance(result, dict): raise ConanException("Unexpected server response %s" % result) return result def upload_recipe(self, ref, files_to_upload): if files_to_upload: urls = {fn: self.router.recipe_file(ref, fn) for fn in files_to_upload} self._upload_files(files_to_upload, urls, str(ref)) def upload_package(self, pref, files_to_upload): urls = {fn: self.router.package_file(pref, fn) for fn in files_to_upload} self._upload_files(files_to_upload, urls, str(pref)) def search(self, pattern=None, ignorecase=True): """ the_files: dict with relative_path: content """ url = self.router.search(pattern, ignorecase) response = self._get_json(url)["results"] # We need to filter the "_/_" user and channel from Artifactory ret = [] for reference in response: ref = RecipeReference.loads(reference) if ref.user == "_": ref.user = None if ref.channel == "_": ref.channel = None ret.append(ref) return ret def search_packages(self, ref, list_only): """Client is filtering by the query""" url = self.router.search_packages(ref, list_only) package_infos = self._get_json(url) return package_infos def _get_file_list_json(self, url): data = self._get_json(url) # Discarding (.keys()) still empty metadata for files # and make sure the paths like metadata/sign/signature are normalized to / data["files"] = list(d.replace("\\", "/") for d in data["files"].keys()) return data def get_recipe(self, ref, dest_folder, metadata, only_metadata): url = self.router.recipe_snapshot(ref) data = self._get_file_list_json(url) server_files = data["files"] result = {} if not only_metadata: accepted_files = ["conanfile.py", CONAN_MANIFEST, "metadata/sign"] files = [f for f in server_files if any(f.startswith(m) for m in accepted_files)] export_file = self._find_compressed_file(ref, server_files, EXPORT_FILE_NAME) if export_file is not None: files.append(export_file) # If we didn't indicated reference, server got the latest, use absolute now, it's safer urls = {fn: self.router.recipe_file(ref, fn) for fn in files} self._download_and_save_files(urls, dest_folder, files, parallel=True) result.update({fn: os.path.join(dest_folder, fn) for fn in files}) if metadata: metadata = [f"metadata/{m}" for m in metadata] files = [f for f in server_files if any(fnmatch.fnmatch(f, m) for m in metadata)] urls = {fn: self.router.recipe_file(ref, fn) for fn in files} self._download_and_save_files(urls, dest_folder, files, parallel=True, metadata=True) result.update({fn: os.path.join(dest_folder, fn) for fn in files}) return result def get_recipe_sources(self, ref, dest_folder): # If revision not specified, check latest assert ref.revision, f"get_recipe_sources() called without revision {ref}" url = self.router.recipe_snapshot(ref) data = self._get_file_list_json(url) files = data["files"] src_file = self._find_compressed_file(ref, files, EXPORT_SOURCES_FILE_NAME) if src_file is None: return None # If we didn't indicated reference, server got the latest, use absolute now, it's safer urls = {src_file: self.router.recipe_file(ref, src_file)} self._download_and_save_files(urls, dest_folder, [src_file, ], scope=str(ref)) ret = {src_file: os.path.join(dest_folder, src_file)} return ret @staticmethod def _find_compressed_file(ref, server_files, artifact, exists=False): pkg_files = [f for f in server_files if f.startswith(artifact)] if len(pkg_files) > 1: raise ConanException(f"{ref} is corrupted in the server, it contains " f"more than one compressed file: {sorted(pkg_files)}") if not pkg_files: if not exists: return None raise ConanException(f"Recipe {ref} is corrupted in the server, it doesn't contain " f"a {artifact} file") return pkg_files[0] def get_package(self, pref, dest_folder, metadata, only_metadata): url = self.router.package_snapshot(pref) data = self._get_file_list_json(url) server_files = data["files"] result = {} # Download only known files, but not metadata (except sign) if not only_metadata: # Retrieve package first, then metadata pkg_file = self._find_compressed_file(pref, server_files, PACKAGE_FILE_NAME, exists=True) accepted_files = [CONANINFO, pkg_file, CONAN_MANIFEST, "metadata/sign"] files = [f for f in server_files if any(f.startswith(m) for m in accepted_files)] # If we didn't indicated reference, server got the latest, use absolute now, it's safer urls = {fn: self.router.package_file(pref, fn) for fn in files} self._download_and_save_files(urls, dest_folder, files, scope=str(pref.ref)) result.update({fn: os.path.join(dest_folder, fn) for fn in files}) if metadata: metadata = [f"metadata/{m}" for m in metadata] files = [f for f in server_files if any(fnmatch.fnmatch(f, m) for m in metadata)] urls = {fn: self.router.package_file(pref, fn) for fn in files} self._download_and_save_files(urls, dest_folder, files, scope=str(pref.ref), metadata=True) result.update({fn: os.path.join(dest_folder, fn) for fn in files}) return result def _upload_files(self, files, urls, ref): failed = [] uploader = FileUploader(self.requester, self.verify_ssl, self._config) # conan_package.tgz and conan_export.tgz are uploaded first to avoid uploading conaninfo.txt # or conanamanifest.txt with missing files due to a network failure for filename in sorted(files): # As the filenames are sorted, the last one is always "conanmanifest.txt" resource_url = urls[filename] try: uploader.upload(resource_url, files[filename], auth=self.auth, dedup=self._checksum_deploy, ref=ref) except (AuthenticationException, ForbiddenException): raise except Exception as exc: ConanOutput().error(f"\nError uploading file: {filename}, '{exc}'", error_type="exception") failed.append(filename) if failed: raise ConanException("Execute upload again to retry upload the failed files: %s" % ", ".join(failed)) def _download_and_save_files(self, urls, dest_folder, files, parallel=False, scope=None, metadata=False): # Take advantage of filenames ordering, so that conan_package.tgz and conan_export.tgz # can be < conanfile, conaninfo, and sent always the last, so smaller files go first retry = self._config.get("core.download:retry", check_type=int, default=2) retry_wait = self._config.get("core.download:retry_wait", check_type=int, default=0) downloader = ConanInternalCacheDownloader(self.requester, self._config, scope=scope) threads = [] for filename in sorted(files, reverse=True): resource_url = urls[filename] abs_path = os.path.join(dest_folder, filename) os.makedirs(os.path.dirname(abs_path), exist_ok=True) # filename in subfolder must exist if parallel: kwargs = {"url": resource_url, "file_path": abs_path, "retry": retry, "retry_wait": retry_wait, "verify_ssl": self.verify_ssl, "auth": self.auth, "metadata": metadata} thread = ExceptionThread(target=downloader.download, kwargs=kwargs) threads.append(thread) thread.start() else: downloader.download(url=resource_url, file_path=abs_path, auth=self.auth, verify_ssl=self.verify_ssl, retry=retry, retry_wait=retry_wait, metadata=metadata) for t in threads: t.join() for t in threads: # Need to join all before raising errors t.raise_errors() def remove_all_packages(self, ref): """ Remove all packages from the specified reference""" self.check_credentials() assert ref.revision is not None, "remove_packages needs RREV" url = self.router.remove_all_packages(ref) response = self.requester.delete(url, auth=self.auth, verify=self.verify_ssl, headers=self.custom_headers) if response.status_code == 404: # Double check if it is a 404 because there are no packages try: package_search_url = self.router.search_packages(ref, list_only=True) if not self._get_json(package_search_url): return except Exception: pass if response.status_code != 200: # Error message is text # To be able to access ret.text (ret.content are bytes) response.charset = "utf-8" _raise_exception_from_error(response.status_code, response.text) def remove_packages(self, prefs): self.check_credentials() for pref in prefs: if not pref.revision: prevs = self.get_package_revisions_references(pref) else: prevs = [pref] for prev in prevs: url = self.router.remove_package(prev) response = self.requester.delete(url, auth=self.auth, headers=self.custom_headers, verify=self.verify_ssl) if response.status_code == 404: raise PackageNotFoundException(pref) if response.status_code != 200: # Error message is text # To be able to access ret.text (ret.content are bytes) response.charset = "utf-8" _raise_exception_from_error(response.status_code, response.text) def remove_recipe(self, ref): """ Remove a recipe and packages """ self.check_credentials() if ref.revision is None: # FIXME: This is unused at the moment, check server implementation # Remove all the RREVs refs = self.get_recipe_revisions_references(ref) else: refs = [ref] for ref in refs: url = self.router.remove_recipe(ref) response = self.requester.delete(url, auth=self.auth, headers=self.custom_headers, verify=self.verify_ssl) if response.status_code == 404: raise RecipeNotFoundException(ref) if response.status_code != 200: # Error message is text # To be able to access ret.text (ret.content are bytes) response.charset = "utf-8" _raise_exception_from_error(response.status_code, response.text) def get_recipe_revision_reference(self, ref): # FIXME: implement this new endpoint in the remotes? assert ref.revision, "recipe_exists has to be called with a complete reference" ref_without_rev = copy.copy(ref) ref_without_rev.revision = None try: remote_refs = self.get_recipe_revisions_references(ref_without_rev) except NotFoundException: raise RecipeNotFoundException(ref) for r in remote_refs: if r == ref: return r raise RecipeNotFoundException(ref) def get_package_revision_reference(self, pref): # FIXME: implement this endpoint in the remotes? assert pref.revision, "get_package_revision_reference has to be called with a complete reference" pref_without_rev = copy.copy(pref) pref_without_rev.revision = None try: remote_prefs = self.get_package_revisions_references(pref_without_rev) except NotFoundException: raise PackageNotFoundException(pref) for p in remote_prefs: if p == pref: return p raise PackageNotFoundException(pref) def get_recipe_revisions_references(self, ref): assert ref.revision is None url = self.router.recipe_revisions(ref) tmp = self._get_json(url)["revisions"] remote_refs = [] for item in tmp: _tmp = copy.copy(ref) _tmp.revision = item.get("revision") _tmp.timestamp = from_iso8601_to_timestamp(item.get("time")) remote_refs.append(_tmp) return remote_refs def get_latest_recipe_reference(self, ref): url = self.router.recipe_latest(ref) data = self._get_json(url) remote_ref = copy.copy(ref) remote_ref.revision = data.get("revision") remote_ref.timestamp = from_iso8601_to_timestamp(data.get("time")) return remote_ref def get_package_revisions_references(self, pref): assert pref.revision is None url = self.router.package_revisions(pref) tmp = self._get_json(url)["revisions"] remote_prefs = [PkgReference(pref.ref, pref.package_id, item.get("revision"), from_iso8601_to_timestamp(item.get("time"))) for item in tmp] return remote_prefs def get_latest_package_reference(self, pref: PkgReference, headers): url = self.router.package_latest(pref) data = self._get_json(url, headers=headers) remote_pref = copy.copy(pref) remote_pref.revision = data.get("revision") remote_pref.timestamp = from_iso8601_to_timestamp(data.get("time")) return remote_pref ================================================ FILE: conan/internal/rest/rest_routes.py ================================================ class RestRoutes: ping = "ping" common_search = "conans/search" common_authenticate = "users/authenticate" common_check_credentials = "users/check_credentials" def __init__(self): self.base = 'conans' @property def recipe(self): return self.base + '/{name}/{version}/{username}/{channel}' @property def recipe_latest(self): return '%s/latest' % self.recipe @property def recipe_revision(self): return '%s/revisions/{revision}' % self.recipe @property def recipe_revision_files(self): return '%s/files' % self.recipe_revision @property def recipe_revisions(self): return '%s/revisions' % self.recipe @property def recipe_revision_file(self): return '%s/files/{path}' % self.recipe_revision @property def packages_revision(self): return '%s/packages' % self.recipe_revision @property def package_recipe_revision(self): """Route for a package specifying the recipe revision but not the package revision""" return '%s/{package_id}' % self.packages_revision @property def package_revisions(self): return '%s/revisions' % self.package_recipe_revision @property def package_revision(self): return '%s/{p_revision}' % self.package_revisions @property def package_revision_files(self): return '%s/files' % self.package_revision @property def package_revision_latest(self): return '%s/latest' % self.package_recipe_revision @property def package_revision_file(self): return '%s/files/{path}' % self.package_revision @property def common_search_packages(self): return "%s/search" % self.recipe @property def common_search_packages_revision(self): return "%s/search" % self.recipe_revision ================================================ FILE: conan/internal/runner/__init__.py ================================================ class RunnerException(Exception): def __init__(self, *args, **kwargs): self.command = kwargs.pop("command", None) self.stdout_log = kwargs.pop("stdout_log", None) self.stderr_log = kwargs.pop("stderr_log", None) super(RunnerException, self).__init__(*args, **kwargs) ================================================ FILE: conan/internal/runner/docker.py ================================================ from argparse import Namespace import os import sys import json import platform import shutil from typing import Optional, NamedTuple, Dict, List import yaml from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern from conan.api.output import Color, ConanOutput from conan.cli import make_abs_path from conan.internal.runner import RunnerException from conan.errors import ConanException from pathlib import Path from conan.internal.model.profile import Profile from conan.internal.model.version import Version from conan.internal.runner.output import RunnerOutput from conan.internal.conan_app import ConanApp class _ContainerConfig(NamedTuple): class Build(NamedTuple): dockerfile: Optional[str] = None build_context: Optional[str] = None build_args: Optional[Dict[str, str]] = None cache_from: Optional[List[str]] = None platform: Optional[str] = None class Run(NamedTuple): name: Optional[str] = None environment: Optional[Dict[str, str]] = None user: Optional[str] = None privileged: Optional[bool] = None cap_add: Optional[List[str]] = None security_opt: Optional[List[str]] = None volumes: Optional[Dict[str, str]] = None network: Optional[str] = None image: Optional[str] = None build: Build = Build() run: Run = Run() @staticmethod def load(file_path: Optional[str]) -> '_ContainerConfig': # Container config # https://containers.dev/implementors/json_reference/ if not file_path: return _ContainerConfig() def _instans_or_error(value, obj): if value and (not isinstance(value, obj)): raise ConanException(f"docker runner configfile syntax error: {value} must be a {obj.__name__}") return value with open(file_path, 'r') as f: runnerfile = yaml.safe_load(f) return _ContainerConfig( image=_instans_or_error(runnerfile.get('image'), str), build=_ContainerConfig.Build( dockerfile=_instans_or_error(runnerfile.get('build', {}).get('dockerfile'), str), build_context=_instans_or_error(runnerfile.get('build', {}).get('build_context'), str), build_args=_instans_or_error(runnerfile.get('build', {}).get('build_args'), dict), cache_from=_instans_or_error(runnerfile.get('build', {}).get('cacheFrom'), list), platform=_instans_or_error(runnerfile.get('build', {}).get('platform'), str), ), run=_ContainerConfig.Run( name=_instans_or_error(runnerfile.get('run', {}).get('name'), str), environment=_instans_or_error(runnerfile.get('run', {}).get('containerEnv'), dict), user=_instans_or_error(runnerfile.get('run', {}).get('containerUser'), str), privileged=_instans_or_error(runnerfile.get('run', {}).get('privileged'), bool), cap_add=_instans_or_error(runnerfile.get('run', {}).get('capAdd'), list), security_opt=_instans_or_error(runnerfile.get('run', {}).get('securityOpt'), list), volumes=_instans_or_error(runnerfile.get('run', {}).get('mounts'), dict), network=_instans_or_error(runnerfile.get('run', {}).get('network'), str), ) ) class DockerRunner: def __init__(self, conan_api: ConanAPI, command: str, host_profile: Profile, build_profile: Profile, args: Namespace, raw_args: list[str]): self.logger = ConanOutput() self.docker_client = self._initialize_docker_client() self.docker_api = self.docker_client.api self.conan_api = conan_api self.build_profile = build_profile self.abs_host_path = self._get_abs_host_path(args.path) self.args = args if args.format: raise ConanException("format argument is forbidden if running in a docker runner") self.configfile = _ContainerConfig.load(host_profile.runner.get('configfile')) self.dockerfile = host_profile.runner.get('dockerfile') or self.configfile.build.dockerfile self.docker_build_context = host_profile.runner.get('build_context') or self.configfile.build.build_context self.platform = host_profile.runner.get('platform') or self.configfile.build.platform self.image = host_profile.runner.get('image') or self.configfile.image if not (self.dockerfile or self.image): raise ConanException("'dockerfile' or docker image name is needed") self.image = self.image or 'conan-runner-default' self.name = self.configfile.run.name or host_profile.runner.get("name", "conan-runner-docker") self.remove = str(host_profile.runner.get('remove', 'false')).lower() == 'true' self.cache = str(host_profile.runner.get('cache', 'clean')) if self.cache not in ['clean', 'copy', 'shared']: raise ConanException(f'Invalid cache value: "{self.cache}". Valid values are: clean, copy, shared') self.container = None self.raw_args = raw_args self.command = command self.runner_logger = RunnerOutput(self.name) def run(self) -> None: """ Run conan inside a Docker container """ self._build_image() self._start_container() try: self._init_container() self._run_command(self.command) self._update_local_cache() except RunnerException as e: raise ConanException(f'"{e.command}" inside docker fail') finally: if self.container: error = sys.exc_info()[0] is not None # Check if error has been raised log = self.logger.error if error else self.logger.status log('Stopping container') self.container.stop() if self.remove: log('Removing container') self.container.remove() def _initialize_docker_client(self): import docker import docker.api.build docker_base_urls = [ None, # Default docker configuration os.environ.get('DOCKER_HOST'), # From DOCKER_HOST environment variable 'unix:///var/run/docker.sock', # Default Linux socket f'unix://{os.path.expanduser("~")}/.rd/docker.sock' # Rancher Linux socket ] for base_url in docker_base_urls: try: self.logger.verbose(f'Trying to connect to Docker: "{base_url or "default"}"') client = docker.DockerClient(base_url=base_url, version='auto') self.logger.verbose(f'Connected to Docker: "{base_url or "default"}"') docker.api.build.process_dockerfile = lambda dockerfile, path: ('Dockerfile', dockerfile) return client except Exception: continue raise ConanException("Docker Client failed to initialize." "\n - Check if docker is installed and running" "\n - Run 'pip install conan[runners]'") def _get_abs_host_path(self, path: str) -> Path: abs_path = make_abs_path(path) if abs_path is None: raise ConanException("Could not determine the absolute path.") return Path(abs_path) def _build_image(self) -> None: if not self.dockerfile: return self.logger.status(f'Building the Docker image: {self.image}') dockerfile_file_path = self.dockerfile if os.path.isdir(self.dockerfile): dockerfile_file_path = os.path.join(self.dockerfile, 'Dockerfile') with open(dockerfile_file_path) as f: build_path = self.docker_build_context or os.path.dirname(dockerfile_file_path) self.logger.highlight(f"Dockerfile path: '{dockerfile_file_path}'") self.logger.highlight(f"Docker build context: '{build_path}'\n") docker_build_logs = self.docker_api.build( path=build_path, dockerfile=f.read(), tag=self.image, platform=self.platform, buildargs=self.configfile.build.build_args, cache_from=self.configfile.build.cache_from, ) for chunk in docker_build_logs: for line in chunk.decode("utf-8").split('\r\n'): if line: stream = json.loads(line).get('stream') if stream: self.logger.status(stream.strip()) def _start_container(self) -> None: volumes, environment = self._create_runner_environment() try: if self.docker_client.containers.list(all=True, filters={'name': self.name}): self.logger.status('Starting the docker container', fg=Color.BRIGHT_MAGENTA) self.container = self.docker_client.containers.get(self.name) self.container.start() else: if self.configfile.run.environment: environment.update(self.configfile.run.environment) if self.configfile.run.volumes: volumes.update(self.configfile.run.volumes) self.logger.status('Creating the docker container', fg=Color.BRIGHT_MAGENTA) self.container = self.docker_client.containers.run( self.image, "/bin/bash -c 'while true; do sleep 30; done;'", name=self.name, volumes=volumes, environment=environment, user=self.configfile.run.user, privileged=self.configfile.run.privileged, cap_add=self.configfile.run.cap_add, security_opt=self.configfile.run.security_opt, detach=True, auto_remove=False, network=self.configfile.run.network) self.logger.status(f'Container {self.name} running', fg=Color.BRIGHT_MAGENTA) except Exception as e: raise ConanException(f'Imposible to run the container "{self.name}" with image "{self.image}"' f'\n\n{str(e)}') def _run_command(self, command: str, workdir: Optional[str] = None, verbose: bool = True) -> tuple[str, str]: workdir = workdir or self.abs_docker_path log = self.runner_logger.status if verbose else self.runner_logger.verbose log(f'$ {command}', fg=Color.BLUE) exec_instance = self.docker_api.exec_create(self.container.id, f"/bin/bash -c '{command}'", workdir=workdir, tty=True) exec_output = self.docker_api.exec_start(exec_instance['Id'], tty=True, stream=True, demux=True,) stderr_log, stdout_log = '', '' try: for (stdout_out, stderr_out) in exec_output: if stdout_out is not None: decoded = stdout_out.decode('utf-8', errors='ignore').strip() stdout_log += decoded log(decoded) if stderr_out is not None: decoded = stderr_out.decode('utf-8', errors='ignore').strip() stderr_log += decoded log(decoded) except Exception as e: if platform.system() == 'Windows': import pywintypes if isinstance(e, pywintypes.error): pass else: raise e exit_metadata = self.docker_api.exec_inspect(exec_instance['Id']) if exit_metadata['Running'] or exit_metadata['ExitCode'] > 0: raise RunnerException(command=command, stdout_log=stdout_log, stderr_log=stderr_log) return stdout_log, stderr_log def _get_volumes_and_docker_path(self) -> tuple[dict, str]: app = ConanApp(self.conan_api) remotes = self.conan_api.remotes.list(self.args.remote) if not self.args.no_remote else [] conanfile = app.loader.load_consumer(self.abs_host_path / "conanfile.py", remotes=remotes) abs_docker_base_path = Path('/') / self.docker_user_name / 'conanrunner' # Check if recipe has defined a root folder # In this case, mount the root folder as the base path and update the abs_docker_path to the # new relative path if hasattr(conanfile, "layout"): try: conanfile.layout() if conanfile.folders.root: abs_path = self._get_abs_host_path(conanfile.folders.root) if self.abs_host_path.is_relative_to(abs_path): abs_docker_base_path /= abs_path.name volumes = {abs_path: {'bind': abs_docker_base_path.as_posix(), 'mode': 'rw'}} abs_docker_path = abs_docker_base_path / self.abs_host_path.relative_to(abs_path) return volumes, abs_docker_path.as_posix() except: pass abs_docker_path = (abs_docker_base_path / self.abs_host_path.name).as_posix() volumes = {self.abs_host_path: {'bind': abs_docker_path, 'mode': 'rw'}} return volumes, abs_docker_path def _create_runner_environment(self) -> tuple[dict, dict]: # Runner configuration self.abs_runner_home_path = self.abs_host_path / '.conanrunner' self.docker_user_name = self.configfile.run.user or 'root' volumes, self.abs_docker_path = self._get_volumes_and_docker_path() shutil.rmtree(self.abs_runner_home_path, ignore_errors=True) environment = {'CONAN_RUNNER_ENVIRONMENT': '1'} # Update conan command and some paths to run inside the container self.raw_args[self.raw_args.index(self.args.path)] = self.abs_docker_path self.command = ' '.join([f'conan {self.command}'] + [f'"{raw_arg}"' if ' ' in raw_arg else raw_arg for raw_arg in self.raw_args] + ['-f json > create.json']) if self.cache == 'shared': volumes[self.conan_api.home_folder] = {'bind': f'/{self.docker_user_name}/.conan2', 'mode': 'rw'} if self.cache in ['clean', 'copy']: # Copy all conan profiles and config files to docker workspace for profile in set(self.args.profile_host + (self.args.profile_build or [])): profile_path = self.conan_api.profiles.get_path(profile) dest_filename = self.abs_runner_home_path / 'profiles' / Path(profile) if not dest_filename.exists(): dest_filename.parent.mkdir(parents=True, exist_ok=True) self.logger.verbose(f"Copying profile '{profile}': {profile_path} -> {dest_filename}") shutil.copy(profile_path, dest_filename) if not self.args.profile_build: dest_filename = self.abs_runner_home_path / 'profiles' / "default" default_build_profile = self.conan_api.profiles.get_default_build() self.logger.verbose(f"Copying default profile: {default_build_profile} -> {dest_filename}") shutil.copy(default_build_profile, dest_filename.as_posix()) for file_name in ['global.conf', 'settings.yml', 'remotes.json']: src_file = Path(self.conan_api.home_folder) / file_name if src_file.exists(): self.logger.verbose(f"Copying {src_file} -> {self.abs_runner_home_path / file_name}") shutil.copy(src_file, self.abs_runner_home_path / file_name) if self.cache == 'copy': tgz_path = self.abs_runner_home_path / 'local_cache_save.tgz' self.logger.status(f'Save host cache in: {tgz_path}') self.conan_api.cache.save(self.conan_api.list.select(ListPattern("*:*")), tgz_path) return volumes, environment def _init_container(self) -> None: min_conan_version = '2.1' stdout, _ = self._run_command('conan --version', verbose=True) docker_conan_version = str(stdout.split('Conan version ')[1].replace('\n', '').replace('\r', '')) # Remove all characters and color if Version(docker_conan_version) <= Version(min_conan_version): raise ConanException(f'conan version inside the container must be greater than {min_conan_version}') if self.cache != 'shared': self._run_command('mkdir -p ${HOME}/.conan2/profiles', verbose=False) self._run_command('cp -r "'+self.abs_docker_path+'/.conanrunner/profiles/." ${HOME}/.conan2/profiles/.', verbose=False) for file_name in ['global.conf', 'settings.yml', 'remotes.json']: if (self.abs_runner_home_path / file_name).exists(): self._run_command('cp "'+self.abs_docker_path+'/.conanrunner/'+file_name+'" ${HOME}/.conan2/'+file_name, verbose=False) if self.cache in ['copy']: self._run_command('conan cache restore "'+self.abs_docker_path+'/.conanrunner/local_cache_save.tgz"') def _update_local_cache(self) -> None: if self.cache != 'shared': self._run_command('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json', verbose=False) self._run_command('conan cache save --list=pkglist.json --file "'+self.abs_docker_path+'"/.conanrunner/docker_cache_save.tgz') tgz_path = self.abs_runner_home_path / 'docker_cache_save.tgz' self.logger.status(f'Restore host cache from: {tgz_path}') self.conan_api.cache.restore(tgz_path) ================================================ FILE: conan/internal/runner/output.py ================================================ from conan.api.output import Color, ConanOutput class RunnerOutput(ConanOutput): def __init__(self, runner_info: str): super().__init__() self.set_warnings_as_errors(True) # Make log errors blocker self._prefix = f"{runner_info} | " def _write_message(self, msg, fg=None, bg=None, newline=True): for line in msg.splitlines(): super()._write_message(self._prefix, Color.BLACK, Color.BRIGHT_YELLOW, newline=False) super()._write_message(line, fg, bg, newline) ================================================ FILE: conan/internal/runner/ssh.py ================================================ from pathlib import Path import pathlib import tempfile from conan.api.output import Color, ConanOutput from conan.errors import ConanException import os from io import BytesIO import sys def ssh_info(msg, error=False): fg=Color.BRIGHT_MAGENTA if error: fg=Color.BRIGHT_RED ConanOutput().status('\n┌'+'─'*(2+len(msg))+'┐', fg=fg) ConanOutput().status(f'| {msg} |', fg=fg) ConanOutput().status('└'+'─'*(2+len(msg))+'┘\n', fg=fg) class SSHRunner: def __init__(self, conan_api, command, host_profile, build_profile, args, raw_args): try: from paramiko.config import SSHConfig from paramiko.client import SSHClient except ImportError: raise ConanException( "Paramiko is required for SSH runner. If conan is installed in a virtual environment, try to install " "the 'paramiko' package, or consider installing conan package with extra requires 'conan[runners]'" ) self.conan_api = conan_api self.command = command self.host_profile = host_profile self.build_profile = build_profile self.remote_host_profile = None self.remote_build_profile = None self.remote_python_command = None self.remote_create_dir = None self.remote_is_windows = None self.args = args self.raw_args = raw_args self.ssh_config = None self.remote_workspace = None self.remote_conan = None self.remote_conan_home = None if host_profile.runner.get('use_ssh_config', False): ssh_config_file = Path.home() / ".ssh" / "config" ssh_config = SSHConfig.from_file(open(ssh_config_file)) hostname = host_profile.runner.get("host") # TODO: this one is required if ssh_config and ssh_config.lookup(hostname): hostname = ssh_config.lookup(hostname)['hostname'] self.client = SSHClient() self.client.load_system_host_keys() self.client.connect(hostname) def run(self, use_cache=True): ssh_info('Got to SSHRunner.run(), doing nothing') self.ensure_runner_environment() self.copy_working_conanfile_path() raw_args = self.raw_args raw_args[raw_args.index(self.args.path)] = self.remote_create_dir raw_args = " ".join(raw_args) _Path = pathlib.PureWindowsPath if self.remote_is_windows else pathlib.PurePath remote_json_output = _Path(self.remote_create_dir).joinpath("conan_create.json").as_posix() command = f"{self.remote_conan} create {raw_args} --format json > {remote_json_output}" ssh_info(f"Remote command: {command}") stdout, _ = self._run_command(command) first_line = True while not stdout.channel.exit_status_ready(): line = stdout.channel.recv(1024) if first_line and self.remote_is_windows: # Avoid clearing and moving the cursor when the remote server is Windows # https://github.com/PowerShell/Win32-OpenSSH/issues/1738#issuecomment-789434169 line = line.replace(b"\x1b[2J\x1b[m\x1b[H",b"") sys.stdout.buffer.write(line) sys.stdout.buffer.flush() first_line = False if stdout.channel.recv_exit_status() == 0: self.update_local_cache(remote_json_output) # self.client.close() def ensure_runner_environment(self): has_python3_command = False python_is_python3 = False _, _stdout, _stderr = self.client.exec_command("python3 --version") has_python3_command = _stdout.channel.recv_exit_status() == 0 if not has_python3_command: _, _stdout, _stderr = self.client.exec_command("python --version") if _stdout.channel.recv_exit_status() == 0 and "Python 3" in _stdout.read().decode(): python_is_python3 = True python_command = "python" if python_is_python3 else "python3" self.remote_python_command = python_command if not has_python3_command and not python_is_python3: raise ConanException("Unable to locate working Python 3 executable in remote SSH environment") # Determine if remote host is Windows _, _stdout, _ = self.client.exec_command(f'{python_command} -c "import os; print(os.name)"') if _stdout.channel.recv_exit_status() != 0: raise ConanException("Unable to determine remote OS type") is_windows = _stdout.read().decode().strip() == "nt" self.remote_is_windows = is_windows # Get remote user home folder _, _stdout, _ = self.client.exec_command(f'{python_command} -c "from pathlib import Path; print(Path.home())"') if _stdout.channel.recv_exit_status() != 0: raise ConanException("Unable to determine remote home user folder") home_folder = _stdout.read().decode().strip() # Expected remote paths remote_folder = Path(home_folder) / ".conan2remote" remote_folder = remote_folder.as_posix().replace("\\", "/") self.remote_workspace = remote_folder remote_conan_home = Path(home_folder) / ".conan2remote" / "conanhome" remote_conan_home = remote_conan_home.as_posix().replace("\\", "/") self.remote_conan_home = remote_conan_home ssh_info(f"Remote workfolder: {remote_folder}") # Ensure remote folders exist for folder in [remote_folder, remote_conan_home]: _, _stdout, _stderr = self.client.exec_command(f"""{python_command} -c "import os; os.makedirs('{folder}', exist_ok=True)""") if _stdout.channel.recv_exit_status() != 0: ssh_info(f"Error creating remote folder: {_stderr.read().decode()}") raise ConanException(f"Unable to create remote workfolder at {folder}") conan_venv = remote_folder + "/venv" if is_windows: conan_cmd = remote_folder + "/venv/Scripts/conan.exe" else: conan_cmd = remote_folder + "/venv/bin/conan" ssh_info(f"Expected remote conan home: {remote_conan_home}") ssh_info(f"Expected remote conan command: {conan_cmd}") # Check if remote Conan executable exists, otherwise invoke pip inside venv sftp = self.client.open_sftp() try: sftp.stat(conan_cmd) has_remote_conan = True except FileNotFoundError: has_remote_conan = False finally: sftp.close() if not has_remote_conan: _, _stdout, _stderr = self.client.exec_command(f"{python_command} -m venv {conan_venv}") if _stdout.channel.recv_exit_status() != 0: ssh_info(f"Unable to create remote venv: {_stderr.read().decode().strip()}") if is_windows: python_command = remote_folder + "/venv" + "/Scripts" + "/python.exe" else: python_command = remote_folder + "/venv" + "/bin" + "/python" _, _stdout, _stderr = self.client.exec_command(f"{python_command} -m pip install git+https://github.com/conan-io/conan@feature/docker_wrapper") if _stdout.channel.recv_exit_status() != 0: # Note: this may fail on windows ssh_info(f"Unable to install conan in venv: {_stderr.read().decode().strip()}") remote_env = { 'CONAN_HOME': remote_conan_home, 'CONAN_RUNNER_ENVIRONMENT': "1" } if is_windows: # Wrapper script with environment variables preset env_lines = "\n".join([f"set {k}={v}" for k,v in remote_env.items()]) conan_bat_contents = f"""@echo off\n{env_lines}\n{conan_cmd} %*\n""" conan_bat = remote_folder + "/conan.bat" try: sftp = self.client.open_sftp() sftp.putfo(BytesIO(conan_bat_contents.encode()), conan_bat) except: raise ConanException("unable to set up Conan remote script") finally: sftp.close() self.remote_conan = conan_bat _, _stdout, _stderr = self.client.exec_command(f"{self.remote_conan} config home") ssh_info(f"Remote conan config home returned: {_stdout.read().decode().strip()}") _, _stdout, _stderr = self.client.exec_command(f"{self.remote_conan} profile detect --force") self._copy_profiles() def _copy_profiles(self): sftp = self.client.open_sftp() # TODO: very questionable choices here try: profiles = { self.args.profile_host[0]: self.host_profile.dumps(), self.args.profile_build[0]: self.build_profile.dumps() } for name, contents in profiles.items(): dest_filename = self.remote_conan_home + f"/profiles/{name}" sftp.putfo(BytesIO(contents.encode()), dest_filename) except: raise ConanException("Unable to copy profiles to remote") finally: sftp.close() def copy_working_conanfile_path(self): resolved_path = Path(self.args.path).resolve() if resolved_path.is_file(): resolved_path = resolved_path.parent if not resolved_path.is_dir(): return ConanException("Error determining conanfile directory") # Create temporary destination directory temp_dir_create_cmd = f"""{self.remote_python_command} -c "import tempfile; print(tempfile.mkdtemp(dir='{self.remote_workspace}'))""" _, _stdout, _ = self.client.exec_command(temp_dir_create_cmd) if _stdout.channel.recv_exit_status() != 0: raise ConanException("Unable to create remote temporary directory") self.remote_create_dir = _stdout.read().decode().strip().replace("\\", '/') # Copy current folder to destination using sftp _Path = pathlib.PureWindowsPath if self.remote_is_windows else pathlib.PurePath sftp = self.client.open_sftp() for root, dirs, files in os.walk(resolved_path.as_posix()): relative_root = Path(root).relative_to(resolved_path) for dir in dirs: dst = _Path(self.remote_create_dir).joinpath(relative_root).joinpath(dir).as_posix() sftp.mkdir(dst) for file in files: orig = os.path.join(root, file) dst = _Path(self.remote_create_dir).joinpath(relative_root).joinpath(file).as_posix() sftp.put(orig, dst) sftp.close() def _run_command(self, command): ''' Run a command in an SSH session. When requesting a pseudo-terminal from the server, ensure we pass width and height that matches the current terminal ''' channel = self.client.get_transport().open_session() if sys.stdout.isatty(): width, height = os.get_terminal_size() channel.get_pty(width=width, height=height) channel.exec_command(command) stdout = channel.makefile("r") stderr = channel.makefile("r") return stdout, stderr def update_local_cache(self, json_result): # ('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json' _Path = pathlib.PureWindowsPath if self.remote_is_windows else pathlib.PurePath pkg_list_json = _Path(self.remote_create_dir).joinpath("pkg_list.json").as_posix() pkg_list_command = f"{self.remote_conan} list --graph={json_result} --graph-binaries=build --format=json > {pkg_list_json}" _, stdout, _ = self.client.exec_command(pkg_list_command) if stdout.channel.recv_exit_status() != 0: raise ConanException("Unable to generate remote package list") conan_cache_tgz = _Path(self.remote_create_dir).joinpath("cache.tgz").as_posix() cache_save_command = f"{self.remote_conan} cache save --list {pkg_list_json} --file {conan_cache_tgz}" _, stdout, _ = self.client.exec_command(cache_save_command) if stdout.channel.recv_exit_status() != 0: raise ConanException("Unable to save remote conan cache state") sftp = self.client.open_sftp() with tempfile.TemporaryDirectory() as tmp: local_cache_tgz = os.path.join(tmp, 'cache.tgz') sftp.get(conan_cache_tgz, local_cache_tgz) package_list = self.conan_api.cache.restore(local_cache_tgz) ================================================ FILE: conan/internal/runner/wsl.py ================================================ from pathlib import PurePosixPath, PureWindowsPath, Path from conan.api.output import Color, ConanOutput from conan.internal.util.runners import conan_run from conan.internal.subsystems import subsystem_path from conan.tools.files import save from io import StringIO import tempfile import os def wsl_info(msg, error=False): fg=Color.BRIGHT_MAGENTA if error: fg=Color.BRIGHT_RED ConanOutput().status('\n┌'+'─'*(2+len(msg))+'┐', fg=fg) ConanOutput().status(f'| {msg} |', fg=fg) ConanOutput().status('└'+'─'*(2+len(msg))+'┘\n', fg=fg) class WSLRunner: def __init__(self, conan_api, command, host_profile, build_profile, args, raw_args): self.conan_api = conan_api self.command = command self.host_profile = host_profile self.build_profile = build_profile self.remote_host_profile = None self.remote_build_profile = None self.remote_python_command = None self.remote_conan = None self.remote_conan_home = None self.args = args self.raw_args = raw_args # to pass to wsl.exe (optional, otherwise run with defaults) distro = host_profile.runner.get("distribution", None) user = host_profile.runner.get("user", None) self.shared_cache = host_profile.runner.get("shared_cache", False) if self.shared_cache: storage_path = Path(conan_api.config.home()) / 'p' # TODO: there's an API for this!! self.remote_conan_cache = subsystem_path("wsl", storage_path.as_posix()) def run(self): self.ensure_runner_environment() raw_args = self.raw_args current_path = Path(self.args.path).resolve() current_path_wsl = subsystem_path("wsl", current_path.as_posix()) raw_args[raw_args.index(self.args.path)] = current_path_wsl raw_args = " ".join(raw_args) with tempfile.TemporaryDirectory() as tmp_dir: if not self.shared_cache: create_json = PureWindowsPath(tmp_dir).joinpath("create.json").as_posix() raw_args += f" --format=json > {create_json}" tmp_dir_wsl = subsystem_path("wsl", tmp_dir) command = f"wsl.exe --cd {tmp_dir_wsl} -- CONAN_RUNNER_ENVIRONMENT=1 CONAN_HOME={self.remote_conan_home} {self.remote_conan} create {raw_args}" rc = conan_run(command) if rc == 0 and not self.shared_cache: create_json_wsl = subsystem_path("wsl", create_json) pkglist_json = PureWindowsPath(tmp_dir).joinpath("pkglist.json").as_posix() pkglist_json_wsl = subsystem_path("wsl", pkglist_json) saved_cache = PureWindowsPath(tmp_dir).joinpath("saved_cache.tgz").as_posix() saved_cache_wsl = subsystem_path("wsl", saved_cache) conan_run(f"wsl.exe --cd {tmp_dir_wsl} -- CONAN_RUNNER_ENVIRONMENT=1 CONAN_HOME={self.remote_conan_home} {self.remote_conan} list --graph={create_json_wsl} --format=json > {pkglist_json}") conan_run(f"wsl.exe --cd {tmp_dir_wsl} -- CONAN_RUNNER_ENVIRONMENT=1 CONAN_HOME={self.remote_conan_home} {self.remote_conan} cache save --list={pkglist_json_wsl} --file {saved_cache_wsl}") self.conan_api.cache.restore(saved_cache) else: pass #print(command) def ensure_runner_environment(self): stdout = StringIO() stderr = StringIO() ret = conan_run('wsl.exe echo $HOME', stdout=stdout) if ret == 0: remote_home = PurePosixPath(stdout.getvalue().strip()) stdout = StringIO() remote_conan = remote_home / ".conan2remote" / "venv" / "bin" / "conan" self.remote_conan = remote_conan.as_posix() wsl_info(self.remote_conan) conan_home = remote_home / ".conan2remote" / "conan_home" self.remote_conan_home = conan_home has_conan = conan_run(f"wsl.exe CONAN_HOME={conan_home.as_posix()} {remote_conan} --version", stdout=stdout, stderr=stderr) == 0 if not has_conan: wsl_info("Bootstrapping Conan in remote") conan_run(f"wsl.exe mkdir -p {remote_home}/.conan2remote") venv = remote_home / ".conan2remote"/ "venv" python = venv / "bin" / "python" self.remote_python_command = python conan_run(f"wsl.exe python3 -m venv {venv.as_posix()}") conan_run(f"wsl.exe {python} -m pip install pip wheel --upgrade") conan_run(f"wsl.exe {python} -m pip install git+https://github.com/conan-io/conan@feature/docker_wrapper") conan_run(f"wsl.exe CONAN_HOME={conan_home.as_posix()} {remote_conan} --version", stdout=stdout) remote_conan_version = stdout.getvalue().strip() wsl_info(f"Remote conan version: {remote_conan_version}") stdout = StringIO() stderr = StringIO() # If this command succeeds, great - if not because it already exists, ignore conan_run(f"wsl.exe CONAN_HOME={conan_home.as_posix()} {remote_conan} profile detect", stdout=stdout, stderr=stderr) conf_content = f"core.cache:storage_path={self.remote_conan_cache}\n" if self.shared_cache else "" with tempfile.TemporaryDirectory() as tmp: global_conf = os.path.join(tmp, "global.conf") save(None, path=global_conf, content=conf_content) global_conf_wsl = subsystem_path("wsl", global_conf) remote_global_conf = self.remote_conan_home.joinpath("global.conf") conan_run(f"wsl.exe cp {global_conf_wsl} {remote_global_conf}") self._copy_profiles() def _copy_profiles(self): # TODO: questionable choices, may fail # Note: see the use of \\wsl$\\, we could place the files # directly. We would need to work out the exact distro name first profiles = { self.args.profile_host[0]: self.host_profile.dumps(), self.args.profile_build[0]: self.build_profile.dumps() } with tempfile.TemporaryDirectory() as tmp: # path = os.path.join(tmp, 'something') for name, contents in profiles.items(): outfile = os.path.join(tmp, name) save(None, path=outfile, content=contents) outfile_wsl = subsystem_path("wsl", outfile) remote_profile = self.remote_conan_home.joinpath("profiles").as_posix() + "/" # This works but copies the file with executable attribute conan_run(f"wsl.exe cp {outfile_wsl} {remote_profile}") ================================================ FILE: conan/internal/source.py ================================================ import os from conan.api.output import ConanOutput from conan.internal.methods import run_source_method from conan.tools.env import VirtualBuildEnv from conan.internal.errors import NotFoundException from conan.errors import ConanException from conan.internal.util.files import is_dirty, mkdir, rmdir, set_dirty_context_manager, merge_directories, clean_dirty def _try_get_sources(ref, remote_manager, recipe_layout, remote): try: remote_manager.get_recipe_sources(ref, recipe_layout, remote) except NotFoundException: return except Exception as e: msg = ("The '%s' package has 'exports_sources' but sources not found in local cache.\n" "Probably it was installed from a remote that is no longer available.\n" % str(ref)) raise ConanException("\n".join([str(e), msg])) return remote def retrieve_exports_sources(remote_manager, recipe_layout, conanfile, ref, remotes): """ the "exports_sources" sources are not retrieved unless necessary to build. In some occassions, conan needs to get them too, like if uploading to a server, to keep the recipes complete """ if conanfile.exports_sources is None and not hasattr(conanfile, "export_sources"): return None export_sources_folder = recipe_layout.export_sources() if os.path.exists(export_sources_folder): return None for r in remotes: sources_remote = _try_get_sources(ref, remote_manager, recipe_layout, r) if sources_remote: break else: msg = ("The '%s' package has 'exports_sources' but sources not found in local cache.\n" "Probably it was installed from a remote that is no longer available.\n" % str(ref)) raise ConanException(msg) ConanOutput(scope=str(ref)).info("Sources downloaded from '{}'".format(sources_remote.name)) def config_source(export_source_folder, conanfile, hook_manager): """ Implements the sources configuration when a package is going to be built in the local cache: - remove old sources if dirty - do a copy of the exports_sources folders to the source folder in the cache - run the source() recipe method """ if is_dirty(conanfile.folders.base_source): conanfile.output.warning("Trying to remove corrupted source folder") conanfile.output.warning("This can take a while for big packages") rmdir(conanfile.folders.base_source) clean_dirty(conanfile.folders.base_source) if not os.path.exists(conanfile.folders.base_source): # No source folder, need to get it with set_dirty_context_manager(conanfile.folders.base_source): mkdir(conanfile.source_folder) mkdir(conanfile.recipe_metadata_folder) # First of all get the exported scm sources (if auto) or clone (if fixed) # Now move the export-sources to the right location merge_directories(export_source_folder, conanfile.folders.base_source) if getattr(conanfile, "source_buildenv", False): with VirtualBuildEnv(conanfile, auto_generate=True).vars().apply(): run_source_method(conanfile, hook_manager) else: run_source_method(conanfile, hook_manager) ================================================ FILE: conan/internal/subsystems.py ================================================ """ Potential scenarios: - Running from a Windows native "cmd" - Targeting Windows native (os.subsystem = None) - No need of bash (no conf at all) - Need to build in bash (tools.microsoft.bash:subsystem=xxx, tools.microsoft.bash:path=, conanfile.win_bash) - Need to run (tests) in bash (tools.microsoft.bash:subsystem=xxx, tools.microsoft.bash:path=, conanfile.win_bash_run) - Targeting Subsystem (os.subsystem = msys2/cygwin) - Always builds and runs in bash (tools.microsoft.bash:path) - Running from a subsytem terminal (tools.microsoft.bash:subsystem=xxx, tools.microsoft.bash:path=None) NO ERROR mode for not specifying it? =CURRENT? - Targeting Windows native (os.subsystem = None) - Targeting Subsystem (os.subsystem = msys2/cygwin) """ import os import platform import re from conan.tools.build import cmd_args_to_string from conan.errors import ConanException WINDOWS = "windows" MSYS2 = 'msys2' MSYS = 'msys' CYGWIN = 'cygwin' WSL = 'wsl' # Windows Subsystem for Linux def command_env_wrapper(conanfile, command, envfiles, envfiles_folder, scope="build"): from conan.tools.env.environment import environment_wrap_command if getattr(conanfile, "conf", None) is None: # TODO: No conf, no profile defined!! This happens at ``export()`` time # Is it possible to run a self.run() in export() in bash? # Is it necessary? Shouldn't be return command active = conanfile.conf.get("tools.microsoft.bash:active", check_type=bool) subsystem = conanfile.conf.get("tools.microsoft.bash:subsystem") if platform.system() == "Windows" and ( (conanfile.win_bash and scope == "build") or (conanfile.win_bash_run and scope == "run")): if subsystem is None: raise ConanException("win_bash/win_bash_run defined but no " "tools.microsoft.bash:subsystem") if active: wrapped_cmd = environment_wrap_command(conanfile, envfiles, envfiles_folder, command) else: wrapped_cmd = _windows_bash_wrapper(conanfile, command, envfiles, envfiles_folder) else: wrapped_cmd = environment_wrap_command(conanfile, envfiles, envfiles_folder, command) return wrapped_cmd def _windows_bash_wrapper(conanfile, command, env, envfiles_folder): from conan.tools.env import Environment from conan.tools.env.environment import environment_wrap_command """ Will wrap a unix command inside a bash terminal It requires to have MSYS2, CYGWIN, or WSL""" subsystem = conanfile.conf.get("tools.microsoft.bash:subsystem") if not platform.system() == "Windows": raise ConanException("Command only for Windows operating system") shell_path = conanfile.conf.get("tools.microsoft.bash:path") if not shell_path: raise ConanException("The config 'tools.microsoft.bash:path' is " "needed to run commands in a Windows subsystem") shell_path = shell_path.replace("\\", "/") # Should work in all terminals env = env or [] if subsystem == MSYS2: # Configure MSYS2 to inherith the PATH msys2_mode_env = Environment() _msystem = {"x86": "MINGW32"}.get(conanfile.settings.get_safe("arch"), "MINGW64") # https://www.msys2.org/wiki/Launchers/ dictates that the shell should be launched with # - MSYSTEM defined # - CHERE_INVOKING is necessary to keep the CWD and not change automatically to the user home msys2_mode_env.define("MSYSTEM", _msystem) msys2_mode_env.define("MSYS2_PATH_TYPE", "inherit") msys2_mode_env.unset("ORIGINAL_PATH") # So --login do not change automatically to the user home msys2_mode_env.define("CHERE_INVOKING", "1") path = os.path.join(conanfile.generators_folder, "msys2_mode.bat") # Make sure we save pure .bat files, without sh stuff wb, conanfile.win_bash = conanfile.win_bash, None msys2_mode_env.vars(conanfile, "build").save_bat(path) conanfile.win_bash = wb env.append(path) wrapped_shell = '"%s"' % shell_path if " " in shell_path else shell_path wrapped_shell = environment_wrap_command(conanfile, env, envfiles_folder, wrapped_shell, accepted_extensions=("bat", "ps1")) # Wrapping the inside_command enable to prioritize our environment, otherwise /usr/bin go # first and there could be commands that we want to skip wrapped_user_cmd = environment_wrap_command(conanfile, env, envfiles_folder, command, accepted_extensions=("sh", )) wrapped_user_cmd = _escape_windows_cmd(wrapped_user_cmd) # according to https://www.msys2.org/wiki/Launchers/, it is necessary to use --login shell # running without it is discouraged final_command = '{} --login -c {}'.format(wrapped_shell, wrapped_user_cmd) return final_command def _escape_windows_cmd(command): """ To use in a regular windows cmd.exe 1. Adds escapes so the argument can be unpacked by CommandLineToArgvW() 2. Adds escapes for cmd.exe so the argument survives cmd.exe's substitutions. Useful to escape commands to be executed in a windows bash (msys2, cygwin etc) """ quoted_arg = cmd_args_to_string([command]) return "".join(["^%s" % arg if arg in r'()%!^"<>&|' else arg for arg in quoted_arg]) def deduce_subsystem(conanfile, scope): """ used by: - EnvVars: to decide if using : ; as path separator, translate paths to subsystem and decide to generate a .bat or .sh - Autotools: to define the full abs path to the "configure" script - GnuDeps: to map all the paths from dependencies - Aggregation of envfiles: to map each aggregated path to the subsystem - unix_path: util for recipes """ scope = "build" if scope is None else scope # let's assume build context if scope=None if scope.startswith("build"): the_os = conanfile.settings_build.get_safe("os") if the_os is None: raise ConanException("The 'build' profile must have a 'os' declared") else: the_os = conanfile.settings.get_safe("os") if not str(the_os).startswith("Windows"): return None subsystem = conanfile.conf.get("tools.microsoft.bash:subsystem") if not subsystem: if conanfile.win_bash: raise ConanException("win_bash=True but tools.microsoft.bash:subsystem " "configuration not defined") if conanfile.win_bash_run: raise ConanException("win_bash_run=True but tools.microsoft.bash:subsystem " "configuration not defined") return WINDOWS active = conanfile.conf.get("tools.microsoft.bash:active", check_type=bool) if active: return subsystem if scope.startswith("build"): # "run" scope do not follow win_bash if conanfile.win_bash: return subsystem elif scope.startswith("run"): if conanfile.win_bash_run: return subsystem return WINDOWS def subsystem_path(subsystem, path): """"Used to translate windows paths to MSYS unix paths like c/users/path/to/file. Not working in a regular console or MinGW! """ if subsystem is None or subsystem == WINDOWS: return path if os.path.exists(path): # if the path doesn't exist (and abs) we cannot guess the casing path = get_cased_path(path) if path.startswith('\\\\?\\'): path = path[4:] path = path.replace(":/", ":\\") append_prefix = re.match(r'[a-z]:\\', path, re.IGNORECASE) pattern = re.compile(r'([a-z]):\\', re.IGNORECASE) path = pattern.sub('/\\1/', path).replace('\\', '/') if append_prefix: if subsystem in (MSYS, MSYS2): return path.lower() elif subsystem == CYGWIN: return '/cygdrive' + path.lower() elif subsystem == WSL: return '/mnt' + path[0:2].lower() + path[2:] else: return path if subsystem == WSL else path.lower() return None def get_cased_path(name): if platform.system() != "Windows": return name if not os.path.isabs(name): name = os.path.abspath(name) result = [] current = name while True: parent, child = os.path.split(current) if parent == current: break child_cased = child if os.path.exists(parent): children = os.listdir(parent) for c in children: if c.upper() == child.upper(): child_cased = c break result.append(child_cased) current = parent drive, _ = os.path.splitdrive(current) result.append(drive) return os.sep.join(reversed(result)) ================================================ FILE: conan/internal/util/__init__.py ================================================ import math import multiprocessing import os from conan.internal.util.files import load def cpu_count(): try: try: # This is necessary to deduce docker cpu_count cfs_quota_us = cfs_period_us = 0 # cgroup2 if os.path.exists("/sys/fs/cgroup/cgroup.controllers"): cpu_max = load("/sys/fs/cgroup/cpu.max").split() if cpu_max and cpu_max[0] != "max": if len(cpu_max) == 1: cfs_quota_us, cfs_period_us = int(cpu_max[0]), 100_000 else: cfs_quota_us, cfs_period_us = map(int, cpu_max) else: # cgroup1 cfs_quota_us = int(load("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")) cfs_period_us = int(load("/sys/fs/cgroup/cpu/cpu.cfs_period_us")) if cfs_quota_us > 0 and cfs_period_us > 0: return int(math.ceil(cfs_quota_us / cfs_period_us)) except (EnvironmentError, TypeError): pass return multiprocessing.cpu_count() except NotImplementedError: # print("multiprocessing.cpu_count() not implemented. Defaulting to 1 cpu") return 1 # Safe guess ================================================ FILE: conan/internal/util/config_parser.py ================================================ import re from conan.errors import ConanException class TextINIParse: """ util class to load a file with sections as [section1] checking the values of those sections, and returns each section as parser.section Currently used in ConanInfo and ConanFileTextLoader """ def __init__(self, text, allowed_fields=None, strip_comments=False): self._sections = {} self._allowed_fields = allowed_fields or [] pattern = re.compile(r"^\[([a-z_]{2,50})]") current_lines = None for line in text.splitlines(): line = line.strip() if not line or line[0] == '#': continue if line[0] == '[': m = pattern.match(line) if not m: raise ConanException("ConfigParser: Bad syntax '%s'" % line) field = m.group(1) if self._allowed_fields and field not in self._allowed_fields: raise ConanException("ConfigParser: Unrecognized field '%s'" % field) current_lines = [] # Duplicated section if field in self._sections: raise ConanException(f"ConfigParser: Duplicated section: [{field}]") self._sections[field] = current_lines else: if current_lines is None: raise ConanException("ConfigParser: Unexpected line '%s'" % line) if strip_comments: line = line.split(' #', 1)[0] line = line.split(' #', 1)[0] line = line.strip() current_lines.append(line) def line_items(self): # Used atm by load_binary_info() return self._sections.items() def __getattr__(self, name): if name in self._sections: return "\n".join(self._sections[name]) else: if self._allowed_fields and name not in self._allowed_fields: raise ConanException("ConfigParser: Unrecognized field '%s'" % name) return "" ================================================ FILE: conan/internal/util/dates.py ================================================ import calendar import datetime import time from dateutil import parser def from_timestamp_to_iso8601(timestamp): # Used exclusively by conan_server to return the date in iso format (same as artifactory) return "%s" % datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).isoformat() def _from_iso8601_to_datetime(iso_str): return parser.isoparse(iso_str) def from_iso8601_to_timestamp(iso_str): # used by RestClient v2 to transform from HTTP API (iso) to Conan internal timestamp datetime_time = _from_iso8601_to_datetime(iso_str) return datetime_time.timestamp() def timestamp_now(): # seconds since epoch 0, easy to store, in UTC # Used in Manifest timestamp, in packagesDB LRU and in timestamp of backup-sources json return calendar.timegm(time.gmtime()) def revision_timestamp_now(): return time.time() def timestamp_to_str(timestamp): # used by ref.repr_humantime() to print human readable time assert timestamp is not None return datetime.datetime.fromtimestamp(int(timestamp), datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') ================================================ FILE: conan/internal/util/files.py ================================================ import errno import hashlib import os import platform import shutil import stat import sys import tarfile import time from contextlib import contextmanager from conan.api.output import ConanOutput from conan.errors import ConanException _DIRTY_FOLDER = ".dirty" def set_dirty(folder): dirty_file = os.path.normpath(folder) + _DIRTY_FOLDER assert not os.path.exists(dirty_file), "Folder '{}' is already dirty".format(folder) save(dirty_file, "") def clean_dirty(folder): dirty_file = os.path.normpath(folder) + _DIRTY_FOLDER os.remove(dirty_file) def is_dirty(folder): dirty_file = os.path.normpath(folder) + _DIRTY_FOLDER return os.path.exists(dirty_file) def remove_if_dirty(item): # TODO: Apply to other places this pattern is common if is_dirty(item): if os.path.exists(item): # To avoid circular import in conan_server from conan.api.output import ConanOutput ConanOutput().warning(f"{item} is dirty, removing it") if os.path.isfile(item): os.remove(item) else: rmdir(item) clean_dirty(item) return True return False @contextmanager def set_dirty_context_manager(folder): set_dirty(folder) yield clean_dirty(folder) @contextmanager def chdir(newdir): old_path = os.getcwd() os.chdir(newdir) try: yield finally: os.chdir(old_path) def md5(content): try: md5alg = hashlib.md5() except ValueError: # FIPS error https://github.com/conan-io/conan/issues/7800 md5alg = hashlib.md5(usedforsecurity=False) if isinstance(content, bytes): tmp = content else: tmp = content.encode("utf-8") md5alg.update(tmp) return md5alg.hexdigest() def md5sum(file_path): return _generic_algorithm_sum(file_path, "md5") def sha1sum(file_path): return _generic_algorithm_sum(file_path, "sha1") def sha256sum(file_path): return _generic_algorithm_sum(file_path, "sha256") def _generic_algorithm_sum(file_path, algorithm_name): with open(file_path, 'rb') as fh: try: m = hashlib.new(algorithm_name) except ValueError: # FIPS error https://github.com/conan-io/conan/issues/7800 m = hashlib.new(algorithm_name, usedforsecurity=False) while True: data = fh.read(8192) if not data: break m.update(data) return m.hexdigest() def check_with_algorithm_sum(algorithm_name, file_path, provided_hash): real_hash = _generic_algorithm_sum(file_path, algorithm_name) if real_hash != provided_hash.lower(): raise ConanException("%s hash failed for '%s' file. \n" " Provided hash: %s \n" " Computed hash: %s" % (algorithm_name, os.path.basename(file_path), provided_hash, real_hash)) def save(path, content, encoding="utf-8"): """ Saves a file with given content Params: path: path to write file to content: contents to save in the file encoding: target file text encoding """ dir_path = os.path.dirname(path) if dir_path: os.makedirs(dir_path, exist_ok=True) with open(path, "w", encoding=encoding, newline="") as handle: handle.write(content) def save_files(path, files, encoding="utf-8"): for name, content in files.items(): save(os.path.join(path, name), content, encoding=encoding) def load(path, encoding="utf-8"): """ Loads a file content """ with open(path, 'r', encoding=encoding, newline="") as handle: tmp = handle.read() return tmp def load_user_encoded(path): """ Exclusive for user side read-only files: - conanfile.txt - profile files """ with open(path, 'rb') as handle: text = handle.read() import codecs encodings = {codecs.BOM_UTF8: "utf_8_sig", codecs.BOM_UTF32_BE: "utf_32_be", codecs.BOM_UTF32_LE: "utf_32_le", codecs.BOM_UTF16_BE: "utf_16_be", codecs.BOM_UTF16_LE: "utf_16_le", b'\x2b\x2f\x76\x38': "utf_7", b'\x2b\x2f\x76\x39': "utf_7", b'\x2b\x2f\x76\x2b': "utf_7", b'\x2b\x2f\x76\x2f': "utf_7", b'\x2b\x2f\x76\x38\x2d': "utf_7"} for bom, encoding in encodings.items(): if text.startswith(bom): return text[len(bom):].decode(encoding) for decoder in ["utf-8", "Windows-1252"]: try: return text.decode(decoder) except UnicodeDecodeError: continue raise Exception(f"Unknown encoding of file: {path}\nIt is recommended to use utf-8 encoding") def _change_permissions(func, path, exc_info): if not os.access(path, os.W_OK): os.chmod(path, stat.S_IWUSR) func(path) else: raise OSError("Cannot change permissions for {}! Exception info: {}".format(path, exc_info)) if platform.system() == "Windows": def rmdir(path): if not os.path.isdir(path): return retries = 3 delay = 0.5 for i in range(retries): try: shutil.rmtree(path, onerror=_change_permissions) break except OSError as err: if i == retries - 1: raise ConanException(f"Couldn't remove folder: {path}\n{str(err)}\n" "Folder might be busy or open. " "Close any app using it and retry.") time.sleep(delay) def renamedir(old_path, new_path): retries = 3 delay = 0.5 for i in range(retries): try: shutil.move(old_path, new_path) break except OSError as err: if i == retries - 1: raise ConanException(f"Couldn't move folder: {old_path}->{new_path}\n" f"{str(err)}\n" "Folder might be busy or open. " "Close any app using it and retry.") time.sleep(delay) else: def rmdir(path): if not os.path.isdir(path): return try: shutil.rmtree(path, onerror=_change_permissions) except OSError as err: raise ConanException(f"Couldn't remove folder: {path}\n{str(err)}\n" "Folder might be busy or open. " "Close any app using it and retry.") def renamedir(old_path, new_path): try: shutil.move(old_path, new_path) except OSError as err: raise ConanException( f"Couldn't move folder: {old_path}->{new_path}\n{str(err)}\n" "Folder might be busy or open. " "Close any app using it and retry.") def remove(path): try: assert os.path.isfile(path) os.remove(path) except (IOError, OSError) as e: # for py3, handle just PermissionError if e.errno == errno.EPERM or e.errno == errno.EACCES: os.chmod(path, stat.S_IRWXU) os.remove(path) return raise def mkdir(path): """Recursive mkdir, doesnt fail if already existing""" if os.path.exists(path): return os.makedirs(path) def tar_extract(fileobj, destination_dir): the_tar = tarfile.open(fileobj=fileobj) # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to # "could not change modification time", with time=0 # the_tar.errorlevel = 2 # raise exception if any error the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break the_tar.extractall(path=destination_dir) the_tar.close() def merge_directories(src, dst): from conan.tools.files import copy copy(None, pattern="*", src=src, dst=dst) def gather_files(folder): file_dict = {} symlinked_folders = {} for root, dirs, files in os.walk(folder): if root != folder and not dirs and not files: # empty folder rel_path = root[len(folder) + 1:].replace("\\", "/") symlinked_folders[rel_path] = root continue for d in dirs: abs_path = os.path.join(root, d) if os.path.islink(abs_path): rel_path = abs_path[len(folder) + 1:].replace("\\", "/") symlinked_folders[rel_path] = abs_path for f in files: if f == ".DS_Store": continue abs_path = os.path.join(root, f) rel_path = abs_path[len(folder) + 1:].replace("\\", "/") file_dict[rel_path] = abs_path return file_dict, symlinked_folders def human_size(size_bytes): """ format a size in bytes into a 'human' file size, e.g. B, KB, MB, GB, TB, PB Note that bytes will be reported in whole numbers but KB and above will have greater precision. e.g. 43 B, 443 KB, 4.3 MB, 4.43 GB, etc """ unit_size = 1000.0 suffixes_table = [('B', 0), ('KB', 1), ('MB', 1), ('GB', 2), ('TB', 2), ('PB', 2)] num = float(size_bytes) the_precision = None the_suffix = None for suffix, precision in suffixes_table: the_precision = precision the_suffix = suffix if num < unit_size: break num /= unit_size if the_precision == 0: formatted_size = "%d" % num else: formatted_size = str(round(num, ndigits=the_precision)) return "%s%s" % (formatted_size, the_suffix) # FIXME: completely remove disutils once we don't support <3.8 any more def copytree_compat(source_folder, dest_folder): if sys.version_info >= (3, 8): shutil.copytree(source_folder, dest_folder, dirs_exist_ok=True) else: from distutils.dir_util import copy_tree copy_tree(source_folder, dest_folder) ================================================ FILE: conan/internal/util/runners.py ================================================ import os import subprocess import sys import tempfile from contextlib import contextmanager from io import StringIO from conan.errors import ConanException from conan.internal.util.files import load if getattr(sys, 'frozen', False) and 'LD_LIBRARY_PATH' in os.environ: # http://pyinstaller.readthedocs.io/en/stable/runtime-information.html#ld-library-path-libpath-considerations pyinstaller_bundle_dir = os.environ['LD_LIBRARY_PATH'].replace( os.environ.get('LD_LIBRARY_PATH_ORIG', ''), '' ).strip(';:') @contextmanager def pyinstaller_bundle_env_cleaned(): """Removes the pyinstaller bundle directory from LD_LIBRARY_PATH """ ld_library_path = os.environ['LD_LIBRARY_PATH'] os.environ['LD_LIBRARY_PATH'] = ld_library_path.replace(pyinstaller_bundle_dir, '').strip(';:') yield os.environ['LD_LIBRARY_PATH'] = ld_library_path else: @contextmanager def pyinstaller_bundle_env_cleaned(): yield def conan_run(command, stdout=None, stderr=None, cwd=None, shell=True): """ @param shell: @param stderr: @param command: Command to execute @param stdout: Instead of print to sys.stdout print to that stream. Could be None @param cwd: Move to directory to execute """ stdout = stdout or sys.stderr stderr = stderr or sys.stderr out = subprocess.PIPE if isinstance(stdout, StringIO) else stdout err = subprocess.PIPE if isinstance(stderr, StringIO) else stderr with pyinstaller_bundle_env_cleaned(): try: proc = subprocess.Popen(command, shell=shell, stdout=out, stderr=err, cwd=cwd) except Exception as e: raise ConanException("Error while running cmd\nError: %s" % (str(e))) proc_stdout, proc_stderr = proc.communicate() # If the output is piped, like user provided a StringIO or testing, the communicate # will capture and return something when thing finished if proc_stdout: stdout.write(proc_stdout.decode("utf-8", errors="ignore")) if proc_stderr: stderr.write(proc_stderr.decode("utf-8", errors="ignore")) return proc.returncode def detect_runner(command): # Running detect.py automatic detection of profile proc = subprocess.Popen(command, shell=True, bufsize=1, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output_buffer = [] while True: line = proc.stdout.readline() if not line: break # output.write(line) output_buffer.append(str(line)) proc.communicate() return proc.returncode, "".join(output_buffer) def check_output_runner(cmd, stderr=None, ignore_error=False): # Used to run several utilities, like Pacman detect, AIX version, uname, SCM assert isinstance(cmd, str) d = tempfile.mkdtemp() tmp_file = os.path.join(d, "output") try: # We don't want stderr to print warnings that will mess the pristine outputs stderr = stderr or subprocess.PIPE command = '{} > "{}"'.format(cmd, tmp_file) process = subprocess.Popen(command, shell=True, stderr=stderr) stdout, stderr = process.communicate() if process.returncode and not ignore_error: # Only in case of error, we print also the stderr to know what happened msg = f"Command '{cmd}' failed with errorcode '{process.returncode}'\n{stderr}" raise ConanException(msg) output = load(tmp_file) return output finally: try: os.unlink(tmp_file) except OSError: pass ================================================ FILE: conan/test/__init__.py ================================================ ================================================ FILE: conan/test/assets/__init__.py ================================================ ================================================ FILE: conan/test/assets/autotools.py ================================================ import textwrap from jinja2 import Template _makefile_am = """ {% if main %} bin_PROGRAMS = {{ main }} {{main}}_SOURCES = {{ main_srcs }} {% endif %} {% if lib %} lib_LIBRARIES = {{ lib }} {{lib.replace(".", "_")}}_SOURCES = {{ lib_srcs }} {% endif %} {% if main and lib %} {{main}}_LDADD = {{ lib }} {% endif %} """ # newline at the end is important: m4: INTERNAL ERROR: recursive push_string! def gen_makefile_am(**context): t = Template(_makefile_am) return t.render(**context) _configure_ac = """ AC_INIT([main], [1.0], [some@email.com]) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_PROG_CXX AC_PROG_RANLIB AM_PROG_AR AC_CONFIG_FILES([Makefile]) AC_OUTPUT """ # newline at the end is important: m4: INTERNAL ERROR: recursive push_string! def gen_configure_ac(**context): t = Template(_configure_ac) return t.render(**context) def gen_makefile(**context): makefile = textwrap.dedent("""\ .PHONY: all all: apps libs apps: {% for s in apps %} {{s}} {% endfor %} libs: {% for s in libs %} lib{{s}}.a {% endfor %} {%- set link_libs = namespace(str='') %} {% for lib in libs %} {%- set link_libs.str = link_libs.str + ' -l' + lib|string %} lib{{lib}}.a: {{lib}}.o $(AR) rcs lib{{lib}}.a {{lib}}.o {%if static_runtime%}--static{%endif%} {% endfor %} {% for s in apps %} {{s}}: {{s}}.o libs $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -o {{s}} {{s}}.o $(LIBS) {{link_libs.str}} -L. {%if static_runtime%}--static{%endif%} {% endfor %} """) t = Template(makefile) return t.render(**context) ================================================ FILE: conan/test/assets/cmake.py ================================================ import textwrap from jinja2 import Template def gen_cmakelists(language="CXX", verify=True, project="project", libname="mylibrary", libsources=None, appname="myapp", appsources=None, cmake_version="3.15", install=False, find_package=None, libtype="", deps=None, public_header=None, custom_content=None): """ language: C, C++, C/C++ project: the project name """ cmake = textwrap.dedent("""\ {% if verify %} set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) {% endif %} cmake_minimum_required(VERSION {{cmake_version}}) project({{project}} {{language}}) {% if find_package is mapping %} {% for s, c in find_package.items() %} find_package({{s}} COMPONENTS {{c}} ) {% endfor %} {% else %} {% for s in find_package %} find_package({{s}}) {% endfor %} {% endif %} {% if libsources %} add_library({{libname}} {{libtype}} {% for s in libsources %} {{s}} {% endfor %}) target_include_directories({{libname}} PUBLIC "include") {% endif %} {% if libsources and find_package %} {% if find_package is mapping %} target_link_libraries({{libname}} {% for s, c in find_package.items() %} {{s}}::{{c}} {% endfor %}) {% else %} target_link_libraries({{libname}} {% for s in find_package %} {{s}}::{{s}} {% endfor %}) {% endif %} {% endif %} {% if libsources and deps %} target_link_libraries({{libname}} {% for s in deps %} {{s}} {% endfor %}) {% endif %} {% if appsources %} add_executable({{appname}} {% for s in appsources %} {{s}} {% endfor %}) target_include_directories({{appname}} PUBLIC "include") {% endif %} {% if appsources and libsources %} target_link_libraries({{appname}} {{libname}}) {% endif %} {% if appsources and not libsources and find_package %} {% if find_package is mapping %} target_link_libraries({{appname}} {% for s, c in find_package.items() %} {{s}}::{{c}} {% endfor %}) {% else %} target_link_libraries({{appname}} {% for s in find_package %} {{s}}::{{s}} {% endfor %}) {% endif %} {% endif %} {% if appsources and deps %} target_link_libraries({{appname}} {% for s in deps %} {{s}} {% endfor %}) {% endif %} {% if libsources and public_header %} set_target_properties({{libname}} PROPERTIES PUBLIC_HEADER "{{public_header}}") {% endif %} {% if install %} {% if appsources %} install(TARGETS {{appname}}) {% endif %} {% if libsources %} install(TARGETS {{libname}}) {% endif %} {% endif %} {% if custom_content %} {{custom_content}} {% endif %} """) t = Template(cmake, trim_blocks=True, lstrip_blocks=True) return t.render({"verify": verify, "language": language, "project": project, "libname": libname, "libsources": libsources, "appname": appname, "appsources": appsources, "cmake_version": cmake_version, "install": install, "find_package": find_package or [], "libtype": libtype, "public_header": public_header, "deps": deps, "custom_content": custom_content }) ================================================ FILE: conan/test/assets/genconanfile.py ================================================ from conan.api.model import RecipeReference class GenConanfile: """ USAGE: x = GenConanfile().with_import("import os").\ with_setting("os").\ with_option("shared", [True, False]).\ with_default_option("shared", True).\ with_build_msg("holaaa").\ with_build_msg("adiooos").\ with_package_file("file.txt", "hola").\ with_package_file("file2.txt", "hola") """ def __init__(self, name=None, version=None): self._imports = ["from conan import ConanFile"] self._name = name self._version = version self._package_type = None self._settings = None self._options = None self._generators = None self._default_options = None self._provides = None self._deprecated = None self._package_lines = None self._finalize_lines = None self._package_files = None self._package_files_env = None self._build_messages = None self._requires = None self._requirements = None self._python_requires = None self._build_requires = None self._build_requirements = None self._tool_requires = None self._tool_requirements = None self._test_requirements = None self._test_requires = None self._revision_mode = None self._package_info = None self._package_id_lines = None self._test_lines = None self._exports_sources = None self._exports = None self._cmake_build = False self._class_attributes = None self._is_tested_ref_build_require = None def with_package_type(self, value): self._package_type = value return self def with_name(self, name): self._name = name return self def with_version(self, version): self._version = version return self def with_provides(self, provides): self._provides = self._provides or [] self._provides.append(provides) return self def with_deprecated(self, deprecated): self._deprecated = deprecated return self def with_revision_mode(self, revision_mode): self._revision_mode = revision_mode return self def with_generator(self, generator): self._generators = self._generators or [] self._generators.append(generator) return self def with_exports_sources(self, *exports): self._exports_sources = self._exports_sources or [] for export in exports: self._exports_sources.append(export) return self def with_exports(self, *exports): self._exports = self._exports or [] for export in exports: self._exports.append(export) return self def with_require(self, ref): self._requires = self._requires or [] ref_str = self._get_full_ref_str(ref) self._requires.append(ref_str) return self def with_requires(self, *refs): for ref in refs: self.with_require(ref) return self @staticmethod def _get_full_ref_str(ref): if isinstance(ref, RecipeReference): ref_str = ref.repr_notime() else: ref_str = ref return ref_str def with_requirement(self, ref, **kwargs): self._requirements = self._requirements or [] ref_str = self._get_full_ref_str(ref) self._requirements.append((ref_str, kwargs)) return self def with_build_requires(self, *refs): self._build_requires = self._build_requires or [] for ref in refs: ref_str = self._get_full_ref_str(ref) self._build_requires.append(ref_str) return self def with_python_requires(self, *refs): self._python_requires = self._python_requires or [] for ref in refs: ref_str = self._get_full_ref_str(ref) self._python_requires.append(ref_str) return self def with_tool_requires(self, *refs): self._tool_requires = self._tool_requires or [] for ref in refs: ref_str = self._get_full_ref_str(ref) self._tool_requires.append(ref_str) return self def with_test_requires(self, *refs): self._test_requires = self._test_requires or [] for ref in refs: ref_str = self._get_full_ref_str(ref) self._test_requires.append(ref_str) return self def with_test_reference_as_build_require(self): self._is_tested_ref_build_require = True return self def with_build_requirement(self, ref, **kwargs): self._build_requirements = self._build_requirements or [] ref_str = self._get_full_ref_str(ref) self._build_requirements.append((ref_str, kwargs)) return self def with_tool_requirement(self, ref, **kwargs): self._tool_requirements = self._tool_requirements or [] ref_str = self._get_full_ref_str(ref) self._tool_requirements.append((ref_str, kwargs)) return self def with_test_requirement(self, ref, **kwargs): self._test_requirements = self._test_requirements or [] ref_str = self._get_full_ref_str(ref) self._test_requirements.append((ref_str, kwargs)) return self def with_import(self, *imports): for i in imports: if i not in self._imports: self._imports.append(i) return self def with_setting(self, setting): self._settings = self._settings or [] self._settings.append(setting) return self def with_settings(self, *settings): self._settings = self._settings or [] self._settings.extend(settings) return self def with_option(self, option_name, values, default=None): self._options = self._options or {} self._options[option_name] = values if default is not None: self.with_default_option(option_name, default) return self def with_default_option(self, option_name, value): self._default_options = self._default_options or {} self._default_options[option_name] = value return self def with_shared_option(self, default=False): return self.with_option("shared", [True, False]).with_default_option("shared", default) def with_package_file(self, file_name, contents=None, env_var=None): assert contents is not None or env_var self._package_files = self._package_files or {} self._package_files_env = self._package_files_env or {} self.with_import("import os") self.with_import("from conan.tools.files import save, chdir") if contents: self._package_files[file_name] = contents if env_var: self._package_files_env[file_name] = env_var return self def with_package(self, *lines): self._package_lines = self._package_lines or [] for line in lines: self._package_lines.append(line) return self def with_finalize(self, *lines): self._finalize_lines = self._finalize_lines or [] for line in lines: self._finalize_lines.append(line) return self def with_build_msg(self, msg): self._build_messages = self._build_messages or [] self._build_messages.append(msg) return self def with_package_info(self, cpp_info=None): assert cpp_info is None or isinstance(cpp_info, dict) self._package_info = self._package_info or {} if cpp_info: self._package_info["cpp_info"] = cpp_info return self def with_package_id(self, *lines): self._package_id_lines = self._package_id_lines or [] for line in lines: self._package_id_lines.append(line) return self def with_test(self, *lines): self._test_lines = self._test_lines or [] for line in lines: self._test_lines.append(line) return self def with_cmake_build(self): self._imports.append("from conan.tools.cmake import CMake") self._generators = self._generators or [] self._generators.append("CMakeDeps") self._generators.append("CMakeToolchain") self.with_settings("os", "compiler", "arch", "build_type") self._cmake_build = True return self def with_class_attribute(self, attr): """.with_class_attribute("no_copy_sources=True") """ self._class_attributes = self._class_attributes or [] self._class_attributes.append(attr) return self @property def _name_render(self): return "name = '{}'".format(self._name) @property def _version_render(self): return "version = '{}'".format(self._version) @property def _package_type_render(self): return "package_type = '{}'".format(self._package_type) @property def _provides_render(self): line = ", ".join('"{}"'.format(provide) for provide in self._provides) return "provides = {}".format(line) @property def _deprecated_render(self): return "deprecated = {}".format(self._deprecated) @property def _generators_render(self): line = ", ".join('"{}"'.format(generator) for generator in self._generators) return "generators = {}".format(line) @property def _revision_mode_render(self): line = "revision_mode=\"{}\"".format(self._revision_mode) return line @property def _settings_render(self): line = ", ".join('"%s"' % s for s in self._settings) return "settings = {}".format(line) @property def _options_render(self): line = ", ".join('"%s": %s' % (k, v) for k, v in self._options.items()) tmp = "options = {%s}" % line return tmp @property def _default_options_render(self): line = ", ".join('"%s": %s' % (k, v) for k, v in self._default_options.items()) tmp = "default_options = {%s}" % line return tmp @property def _build_requires_render(self): line = ", ".join(['"{}"'.format(r) for r in self._build_requires]) tmp = "build_requires = %s" % line return tmp @property def _python_requires_render(self): line = ", ".join(['"{}"'.format(r) for r in self._python_requires]) tmp = "python_requires = %s" % line return tmp @property def _tool_requires_render(self): line = ", ".join(['"{}"'.format(r) for r in self._tool_requires]) tmp = "tool_requires = %s" % line return tmp @property def _requires_render(self): items = [] for ref in self._requires: items.append('"{}"'.format(ref)) return "requires = ({}, )".format(", ".join(items)) @property def _test_requires_render(self): line = ", ".join(['"{}"'.format(r) for r in self._test_requires]) tmp = "test_requires = {}".format(line) return tmp @property def _requirements_render(self): lines = ["", " def requirements(self):"] for ref, kwargs in self._requirements or []: args = ", ".join("{}={}".format(k, f'"{v}"' if isinstance(v, str) else v) for k, v in kwargs.items()) lines.append(' self.requires("{}", {})'.format(ref, args)) for ref, kwargs in self._build_requirements or []: args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, (bool, dict)) else v) for k, v in kwargs.items()) lines.append(' self.build_requires("{}", {})'.format(ref, args)) for ref, kwargs in self._tool_requirements or []: args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, (bool, dict)) else v) for k, v in kwargs.items()) lines.append(' self.tool_requires("{}", {})'.format(ref, args)) for ref, kwargs in self._test_requirements or []: args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, (bool, dict)) else v) for k, v in kwargs.items()) lines.append(' self.test_requires("{}", {})'.format(ref, args)) return "\n".join(lines) @property def _package_method(self): return self._package_lines or self._package_files or self._package_files_env @property def _finalize_method(self): return self._finalize_lines @property def _package_method_render(self): lines = [] if self._package_lines: lines.extend(" {}".format(line) for line in self._package_lines) if self._package_files: lines = [' save(self, os.path.join(self.package_folder, "{}"), "{}")' ''.format(key, value) for key, value in self._package_files.items()] if self._package_files_env: lines.extend([' save(self, os.path.join(self.package_folder, "{}"), ' 'os.getenv("{}"))'.format(key, value) for key, value in self._package_files_env.items()]) return """ def package(self): {} """.format("\n".join(lines)) @property def _finalize_method_render(self): lines = [] if self._finalize_lines: lines.extend(" {}".format(line) for line in self._finalize_lines) return """ def finalize(self): {} """.format("\n".join(lines)) @property def _build_render(self): if not self._build_messages and not self._cmake_build: return None lines = [] if self._build_messages: lines = [' self.output.warning("{}")'.format(m) for m in self._build_messages] if self._cmake_build: lines.extend([' cmake = CMake(self)', ' cmake.configure()', ' cmake.build()']) return """ def build(self): {} """.format("\n".join(lines)) @property def _package_info_render(self): lines = [] if "cpp_info" in self._package_info: for k, v in self._package_info["cpp_info"].items(): if k == "components": for comp_name, comp in v.items(): for comp_attr_name, comp_attr_value in comp.items(): lines.append(' self.cpp_info.components["{}"].{} = {}'.format( comp_name, comp_attr_name, str(comp_attr_value))) else: lines.append(' self.cpp_info.{} = {}'.format(k, str(v))) return """ def package_info(self): {} """.format("\n".join(lines)) @property def _package_id_lines_render(self): lines = [' {}'.format(line) for line in self._package_id_lines] return """ def package_id(self): {} """.format("\n".join(lines)) @property def _test_lines_render(self): if self._is_tested_ref_build_require: lines = ["", " def build_requirements(self):", ' self.tool_requires(self.tested_reference_str)', "", ' def test(self):'] else: lines = ["", " def requirements(self):", ' self.requires(self.tested_reference_str)', "", ' def test(self):'] lines += [' %s' % m for m in self._test_lines] return "\n".join(lines) @property def _exports_sources_render(self): line = ", ".join('"{}"'.format(e) for e in self._exports_sources) return "exports_sources = {}".format(line) @property def _exports_render(self): line = ", ".join('"{}"'.format(e) for e in self._exports) return "exports = {}".format(line) @property def _class_attributes_render(self): self._class_attributes = self._class_attributes or [] return [" {}".format(a) for a in self._class_attributes] def __repr__(self): ret = [] ret.extend(self._imports) ret.append("class HelloConan(ConanFile):") for member in ("name", "version", "package_type", "provides", "deprecated", "exports_sources", "exports", "generators", "requires", "build_requires", "tool_requires", "test_requires", "requirements", "python_requires", "revision_mode", "settings", "options", "default_options", "build", "package_method", "package_info", "package_id_lines", "test_lines", "finalize_method" ): if member == "requirements": # FIXME: This seems exclusive, but we could mix them? v = self._requirements or self._tool_requirements or self._build_requirements else: v = getattr(self, "_{}".format(member), None) if v is not None: ret.append(" {}".format(getattr(self, "_{}_render".format(member)))) ret.extend(self._class_attributes_render) build = self._build_render if build is not None: ret.append(" {}".format(self._build_render)) if ret[-1] == "class HelloConan(ConanFile):": ret.append(" pass") return "\n".join(ret) ================================================ FILE: conan/test/assets/premake.py ================================================ import textwrap from jinja2 import Template def gen_premake5(workspace, projects, includedirs=None, configurations=None): includedirs = includedirs if includedirs is not None else ["."] premake5 = textwrap.dedent("""\ workspace "{{workspace}}" cppdialect "C++17" configurations {{premake_quote(configurations)}} fatalwarnings {"All"} floatingpoint "Fast" includedirs {{premake_quote(includedirs)}} filter "configurations:Debug" defines { "DEBUG" } symbols "On" filter "configurations:Release" defines { "NDEBUG" } optimize "On" {% for project in projects %} project "{{project["name"]}}" kind "{{project["kind"] or 'StaticLib'}}" language "{{project["language"] or 'C++'}}" files {{premake_quote(project["files"])}} links {{premake_quote(project["links"])}} {% endfor %} """) t = Template(premake5, trim_blocks=True, lstrip_blocks=True) def premake_quote(s): return '{' + ', '.join(['"{}"'.format(s) for s in s]) + '}' return t.render( { "premake_quote": premake_quote, "workspace": workspace, "configurations": configurations or ["Debug", "Release"], "includedirs": includedirs, "projects": projects, } ) ================================================ FILE: conan/test/assets/sources.py ================================================ from jinja2 import Template _function_cpp = r""" {% if name != "main" %} #include "{{name}}.h" {% endif %} #include {% for it in includes -%} #include "{{it}}.h" {% endfor %} int {{name}}(){ #ifdef NDEBUG std::cout << "{{ msg or name }}: Release!\n"; #else std::cout << "{{ msg or name }}: Debug!\n"; #endif // ARCHITECTURES #ifdef _M_X64 std::cout << " {{ msg or name }} _M_X64 defined\n"; #endif #ifdef _M_IX86 std::cout << " {{ msg or name }} _M_IX86 defined\n"; #endif #ifdef _M_ARM64 std::cout << " {{ msg or name }}: _M_ARM64 defined\n"; #endif #if __i386__ std::cout << " {{ msg or name }} __i386__ defined\n"; #endif #if __x86_64__ std::cout << " {{ msg or name }} __x86_64__ defined\n"; #endif #if __aarch64__ std::cout << " {{ msg or name }} __aarch64__ defined\n"; #endif // Libstdc++ #if defined _GLIBCXX_USE_CXX11_ABI std::cout << " {{ msg or name }} _GLIBCXX_USE_CXX11_ABI "<< _GLIBCXX_USE_CXX11_ABI << "\n"; #endif // COMPILER VERSIONS #if _MSC_VER std::cout << " {{ msg or name }} _MSC_VER" << _MSC_VER<< "\n"; #endif #if _MSVC_LANG std::cout << " {{ msg or name }} _MSVC_LANG" << _MSVC_LANG<< "\n"; #endif #if __cplusplus std::cout << " {{ msg or name }} __cplusplus" << __cplusplus<< "\n"; #endif #if __INTEL_COMPILER std::cout << " {{ msg or name }} __INTEL_COMPILER" << __INTEL_COMPILER<< "\n"; #endif #if __GNUC__ std::cout << " {{ msg or name }} __GNUC__" << __GNUC__<< "\n"; #endif #if __GNUC_MINOR__ std::cout << " {{ msg or name }} __GNUC_MINOR__" << __GNUC_MINOR__<< "\n"; #endif #if __clang_major__ std::cout << " {{ msg or name }} __clang_major__" << __clang_major__<< "\n"; #endif #if __clang_minor__ std::cout << " {{ msg or name }} __clang_minor__" << __clang_minor__<< "\n"; #endif #if __apple_build_version__ std::cout << " {{ msg or name }} __apple_build_version__" << __apple_build_version__<< "\n"; #endif // SUBSYSTEMS #if __MSYS__ std::cout << " {{ msg or name }} __MSYS__" << __MSYS__<< "\n"; #endif #if __MINGW32__ std::cout << " {{ msg or name }} __MINGW32__" << __MINGW32__<< "\n"; #endif #if __MINGW64__ std::cout << " {{ msg or name }} __MINGW64__" << __MINGW64__<< "\n"; #endif #if __CYGWIN__ std::cout << " {{ msg or name }} __CYGWIN__" << __CYGWIN__<< "\n"; #endif {% for it in preprocessor -%} std::cout << " {{msg}} {{it}}: " << {{it}} << "\n"; {%- endfor %} {% for it in calls -%} {{it}}(); {% endfor %} return 0; } """ def gen_function_cpp(**context): t = Template(_function_cpp) return t.render(**context) _function_c = r""" #define STRINGIFY_(x) #x #define STRINGIFY(y) STRINGIFY_(y) {% if name != "main" %} #include "{{name}}.h" {% endif %} #include {% for it in includes -%} #include "{{it}}.h" {% endfor %} int {{name}}(){ #ifdef NDEBUG printf("{{ msg or name }}: Release!\n"); #else printf("{{ msg or name }}: Debug!\n"); #endif // ARCHITECTURES #ifdef _M_X64 printf(" {{ msg or name }} _M_X64 defined\n"); #endif #ifdef _M_IX86 printf(" {{ msg or name }} _M_IX86 defined\n"); #endif #ifdef _M_ARM64 printf(" {{ msg or name }} _M_ARM64 defined\n"); #endif #if __i386__ printf(" {{ msg or name }} __i386__ defined\n"); #endif #if __x86_64__ printf(" {{ msg or name }} __x86_64__ defined\n"); #endif #if __aarch64__ printf(" {{ msg or name }} __aarch64__ defined\n"); #endif // Libstdc++ #if defined _GLIBCXX_USE_CXX11_ABI printf(" {{ msg or name }} _GLIBCXX_USE_CXX11_ABI %s\n", STRINGIFY(_GLIBCXX_USE_CXX11_ABI)); #endif // COMPILER VERSIONS #if _MSC_VER printf(" {{ msg or name }} _MSC_VER%s\n", STRINGIFY(_MSC_VER)); #endif #if _MSVC_LANG printf(" {{ msg or name }} _MSVC_LANG%s\n", STRINGIFY(_MSVC_LANG)); #endif #if __cplusplus printf(" {{ msg or name }} __cplusplus%s\n", STRINGIFY(__cplusplus)); #endif #if __INTEL_COMPILER printf(" {{ msg or name }} __INTEL_COMPILER%s\n", STRINGIFY(__INTEL_COMPILER)); #endif #if __GNUC__ printf(" {{ msg or name }} __GNUC__%s\n", STRINGIFY(__GNUC__)); #endif #if __GNUC_MINOR__ printf(" {{ msg or name }} __GNUC_MINOR__%s\n", STRINGIFY(__GNUC_MINOR__)); #endif #if __clang_major__ printf(" {{ msg or name }} __clang_major__%s\n", STRINGIFY(__clang_major__)); #endif #if __clang_minor__ printf(" {{ msg or name }} __clang_minor__%s\n", STRINGIFY(__clang_minor__)); #endif #if __apple_build_version__ printf(" {{ msg or name }} __apple_build_version__%s\n", STRINGIFY(__apple_build_version__)); #endif // SUBSYSTEMS #if __MSYS__ printf(" {{ msg or name }} __MSYS__%s\n", STRINGIFY(__MSYS__)); #endif #if __MINGW32__ printf(" {{ msg or name }} __MINGW32__%s\n", STRINGIFY(__MINGW32__)); #endif #if __MINGW64__ printf(" {{ msg or name }} __MINGW64__%s\n", STRINGIFY(__MINGW64__)); #endif #if __CYGWIN__ printf(" {{ msg or name }} __CYGWIN__%s\n", STRINGIFY(__CYGWIN__)); #endif {% for it in preprocessor -%} printf(" {{msg}} {{it}}: %s\n", {{it}} ); {%- endfor %} {% for it in calls -%} {{it}}(); {% endfor %} return 0; } """ def gen_function_c(**context): t = Template(_function_c) return t.render(**context) _function_h = """ #pragma once {% for it in includes -%} #include "{{it}}.h" {%- endfor %} #ifdef _WIN32 #define {{name.upper()}}_EXPORT __declspec(dllexport) #else #define {{name.upper()}}_EXPORT #endif {{name.upper()}}_EXPORT int {{name}}(); """ def gen_function_h(**context): t = Template(_function_h) return t.render(**context) ================================================ FILE: conan/test/assets/visual_project_files.py ================================================ # All harcoded (names, paths etc), refactor it if needed sln_file = r''' Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 VisualStudioVersion = 12.0.31101.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MyProject", "MyProject\MyProject.vcxproj", "{143D99A7-C9F3-434F-BA39-514BB63835E8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM = Debug|ARM Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|ARM = Release|ARM Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {143D99A7-C9F3-434F-BA39-514BB63835E8}.Debug|ARM.ActiveCfg = Debug|ARM {143D99A7-C9F3-434F-BA39-514BB63835E8}.Debug|ARM.Build.0 = Debug|ARM {143D99A7-C9F3-434F-BA39-514BB63835E8}.Debug|x64.ActiveCfg = Debug|x64 {143D99A7-C9F3-434F-BA39-514BB63835E8}.Debug|x64.Build.0 = Debug|x64 {143D99A7-C9F3-434F-BA39-514BB63835E8}.Debug|x86.ActiveCfg = Debug|Win32 {143D99A7-C9F3-434F-BA39-514BB63835E8}.Debug|x86.Build.0 = Debug|Win32 {143D99A7-C9F3-434F-BA39-514BB63835E8}.Release|ARM.ActiveCfg = Release|ARM {143D99A7-C9F3-434F-BA39-514BB63835E8}.Release|ARM.Build.0 = Release|ARM {143D99A7-C9F3-434F-BA39-514BB63835E8}.Release|x64.ActiveCfg = Release|x64 {143D99A7-C9F3-434F-BA39-514BB63835E8}.Release|x64.Build.0 = Release|x64 {143D99A7-C9F3-434F-BA39-514BB63835E8}.Release|x86.ActiveCfg = Release|Win32 {143D99A7-C9F3-434F-BA39-514BB63835E8}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal ''' vcxproj_file = r''' Debug ARM Debug Win32 Debug x64 Release ARM Release Win32 Release x64 {143D99A7-C9F3-434F-BA39-514BB63835E8} MyProject Application true v140 MultiByte Application true v140 MultiByte Application true v140 MultiByte Application false v140 true MultiByte Application false v140 true MultiByte Application false v140 true MultiByte Level3 Disabled true true Level3 Disabled true true Level3 Disabled true true Level3 MaxSpeed true true true true true true Level3 MaxSpeed true true true true true true Level3 MaxSpeed true true true true true true ''' filters_file = ''' {4FC737F1-C7A5-4376-A066-2A32D752A2FF} cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hh;hpp;hxx;hm;inl;inc;xsd {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms Source Files ''' main_file = '''#include int main() { std::cout << "Hello World!" << std::endl; return 0; } ''' def get_vs_project_files(): return {"MyProject.sln": sln_file, "MyProject/MyProject.vcxproj": vcxproj_file, "MyProject/MyProject.vcxproj.filters": filters_file, "MyProject/main.cpp": main_file} ================================================ FILE: conan/test/utils/__init__.py ================================================ ================================================ FILE: conan/test/utils/artifactory.py ================================================ import os import time import uuid import requests from conan.internal.errors import RecipeNotFoundException, PackageNotFoundException from conans.server.revision_list import _RevisionEntry ARTIFACTORY_DEFAULT_USER = os.getenv("ARTIFACTORY_DEFAULT_USER", "admin") ARTIFACTORY_DEFAULT_PASSWORD = os.getenv("ARTIFACTORY_DEFAULT_PASSWORD", "password") ARTIFACTORY_DEFAULT_URL = os.getenv("ARTIFACTORY_DEFAULT_URL", "http://localhost:8090/artifactory") class _ArtifactoryServerStore: def __init__(self, repo_url, user, password): self._user = user or ARTIFACTORY_DEFAULT_USER self._password = password or ARTIFACTORY_DEFAULT_PASSWORD self._repo_url = repo_url @property def _auth(self): return self._user, self._password @staticmethod def _root_recipe(ref): return "{}/{}/{}/{}".format(ref.user or "_", ref.name, ref.version, ref.channel or "_") @staticmethod def _ref_index(ref): return "{}/index.json".format(_ArtifactoryServerStore._root_recipe(ref)) @staticmethod def _pref_index(pref): tmp = _ArtifactoryServerStore._root_recipe(pref.ref) return "{}/{}/package/{}/index.json".format(tmp, pref.ref.revision, pref.package_id) def get_recipe_revisions_references(self, ref): time.sleep(0.1) # Index appears to not being updated immediately after a remove url = "{}/{}".format(self._repo_url, self._ref_index(ref)) response = requests.get(url, auth=self._auth) response.raise_for_status() the_json = response.json() if not the_json["revisions"]: raise RecipeNotFoundException(ref) tmp = [_RevisionEntry(i["revision"], i["time"]) for i in the_json["revisions"]] return tmp def get_package_revisions_references(self, pref): time.sleep(0.1) # Index appears to not being updated immediately url = "{}/{}".format(self._repo_url, self._pref_index(pref)) response = requests.get(url, auth=self._auth) response.raise_for_status() the_json = response.json() if not the_json["revisions"]: raise PackageNotFoundException(pref) tmp = [_RevisionEntry(i["revision"], i["time"]) for i in the_json["revisions"]] return tmp def get_last_revision(self, ref): revisions = self.get_recipe_revisions_references(ref) return revisions[0] def get_last_package_revision(self, ref): revisions = self.get_package_revisions_references(ref) return revisions[0] class ArtifactoryServer: def __init__(self, *args, **kwargs): self._user = ARTIFACTORY_DEFAULT_USER self._password = ARTIFACTORY_DEFAULT_PASSWORD self._url = ARTIFACTORY_DEFAULT_URL self._repo_name = "conan_{}".format(str(uuid.uuid4()).replace("-", "")) self.create_repository() self.server_store = _ArtifactoryServerStore(self.repo_url, self._user, self._password) @property def _auth(self): return self._user, self._password @property def repo_url(self): return "{}/{}".format(self._url, self._repo_name) @property def repo_api_url(self): return "{}/api/conan/{}".format(self._url, self._repo_name) def package_revision_time(self, pref): revs = self.server_store.get_package_revisions_references(pref) for r in revs: if r.revision == pref.revision: return r.time return None def create_repository(self): url = "{}/api/repositories/{}".format(self._url, self._repo_name) config = {"key": self._repo_name, "rclass": "local", "packageType": "conan"} ret = requests.put(url, auth=self._auth, json=config) ret.raise_for_status() def package_exists(self, pref): try: revisions = self.server_store.get_package_revisions_references(pref) if pref.revision: for r in revisions: if pref.revision == r.revision: return True return False return True except Exception: # When resolves the latest and there is no package return False def recipe_exists(self, ref): try: revisions = self.server_store.get_recipe_revisions_references(ref) if ref.revision: for r in revisions: if ref.revision == r.revision: return True return False return True except Exception: # When resolves the latest and there is no package return False ================================================ FILE: conan/test/utils/env.py ================================================ import os from contextlib import contextmanager @contextmanager def environment_update(env_vars): old_env = dict(os.environ) sets = {k: v for k, v in env_vars.items() if v is not None} unsets = [k for k, v in env_vars.items() if v is None] os.environ.update(sets) for var in unsets: os.environ.pop(var, None) try: yield finally: os.environ.clear() os.environ.update(old_env) ================================================ FILE: conan/test/utils/file_server.py ================================================ import os import uuid import bottle from webtest import TestApp from conan.test.utils.test_files import temp_folder from conan.internal.util.files import mkdir class TestFileServer: __test__ = False def __init__(self, store=None): self.store = store or temp_folder(path_with_spaces=False) mkdir(self.store) self.fake_url = "http://fake%s.com" % str(uuid.uuid4()).replace("-", "") self.root_app = bottle.Bottle() self.app = TestApp(self.root_app) self._attach_to(self.root_app, self.store) @staticmethod def _attach_to(app, store): @app.route("/", method=["GET"]) def get(file): if bottle.request.query: # For a test using ?q file = bottle.request.query["q"] mimetype = "application/octet-stream" if file.endswith("tgz") else None return bottle.static_file(file, store, mimetype=mimetype) @app.route("//", method=["PUT"]) def put(folder, file): content = bottle.request.body.read() folder = os.path.join(store, folder) mkdir(folder) with open(os.path.join(folder, file), 'wb') as f: f.write(content) @app.route("//", method=["HEAD"]) def head(folder, file): exists = os.path.exists(os.path.join(store, folder, file)) if exists: return bottle.HTTPResponse(status=200) return bottle.HTTPError(404, "Not found") @app.route("/forbidden", method=["GET"]) def get_forbidden(): return bottle.HTTPError(403, "Access denied.") @app.route("/basic-auth/", method=["GET"]) def get_user_passwd(file): auth = bottle.request.auth if auth is not None: if auth != ("user", "password"): return bottle.HTTPError(401, "Bad credentials") return bottle.static_file(file, store) auth = bottle.request.headers.get("Authorization") if auth is None or auth != "Bearer password": return bottle.HTTPError(401, "Not authorized") return bottle.static_file(file, store) @app.route("/internal_error/", method=["GET"]) @app.route("/internal_error", method=["GET"]) def internal_error(): return bottle.HTTPError(500, "Internal error") @app.route("/gz/", method=["GET"]) def get_gz(file): return bottle.static_file(file, store + "/gz", mimetype="application/octet-stream") @app.route("//", method=["GET"]) def get_folder(folder, file): return bottle.static_file(file, os.path.join(store, folder)) def __repr__(self): return "TestFileServer @ " + self.fake_url ================================================ FILE: conan/test/utils/mocks.py ================================================ from collections import defaultdict from io import StringIO from conan import ConanFile from conan.errors import ConanException from conan.internal.conan_app import ConanFileHelpers from conan.internal.model.conf import Conf from conan.internal.model.layout import Folders, Infos from conan.internal.model.options import Options class RedirectedInputStream: """ Mock for testing. If get_username or get_password is requested will raise an exception except we have a value to return. """ def __init__(self, answers: list): self.answers = answers def readline(self): if not self.answers: raise Exception("\n\n**********\n\nClass MockedInputStream: " "There are no more inputs to be returned.\n" "CHECK THE 'inputs=[]' ARGUMENT OF THE TESTCLIENT\n**********+*\n\n\n") ret = self.answers.pop(0) return ret class MockSettings: def __init__(self, values): self.values = values def get_safe(self, value, default=None): return self.values.get(value, default) def __getattr__(self, name): try: return self.values[name] except KeyError: raise ConanException("'%s' value not defined" % name) def rm_safe(self, name): self.values.pop(name, None) def possible_values(self): return defaultdict(lambda: []) class ConanFileMock(ConanFile): def __init__(self, settings=None, options=None, runner=None, display_name=""): self.display_name = display_name self._conan_node = None self.package_type = "unknown" self.settings = settings or MockSettings({"os": "Linux", "arch": "x86_64"}) self.settings_build = settings or MockSettings({"os": "Linux", "arch": "x86_64"}) self.settings_target = None self.runner = runner self.options = options or Options() self.generators = [] self.conf = Conf() self.conf_build = Conf() self.folders = Folders() self.folders.set_base_source(".") self.folders.set_base_export_sources(".") self.folders.set_base_build(".") self.folders.set_base_generators(".") self.cpp = Infos() self.env_scripts = {} self.system_requires = {} self.win_bash = None self.command = None self._commands = [] self._conan_helpers = ConanFileHelpers(None, None, self.conf, None, None, None) def run(self, *args, **kwargs): self.command = args[0] self._commands.append(args[0]) if self.runner: kwargs.pop("quiet", None) return self.runner(*args, **kwargs) return 0 # simulating it was OK! @property def commands(self): result = self._commands self._commands = [] return result MockOptions = MockSettings class RedirectedTestOutput(StringIO): def __init__(self): # Chage to super() for Py3 StringIO.__init__(self) def clear(self): self.seek(0) self.truncate(0) def __repr__(self): return self.getvalue() def __str__(self, *args, **kwargs): return self.__repr__() def __eq__(self, value): return self.__repr__() == value def __contains__(self, value): return value in self.__repr__() ================================================ FILE: conan/test/utils/profiles.py ================================================ import os from conan.internal.model.options import Options from conan.internal.model.profile import Profile from conan.internal.util.files import save def create_profile(folder, name, settings=None, package_settings=None, options=None): profile = Profile() profile.settings = settings or {} if package_settings: profile.package_settings = package_settings if options: profile.options = Options(options_values=options) save(os.path.join(folder, name), profile.dumps()) ================================================ FILE: conan/test/utils/scm.py ================================================ import os from conan.test.utils.test_files import temp_folder from conan.internal.util.files import save_files, chdir from conan.internal.util.runners import detect_runner def git_create_bare_repo(folder=None, reponame="repo.git"): folder = folder or temp_folder() cwd = os.getcwd() try: os.chdir(folder) detect_runner('git init --bare {}'.format(reponame)) return os.path.join(folder, reponame).replace("\\", "/") finally: os.chdir(cwd) def create_local_git_repo(files=None, branch=None, submodules=None, folder=None, commits=1, tags=None, origin_url=None, main_branch="master"): tmp = folder or temp_folder() if files: save_files(tmp, files) def _run(cmd, p): with chdir(p): _, out = detect_runner("git {}".format(cmd)) return out.strip() _run("init .", tmp) _run('config user.name "Your Name"', tmp) _run('config user.email "you@example.com"', tmp) _run("checkout -b {}".format(branch or main_branch), tmp) _run("add .", tmp) for i in range(0, commits): _run('commit --allow-empty -m "commiting"', tmp) tags = tags or [] for tag in tags: _run("tag %s" % tag, tmp) if submodules: for submodule in submodules: _run('submodule add "%s"' % submodule, tmp) _run('commit -m "add submodules"', tmp) if origin_url: _run('remote add origin {}'.format(origin_url), tmp) commit = _run('rev-list HEAD -n 1', tmp) return tmp.replace("\\", "/"), commit def git_add_changes_commit(folder, msg="fix"): cwd = os.getcwd() try: os.chdir(folder) # Make sure user and email exist, otherwise it can error detect_runner('git config user.name "Your Name"') detect_runner('git config user.email "you@example.com"') detect_runner('git add . && git commit -m "{}"'.format(msg)) _, out = detect_runner("git rev-parse HEAD") return out.strip() finally: os.chdir(cwd) ================================================ FILE: conan/test/utils/server_launcher.py ================================================ #!/usr/bin/python import os import shutil import time from conan.internal import REVISIONS from conans.server import SERVER_CAPABILITIES from conans.server.conf import get_server_store from conans.server.crypto.jwt.jwt_credentials_manager import JWTCredentialsManager from conans.server.migrate import migrate_and_get_server_config from conans.server.rest.server import ConanServer from conans.server.service.authorize import BasicAuthenticator, BasicAuthorizer from conan.test.utils.test_files import temp_folder TESTING_REMOTE_PRIVATE_USER = "private_user" TESTING_REMOTE_PRIVATE_PASS = "private_pass" class TestServerLauncher: def __init__(self, base_path=None, read_permissions=None, write_permissions=None, users=None, base_url=None, plugins=None, server_capabilities=None): plugins = plugins or [] if not base_path: base_path = temp_folder() if not os.path.exists(base_path): raise Exception("Base path not exist! %s") self._base_path = base_path server_config = migrate_and_get_server_config(base_path) if server_capabilities is None: server_capabilities = set(SERVER_CAPABILITIES) elif REVISIONS not in server_capabilities: server_capabilities.append(REVISIONS) base_url = base_url or server_config.public_url self.server_store = get_server_store(server_config.disk_storage_path, base_url) # Prepare some test users if not read_permissions: read_permissions = server_config.read_permissions read_permissions.append(("private_library/1.0.0@private_user/testing", "*")) read_permissions.append(("*/*@*/*", "*")) if not write_permissions: write_permissions = server_config.write_permissions if not users: users = dict(server_config.users) users[TESTING_REMOTE_PRIVATE_USER] = TESTING_REMOTE_PRIVATE_PASS authorizer = BasicAuthorizer(read_permissions, write_permissions) authenticator = BasicAuthenticator(users) credentials_manager = JWTCredentialsManager(server_config.jwt_secret, server_config.jwt_expire_time) self.port = server_config.port self.ra = ConanServer(self.port, credentials_manager, authorizer, authenticator, self.server_store, server_capabilities) for plugin in plugins: self.ra.api_v2.install(plugin) def start(self, daemon=True): """from multiprocessing import Process self.p1 = Process(target=ra.run, kwargs={"host": "0.0.0.0"}) self.p1.start() self.p1""" import threading class StoppableThread(threading.Thread): """Thread class with a stop() method. The thread itself has to check regularly for the stopped() condition.""" def __init__(self, *args, **kwargs): super(StoppableThread, self).__init__(*args, **kwargs) self._stop = threading.Event() def stop(self): self._stop.set() def stopped(self): return self._stop.is_set() self.t1 = StoppableThread(target=self.ra.run, kwargs={"host": "0.0.0.0", "quiet": True}) self.t1.daemon = daemon self.t1.start() time.sleep(1) def stop(self): self.ra.root_app.close() self.t1.stop() def clean(self): if os.path.exists(self._base_path): try: shutil.rmtree(self._base_path) except Exception: print("Can't clean the test server data, probably a server process is still opened") if __name__ == "__main__": server = TestServerLauncher() server.start(daemon=False) ================================================ FILE: conan/test/utils/test_files.py ================================================ import copy import os import platform import shutil import tarfile import tempfile import time from io import BytesIO from conan.internal.api.uploader import gzopen_without_timestamps from conan.tools.files.files import untargz from conan.internal.subsystems import get_cased_path from conan.errors import ConanException def wait_until_removed(folder): latest_exception = None for _ in range(50): # Max 5 seconds time.sleep(0.1) try: shutil.rmtree(folder) break except Exception as e: latest_exception = e else: raise Exception("Could remove folder %s: %s" % (folder, latest_exception)) CONAN_TEST_FOLDER = os.getenv('CONAN_TEST_FOLDER', None) if CONAN_TEST_FOLDER and not os.path.exists(CONAN_TEST_FOLDER): os.makedirs(CONAN_TEST_FOLDER) def temp_folder(path_with_spaces=True, create_dir=True): t = tempfile.mkdtemp(suffix='conans', dir=CONAN_TEST_FOLDER) # Make sure that the temp folder is correctly cased, as tempfile return lowercase for Win t = get_cased_path(t) # necessary for Mac OSX, where the temp folders in /var/ are symlinks to /private/var/ t = os.path.realpath(t) # FreeBSD and Solaris do not use GNU Make as a the default 'make' program which has trouble # with spaces in paths generated by CMake if not path_with_spaces or platform.system() == "FreeBSD" or platform.system() == "SunOS": path = "pathwithoutspaces" else: path = "path with spaces" nt = os.path.join(t, path) if create_dir: os.makedirs(nt) return nt def uncompress_packaged_files(paths, pref): _tmp = copy.copy(pref) _tmp.revision = None prev = paths.get_last_package_revision(_tmp).revision pref.revision = prev package_path = paths.package(pref) PACKAGE_TGZ_NAME = "conan_package.tgz" if not (os.path.exists(os.path.join(package_path, PACKAGE_TGZ_NAME))): raise ConanException("%s not found in %s" % (PACKAGE_TGZ_NAME, package_path)) tmp = temp_folder() untargz(os.path.join(package_path, PACKAGE_TGZ_NAME), tmp) return tmp def scan_folder(folder): scanned_files = [] for root, dirs, files in os.walk(folder): dirs[:] = [d for d in dirs if d != "__pycache__"] relative_path = os.path.relpath(root, folder) for f in files: if f.endswith(".pyc"): continue relative_name = os.path.normpath(os.path.join(relative_path, f)).replace("\\", "/") scanned_files.append(relative_name) return sorted(scanned_files) def tgz_with_contents(files, output_path=None): folder = temp_folder() file_path = output_path or os.path.join(folder, "myfile.tar.gz") with open(file_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps("myfile.tar.gz", fileobj=tgz_handle) for name, content in files.items(): info = tarfile.TarInfo(name=name) data = content.encode('utf-8') info.size = len(data) tgz.addfile(tarinfo=info, fileobj=BytesIO(data)) tgz.close() return file_path ================================================ FILE: conan/test/utils/tools.py ================================================ import copy import json import os import platform import re import shlex import shutil import socket import sys import textwrap import traceback import uuid import zipfile import subprocess from contextlib import contextmanager from inspect import getframeinfo, stack from urllib.parse import urlsplit, urlunsplit from unittest import mock import pytest import requests from unittest.mock import Mock from requests.exceptions import HTTPError from webtest.app import TestApp from conan.api.subapi.audit import CONAN_CENTER_AUDIT_PROVIDER_NAME, _save_providers from conan.api.subapi.remotes import _save from conan.cli.exit_codes import SUCCESS from conan.internal.cache.cache import PackageLayout, RecipeLayout, PkgCache from conan.internal.cache.home_paths import HomePaths from conan.internal import REVISIONS from conan.api.conan_api import ConanAPI from conan.api.model import Remote from conan.cli.cli import Cli, _CONAN_INTERNAL_CUSTOM_COMMANDS_PATH from conan.internal.model.conf import load_global_conf from conan.test.utils.env import environment_update from conan.internal.errors import NotFoundException from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.artifactory import ArtifactoryServer from conan.test.utils.mocks import RedirectedInputStream from conan.test.utils.mocks import RedirectedTestOutput from conan.test.utils.scm import create_local_git_repo from conan.test.utils.server_launcher import (TestServerLauncher) from conan.test.utils.test_files import temp_folder from conan.internal.util.files import mkdir, save_files, save, load NO_SETTINGS_PACKAGE_ID = "da39a3ee5e6b4b0d3255bfef95601890afd80709" arch = platform.machine() arch_setting = "armv8" if arch in ["arm64", "aarch64"] else arch default_profiles = { "Windows": textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release """), "Linux": textwrap.dedent(f"""\ [settings] os=Linux arch={arch_setting} compiler=gcc compiler.version=8 compiler.libcxx=libstdc++11 build_type=Release """), "Darwin": textwrap.dedent(f"""\ [settings] os=Macos arch={arch_setting} compiler=apple-clang compiler.version=17 compiler.libcxx=libc++ build_type=Release """) } class TestingResponse: """Wraps a response from TestApp external tool to guarantee the presence of response.ok, response.content and response.status_code, as it was a requests library object. Is instanced by TestRequester on each request""" def __init__(self, test_response): self.test_response = test_response def close(self): pass # Compatibility with close() method of a requests when stream=True @property def headers(self): return self.test_response.headers @property def ok(self): return self.test_response.status_code == 200 def raise_for_status(self): """Raises stored :class:`HTTPError`, if one occurred.""" http_error_msg = '' if 400 <= self.status_code < 500: http_error_msg = u'%s Client Error: %s' % (self.status_code, self.content) elif 500 <= self.status_code < 600: http_error_msg = u'%s Server Error: %s' % (self.status_code, self.content) if http_error_msg: raise HTTPError(http_error_msg, response=self) @property def content(self): return self.test_response.body @property def charset(self): return self.test_response.charset @charset.setter def charset(self, newcharset): self.test_response.charset = newcharset @property def text(self): return self.test_response.text def iter_content(self, chunk_size=1): # @UnusedVariable return [self.content] @property def status_code(self): return self.test_response.status_code def json(self): try: return json.loads(self.test_response.content) except: raise ValueError("The response is not a JSON") class TestRequester: """Fake requests module calling server applications with TestApp""" def __init__(self, test_servers): self.test_servers = test_servers self.utils = Mock() self.utils.default_user_agent.return_value = "TestRequester Agent" @staticmethod def _get_url_path(url): # Remove schema from url _, _, path, query, _ = urlsplit(url) url = urlunsplit(("", "", path, query, "")) return url def _get_wsgi_app(self, url): for test_server in self.test_servers.values(): if url.startswith(test_server.fake_url): return test_server.app raise Exception("Testing error: Not remote found") def get(self, url, **kwargs): app, url = self._prepare_call(url, kwargs) if app: response = app.get(url, **kwargs) return TestingResponse(response) else: return requests.get(url, **kwargs) def put(self, url, **kwargs): app, url = self._prepare_call(url, kwargs) if app: response = app.put(url, **kwargs) return TestingResponse(response) else: return requests.put(url, **kwargs) def head(self, url, **kwargs): app, url = self._prepare_call(url, kwargs) if app: response = app.head(url, **kwargs) return TestingResponse(response) else: return requests.head(url, **kwargs) def delete(self, url, **kwargs): app, url = self._prepare_call(url, kwargs) if app: response = app.delete(url, **kwargs) return TestingResponse(response) else: return requests.delete(url, **kwargs) def post(self, url, **kwargs): app, url = self._prepare_call(url, kwargs) if app: response = app.post(url, **kwargs) return TestingResponse(response) else: requests.post(url, **kwargs) def _prepare_call(self, url, kwargs): if not url.startswith("http://fake"): # Call to S3 (or external), perform a real request return None, url app = self._get_wsgi_app(url) url = self._get_url_path(url) # Remove http://server.com self._set_auth_headers(kwargs) if app: kwargs["expect_errors"] = True kwargs.pop("stream", None) kwargs.pop("verify", None) kwargs.pop("source_credentials", None) auth = kwargs.pop("auth", None) if auth and isinstance(auth, tuple): app.set_authorization(("Basic", auth)) kwargs.pop("cert", None) kwargs.pop("timeout", None) if "data" in kwargs: total_data = kwargs["data"].read() kwargs["params"] = total_data del kwargs["data"] # Parameter in test app is called "params" if kwargs.get("json"): # json is a high level parameter of requests, not a generic one # translate it to data and content_type kwargs["params"] = json.dumps(kwargs["json"]) kwargs["content_type"] = "application/json" kwargs.pop("json", None) return app, url @staticmethod def _set_auth_headers(kwargs): if kwargs.get("auth"): if isinstance(kwargs.get("auth"), tuple): # For download(..., auth=(user, paswd)) return mock_request = Mock() mock_request.headers = {} kwargs["auth"](mock_request) if kwargs.get("headers") is None: kwargs["headers"] = {} kwargs["headers"].update(mock_request.headers) def mount(self, *args, **kwargs): pass def Session(self): return self @property def codes(self): return requests.codes class TestServer: __test__ = False def __init__(self, read_permissions=None, write_permissions=None, users=None, plugins=None, base_path=None, server_capabilities=None, complete_urls=False): """ 'read_permissions' and 'write_permissions' is a list of: [("opencv/2.3.4@lasote/testing", "user1, user2")] 'users': {username: plain-text-passwd} """ # Unique identifier for this server, will be used by TestRequester # to determine where to call. Why? remote_manager just assing an url # to the rest_client, so rest_client doesn't know about object instances, # just urls, so testing framework performs a map between fake urls and instances if read_permissions is None: read_permissions = [("*/*@*/*", "*")] if write_permissions is None: write_permissions = [("*/*@*/*", "*")] if users is None: users = {"admin": "password"} if server_capabilities is None: server_capabilities = [REVISIONS] elif REVISIONS not in server_capabilities: server_capabilities.append(REVISIONS) self.fake_url = "http://fake%s.com" % str(uuid.uuid4()).replace("-", "") base_url = "%s/v1" % self.fake_url if complete_urls else "v1" self.test_server = TestServerLauncher(base_path, read_permissions, write_permissions, users, base_url=base_url, plugins=plugins, server_capabilities=server_capabilities) self.app = TestApp(self.test_server.ra.root_app) @property def server_store(self): return self.test_server.server_store def __repr__(self): return "TestServer @ " + self.fake_url def __str__(self): return self.fake_url def recipe_exists(self, ref): try: if not ref.revision: path = self.test_server.server_store.conan_revisions_root(ref) else: path = self.test_server.server_store.base_folder(ref) return self.test_server.server_store.path_exists(path) except NotFoundException: # When resolves the latest and there is no package return False def package_exists(self, pref): try: if pref.revision: path = self.test_server.server_store.package(pref) else: path = self.test_server.server_store.package_revisions_root(pref) return self.test_server.server_store.path_exists(path) except NotFoundException: # When resolves the latest and there is no package return False def latest_recipe(self, ref): ref = self.test_server.server_store.get_last_revision(ref) return ref def latest_package(self, pref): if not pref.ref.revision: raise Exception("Pass a pref with .rev.revision (Testing framework)") prev = self.test_server.server_store.get_last_package_revision(pref) _tmp = copy.copy(prev) _tmp.revision = prev return _tmp def package_revision_time(self, pref): if not pref: raise Exception("Pass a pref with revision (Testing framework)") tmp = self.test_server.server_store.get_package_revision_time(pref) return tmp if os.environ.get("CONAN_TEST_WITH_ARTIFACTORY"): TestServer = ArtifactoryServer @contextmanager def redirect_output(stderr, stdout=None): original_stdout = sys.stdout original_stderr = sys.stderr # TODO: change in 2.0 # redirecting both of them to the same target for the moment # to assign to Testclient out sys.stdout = stdout or stderr sys.stderr = stderr try: yield finally: sys.stdout = original_stdout sys.stderr = original_stderr @contextmanager def redirect_input(target): original_stdin = sys.stdin sys.stdin = target try: yield finally: sys.stdin = original_stdin class TestClient: """ Test wrap of the conans application to launch tests in the same way as in command line """ # Preventing Pytest collects any tests from here __test__ = False def __init__(self, cache_folder=None, current_folder=None, servers=None, inputs=None, requester_class=None, path_with_spaces=True, default_server_user=None, light=False, custom_commands_folder=None): """ current_folder: Current execution folder servers: dict of {remote_name: TestServer} logins is a list of (user, password) for auto input in order if required==> [("lasote", "mypass"), ("other", "otherpass")] """ if default_server_user is not None: assert isinstance(default_server_user, bool), \ "default_server_user has to be True or False" if servers is not None: raise Exception("Cannot define both 'servers' and 'default_server_user'") if inputs is not None: raise Exception("Cannot define both 'inputs' and 'default_server_user'") server_users = {"admin": "password"} inputs = ["admin", "password"] # Allow writing permissions to users server = TestServer(users=server_users, write_permissions=[("*/*@*/*", "*")]) servers = {"default": server} # Adding the .conan2, so we know clearly while debugging this is a cache folder self.cache_folder = cache_folder or os.path.join(temp_folder(path_with_spaces), ".conan2") self.requester_class = requester_class self.servers = servers or {} if servers is not False: # Do not mess with registry remotes self.update_servers() self.update_providers() self.current_folder = current_folder or temp_folder(path_with_spaces) # Once the client is ready, modify the configuration mkdir(self.current_folder) self.out = "" self.stdout = RedirectedTestOutput() self.stderr = RedirectedTestOutput() self.user_inputs = RedirectedInputStream([]) self.inputs = inputs or [] # create default profile if light: text = "[settings]\nos=Linux" # Needed at least build-os save(self.paths.settings_path, "os: [Linux, Windows]") else: text = default_profiles[platform.system()] save(os.path.join(self.cache_folder, "profiles", "default"), text) # Using internal env variable to add another custom commands folder self._custom_commands_folder = custom_commands_folder def load(self, filename): return load(os.path.join(self.current_folder, filename)) def load_home(self, filename): try: return load(os.path.join(self.cache_folder, filename)) except IOError: return None def open(self, filename): # CI is set by default by GitHub Actions if os.environ.get("CI", False): assert False, "TestClient::open should not be used in CI" current_path = os.path.join(self.current_folder, filename) if platform.system() == "Windows": os.startfile(os.path.normpath(current_path)) elif platform.system() == "Darwin": subprocess.call(["open", current_path]) else: subprocess.call(["xdg-open", current_path]) def open_home(self, filename): return self.open(os.path.join(self.cache_folder, filename)) @property def cache(self) -> PkgCache: # Returns a temporary cache object intended for inspecting it return PkgCache(self.cache_folder, load_global_conf(self.cache_folder)) @property def paths(self): return HomePaths(self.cache_folder) @property def base_folder(self): # Temporary hack to refactor ConanApp with less changes return self.cache_folder @property def storage_folder(self): return self.cache.store def update_servers(self): remotes = [] for name, server in self.servers.items(): if isinstance(server, ArtifactoryServer): remotes.append(Remote(name, server.repo_api_url)) elif isinstance(server, TestServer): remotes.append(Remote(name, server.fake_url)) else: remotes.append(Remote(name, server)) _save(HomePaths(self.cache_folder).remotes_path, remotes) def update_providers(self): default_providers = { CONAN_CENTER_AUDIT_PROVIDER_NAME: { "url": "https://fakeurl/", "type": "conan-center-proxy" } } _save_providers(HomePaths(self.cache_folder).providers_path, default_providers) @contextmanager def chdir(self, newdir): old_dir = self.current_folder if not os.path.isabs(newdir): newdir = os.path.join(old_dir, newdir) mkdir(newdir) self.current_folder = newdir try: yield finally: self.current_folder = old_dir @contextmanager def mocked_servers(self, requester=None): _req = requester or TestRequester(self.servers) with mock.patch("conan.internal.rest.conan_requester.requests", _req): yield @contextmanager def mocked_io(self): def mock_get_pass(*args, **kwargs): return self.user_inputs.readline() with redirect_output(self.stderr, self.stdout): with redirect_input(self.user_inputs): with mock.patch("getpass.getpass", mock_get_pass): yield def _run_cli(self, command_line, assert_error=False): args = shlex.split(command_line) error = SUCCESS trace = None # save state current_dir = os.getcwd() os.chdir(self.current_folder) old_path = sys.path[:] old_modules = list(sys.modules.keys()) try: self.api = ConanAPI(cache_folder=self.cache_folder) command = Cli(self.api) if self._custom_commands_folder: with environment_update({_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH: self._custom_commands_folder}): command.run(args) else: command.run(args) except BaseException as e: # Capture all exceptions as argparse trace = traceback.format_exc() error = Cli.exception_exit_error(e) finally: sys.path = old_path os.chdir(current_dir) # Reset sys.modules to its prev state. A .copy() DOES NOT WORK added_modules = set(sys.modules).difference(old_modules) for added in added_modules: sys.modules.pop(added, None) self._handle_cli_result(command_line, assert_error=assert_error, error=error, trace=trace) return error def run(self, command_line, assert_error=False, redirect_stdout=None, redirect_stderr=None, inputs=None): """ run a single command as in the command line. If user or password is filled, user_io will be mocked to return this tuple if required """ from conan.test.utils.mocks import RedirectedTestOutput with environment_update({"NO_COLOR": "1"}): # Not initialize colorama in testing self.user_inputs = RedirectedInputStream(inputs or self.inputs) self.stdout = RedirectedTestOutput() # Initialize each command self.stderr = RedirectedTestOutput() self.out = "" with self.mocked_io(): real_servers = any(isinstance(s, (str, ArtifactoryServer)) for s in self.servers.values()) http_requester = None if not real_servers: if self.requester_class: http_requester = self.requester_class(self.servers) else: http_requester = TestRequester(self.servers) try: if http_requester: with self.mocked_servers(http_requester): return self._run_cli(command_line, assert_error=assert_error) else: return self._run_cli(command_line, assert_error=assert_error) finally: self.stdout = str(self.stdout) self.stderr = str(self.stderr) self.out = self.stderr + self.stdout if redirect_stdout: save(os.path.join(self.current_folder, redirect_stdout), self.stdout) if redirect_stderr: save(os.path.join(self.current_folder, redirect_stderr), self.stderr) def run_command(self, command, cwd=None, assert_error=False): from conan.test.utils.mocks import RedirectedTestOutput self.stdout = RedirectedTestOutput() # Initialize each command self.stderr = RedirectedTestOutput() try: with redirect_output(self.stderr, self.stdout): from conan.internal.util.runners import conan_run ret = conan_run(command, cwd=cwd or self.current_folder) finally: self.stdout = str(self.stdout) self.stderr = str(self.stderr) self.out = self.stderr + self.stdout self._handle_cli_result(command, assert_error=assert_error, error=ret) return ret def _handle_cli_result(self, command, assert_error, error, trace=None): if (assert_error and not error) or (not assert_error and error): if assert_error: msg = " Command succeeded (failure expected): " else: msg = " Command failed (unexpectedly): " output = str(self.stderr) + str(self.stdout) + "\n" exc_message = f"\n{msg:=^80}\n{command}\n{' Output: ':=^80}\n{output}\n" if trace: exc_message += f'{" Traceback: ":=^80}\n{trace}' caller = getframeinfo(stack()[3][0]) exc_message = f"{caller.filename}:{caller.lineno}" + exc_message pytest.fail(exc_message, pytrace=False) def save(self, files, path=None, clean_first=False): """ helper metod, will store files in the current folder param files: dict{filename: filecontents} """ path = path or self.current_folder if clean_first: shutil.rmtree(self.current_folder, ignore_errors=True) files = {f: str(content) for f, content in files.items()} save_files(path, files) if not files: mkdir(self.current_folder) def save_home(self, files): self.save(files, path=self.cache_folder) # Higher level operations def remove_all(self): self.run("remove '*' -c") def export(self, ref, conanfile=GenConanfile(), args=None): """ export a ConanFile with as "ref" and return the reference with recipe revision """ if conanfile: self.save({"conanfile.py": conanfile}) if ref: self.run(f"export . --name={ref.name} --version={ref.version} --user={ref.user} --channel={ref.channel}") else: self.run("export .") tmp = copy.copy(ref) tmp.revision = None rrev = self.cache.get_latest_recipe_revision(tmp).revision tmp = copy.copy(ref) tmp.revision = rrev return tmp def alias(self, source, target): """ creates a new recipe with "conan new alias" template, "conan export" it, and remove it @param source: the reference of the current recipe @param target: the target reference that this recipe is pointing (aliasing to) """ source = RecipeReference.loads(source) target = target.split("/", 1)[1] self.run(f"new alias -d name={source.name} -d version={source.version} " f"-d target={target} -f") user = f"--user={source.user}" if source.user else "" channel = f"--channel={source.channel}" if source.channel else "" self.run(f"export . {user} {channel}") os.remove(os.path.join(self.current_folder, "conanfile.py")) def init_git_repo(self, files=None, branch=None, submodules=None, folder=None, origin_url=None, main_branch="master"): if folder is not None: folder = os.path.join(self.current_folder, folder) else: folder = self.current_folder _, commit = create_local_git_repo(files, branch, submodules, folder=folder, origin_url=origin_url, main_branch=main_branch) return commit def get_latest_package_reference(self, ref, package_id=None) -> PkgReference: """Get the latest PkgReference given a ConanReference""" ref_ = RecipeReference.loads(ref) if isinstance(ref, str) else ref latest_rrev = self.cache.get_latest_recipe_revision(ref_) if package_id: pref = PkgReference(latest_rrev, package_id) else: package_ids = self.cache.get_package_references(latest_rrev) # Let's check if there are several packages because we don't want random behaviours assert len(package_ids) == 1, f"There are several packages for {latest_rrev}, please, " \ f"provide a single package_id instead" \ if len(package_ids) > 0 else "No binary packages found" pref = package_ids[0] return self.cache.get_latest_package_revision(pref) def get_latest_pkg_layout(self, pref: PkgReference) -> PackageLayout: """Get the latest PackageLayout given a file reference""" # Let's make it easier for all the test clients latest_prev = self.cache.get_latest_package_revision(pref) pkg_layout = self.cache.pkg_layout(latest_prev) return pkg_layout def get_latest_ref_layout(self, ref) -> RecipeLayout: """Get the latest RecipeLayout given a file reference""" if not ref.revision: ref = self.cache.get_latest_recipe_revision(ref) ref_layout = self.cache.recipe_layout(ref) return ref_layout def get_default_host_profile(self): api = ConanAPI(cache_folder=self.cache_folder) return api.profiles.get_profile([api.profiles.get_default_host()]) def get_default_build_profile(self): api = ConanAPI(cache_folder=self.cache_folder) return api.profiles.get_profile([api.profiles.get_default_build()]) def recipe_exists(self, ref): rrev = self.cache.get_recipe_revisions(ref) return True if rrev else False def package_exists(self, pref): prev = self.cache.get_package_revisions(pref) return True if prev else False def assert_listed_require(self, requires, build=False, python=False, test=False, test_package=False): """ parses the current command output, and extract the first "Requirements" section """ lines = self.out.splitlines() if test_package: line_req = lines.index("======== Launching test_package ========") lines = lines[line_req:] header = "Requirements" if not build else "Build requirements" if python: header = "Python requires" if test: header = "Test requirements" line_req = lines.index(header) reqs = [] for line in lines[line_req+1:]: if not line.startswith(" "): break reqs.append(line.strip()) for r, kind in requires.items(): for req in reqs: if req.startswith(r) and req.endswith(kind): break else: raise AssertionError(f"Cant find {r}-{kind} in {reqs}") def assert_overrides(self, overrides): """ parses the current command output, and extract the first "Requirements" section """ lines = self.out.splitlines() header = "Overrides" line_req = lines.index(header) reqs = [] for line in lines[line_req+1:]: if not line.startswith(" "): break reqs.append(line.strip()) for r, o in overrides.items(): msg = f"{r}: {o}" if msg not in reqs: raise AssertionError(f"Cant find {msg} in {reqs}") def assert_listed_binary(self, requires, build=False, test=False, test_package=False): """ parses the current command output, and extract the second "Requirements" section belonging to the computed package binaries """ lines = self.out.splitlines() if test_package: line_req = lines.index("======== Launching test_package ========") lines = lines[line_req:] line_req = lines.index("======== Computing necessary packages ========") header = "Requirements" if not build else "Build requirements" if test: header = "Test requirements" line_req = lines.index(header, line_req) reqs = [] for line in lines[line_req+1:]: if not line.startswith(" "): break reqs.append(line.strip()) for r, kind in requires.items(): package_id, binary = kind for req in reqs: if req.startswith(r) and package_id in req and req.endswith(binary): break else: raise AssertionError(f"Cant find {r}-{kind} in {reqs}") def created_test_build_folder(self, ref): build_folder = re.search(r"{} \(test package\): Test package build: (.*)".format(str(ref)), str(self.out)).group(1) return build_folder.replace("\\", "/") def created_package_id(self, ref): package_id = re.search(r"{}: Package '(\S+)' created".format(str(ref)), str(self.out)).group(1) return package_id def created_package_revision(self, ref): package_id = re.search(r"{}: Created package revision (\S+)".format(str(ref)), str(self.out)).group(1) return package_id def created_package_reference(self, ref): pref = re.search(r"{}: Full package reference: (\S+)".format(str(ref)), str(self.out)).group(1) return PkgReference.loads(pref) def exported_recipe_revision(self): return re.search(r": Exported: .*#(\S+)", str(self.out)).group(1) def exported_layout(self): m = re.search(r": Exported: (\S+)", str(self.out)).group(1) ref = RecipeReference.loads(m) return self.cache.recipe_layout(ref) def created_layout(self): pref = re.search(r"(?s:.*)Full package reference: (\S+)", str(self.out)).group(1) pref = PkgReference.loads(pref) return self.cache.pkg_layout(pref) def get_free_port(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 0)) ret = sock.getsockname()[1] sock.close() return ret def zipdir(path, zipfilename): with zipfile.ZipFile(zipfilename, 'w', zipfile.ZIP_DEFLATED) as z: for root, _, files in os.walk(path): for f in files: file_path = os.path.join(root, f) if file_path == zipfilename: continue relpath = os.path.relpath(file_path, path) z.write(file_path, relpath) ================================================ FILE: conan/tools/__init__.py ================================================ from conan.internal.model.cpp_info import CppInfo as _CppInfo def CppInfo(conanfile): # Creation of a CppInfo object, to decouple the creation from the actual internal location # that at the moment doesn't require a ``conanfile`` argument, but might require in the future # and allow us to refactor the location of conans.model.build_info import CppInfo return _CppInfo() ================================================ FILE: conan/tools/android/__init__.py ================================================ from conan.tools.android.utils import android_abi ================================================ FILE: conan/tools/android/utils.py ================================================ from conan.errors import ConanException def android_abi(conanfile, context="host"): """ Returns Android-NDK ABI :param conanfile: ConanFile instance :param context: either "host", "build" or "target" :return: Android-NDK ABI """ if context not in ("host", "build", "target"): raise ConanException(f"context argument must be either 'host', 'build' or 'target', " f"was '{context}'") try: settings = getattr(conanfile, f"settings_{context}") except AttributeError: if context == "host": settings = conanfile.settings else: raise ConanException(f"settings_{context} not declared in recipe") if settings is None: raise ConanException(f"settings_{context}=None in recipe") arch = settings.get_safe("arch") # https://cmake.org/cmake/help/latest/variable/CMAKE_ANDROID_ARCH_ABI.html return { "armv5el": "armeabi", "armv5hf": "armeabi", "armv5": "armeabi", "armv6": "armeabi-v6", "armv7": "armeabi-v7a", "armv7hf": "armeabi-v7a", "armv8": "arm64-v8a", }.get(arch, arch) ================================================ FILE: conan/tools/apple/__init__.py ================================================ # Keep everything private until we review what is really needed and refactor passing "conanfile" # from conan.tools.apple.apple import apple_dot_clean # from conan.tools.apple.apple import apple_sdk_name # from conan.tools.apple.apple import apple_deployment_target_flag from conan.tools.apple.apple import fix_apple_shared_install_name, is_apple_os, to_apple_arch, XCRun from conan.tools.apple.xcodedeps import XcodeDeps from conan.tools.apple.xcodebuild import XcodeBuild from conan.tools.apple.xcodetoolchain import XcodeToolchain ================================================ FILE: conan/tools/apple/apple.py ================================================ import os from io import StringIO from conan.internal.internal_tools import universal_arch_separator from conan.internal.util.runners import check_output_runner from conan.tools.build import cmd_args_to_string from conan.errors import ConanException def is_apple_os(conanfile, build_context=False): """returns True if OS is Apple one (Macos, iOS, watchOS, tvOS or visionOS)""" settings = conanfile.settings_build if build_context else conanfile.settings return str(settings.get_safe("os")) in ['Macos', 'iOS', 'watchOS', 'tvOS', 'visionOS'] def _to_apple_arch(arch, default=None): """converts conan-style architecture into Apple-style arch""" return {'x86': 'i386', 'x86_64': 'x86_64', 'armv7': 'armv7', 'armv8': 'arm64', 'armv8_32': 'arm64_32', 'armv8.3': 'arm64e', 'armv7s': 'armv7s', 'armv7k': 'armv7k'}.get(str(arch), default) def to_apple_arch(conanfile, default=None): """converts conan-style architecture into Apple-style arch""" arch_ = conanfile.settings.get_safe("arch") return _to_apple_arch(arch_, default) def apple_sdk_path(conanfile, is_cross_building=True): sdk_path = conanfile.conf.get("tools.apple:sdk_path") if not sdk_path: # XCRun already knows how to extract os.sdk from conanfile.settings sdk_path = XCRun(conanfile).sdk_path if not sdk_path and is_cross_building: raise ConanException( "Apple SDK path not found. For cross-compilation, you must " "provide a valid SDK path in 'tools.apple:sdk_path' config." ) return sdk_path def get_apple_sdk_fullname(conanfile): """ Returns the 'os.sdk' + 'os.sdk_version ' value. Every user should specify it because there could be several ones depending on the OS architecture. Note: In case of MacOS it'll be the same for all the architectures. """ os_ = conanfile.settings.get_safe('os') os_sdk = conanfile.settings.get_safe('os.sdk') os_sdk_version = conanfile.settings.get_safe('os.sdk_version') or "" if os_sdk: return "{}{}".format(os_sdk, os_sdk_version) elif os_ == "Macos": # it has only a single value for all the architectures return "{}{}".format("macosx", os_sdk_version) elif is_apple_os(conanfile): raise ConanException("Please, specify a suitable value for os.sdk.") def apple_min_version_flag(conanfile): """compiler flag name which controls deployment target""" os_ = conanfile.settings.get_safe('os') os_sdk = conanfile.settings.get_safe('os.sdk') os_sdk = os_sdk or ("macosx" if os_ == "Macos" else None) os_version = conanfile.settings.get_safe("os.version") if not os_sdk or not os_version: # Legacy behavior return "" if conanfile.settings.get_safe("os.subsystem") == 'catalyst': os_sdk = "iphoneos" return { "macosx": f"-mmacosx-version-min={os_version}", "iphoneos": f"-mios-version-min={os_version}", "iphonesimulator": f"-mios-simulator-version-min={os_version}", "watchos": f"-mwatchos-version-min={os_version}", "watchsimulator": f"-mwatchos-simulator-version-min={os_version}", "appletvos": f"-mtvos-version-min={os_version}", "appletvsimulator": f"-mtvos-simulator-version-min={os_version}", "xros": f"--target=arm64-apple-xros{os_version}", "xrsimulator": f"--target=arm64-apple-xros{os_version}-simulator", }.get(os_sdk, "") def resolve_apple_flags(conanfile, is_cross_building=False, is_universal=False): """ Gets the most common flags in Apple systems. If it's a cross-building context SDK path is mandatory so if it could raise an exception if SDK is not found. :param conanfile: instance. :param is_cross_building: boolean to indicate if it's a cross-building context. :param is_universal: boolean to indicate if it's a universal binary. :return: tuple of Apple flags (apple_min_version_flag, apple_arch_flags, apple_isysroot_flag). """ if not is_apple_os(conanfile): # Keeping legacy defaults return "", None, None apple_arch_flags = apple_isysroot_flag = None if is_universal: arch_ = conanfile.settings.get_safe("arch") apple_arch_flags = " ".join([f"-arch {_to_apple_arch(arch, default=arch)}" for arch in arch_.split(universal_arch_separator)]) sdk_path = conanfile.conf.get("tools.apple:sdk_path") if sdk_path: # Ideally, -isysroot should be added whenever sdk_path is defined. # For now, we only set it in this case to avoid changing existing behavior. apple_isysroot_flag = f"-isysroot {sdk_path}" elif is_cross_building: arch = to_apple_arch(conanfile) sdk_path = apple_sdk_path(conanfile, is_cross_building=is_cross_building) apple_isysroot_flag = f"-isysroot {sdk_path}" if sdk_path else "" apple_arch_flags = f"-arch {arch}" if arch else "" min_version_flag = apple_min_version_flag(conanfile) return min_version_flag, apple_arch_flags, apple_isysroot_flag def xcodebuild_deployment_target_key(os_name): return { "Macos": "MACOSX_DEPLOYMENT_TARGET", "iOS": "IPHONEOS_DEPLOYMENT_TARGET", "tvOS": "TVOS_DEPLOYMENT_TARGET", "watchOS": "WATCHOS_DEPLOYMENT_TARGET", "visionOS": "XROS_DEPLOYMENT_TARGET", }.get(os_name) if os_name else None class XCRun: """ XCRun is a wrapper for the Apple **xcrun** tool used to get information for building. """ def __init__(self, conanfile, sdk=None, use_settings_target=False): """ :param conanfile: Conanfile instance. :param sdk: Will skip the flag when ``False`` is passed and will try to adjust the sdk it automatically if ``None`` is passed. :param use_settings_target: Try to use ``settings_target`` in case they exist (``False`` by default) """ settings = conanfile.settings if use_settings_target and conanfile.settings_target is not None: settings = conanfile.settings_target if sdk is None and settings: sdk = settings.get_safe('os.sdk') self._conanfile = conanfile self.settings = settings self.sdk = sdk def _invoke(self, args): command = ['xcrun'] if self.sdk: command.extend(['-sdk', self.sdk]) command.extend(args) output = StringIO() cmd_str = cmd_args_to_string(command) self._conanfile.run(f"{cmd_str}", stdout=output, quiet=True) return output.getvalue().strip() def find(self, tool): """find SDK tools (e.g. clang, ar, ranlib, lipo, codesign, etc.)""" return self._invoke(['--find', tool]) @property def sdk_path(self): """obtain sdk path (aka apple sysroot or -isysroot""" return self._invoke(['--show-sdk-path']) @property def sdk_version(self): """obtain sdk version""" return self._invoke(['--show-sdk-version']) @property def sdk_platform_path(self): """obtain sdk platform path""" return self._invoke(['--show-sdk-platform-path']) @property def sdk_platform_version(self): """obtain sdk platform version""" return self._invoke(['--show-sdk-platform-version']) @property def cc(self): """path to C compiler (CC)""" return self.find('clang') @property def cxx(self): """path to C++ compiler (CXX)""" return self.find('clang++') @property def ar(self): """path to archiver (AR)""" return self.find('ar') @property def ranlib(self): """path to archive indexer (RANLIB)""" return self.find('ranlib') @property def strip(self): """path to symbol removal utility (STRIP)""" return self.find('strip') @property def libtool(self): """path to libtool""" return self.find('libtool') @property def otool(self): """path to otool""" return self.find('otool') @property def install_name_tool(self): """path to install_name_tool""" return self.find('install_name_tool') def _get_dylib_install_name(otool, path_to_dylib): command = f"{otool} -D {path_to_dylib}" output = iter(check_output_runner(command).splitlines()) # Note: if otool return multiple entries for different architectures # assume they are the same and pick the first one. for line in output: if ":" in line: return next(output) raise ConanException(f"Unable to extract install_name for {path_to_dylib}") def fix_apple_shared_install_name(conanfile): """ Search for all the *dylib* files in the conanfile's *package_folder* and fix both the ``LC_ID_DYLIB`` and ``LC_LOAD_DYLIB`` fields on those files using the *install_name_tool* utility available in macOS to set ``@rpath``. """ if not is_apple_os(conanfile): return xcrun = XCRun(conanfile) otool = xcrun.otool install_name_tool = xcrun.install_name_tool def _darwin_is_binary(file, binary_type): if binary_type not in ("DYLIB", "EXECUTE") or os.path.islink(file) or os.path.isdir(file): return False check_file = f"{otool} -hv {file}" return binary_type in check_output_runner(check_file) def _darwin_collect_binaries(folder, binary_type): return [os.path.join(folder, f) for f in os.listdir(folder) if _darwin_is_binary(os.path.join(folder, f), binary_type)] def _fix_install_name(dylib_path, new_name): command = f"{install_name_tool} {dylib_path} -id {new_name}" conanfile.run(command) def _fix_dep_name(dylib_path, old_name, new_name): command = f"{install_name_tool} {dylib_path} -change {old_name} {new_name}" conanfile.run(command) def _get_rpath_entries(binary_file): entries = [] command = f"{otool} -l {binary_file}" otool_output = check_output_runner(command).splitlines() for count, text in enumerate(otool_output): pass if "LC_RPATH" in text: rpath_entry = otool_output[count+2].split("path ")[1].split(" ")[0] entries.append(rpath_entry) return entries def _get_shared_dependencies(binary_file): command = f"{otool} -L {binary_file}" all_shared = check_output_runner(command).strip().split(":")[1].strip() ret = [s.split("(")[0].strip() for s in all_shared.splitlines()] return ret def _fix_dylib_files(): substitutions = {} libdirs = getattr(conanfile.cpp.package, "libdirs") for libdir in libdirs: full_folder = os.path.join(conanfile.package_folder, libdir) if not os.path.exists(full_folder): raise ConanException(f"Trying to locate shared libraries, but `{libdir}` " f" not found inside package folder {conanfile.package_folder}") shared_libs = _darwin_collect_binaries(full_folder, "DYLIB") # fix LC_ID_DYLIB in first pass for shared_lib in shared_libs: install_name = _get_dylib_install_name(otool, shared_lib) # TODO: we probably only want to fix the install the name if # it starts with `/`. rpath_name = f"@rpath/{os.path.basename(install_name)}" _fix_install_name(shared_lib, rpath_name) substitutions[install_name] = rpath_name # fix dependencies in second pass for shared_lib in shared_libs: for old, new in substitutions.items(): _fix_dep_name(shared_lib, old, new) return substitutions def _fix_executables(substitutions): # Fix the install name for executables inside the package # that reference libraries we just patched bindirs = getattr(conanfile.cpp.package, "bindirs") for bindir in bindirs: full_folder = os.path.join(conanfile.package_folder, bindir) if not os.path.exists(full_folder): # Skip if the folder does not exist inside the package # (e.g. package does not contain executables but bindirs is defined) continue executables = _darwin_collect_binaries(full_folder, "EXECUTE") for executable in executables: # Fix install names of libraries from within the same package deps = _get_shared_dependencies(executable) for dep in deps: dep_base = os.path.join(os.path.dirname(dep), os.path.basename(dep).split('.')[0]) match = [k for k in substitutions.keys() if k.startswith(dep_base)] if match: _fix_dep_name(executable, dep, substitutions[match[0]]) # Add relative rpath to library directories, avoiding possible # existing duplicates libdirs = getattr(conanfile.cpp.package, "libdirs") libdirs = [os.path.join(conanfile.package_folder, libdir) for libdir in libdirs] rel_paths = [f"@executable_path/{os.path.relpath(libdir, full_folder)}" for libdir in libdirs] existing_rpaths = _get_rpath_entries(executable) rpaths_to_add = list(set(rel_paths) - set(existing_rpaths)) for entry in rpaths_to_add: command = f"{install_name_tool} {executable} -add_rpath {entry}" conanfile.run(command) substitutes = _fix_dylib_files() # Only "fix" executables if dylib files were patched, otherwise # there is nothing to do. if substitutes: _fix_executables(substitutes) def apple_extra_flags(conanfile): if not is_apple_os(conanfile): return [] enable_bitcode = conanfile.conf.get("tools.apple:enable_bitcode", check_type=bool) enable_visibility = conanfile.conf.get("tools.apple:enable_visibility", check_type=bool) is_debug = conanfile.settings.get_safe('build_type') == "Debug" flags = [] if enable_bitcode: if is_debug: flags.append("-fembed-bitcode-marker") else: flags.append("-fembed-bitcode") if enable_visibility: flags.append("-fvisibility=default") if enable_visibility is False: flags.extend(["-fvisibility=hidden", "-fvisibility-inlines-hidden"]) return flags ================================================ FILE: conan/tools/apple/xcodebuild.py ================================================ from conan.tools.apple.apple import to_apple_arch, xcodebuild_deployment_target_key from conan.tools.build import cmd_args_to_string class XcodeBuild: def __init__(self, conanfile): self._conanfile = conanfile self._build_type = conanfile.settings.get_safe("build_type") self._arch = to_apple_arch(self._conanfile) self._sdk = conanfile.settings.get_safe("os.sdk") or "" self._sdk_version = conanfile.settings.get_safe("os.sdk_version") or "" self._os = conanfile.settings.get_safe("os") self._os_version = conanfile.settings.get_safe("os.version") @property def _verbosity(self): verbosity = self._conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) \ or self._conanfile.conf.get("tools.compilation:verbosity", choices=("quiet", "verbose")) return "-" + verbosity if verbosity is not None else "" @property def _sdkroot(self): # User's sdk_path has priority, then if specified try to compose sdk argument # with sdk/sdk_version settings, leave blank otherwise and the sdk will be automatically # chosen by the build system sdk = self._conanfile.conf.get("tools.apple:sdk_path") if not sdk and self._sdk: sdk = "{}{}".format(self._sdk, self._sdk_version) return "SDKROOT={}".format(sdk) if sdk else "" def build(self, xcodeproj, target=None, configuration=None, cli_args=None): """ Call to ``xcodebuild`` to build a Xcode project. :param xcodeproj: the *xcodeproj* file to build. :param target: the target to build, in case this argument is passed to the ``build()`` method it will add the ``-target`` argument to the build system call. If not passed, it will build all the targets passing the ``-alltargets`` argument instead. :param configuration: Build configuration to use (e.g., ``Debug``, ``Release``). Defaults to the recipe's ``settings.build_type``. :param cli_args: Extra options to pass directly to ``xcodebuild`` (list of strings). Examples: ``["-xcconfig", ""]`` or custom Xcode build settings like ``["BUILD_LIBRARY_FOR_DISTRIBUTION=YES"]``. :return: the return code for the launched ``xcodebuild`` command. """ target = "-target '{}'".format(target) if target else "-alltargets" build_config = configuration or self._build_type cmd = "xcodebuild -project '{}' -configuration {} -arch {} " \ "{} {} {}".format(xcodeproj, build_config, self._arch, self._sdkroot, self._verbosity, target) deployment_target_key = xcodebuild_deployment_target_key(self._os) if deployment_target_key and self._os_version: cmd += f" {deployment_target_key}={self._os_version}" if cli_args: cmd += " " + cmd_args_to_string(cli_args) self._conanfile.run(cmd) ================================================ FILE: conan/tools/apple/xcodedeps.py ================================================ import os import re import textwrap from collections import OrderedDict from jinja2 import Template from conan.internal import check_duplicated_generator from conan.errors import ConanException from conan.internal.model.dependencies import get_transitive_requires from conan.internal.util.files import load, save from conan.tools.apple.apple import _to_apple_arch GLOBAL_XCCONFIG_TEMPLATE = textwrap.dedent("""\ // Includes both the toolchain and the dependencies // files if they exist """) GLOBAL_XCCONFIG_FILENAME = "conan_config.xcconfig" def _format_name(name): return re.sub(r'[^A-Za-z0-9_]', '_', name).lower() def _xcconfig_settings_filename(settings, configuration): arch = settings.get_safe("arch") architecture = _to_apple_arch(arch) or arch props = [("configuration", configuration), ("architecture", architecture), ("sdk name", settings.get_safe("os.sdk")), ("sdk version", settings.get_safe("os.sdk_version"))] name = "".join("_{}".format(v) for _, v in props if v is not None and v) return _format_name(name) def _xcconfig_conditional(settings, configuration): sdk_condition = "*" arch = settings.get_safe("arch") architecture = _to_apple_arch(arch) or arch sdk = settings.get_safe("os.sdk") if settings.get_safe("os") != "Macos" else "macosx" if sdk: sdk_condition = "{}{}".format(sdk, settings.get_safe("os.sdk_version") or "*") return "[config={}][arch={}][sdk={}]".format(configuration, architecture, sdk_condition) def _add_includes_to_file_or_create(filename, template, files_to_include): if os.path.isfile(filename): content = load(filename) else: content = template for include in files_to_include: if include not in content: content = content + '#include "{}"\n'.format(include) return content class XcodeDeps: general_name = "conandeps.xcconfig" _conf_xconfig = textwrap.dedent("""\ PACKAGE_ROOT_{{pkg_name}}{{condition}} = {{root}} // Compiler options for {{pkg_name}}::{{comp_name}} SYSTEM_HEADER_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}{{condition}} = {{include_dirs}} GCC_PREPROCESSOR_DEFINITIONS_{{pkg_name}}_{{comp_name}}{{condition}} = {{definitions}} OTHER_CFLAGS_{{pkg_name}}_{{comp_name}}{{condition}} = {{c_compiler_flags}} OTHER_CPLUSPLUSFLAGS_{{pkg_name}}_{{comp_name}}{{condition}} = {{cxx_compiler_flags}} FRAMEWORK_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}{{condition}} = {{frameworkdirs}} // Link options for {{pkg_name}}::{{comp_name}} LIBRARY_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}{{condition}} = {{lib_dirs}} OTHER_LDFLAGS_{{pkg_name}}_{{comp_name}}{{condition}} = {{linker_flags}} {{libs}} {{system_libs}} {{frameworks}} """) _dep_xconfig = textwrap.dedent("""\ // Conan XcodeDeps generated file for {{pkg_name}}::{{comp_name}} // Includes all configurations for each dependency {% for include in deps_includes %} #include "{{include}}" {% endfor %} #include "{{dep_xconfig_filename}}" SYSTEM_HEADER_SEARCH_PATHS = $(inherited) $(SYSTEM_HEADER_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}) GCC_PREPROCESSOR_DEFINITIONS = $(inherited) $(GCC_PREPROCESSOR_DEFINITIONS_{{pkg_name}}_{{comp_name}}) OTHER_CFLAGS = $(inherited) $(OTHER_CFLAGS_{{pkg_name}}_{{comp_name}}) OTHER_CPLUSPLUSFLAGS = $(inherited) $(OTHER_CPLUSPLUSFLAGS_{{pkg_name}}_{{comp_name}}) FRAMEWORK_SEARCH_PATHS = $(inherited) $(FRAMEWORK_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}) // Link options for {{pkg_name}}_{{comp_name}} LIBRARY_SEARCH_PATHS = $(inherited) $(LIBRARY_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}) OTHER_LDFLAGS = $(inherited) $(OTHER_LDFLAGS_{{pkg_name}}_{{comp_name}}) """) _all_xconfig = textwrap.dedent("""\ // Conan XcodeDeps generated file // Includes all direct dependencies """) _pkg_xconfig = textwrap.dedent("""\ // Conan XcodeDeps generated file // Includes all components for the package """) def __init__(self, conanfile): self._conanfile = conanfile self.configuration = conanfile.settings.get_safe("build_type") arch = conanfile.settings.get_safe("arch") self.architecture = _to_apple_arch(arch, default=arch) self.os_version = conanfile.settings.get_safe("os.version") self.sdk = conanfile.settings.get_safe("os.sdk") self.sdk_version = conanfile.settings.get_safe("os.sdk_version") def generate(self): check_duplicated_generator(self, self._conanfile) if self.configuration is None: raise ConanException("XcodeDeps.configuration is None, it should have a value") if self.architecture is None: raise ConanException("XcodeDeps.architecture is None, it should have a value") generator_files = self._content() for generator_file, content in generator_files.items(): save(generator_file, content) def _conf_xconfig_file(self, require, pkg_name, comp_name, package_folder, transitive_cpp_infos): """ content for conan_poco_x86_release.xcconfig, containing the activation """ def _merged_vars(name): merged = [var for cpp_info in transitive_cpp_infos for var in getattr(cpp_info, name)] return list(OrderedDict.fromkeys(merged).keys()) # TODO: Investigate if paths can be made relative to "root" folder fields = { 'pkg_name': pkg_name, 'comp_name': comp_name, 'root': package_folder, 'include_dirs': " ".join('"{}"'.format(p) for p in _merged_vars("includedirs")), 'lib_dirs': " ".join('"{}"'.format(p) for p in _merged_vars("libdirs")), 'libs': " ".join("-l{}".format(lib) for lib in _merged_vars("libs")), 'system_libs': " ".join("-l{}".format(sys_lib) for sys_lib in _merged_vars("system_libs")), 'frameworkdirs': " ".join('"{}"'.format(p) for p in _merged_vars("frameworkdirs")), 'frameworks': " ".join("-framework {}".format(framework) for framework in _merged_vars("frameworks")), 'definitions': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("defines")), 'c_compiler_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("cflags")), 'cxx_compiler_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("cxxflags")), 'linker_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("sharedlinkflags")), 'exe_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("exelinkflags")), 'condition': _xcconfig_conditional(self._conanfile.settings, self.configuration) } if not require.headers: fields["include_dirs"] = "" if not require.libs: fields["lib_dirs"] = "" fields["libs"] = "" fields["system_libs"] = "" fields["frameworkdirs"] = "" fields["frameworks"] = "" if not require.libs and not require.headers: fields["definitions"] = "" fields["c_compiler_flags"] = "" fields["cxx_compiler_flags"] = "" fields["linker_flags"] = "" fields["exe_flags"] = "" template = Template(self._conf_xconfig) content_multi = template.render(**fields) return content_multi def _dep_xconfig_file(self, pkg_name, comp_name, name_general, dep_xconfig_filename, reqs): # Current directory is the generators_folder multi_path = name_general if os.path.isfile(multi_path): content_multi = load(multi_path) else: content_multi = self._dep_xconfig def _get_includes(components): # if we require the root component dep::dep include conan_dep.xcconfig # for components (dep::component) include conan_dep_component.xcconfig return [f"conan_{_format_name(component[0])}.xcconfig" if component[0] == component[1] else f"conan_{_format_name(component[0])}_{_format_name(component[1])}.xcconfig" for component in components] content_multi = Template(content_multi).render({"pkg_name": pkg_name, "comp_name": comp_name, "dep_xconfig_filename": dep_xconfig_filename, "deps_includes": _get_includes(reqs)}) if dep_xconfig_filename not in content_multi: content_multi = content_multi.replace('.xcconfig"', '.xcconfig"\n#include "{}"'.format(dep_xconfig_filename), 1) return content_multi def _all_xconfig_file(self, deps, content): """ this is a .xcconfig file including all declared dependencies """ content_multi = content or self._all_xconfig for dep in deps.values(): include_file = f'conan_{_format_name(dep.ref.name)}.xcconfig' if include_file not in content_multi: content_multi = content_multi + f'\n#include "{include_file}"\n' return content_multi def _pkg_xconfig_file(self, components): """ this is a .xcconfig file including the components for each package """ content_multi = self._pkg_xconfig for pkg_name, comp_name in components: content_multi = content_multi + '\n#include "conan_{}_{}.xcconfig"\n'.format(pkg_name, comp_name) return content_multi @property def _global_xconfig_content(self): return _add_includes_to_file_or_create(GLOBAL_XCCONFIG_FILENAME, GLOBAL_XCCONFIG_TEMPLATE, [self.general_name]) def get_content_for_component(self, require, pkg_name, component_name, package_folder, transitive_internal, transitive_external): result = {} conf_name = _xcconfig_settings_filename(self._conanfile.settings, self.configuration) props_name = "conan_{}_{}{}.xcconfig".format(pkg_name, component_name, conf_name) result[props_name] = self._conf_xconfig_file(require, pkg_name, component_name, package_folder, transitive_internal) # The entry point for each package file_dep_name = "conan_{}_{}.xcconfig".format(pkg_name, component_name) dep_content = self._dep_xconfig_file(pkg_name, component_name, file_dep_name, props_name, transitive_external) result[file_dep_name] = dep_content return result def _content(self): result = {} # Generate the config files for each component with name conan_pkgname_compname.xcconfig # If a package has no components the name is conan_pkgname_pkgname.xcconfig # All components are included in the conan_pkgname.xcconfig file host_req = self._conanfile.dependencies.host test_req = self._conanfile.dependencies.test requires = list(host_req.items()) + list(test_req.items()) for require, dep in requires: dep_name = _format_name(dep.ref.name) include_components_names = [] transitive_requires = [r for r, _ in get_transitive_requires(self._conanfile, dep).items()] if dep.cpp_info.has_components: transitive_dep_names = [_format_name(dep.ref.name) for dep in transitive_requires] sorted_components = dep.cpp_info.get_sorted_components().items() for comp_name, comp_cpp_info in sorted_components: comp_name = _format_name(comp_name) # returns: ("list of cpp infos from required components in same package", # "list of names from required components from other packages") def _get_component_requires(component): requires_external = [(req.split("::")[0], req.split("::")[1]) for req in component.requires if "::" in req and req.split("::")[0] in transitive_dep_names] requires_internal = [dep.cpp_info.components.get(req) for req in component.requires if "::" not in req] return requires_internal, requires_external # these are the transitive dependencies between components of the same package transitive_internal = [] # these are the transitive dependencies to components from other packages transitive_external = [] # return the internal cpp_infos and external components names def _transitive_components(component): requires_internal, requires_external = _get_component_requires(component) transitive_internal.append(component) transitive_internal.extend(requires_internal) transitive_external.extend(requires_external) for treq in requires_internal: _transitive_components(treq) _transitive_components(comp_cpp_info) # remove duplicates transitive_internal = list(OrderedDict.fromkeys(transitive_internal).keys()) transitive_external = list(OrderedDict.fromkeys(transitive_external).keys()) # In case dep is editable and package_folder=None pkg_folder = dep.package_folder or dep.recipe_folder component_content = self.get_content_for_component(require, dep_name, comp_name, pkg_folder, transitive_internal, transitive_external) include_components_names.append((dep_name, comp_name)) result.update(component_content) else: public_deps = [] for r, d in dep.dependencies.direct_host.items(): if r not in transitive_requires: continue if d.cpp_info.has_components: sorted_components = d.cpp_info.get_sorted_components().items() for comp_name, comp_cpp_info in sorted_components: public_deps.append((_format_name(d.ref.name), _format_name(comp_name))) else: public_deps.append((_format_name(d.ref.name),) * 2) required_components = dep.cpp_info.required_components if dep.cpp_info.required_components else public_deps # In case dep is editable and package_folder=None pkg_folder = dep.package_folder or dep.recipe_folder root_content = self.get_content_for_component(require, dep_name, dep_name, pkg_folder, [dep.cpp_info], required_components) include_components_names.append((dep_name, dep_name)) result.update(root_content) result["conan_{}.xcconfig".format(dep_name)] = self._pkg_xconfig_file(include_components_names) all_file_content = "" # Include direct requires direct_deps = self._conanfile.dependencies.filter({"direct": True, "build": False, "skip": False}) result[self.general_name] = self._all_xconfig_file(direct_deps, all_file_content) result[GLOBAL_XCCONFIG_FILENAME] = self._global_xconfig_content return result ================================================ FILE: conan/tools/apple/xcodetoolchain.py ================================================ import textwrap from conan.internal import check_duplicated_generator from conan.tools.apple.apple import to_apple_arch, xcodebuild_deployment_target_key from conan.tools.apple.xcodedeps import GLOBAL_XCCONFIG_FILENAME, GLOBAL_XCCONFIG_TEMPLATE, \ _add_includes_to_file_or_create, _xcconfig_settings_filename, _xcconfig_conditional from conan.internal.util.files import save class XcodeToolchain: filename = "conantoolchain" extension = ".xcconfig" _vars_xconfig = textwrap.dedent("""\ // Definition of toolchain variables {apple_deployment_target} {clang_cxx_library} {clang_cxx_language_standard} """) _flags_xconfig = textwrap.dedent("""\ // Global flags {defines} {cflags} {cppflags} {ldflags} """) _agreggated_xconfig = textwrap.dedent("""\ // Conan XcodeToolchain generated file // Includes all installed configurations """) def __init__(self, conanfile): self._conanfile = conanfile arch = conanfile.settings.get_safe("arch") self.architecture = to_apple_arch(self._conanfile, default=arch) self.configuration = conanfile.settings.build_type self.libcxx = conanfile.settings.get_safe("compiler.libcxx") self.os_version = conanfile.settings.get_safe("os.version") self._global_defines = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list) self._global_cxxflags = self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) self._global_cflags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list) sharedlinkflags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) exelinkflags = self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list) self._global_ldflags = sharedlinkflags + exelinkflags def generate(self): check_duplicated_generator(self, self._conanfile) save(self._agreggated_xconfig_filename, self._agreggated_xconfig_content) save(self._vars_xconfig_filename, self._vars_xconfig_content) if self._check_if_extra_flags: save(self._flags_xcconfig_filename, self._flags_xcconfig_content) save(GLOBAL_XCCONFIG_FILENAME, self._global_xconfig_content) @property def _cppstd(self): from conan.tools.build.flags import cppstd_flag cppstd = cppstd_flag(self._conanfile) if cppstd.startswith("-std="): return cppstd[5:] return cppstd @property def _apple_deployment_target(self): deployment_target_key = xcodebuild_deployment_target_key(self._conanfile.settings.get_safe("os")) return '{}{}={}'.format(deployment_target_key, _xcconfig_conditional(self._conanfile.settings, self.configuration), self.os_version) if deployment_target_key and self.os_version else "" @property def _clang_cxx_library(self): return 'CLANG_CXX_LIBRARY{}={}'.format(_xcconfig_conditional(self._conanfile.settings, self.configuration), self.libcxx) if self.libcxx else "" @property def _clang_cxx_language_standard(self): return 'CLANG_CXX_LANGUAGE_STANDARD{}={}'.format(_xcconfig_conditional(self._conanfile.settings, self.configuration), self._cppstd) if self._cppstd else "" @property def _vars_xconfig_filename(self): return "conantoolchain{}{}".format(_xcconfig_settings_filename(self._conanfile.settings, self.configuration), self.extension) @property def _vars_xconfig_content(self): ret = self._vars_xconfig.format(apple_deployment_target=self._apple_deployment_target, clang_cxx_library=self._clang_cxx_library, clang_cxx_language_standard=self._clang_cxx_language_standard) return ret @property def _agreggated_xconfig_content(self): return _add_includes_to_file_or_create(self._agreggated_xconfig_filename, self._agreggated_xconfig, [self._vars_xconfig_filename]) @property def _global_xconfig_content(self): files_to_include = [self._agreggated_xconfig_filename] if self._check_if_extra_flags: files_to_include.append(self._flags_xcconfig_filename) content = _add_includes_to_file_or_create(GLOBAL_XCCONFIG_FILENAME, GLOBAL_XCCONFIG_TEMPLATE, files_to_include) return content @property def _agreggated_xconfig_filename(self): return self.filename + self.extension @property def _check_if_extra_flags(self): return self._global_cflags or self._global_cxxflags or self._global_ldflags or self._global_defines @property def _flags_xcconfig_content(self): defines = "GCC_PREPROCESSOR_DEFINITIONS = $(inherited) {}".format(" ".join(self._global_defines)) if self._global_defines else "" cflags = "OTHER_CFLAGS = $(inherited) {}".format(" ".join(self._global_cflags)) if self._global_cflags else "" cppflags = "OTHER_CPLUSPLUSFLAGS = $(inherited) {}".format(" ".join(self._global_cxxflags)) if self._global_cxxflags else "" ldflags = "OTHER_LDFLAGS = $(inherited) {}".format(" ".join(self._global_ldflags)) if self._global_ldflags else "" ret = self._flags_xconfig.format(defines=defines, cflags=cflags, cppflags=cppflags, ldflags=ldflags) return ret @property def _flags_xcconfig_filename(self): return "conan_global_flags" + self.extension ================================================ FILE: conan/tools/build/__init__.py ================================================ import configparser import os import sys from shlex import quote from conan.tools.build.flags import cppstd_flag from conan.tools.build.cppstd import check_max_cppstd, check_min_cppstd, \ valid_max_cppstd, valid_min_cppstd, default_cppstd, supported_cppstd from conan.tools.build.cstd import check_max_cstd, check_min_cstd, \ valid_max_cstd, valid_min_cstd, default_cstd, supported_cstd from conan.tools.build.cpu import build_jobs from conan.tools.build.cross_building import cross_building, can_run from conan.tools.build.stdcpp_library import stdcpp_library from conan.tools.build.compiler import check_min_compiler_version from conan.errors import ConanException CONAN_TOOLCHAIN_ARGS_FILE = "conanbuild.conf" CONAN_TOOLCHAIN_ARGS_SECTION = "toolchain" def use_win_mingw(conanfile): os_build = conanfile.settings_build.get_safe('os') if os_build == "Windows": compiler = conanfile.settings.get_safe("compiler") sub = conanfile.settings.get_safe("os.subsystem") if sub in ("cygwin", "msys2", "msys") or compiler == "qcc": return False else: return True return False def cmd_args_to_string(args): if not args: return "" if sys.platform == 'win32': return _windows_cmd_args_to_string(args) else: return _unix_cmd_args_to_string(args) def _unix_cmd_args_to_string(args): """Return a shell-escaped string from *split_command*.""" return ' '.join(quote(arg) for arg in args) def _windows_cmd_args_to_string(args): # FIXME: This is not managing all the parsing from list2cmdline, but covering simplified cases ret = [] for arg in args: # escaped quotes have to escape the \ and then the ". Replace with so next # replace doesn't interfere arg = arg.replace(r'\"', r'\\\') # quotes have to be escaped arg = arg.replace(r'"', r'\"') # restore the quotes arg = arg.replace("", '"') # if argument have spaces, quote it if ' ' in arg or '\t' in arg: ret.append('"{}"'.format(arg)) else: ret.append(arg) return " ".join(ret) def load_toolchain_args(generators_folder=None, namespace=None): """ Helper function to load the content of any CONAN_TOOLCHAIN_ARGS_FILE :param generators_folder: `str` folder where is located the CONAN_TOOLCHAIN_ARGS_FILE. :param namespace: `str` namespace to be prepended to the filename. :return: """ namespace_name = "{}_{}".format(namespace, CONAN_TOOLCHAIN_ARGS_FILE) if namespace \ else CONAN_TOOLCHAIN_ARGS_FILE args_file = os.path.join(generators_folder, namespace_name) if generators_folder \ else namespace_name toolchain_config = configparser.ConfigParser() toolchain_file = toolchain_config.read(args_file) if not toolchain_file: raise ConanException("The file %s does not exist. Please, make sure that it was not" " generated in another folder." % args_file) try: return toolchain_config[CONAN_TOOLCHAIN_ARGS_SECTION] except KeyError: raise ConanException("The primary section [%s] does not exist in the file %s. Please, add it" " as the default one of all your configuration variables." % (CONAN_TOOLCHAIN_ARGS_SECTION, args_file)) def save_toolchain_args(content, generators_folder=None, namespace=None): """ Helper function to save the content into the CONAN_TOOLCHAIN_ARGS_FILE :param content: `dict` all the information to be saved into the toolchain file. :param namespace: `str` namespace to be prepended to the filename. :param generators_folder: `str` folder where is located the CONAN_TOOLCHAIN_ARGS_FILE """ # Let's prune None values content_ = {k: v for k, v in content.items() if v is not None} namespace_name = "{}_{}".format(namespace, CONAN_TOOLCHAIN_ARGS_FILE) if namespace \ else CONAN_TOOLCHAIN_ARGS_FILE args_file = os.path.join(generators_folder, namespace_name) if generators_folder \ else namespace_name toolchain_config = configparser.ConfigParser() toolchain_config[CONAN_TOOLCHAIN_ARGS_SECTION] = content_ with open(args_file, "w") as f: toolchain_config.write(f) ================================================ FILE: conan/tools/build/compiler.py ================================================ from conan.errors import ConanInvalidConfiguration, ConanException from conan.internal.model.version import Version def check_min_compiler_version(conanfile, compiler_restrictions): """(Experimental) Checks if the current compiler and its version meet the minimum requirements. :param conanfile: The current recipe object. Always use ``self``. :param compiler_restrictions: A list of tuples, where each tuple contains: - **compiler** (*str*): The name of the compiler (e.g., "gcc", "msvc"). - **min_version** (*str*): The minimum required version as a string (e.g., "14", "19.0"). - **reason** (*str*): A string explaining the reason for the version requirement. :raises ConanException: If the 'compiler' or 'compiler.version' settings are not defined. :raises ConanInvalidConfiguration: If the found compiler version is less than the specified minimum version for that compiler. :Example: .. code-block:: python def validate(self): compiler_restrictions = [ ("clang", "14", "requires C++20 coroutines support"), ("gcc", "12", "requires C++20 modules support") ] check_min_compiler_version(self, compiler_restrictions) """ compiler_value = conanfile.settings.get_safe("compiler") if not compiler_value: raise ConanException("Called check_min_compiler_version with no compiler defined") compiler_version = conanfile.settings.get_safe("compiler.version") if not compiler_version: raise ConanException("Called check_min_compiler_version with no compiler.version defined") for compiler, min_version, reason in compiler_restrictions: if compiler_value == compiler: if Version(compiler_version) < Version(min_version): ref = conanfile.ref if hasattr(conanfile, "ref") else conanfile.name raise ConanInvalidConfiguration( f"{ref} requires {compiler} >= {min_version}, but {compiler} {compiler_version} was found\n" f"Reason: {reason}") break ================================================ FILE: conan/tools/build/cppstd.py ================================================ import operator from conan.errors import ConanInvalidConfiguration, ConanException from conan.internal.api.detect.detect_api import default_cppstd as default_cppstd_ from conan.internal.model.version import Version def check_min_cppstd(conanfile, cppstd, gnu_extensions=False): """ Check if current cppstd fits the minimal version required. In case the current cppstd doesn't fit the minimal version required by cppstd, a ConanInvalidConfiguration exception will be raised. settings.compiler.cppstd must be defined, otherwise ConanInvalidConfiguration is raised :param conanfile: The current recipe object. Always use ``self``. :param cppstd: Minimal cppstd version required :param gnu_extensions: GNU extension is required (e.g gnu17) """ _check_cppstd(conanfile, cppstd, operator.lt, gnu_extensions) def check_max_cppstd(conanfile, cppstd, gnu_extensions=False): """ Check if current cppstd fits the maximum version required. In case the current cppstd doesn't fit the maximum version required by cppstd, a ConanInvalidConfiguration exception will be raised. settings.compiler.cppstd must be defined, otherwise ConanInvalidConfiguration is raised :param conanfile: The current recipe object. Always use ``self``. :param cppstd: Maximum cppstd version required :param gnu_extensions: GNU extension is required (e.g gnu17) """ _check_cppstd(conanfile, cppstd, operator.gt, gnu_extensions) def valid_min_cppstd(conanfile, cppstd, gnu_extensions=False): """ Validate if current cppstd fits the minimal version required. :param conanfile: The current recipe object. Always use ``self``. :param cppstd: Minimal cppstd version required :param gnu_extensions: GNU extension is required (e.g gnu17). This option ONLY works on Linux. :return: True, if current cppstd matches the required cppstd version. Otherwise, False. """ try: check_min_cppstd(conanfile, cppstd, gnu_extensions) except ConanInvalidConfiguration: return False return True def valid_max_cppstd(conanfile, cppstd, gnu_extensions=False): """ Validate if current cppstd fits the maximum version required. :param conanfile: The current recipe object. Always use ``self``. :param cppstd: Maximum cppstd version required :param gnu_extensions: GNU extension is required (e.g gnu17). This option ONLY works on Linux. :return: True, if current cppstd matches the required cppstd version. Otherwise, False. """ try: check_max_cppstd(conanfile, cppstd, gnu_extensions) except ConanInvalidConfiguration: return False return True def default_cppstd(conanfile, compiler=None, compiler_version=None): """ Get the default ``compiler.cppstd`` for the "conanfile.settings.compiler" and "conanfile settings.compiler_version" or for the parameters "compiler" and "compiler_version" if specified. :param conanfile: The current recipe object. Always use ``self``. :param compiler: Name of the compiler e.g. gcc :param compiler_version: Version of the compiler e.g. 12 :return: The default ``compiler.cppstd`` for the specified compiler """ compiler = compiler or conanfile.settings.get_safe("compiler") compiler_version = compiler_version or conanfile.settings.get_safe("compiler.version") if not compiler or not compiler_version: raise ConanException("Called default_cppstd with no compiler or no compiler.version") return default_cppstd_(compiler, Version(compiler_version)) def supported_cppstd(conanfile, compiler=None, compiler_version=None): """ Get a list of supported ``compiler.cppstd`` for the "conanfile.settings.compiler" and "conanfile.settings.compiler_version" or for the parameters "compiler" and "compiler_version" if specified. :param conanfile: The current recipe object. Always use ``self``. :param compiler: Name of the compiler e.g: gcc :param compiler_version: Version of the compiler e.g: 12 :return: a list of supported ``cppstd`` values. """ compiler = compiler or conanfile.settings.get_safe("compiler") compiler_version = compiler_version or conanfile.settings.get_safe("compiler.version") if not compiler or not compiler_version: raise ConanException("Called supported_cppstd with no compiler or no compiler.version") func = {"apple-clang": _apple_clang_supported_cppstd, "gcc": _gcc_supported_cppstd, "msvc": _msvc_supported_cppstd, "clang": _clang_supported_cppstd, "mcst-lcc": _mcst_lcc_supported_cppstd, "qcc": _qcc_supported_cppstd, "emcc": _emcc_supported_cppstd, }.get(compiler) if func: return func(Version(compiler_version)) return None def _check_cppstd(conanfile, cppstd, comparator, gnu_extensions): """ Check if current cppstd fits the version required according to a given comparator. In case the current cppstd doesn't fit the maximum version required by cppstd, a ConanInvalidConfiguration exception will be raised. settings.compiler.cppstd must be defined, otherwise ConanInvalidConfiguration is raised :param conanfile: The current recipe object. Always use ``self``. :param cppstd: Required cppstd version. :param comparator: Operator to use to compare the detected and the required cppstd versions. :param gnu_extensions: GNU extension is required (e.g gnu17) """ if not str(cppstd).isdigit(): raise ConanException("cppstd parameter must be a number") def compare(lhs, rhs, comp): def extract_cpp_version(_cppstd): return str(_cppstd).replace("gnu", "") def add_millennium(_cppstd): return "19%s" % _cppstd if _cppstd == "98" else "20%s" % _cppstd lhs = add_millennium(extract_cpp_version(lhs)) rhs = add_millennium(extract_cpp_version(rhs)) return not comp(lhs, rhs) current_cppstd = conanfile.settings.get_safe("compiler.cppstd") if current_cppstd is None: raise ConanInvalidConfiguration("The compiler.cppstd is not defined for this configuration") if gnu_extensions and "gnu" not in current_cppstd: raise ConanInvalidConfiguration("The cppstd GNU extension is required") if not compare(current_cppstd, cppstd, comparator): raise ConanInvalidConfiguration( "Current cppstd ({}) is {} than the required C++ standard ({}).".format( current_cppstd, "higher" if comparator == operator.gt else "lower", cppstd)) def _apple_clang_supported_cppstd(version): """ ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"] """ if version < "4.0": return [] if version < "5.1": return ["98", "gnu98", "11", "gnu11"] if version < "6.1": return ["98", "gnu98", "11", "gnu11", "14", "gnu14"] if version < "10.0": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"] if version < "13.0": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"] # https://github.com/conan-io/conan/pull/17092 doesn't show c++23 full support until 16 # but it was this before Conan 2.9, so keeping it to not break if version < "16.0": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"] return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23", "26", "gnu26"] def _gcc_supported_cppstd(version): """ ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"] """ if version < "3.4": return [] if version < "4.3": return ["98", "gnu98"] if version < "4.8": return ["98", "gnu98", "11", "gnu11"] if version < "5": return ["98", "gnu98", "11", "gnu11", "14", "gnu14"] if version < "8": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"] if version < "11": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"] # https://github.com/conan-io/conan/pull/17092 if version < "14.0": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"] return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23", "26", "gnu26"] def _msvc_supported_cppstd(version): """ https://learn.microsoft.com/en-us/cpp/build/reference/std-specify-language-standard-version?view=msvc-170 - /std:c++14 starting in Visual Studio 2015 Update 3 (190) - /std:c++17 starting in Visual Studio 2017 version 15.3. (191) - /std:c++20 starting in Visual Studio 2019 version 16.11 (192) [14, 17, 20, 23] """ if version < "190": # pre VS 2015 return [] if version < "191": # VS 2015 return ["14"] if version < "192": # VS 2017 return ["14", "17"] if version < "193": return ["14", "17", "20"] return ["14", "17", "20", "23"] def _clang_supported_cppstd(version): """ ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"] """ if version < "2.1": return [] if version < "3.4": return ["98", "gnu98", "11", "gnu11"] if version < "3.5": return ["98", "gnu98", "11", "gnu11", "14", "gnu14"] if version < "6": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"] if version < "12": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"] # https://github.com/conan-io/conan/pull/17092 if version < "17.0": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"] return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23", "26", "gnu26"] def _mcst_lcc_supported_cppstd(version): """ ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"] """ if version < "1.21": return ["98", "gnu98"] if version < "1.24": return ["98", "gnu98", "11", "gnu11", "14", "gnu14"] if version < "1.25": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"] # FIXME: When cppstd 23 was introduced???? return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"] def _qcc_supported_cppstd(version): """ [98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17] """ if version < "5": return ["98", "gnu98"] elif version < "12": return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"] else: return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"] def _emcc_supported_cppstd(version): """ emcc is based on clang but follow different versioning scheme. """ if version <= "3.0.1": return _clang_supported_cppstd(Version("14")) if version <= "3.1.50": return _clang_supported_cppstd(Version("18")) if version <= "4.0.1": return _clang_supported_cppstd(Version("20")) # Since emcc 4.0.2 clang version is 21 return _clang_supported_cppstd(Version("21")) ================================================ FILE: conan/tools/build/cpu.py ================================================ from conan.internal.util import cpu_count def build_jobs(conanfile): """ Returns the number of CPUs available for parallel builds. It returns the configuration value for ``tools.build:jobs`` if exists, otherwise, it defaults to the helper function ``_cpu_count()``. ``_cpu_count()`` reads cgroup to detect the configured number of CPUs. Currently, there are two versions of cgroup available. In the case of cgroup v1, if the data in cgroup is invalid, processor detection comes into play. Whenever processor detection is not enabled, ``build_jobs()`` will safely return 1. In the case of cgroup v2, if no limit is set, processor detection is used. When the limit is set, the behavior is as described in cgroup v1. :param conanfile: The current recipe object. Always use ``self``. :return: ``int`` with the number of jobs """ return conanfile.conf.get("tools.build:jobs", default=cpu_count(), check_type=int) ================================================ FILE: conan/tools/build/cross_building.py ================================================ def cross_building(conanfile=None, skip_x64_x86=False): """ Check if we are cross building comparing the *build* and *host* settings. Returns ``True`` in the case that we are cross-building. :param conanfile: The current recipe object. Always use ``self``. :param skip_x64_x86: Do not consider cross building when building to 32 bits from 64 bits: x86_64 to x86, sparcv9 to sparc or ppc64 to ppc32 :return: ``bool`` value from ``tools.build.cross_building:cross_build`` if exists, otherwise, it returns ``True`` if we are cross-building, else, ``False``. """ cross_build = conanfile.conf.get("tools.build.cross_building:cross_build", check_type=bool) if cross_build is not None: return cross_build build_os = conanfile.settings_build.get_safe('os') build_arch = conanfile.settings_build.get_safe('arch') host_os = conanfile.settings.get_safe("os") host_arch = conanfile.settings.get_safe("arch") if skip_x64_x86 and host_os is not None and (build_os == host_os) and \ host_arch is not None and ((build_arch == "x86_64") and (host_arch == "x86") or (build_arch == "sparcv9") and (host_arch == "sparc") or (build_arch == "ppc64") and (host_arch == "ppc32")): return False if host_os is not None and (build_os != host_os): return True if host_arch is not None and (build_arch != host_arch): return True return False def can_run(conanfile): """ Validates whether is possible to run a non-native app on the same architecture. It’s a useful feature for the case your architecture can run more than one target. For instance, Mac M1 machines can run both `armv8` and `x86_64`. :param conanfile: The current recipe object. Always use ``self``. :return: ``bool`` value from ``tools.build.cross_building:can_run`` if exists, otherwise, it returns ``False`` if we are cross-building, else, ``True``. """ # Issue related: https://github.com/conan-io/conan/issues/11035 allowed = conanfile.conf.get("tools.build.cross_building:can_run", check_type=bool) if allowed is None: return not cross_building(conanfile) return allowed ================================================ FILE: conan/tools/build/cstd.py ================================================ import operator from conan.errors import ConanInvalidConfiguration, ConanException from conan.internal.api.detect.detect_api import default_cstd as default_cstd_ from conan.internal.model.version import Version def check_min_cstd(conanfile, cstd, gnu_extensions=False): """ Check if current cstd fits the minimal version required. In case the current cstd doesn't fit the minimal version required by cstd, a ConanInvalidConfiguration exception will be raised. 1. If settings.compiler.cstd, the tool will use settings.compiler.cstd to compare 2. It not settings.compiler.cstd, the tool will use compiler to compare (reading the default from cstd_default) 3. If not settings.compiler is present (not declared in settings) will raise because it cannot compare. 4. If can not detect the default cstd for settings.compiler, a exception will be raised. :param conanfile: The current recipe object. Always use ``self``. :param cstd: Minimal cstd version required :param gnu_extensions: GNU extension is required (e.g gnu17) """ _check_cstd(conanfile, cstd, operator.lt, gnu_extensions) def check_max_cstd(conanfile, cstd, gnu_extensions=False): """ Check if current cstd fits the maximum version required. In case the current cstd doesn't fit the maximum version required by cstd, a ConanInvalidConfiguration exception will be raised. 1. If settings.compiler.cstd, the tool will use settings.compiler.cstd to compare 2. It not settings.compiler.cstd, the tool will use compiler to compare (reading the default from cstd_default) 3. If not settings.compiler is present (not declared in settings) will raise because it cannot compare. 4. If can not detect the default cstd for settings.compiler, a exception will be raised. :param conanfile: The current recipe object. Always use ``self``. :param cstd: Maximum cstd version required :param gnu_extensions: GNU extension is required (e.g gnu17) """ _check_cstd(conanfile, cstd, operator.gt, gnu_extensions) def valid_min_cstd(conanfile, cstd, gnu_extensions=False): """ Validate if current cstd fits the minimal version required. :param conanfile: The current recipe object. Always use ``self``. :param cstd: Minimal cstd version required :param gnu_extensions: GNU extension is required (e.g gnu17). This option ONLY works on Linux. :return: True, if current cstd matches the required cstd version. Otherwise, False. """ try: check_min_cstd(conanfile, cstd, gnu_extensions) except ConanInvalidConfiguration: return False return True def valid_max_cstd(conanfile, cstd, gnu_extensions=False): """ Validate if current cstd fits the maximum version required. :param conanfile: The current recipe object. Always use ``self``. :param cstd: Maximum cstd version required :param gnu_extensions: GNU extension is required (e.g gnu17). This option ONLY works on Linux. :return: True, if current cstd matches the required cstd version. Otherwise, False. """ try: check_max_cstd(conanfile, cstd, gnu_extensions) except ConanInvalidConfiguration: return False return True def default_cstd(conanfile, compiler=None, compiler_version=None): """ Get the default ``compiler.cstd`` for the "conanfile.settings.compiler" and "conanfile settings.compiler_version" or for the parameters "compiler" and "compiler_version" if specified. :param conanfile: The current recipe object. Always use ``self``. :param compiler: Name of the compiler e.g. gcc :param compiler_version: Version of the compiler e.g. 12 :return: The default ``compiler.cstd`` for the specified compiler """ compiler = compiler or conanfile.settings.get_safe("compiler") compiler_version = compiler_version or conanfile.settings.get_safe("compiler.version") if not compiler or not compiler_version: raise ConanException("Called default_cppstd with no compiler or no compiler.version") return default_cstd_(compiler, Version(compiler_version)) def supported_cstd(conanfile, compiler=None, compiler_version=None): """ Get a list of supported ``compiler.cstd`` for the "conanfile.settings.compiler" and "conanfile.settings.compiler_version" or for the parameters "compiler" and "compiler_version" if specified. :param conanfile: The current recipe object. Always use ``self``. :param compiler: Name of the compiler e.g: gcc :param compiler_version: Version of the compiler e.g: 12 :return: a list of supported ``cstd`` values. """ compiler = compiler or conanfile.settings.get_safe("compiler") compiler_version = compiler_version or conanfile.settings.get_safe("compiler.version") if not compiler or not compiler_version: raise ConanException("Called supported_cstd with no compiler or no compiler.version") func = {"apple-clang": _apple_clang_supported_cstd, "gcc": _gcc_supported_cstd, "msvc": _msvc_supported_cstd, "clang": _clang_supported_cstd, "emcc": _emcc_supported_cstd, }.get(compiler) if func: return func(Version(compiler_version)) return None def _check_cstd(conanfile, cstd, comparator, gnu_extensions): """ Check if current cstd fits the version required according to a given comparator. In case the current cstd doesn't fit the maximum version required by cstd, a ConanInvalidConfiguration exception will be raised. 1. If settings.compiler.cstd, the tool will use settings.compiler.cstd to compare 2. It not settings.compiler.cstd, the tool will use compiler to compare (reading the default from cstd_default) 3. If not settings.compiler is present (not declared in settings) will raise because it cannot compare. 4. If can not detect the default cstd for settings.compiler, a exception will be raised. :param conanfile: The current recipe object. Always use ``self``. :param cstd: Required cstd version. :param comparator: Operator to use to compare the detected and the required cstd versions. :param gnu_extensions: GNU extension is required (e.g gnu17) """ if not str(cstd).isdigit(): raise ConanException("cstd parameter must be a number") def compare(lhs, rhs, comp): def extract_cpp_version(_cstd): return str(_cstd).replace("gnu", "") def add_millennium(_cstd): return "19%s" % _cstd if _cstd == "99" else "20%s" % _cstd lhs = add_millennium(extract_cpp_version(lhs)) rhs = add_millennium(extract_cpp_version(rhs)) return not comp(lhs, rhs) current_cstd = conanfile.settings.get_safe("compiler.cstd") if current_cstd is None: raise ConanInvalidConfiguration("The compiler.cstd is not defined for this configuration") if gnu_extensions and "gnu" not in current_cstd: raise ConanInvalidConfiguration("The cstd GNU extension is required") if not compare(current_cstd, cstd, comparator): raise ConanInvalidConfiguration( "Current cstd ({}) is {} than the required C standard ({}).".format( current_cstd, "higher" if comparator == operator.gt else "lower", cstd)) def _apple_clang_supported_cstd(version): # TODO: Per-version support return ["99", "gnu99", "11", "gnu11", "17", "gnu17", "23", "gnu23"] def _gcc_supported_cstd(version): if version < "4.7": return ["99", "gnu99"] if version < "8": return ["99", "gnu99", "11", "gnu11"] if version < "14": return ["99", "gnu99", "11", "gnu11", "17", "gnu17"] return ["99", "gnu99", "11", "gnu11", "17", "gnu17", "23", "gnu23"] def _msvc_supported_cstd(version): if version < "192": return [] return ["11", "17"] def _clang_supported_cstd(version): if version < "3": return ["99", "gnu99"] if version < "6": return ["99", "gnu99", "11", "gnu11"] if version < "18": return ["99", "gnu99", "11", "gnu11", "17", "gnu17"] return ["99", "gnu99", "11", "gnu11", "17", "gnu17", "23", "gnu23"] def _emcc_supported_cstd(version): """ emcc is based on clang but follow different versioning scheme. """ if version <= "3.0.1": return _clang_supported_cstd(Version("14")) if version <= "3.1.50": return _clang_supported_cstd(Version("18")) if version <= "4.0.1": return _clang_supported_cstd(Version("20")) # Since emcc 4.0.2 clang version is 21 return _clang_supported_cstd(Version("21")) ================================================ FILE: conan/tools/build/flags.py ================================================ from conan.errors import ConanException from conan.internal.model.version import Version def disable_flag(conanfile, flag): disable_flags = conanfile.conf.get("tools.gnu:disable_flags", check_type=list) if disable_flags is None: return False valid = ["arch", "arch_link", "libcxx", "build_type", "build_type_link", "threads", "cppstd", "cstd"] for v in disable_flags: if v not in valid: raise ConanException(f"tools.gnu:disable_flags value '{v}', must be one of: {valid}") return flag in disable_flags def architecture_flag(conanfile): """ returns flags specific to the target architecture and compiler Used by CMakeToolchain and AutotoolsToolchain """ if disable_flag(conanfile, "arch"): return "" settings = conanfile.settings from conan.tools.apple.apple import _to_apple_arch compiler = settings.get_safe("compiler") arch = settings.get_safe("arch") the_os = settings.get_safe("os") subsystem = settings.get_safe("os.subsystem") subsystem_ios_version = settings.get_safe("os.subsystem.ios_version") if not compiler or not arch: return "" if the_os == "Android": return "" if compiler == "clang" and the_os == "Windows": comp_exes = conanfile.conf.get("tools.build:compiler_executables", check_type=dict, default={}) clangcl = "clang-cl" in (comp_exes.get("c") or comp_exes.get("cpp", "")) if clangcl: return "" # Do not add arch flags for clang-cl, can happen in cross-build runtime=None # LLVM/Clang and VS/Clang must define runtime. msys2 clang won't runtime = settings.get_safe("compiler.runtime") # runtime is Windows only if runtime is not None: return "" # TODO: Maybe Clang-Mingw runtime does, but with C++ is impossible to test return {"x86_64": "-m64", "x86": "-m32"}.get(arch, "") elif compiler in ['gcc', 'apple-clang', 'clang', 'sun-cc']: if the_os == 'Macos' and subsystem == 'catalyst': # FIXME: This might be conflicting with Autotools --target cli arg apple_arch = _to_apple_arch(arch) if apple_arch: # TODO: Could we define anything like `to_apple_target()`? # Check https://github.com/rust-lang/rust/issues/48862 return f'--target={apple_arch}-apple-ios{subsystem_ios_version}-macabi' elif arch in ['x86_64', 'sparcv9', 's390x']: return '-m64' elif arch in ['x86', 'sparc']: return '-m32' elif arch in ['s390']: return '-m31' elif arch in ['tc131', 'tc16', 'tc161', 'tc162', 'tc18']: return '-m{}'.format(arch) elif the_os == 'AIX': if arch in ['ppc32']: return '-maix32' elif arch in ['ppc64']: return '-maix64' elif compiler == "intel-cc": # https://software.intel.com/en-us/cpp-compiler-developer-guide-and-reference-m32-m64-qm32-qm64 if arch == "x86": return "/Qm32" if the_os == "Windows" else "-m32" elif arch == "x86_64": return "/Qm64" if the_os == "Windows" else "-m64" elif compiler == "mcst-lcc": return {"e2k-v2": "-march=elbrus-v2", "e2k-v3": "-march=elbrus-v3", "e2k-v4": "-march=elbrus-v4", "e2k-v5": "-march=elbrus-v5", "e2k-v6": "-march=elbrus-v6", "e2k-v7": "-march=elbrus-v7"}.get(arch, "") elif compiler == "emcc": if arch == "wasm64": return "-sMEMORY64=1" return "" def architecture_link_flag(conanfile): """ returns exclusively linker flags specific to the target architecture and compiler """ if disable_flag(conanfile, "arch_link"): return "" compiler = conanfile.settings.get_safe("compiler") arch = conanfile.settings.get_safe("arch") if compiler == "emcc": # Emscripten default output is WASM since 1.37.x (long time ago) # Deactivate WASM output forcing asm.js output instead if arch == "asm.js": return "-sWASM=0" return "" def libcxx_flags(conanfile): libcxx = conanfile.settings.get_safe("compiler.libcxx") if not libcxx: return None, None if disable_flag(conanfile, "libcxx"): return None, None compiler = conanfile.settings.get_safe("compiler") lib = stdlib11 = None if compiler == "apple-clang": # In apple-clang 2 only values atm are "libc++" and "libstdc++" lib = f'-stdlib={libcxx}' elif compiler in ("clang", "intel-cc", "emcc"): if libcxx == "libc++": lib = "-stdlib=libc++" elif libcxx == "libstdc++" or libcxx == "libstdc++11": lib = "-stdlib=libstdc++" # FIXME, something to do with the other values? Android c++_shared? elif compiler == "sun-cc": lib = {"libCstd": "-library=Cstd", "libstdcxx": "-library=stdcxx4", "libstlport": "-library=stlport4", "libstdc++": "-library=stdcpp" }.get(libcxx) elif compiler == "qcc": lib = f'-Y _{libcxx}' if compiler in ['clang', 'apple-clang', 'gcc', 'emcc']: if libcxx == "libstdc++": stdlib11 = "_GLIBCXX_USE_CXX11_ABI=0" elif libcxx == "libstdc++11" and conanfile.conf.get("tools.gnu:define_libcxx11_abi", check_type=bool): stdlib11 = "_GLIBCXX_USE_CXX11_ABI=1" return lib, stdlib11 def build_type_link_flags(settings): """ returns link flags specific to the build type (Debug, Release, etc.) [-debug] """ compiler = settings.get_safe("compiler") build_type = settings.get_safe("build_type") if not compiler or not build_type: return [] # https://github.com/Kitware/CMake/blob/d7af8a34b67026feaee558433db3a835d6007e06/ # Modules/Platform/Windows-MSVC.cmake if compiler == "msvc": if build_type in ("Debug", "RelWithDebInfo"): return ["-debug"] return [] def build_type_flags(conanfile): """ returns flags specific to the build type (Debug, Release, etc.) (-s, -g, /Zi, etc.) Used only by AutotoolsToolchain """ if disable_flag(conanfile, "build_type"): return [] settings = conanfile.settings compiler = settings.get_safe("compiler") build_type = settings.get_safe("build_type") vs_toolset = settings.get_safe("compiler.toolset") if not compiler or not build_type: return [] comp_exes = conanfile.conf.get("tools.build:compiler_executables", check_type=dict, default={}) clangcl = "clang-cl" in (comp_exes.get("c") or comp_exes.get("cpp", "")) if compiler == "msvc" or clangcl: # https://github.com/Kitware/CMake/blob/d7af8a34b67026feaee558433db3a835d6007e06/ # Modules/Platform/Windows-MSVC.cmake # FIXME: This condition seems legacy, as no more "clang" exists in Conan toolsets if vs_toolset and "clang" in vs_toolset: flags = {"Debug": ["-gline-tables-only", "-fno-inline", "-O0"], "Release": ["-O2"], "RelWithDebInfo": ["-gline-tables-only", "-O2", "-fno-inline"], "MinSizeRel": [] }.get(build_type, ["-O2", "-Ob2"]) else: flags = {"Debug": ["-Zi", "-Ob0", "-Od"], "Release": ["-O2", "-Ob2"], "RelWithDebInfo": ["-Zi", "-O2", "-Ob1"], "MinSizeRel": ["-O1", "-Ob1"], }.get(build_type, []) return flags else: # https://github.com/Kitware/CMake/blob/f3bbb37b253a1f4a26809d6f132b3996aa2e16fc/ # Modules/Compiler/GNU.cmake # clang include the gnu (overriding some things, but not build type) and apple clang # overrides clang but it doesn't touch clang either if compiler in ["clang", "gcc", "apple-clang", "qcc", "mcst-lcc"]: flags = {"Debug": ["-g"], "Release": ["-O3"], "RelWithDebInfo": ["-O2", "-g"], "MinSizeRel": ["-Os"], }.get(build_type, []) return flags elif compiler == "sun-cc": # https://github.com/Kitware/CMake/blob/f3bbb37b253a1f4a26809d6f132b3996aa2e16fc/ # Modules/Compiler/SunPro-CXX.cmake flags = {"Debug": ["-g"], "Release": ["-xO3"], "RelWithDebInfo": ["-xO2", "-g"], "MinSizeRel": ["-xO2", "-xspace"], }.get(build_type, []) return flags return [] def threads_flags(conanfile): """ returns flags specific to the threading model used by the compiler """ if disable_flag(conanfile, "threads"): return [] compiler = conanfile.settings.get_safe("compiler") threads = conanfile.settings.get_safe("compiler.threads") if compiler == "emcc": if threads == "posix": return ["-pthread"] elif threads == "wasm_workers": return ["-sWASM_WORKERS=1"] return [] def llvm_clang_front(conanfile): # Only Windows clang with MSVC backend (LLVM/Clang, not MSYS2 clang) if (conanfile.settings.get_safe("os") != "Windows" or conanfile.settings.get_safe("compiler") != "clang" or not conanfile.settings.get_safe("compiler.runtime")): return compilers = conanfile.conf.get("tools.build:compiler_executables", default={}) if "clang-cl" in compilers.get("c", "") or "clang-cl" in compilers.get("cpp", ""): return "clang-cl" # The MSVC-compatible front return "clang" # The GNU-compatible front def cppstd_flag(conanfile) -> str: """ Returns flags specific to the C++ standard based on the ``conanfile.settings.compiler``, ``conanfile.settings.compiler.version`` and ``conanfile.settings.compiler.cppstd``. It also considers when using GNU extension in ``settings.compiler.cppstd``, reflecting it in the compiler flag. Currently, it supports GCC, Clang, AppleClang, MSVC, Intel, MCST-LCC. In case there is no ``settings.compiler`` or ``settings.cppstd`` in the profile, the result will be an **empty string**. :param conanfile: The current recipe object. Always use ``self``. :return: ``str`` with the standard C++ flag used by the compiler. e.g. "-std=c++11", "/std:c++latest" """ compiler = conanfile.settings.get_safe("compiler") compiler_version = conanfile.settings.get_safe("compiler.version") cppstd = conanfile.settings.get_safe("compiler.cppstd") if not compiler or not compiler_version or not cppstd: return "" if disable_flag(conanfile, "cppstd"): return "" func = {"gcc": _cppstd_gcc, "clang": _cppstd_clang, "apple-clang": _cppstd_apple_clang, "msvc": _cppstd_msvc, "intel-cc": _cppstd_intel_cc, "mcst-lcc": _cppstd_mcst_lcc}.get(compiler) flag = None if func: flag = func(Version(compiler_version), str(cppstd)) if flag and llvm_clang_front(conanfile) == "clang-cl": flag = flag.replace("=", ":") return flag def cppstd_msvc_flag(visual_version, cppstd): # https://docs.microsoft.com/en-us/cpp/build/reference/std-specify-language-standard-version if cppstd == "23": if visual_version >= "193": return "c++latest" elif cppstd == "20": if visual_version >= "192": return "c++20" elif visual_version >= "191": return "c++latest" elif cppstd == "17": if visual_version >= "191": return "c++17" elif visual_version >= "190": return "c++latest" elif cppstd == "14": if visual_version >= "190": return "c++14" return None def _cppstd_msvc(visual_version, cppstd): flag = cppstd_msvc_flag(visual_version, cppstd) return f'/std:{flag}' if flag else None def _cppstd_apple_clang(clang_version, cppstd): """ Inspired in: https://github.com/Kitware/CMake/blob/master/Modules/Compiler/AppleClang-CXX.cmake """ v98 = vgnu98 = v11 = vgnu11 = v14 = vgnu14 = v17 = vgnu17 = v20 = vgnu20 = v23 = vgnu23 = v26 = vgnu26 = None if clang_version >= "4.0": v98 = "c++98" vgnu98 = "gnu++98" v11 = "c++11" vgnu11 = "gnu++11" if clang_version >= "6.1": v14 = "c++14" vgnu14 = "gnu++14" elif clang_version >= "5.1": v14 = "c++1y" vgnu14 = "gnu++1y" # Not confirmed that it didn't work before 9.1 but 1z is still valid, so we are ok # Note: cmake allows c++17 since version 10.0 if clang_version >= "9.1": v17 = "c++17" vgnu17 = "gnu++17" elif clang_version >= "6.1": v17 = "c++1z" vgnu17 = "gnu++1z" if clang_version >= "13.0": v20 = "c++20" vgnu20 = "gnu++20" elif clang_version >= "10.0": v20 = "c++2a" vgnu20 = "gnu++2a" if clang_version >= "16.0": v23 = "c++23" vgnu23 = "gnu++23" v26 = "c++26" vgnu26 = "gnu++26" elif clang_version >= "13.0": v23 = "c++2b" vgnu23 = "gnu++2b" flag = {"98": v98, "gnu98": vgnu98, "11": v11, "gnu11": vgnu11, "14": v14, "gnu14": vgnu14, "17": v17, "gnu17": vgnu17, "20": v20, "gnu20": vgnu20, "23": v23, "gnu23": vgnu23, "26": v26, "gnu26": vgnu26}.get(cppstd) return f'-std={flag}' if flag else None def _cppstd_clang(clang_version, cppstd): """ Inspired in: https://github.com/Kitware/CMake/blob/ 1fe2dc5ef2a1f262b125a2ba6a85f624ce150dd2/Modules/Compiler/Clang-CXX.cmake https://clang.llvm.org/cxx_status.html """ v98 = vgnu98 = v11 = vgnu11 = v14 = vgnu14 = v17 = vgnu17 = v20 = vgnu20 = v23 = vgnu23 = v26 = vgnu26 = None if clang_version >= "2.1": v98 = "c++98" vgnu98 = "gnu++98" if clang_version >= "3.1": v11 = "c++11" vgnu11 = "gnu++11" elif clang_version >= "2.1": v11 = "c++0x" vgnu11 = "gnu++0x" if clang_version >= "3.5": v14 = "c++14" vgnu14 = "gnu++14" elif clang_version >= "3.4": v14 = "c++1y" vgnu14 = "gnu++1y" if clang_version >= "5": v17 = "c++17" vgnu17 = "gnu++17" elif clang_version >= "3.5": v17 = "c++1z" vgnu17 = "gnu++1z" if clang_version >= "6": v20 = "c++2a" vgnu20 = "gnu++2a" if clang_version >= "12": v20 = "c++20" vgnu20 = "gnu++20" v23 = "c++2b" vgnu23 = "gnu++2b" if clang_version >= "17": v23 = "c++23" vgnu23 = "gnu++23" v26 = "c++26" vgnu26 = "gnu++26" flag = {"98": v98, "gnu98": vgnu98, "11": v11, "gnu11": vgnu11, "14": v14, "gnu14": vgnu14, "17": v17, "gnu17": vgnu17, "20": v20, "gnu20": vgnu20, "23": v23, "gnu23": vgnu23, "26": v26, "gnu26": vgnu26}.get(cppstd) return f'-std={flag}' if flag else None def _cppstd_gcc(gcc_version, cppstd): """https://github.com/Kitware/CMake/blob/master/Modules/Compiler/GNU-CXX.cmake""" # https://gcc.gnu.org/projects/cxx-status.html v98 = vgnu98 = v11 = vgnu11 = v14 = vgnu14 = v17 = vgnu17 = v20 = vgnu20 = v23 = vgnu23 = v26 = vgnu26 = None if gcc_version >= "3.4": v98 = "c++98" vgnu98 = "gnu++98" if gcc_version >= "4.7": v11 = "c++11" vgnu11 = "gnu++11" elif gcc_version >= "4.3": v11 = "c++0x" vgnu11 = "gnu++0x" if gcc_version >= "4.9": v14 = "c++14" vgnu14 = "gnu++14" elif gcc_version >= "4.8": v14 = "c++1y" vgnu14 = "gnu++1y" if gcc_version >= "5": v17 = "c++1z" vgnu17 = "gnu++1z" if gcc_version >= "5.2": # Not sure if even in 5.1 gnu17 is valid, but gnu1z is v17 = "c++17" vgnu17 = "gnu++17" if gcc_version >= "8": v20 = "c++2a" vgnu20 = "gnu++2a" if gcc_version >= "10": v20 = "c++20" vgnu20 = "gnu++20" if gcc_version >= "11": v23 = "c++23" vgnu23 = "gnu++23" if gcc_version >= "14": v26 = "c++26" vgnu26 = "gnu++26" flag = {"98": v98, "gnu98": vgnu98, "11": v11, "gnu11": vgnu11, "14": v14, "gnu14": vgnu14, "17": v17, "gnu17": vgnu17, "20": v20, "gnu20": vgnu20, "23": v23, "gnu23": vgnu23, "26": v26, "gnu26": vgnu26}.get(cppstd) return f'-std={flag}' if flag else None def _cppstd_mcst_lcc(mcst_lcc_version, cppstd): v11 = vgnu11 = v14 = vgnu14 = v17 = vgnu17 = v20 = vgnu20 = None if mcst_lcc_version >= "1.21": v11 = "c++11" vgnu11 = "gnu++11" v14 = "c++14" vgnu14 = "gnu++14" if mcst_lcc_version >= "1.24": v17 = "c++17" vgnu17 = "gnu++17" if mcst_lcc_version >= "1.25": v20 = "c++2a" vgnu20 = "gnu++2a" # FIXME: What is this "03"?? that is not a valid cppstd in the settings.yml flag = {"98": "c++98", "gnu98": "gnu++98", "03": "c++03", "gnu03": "gnu++03", "11": v11, "gnu11": vgnu11, "14": v14, "gnu14": vgnu14, "17": v17, "gnu17": vgnu17, "20": v20, "gnu20": vgnu20}.get(cppstd) return f'-std={flag}' if flag else None def _cppstd_intel_cc(_, cppstd): """ Inspired in: https://software.intel.com/content/www/us/en/develop/documentation/ oneapi-dpcpp-cpp-compiler-dev-guide-and-reference/top/compiler-reference/ compiler-options/compiler-option-details/language-options/std-qstd.html """ # Note: for now, we don't care about compiler version v98 = "c++98" vgnu98 = "gnu++98" v03 = "c++03" vgnu03 = "gnu++03" v11 = "c++11" vgnu11 = "gnu++11" v14 = "c++14" vgnu14 = "gnu++14" v17 = "c++17" vgnu17 = "gnu++17" v20 = "c++20" vgnu20 = "gnu++20" v23 = "c++2b" vgnu23 = "gnu++2b" flag = {"98": v98, "gnu98": vgnu98, "03": v03, "gnu03": vgnu03, "11": v11, "gnu11": vgnu11, "14": v14, "gnu14": vgnu14, "17": v17, "gnu17": vgnu17, "20": v20, "gnu20": vgnu20, "23": v23, "gnu23": vgnu23}.get(cppstd) return f'-std={flag}' if flag else None def cstd_flag(conanfile) -> str: """ Returns flags specific to the C+standard based on the ``conanfile.settings.compiler``, ``conanfile.settings.compiler.version`` and ``conanfile.settings.compiler.cstd``. It also considers when using GNU extension in ``settings.compiler.cstd``, reflecting it in the compiler flag. Currently, it supports GCC, Clang, AppleClang, MSVC, Intel, MCST-LCC. In case there is no ``settings.compiler`` or ``settings.cstd`` in the profile, the result will be an **empty string**. :param conanfile: The current recipe object. Always use ``self``. :return: ``str`` with the standard C flag used by the compiler. """ compiler = conanfile.settings.get_safe("compiler") compiler_version = conanfile.settings.get_safe("compiler.version") cstd = conanfile.settings.get_safe("compiler.cstd") if not compiler or not compiler_version or not cstd: return "" if disable_flag(conanfile, "cstd"): return "" func = {"gcc": _cstd_gcc, "clang": _cstd_clang, "apple-clang": _cstd_apple_clang, "msvc": _cstd_msvc}.get(compiler) flag = None if func: flag = func(Version(compiler_version), str(cstd)) return flag def _cstd_gcc(gcc_version, cstd): # TODO: Verify flags per version flag = {"99": "c99", "11": "c11", "17": "c17", "23": "c23"}.get(cstd, cstd) return f'-std={flag}' if flag else None def _cstd_clang(gcc_version, cstd): # TODO: Verify flags per version flag = {"99": "c99", "11": "c11", "17": "c17", "23": "c23"}.get(cstd, cstd) return f'-std={flag}' if flag else None def _cstd_apple_clang(gcc_version, cstd): # TODO: Verify flags per version flag = {"99": "c99", "11": "c11", "17": "c17", "23": "c23"}.get(cstd, cstd) return f'-std={flag}' if flag else None def cstd_msvc_flag(visual_version, cstd): if cstd == "17": if visual_version >= "192": return "c17" elif cstd == "11": if visual_version >= "192": return "c11" return None def _cstd_msvc(visual_version, cstd): flag = cstd_msvc_flag(visual_version, cstd) return f'/std:{flag}' if flag else None ================================================ FILE: conan/tools/build/stdcpp_library.py ================================================ def stdcpp_library(conanfile): """ Returns the name of the C++ standard library that can be passed to the linker, based on the current settings. Returs None if the name of the C++ standard library file is not known. """ libcxx = conanfile.settings.get_safe("compiler.libcxx") if libcxx in ["libstdc++", "libstdc++11"]: return "stdc++" elif libcxx in ["libc++"]: return "c++" elif libcxx in ["c++_shared"]: return "c++_shared" elif libcxx in ["c++_static"]: return "c++_static" return None ================================================ FILE: conan/tools/cmake/__init__.py ================================================ from conan.tools.cmake.toolchain.toolchain import CMakeToolchain from conan.tools.cmake.cmake import CMake from conan.tools.cmake.cmakeconfigdeps.cmakeconfigdeps import CMakeConfigDeps from conan.tools.cmake.layout import cmake_layout def CMakeDeps(conanfile): # noqa if conanfile.conf.get("tools.cmake.cmakedeps:new", choices=["will_break_next", "recipe_will_break"]) == "will_break_next": conanfile.output.warning("On the fly replacement of CMakeDeps by CMakeConfigDeps generator, " "because 'tools.cmake.cmakedeps:new' incubating conf activated. " "This conf is incubating and will break in next releases. " "CMakeConfigDeps is now experimental and can be used as such in " "recipes.") return CMakeConfigDeps(conanfile) from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps as _CMakeDeps return _CMakeDeps(conanfile) ================================================ FILE: conan/tools/cmake/cmake.py ================================================ import os from conan.tools.build import build_jobs, cmd_args_to_string from conan.tools.cmake.presets import load_cmake_presets from conan.tools.cmake.utils import is_multi_configuration from conan.tools.files import chdir, mkdir from conan.tools.microsoft.msbuild import msbuild_verbosity_cmd_line_arg from conan.errors import ConanException def _cmake_cmd_line_args(conanfile, generator): args = [] if not generator: return args # Arguments related to parallel njobs = build_jobs(conanfile) if njobs and ("Makefiles" in generator or "Ninja" in generator) and "NMake" not in generator: args.append("-j{}".format(njobs)) maxcpucount = conanfile.conf.get("tools.microsoft.msbuild:max_cpu_count", check_type=int) if maxcpucount is not None and "Visual Studio" in generator: args.append(f"/m:{maxcpucount}" if maxcpucount > 0 else "/m") # Arguments for verbosity if "Visual Studio" in generator: verbosity = msbuild_verbosity_cmd_line_arg(conanfile) if verbosity: # trying to avoid issues with powershell and - : characters preceded by -- # -verbosity -> /verbosity # https://github.com/PowerShell/PowerShell/issues/17399 if conanfile.conf.get("tools.env.virtualenv:powershell"): verbosity = verbosity.replace('-', '/', 1) args.append(verbosity) return args class CMake: """ CMake helper to use together with the CMakeToolchain feature """ def __init__(self, conanfile): """ :param conanfile: The current recipe object. Always use ``self``. """ # Store a reference to useful data self._conanfile = conanfile cmake_presets = load_cmake_presets(conanfile.generators_folder) # Conan generated presets will have exactly 1 configurePresets, no more configure_preset = cmake_presets["configurePresets"][0] self._generator = configure_preset["generator"] self._toolchain_file = configure_preset.get("toolchainFile") self._cache_variables = configure_preset["cacheVariables"] self._cmake_program = conanfile.conf.get("tools.cmake:cmake_program", default="cmake") @property def is_multi_configuration(self): return is_multi_configuration(self._generator) def configure(self, variables=None, build_script_folder=None, cli_args=None, stdout=None, stderr=None, subfolder=None): """ Reads the ``CMakePresets.json`` file generated by the :ref:`CMakeToolchain` to get: - The generator, to append ``-G="xxx"``. - Toolchain path to append ``-DCMAKE_TOOLCHAIN_FILE=/path/conan_toolchain.cmake`` - The declared ``cache variables`` to append ``-Dxxx``. and call ``cmake``. :param variables: Should be a dictionary of CMake variables and values, that will be mapped to command line ``-DVAR=VALUE`` arguments. Recall that in the general case information to CMake should be passed in ``CMakeToolchain`` to be provided in the ``conan_toolchain.cmake`` file. This ``variables`` argument is intended for exceptional cases that wouldn't work in the toolchain approach. :param build_script_folder: Path to the CMakeLists.txt in case it is not in the declared ``self.folders.source`` at the ``layout()`` method. :param cli_args: List of arguments ``[arg1, arg2, ...]`` that will be passed as extra CLI arguments to pass to cmake invocation :param subfolder: (Experimental): subfolder to be created inside the ``build_folder`` and the ``package_folder``. If not provided, files will be placed in the ``build_folder`` and ``package_folder`` root. :param stdout: Use it to redirect stdout to this stream :param stderr: Use it to redirect stderr to this stream """ self._conanfile.output.info("Running CMake.configure()") cmakelist_folder = self._conanfile.source_folder if build_script_folder: cmakelist_folder = os.path.join(self._conanfile.source_folder, build_script_folder) cmakelist_folder = cmakelist_folder.replace("\\", "/") build_folder = self._conanfile.build_folder if subfolder: build_folder = os.path.join(self._conanfile.build_folder, subfolder) mkdir(self._conanfile, build_folder) arg_list = [self._cmake_program] if self._generator: arg_list.append('-G "{}"'.format(self._generator)) if self._toolchain_file: toolpath = self._toolchain_file if subfolder: toolpath = os.path.relpath(self._toolchain_file, start=subfolder) toolpath = toolpath.replace("\\", "/") arg_list.append('-DCMAKE_TOOLCHAIN_FILE="{}"'.format(toolpath)) if self._conanfile.package_folder: pkg_folder = self._conanfile.package_folder.replace("\\", "/") arg_list.append('-DCMAKE_INSTALL_PREFIX="{}"'.format(pkg_folder)) if not variables: variables = {} self._cache_variables.update(variables) arg_list.extend(['-D{}="{}"'.format(k, v) for k, v in self._cache_variables.items()]) arg_list.append('"{}"'.format(cmakelist_folder)) if not cli_args or ("--log-level" not in cli_args and "--loglevel" not in cli_args): arg_list.extend(self._cmake_log_levels_args) if cli_args: arg_list.extend(cli_args) command = " ".join(arg_list) with chdir(self, build_folder): self._conanfile.run(command, stdout=stdout, stderr=stderr) def _config_arg(self, build_type): """ computes the '--config Release' arg when necessary, or warn or error if wrong """ is_multi = is_multi_configuration(self._generator) if build_type and not is_multi: self._conanfile.output.error("Don't specify 'build_type' at build time for " "single-config build systems") if not build_type: try: build_type = self._conanfile.settings.build_type # declared, but can be None if not build_type: raise ConanException("CMake: build_type setting should be defined.") except ConanException: if is_multi: raise ConanException("CMake: build_type setting should be defined.") build_config = "--config {}".format(build_type) if build_type and is_multi else "" return build_config def _build(self, build_type=None, target=None, cli_args=None, build_tool_args=None, env="", stdout=None, stderr=None, subfolder=None): bf = self._conanfile.build_folder if subfolder: bf = os.path.join(self._conanfile.build_folder, subfolder) build_config = self._config_arg(build_type) args = [] if target is not None: target_list = [target] if isinstance(target, str) else target args.extend(["--target"] + target_list) if cli_args: args.extend(cli_args) cmd_line_args = _cmake_cmd_line_args(self._conanfile, self._generator) if build_tool_args: cmd_line_args.extend(build_tool_args) args.extend(self._compilation_verbosity_arg) if cmd_line_args: args += ['--'] + cmd_line_args arg_list = ['"{}"'.format(bf), build_config, cmd_args_to_string(args)] arg_list = " ".join(filter(None, arg_list)) command = "%s --build %s" % (self._cmake_program, arg_list) self._conanfile.run(command, env=env, stdout=stdout, stderr=stderr) def build(self, build_type=None, target=None, cli_args=None, build_tool_args=None, stdout=None, stderr=None, subfolder=None): """ :param build_type: Use it only to override the value defined in the ``settings.build_type`` for a multi-configuration generator (e.g. Visual Studio, XCode). This value will be ignored for single-configuration generators, they will use the one defined in the toolchain file during the install step. :param target: The name of a single build target as a string, or names of multiple build targets in a list of strings to be passed to the ``--target`` argument. :param cli_args: A list of arguments ``[arg1, arg2, ...]`` that will be passed to the ``cmake --build ... arg1 arg2`` command directly. :param build_tool_args: A list of arguments ``[barg1, barg2, ...]`` for the underlying build system that will be passed to the command line after the ``--`` indicator: ``cmake --build ... -- barg1 barg2`` :param subfolder: (Experimental): subfolder to be created inside the ``build_folder`` and the ``package_folder``. If not provided, files will be placed in the ``build_folder`` and ``package_folder`` root. :param stdout: Use it to redirect stdout to this stream :param stderr: Use it to redirect stderr to this stream """ self._conanfile.output.info("Running CMake.build()") self._build(build_type, target, cli_args, build_tool_args, subfolder=subfolder, stdout=stdout, stderr=stderr) def install(self, build_type=None, component=None, cli_args=None, stdout=None, stderr=None, subfolder=None): """ Equivalent to running ``cmake --install`` :param component: The specific component to install, if any :param build_type: Use it only to override the value defined in the settings.build_type. It can fail if the build is single configuration (e.g. Unix Makefiles), as in that case the build type must be specified at configure time, not build type. :param cli_args: A list of arguments ``[arg1, arg2, ...]`` for the underlying build system that will be passed to the command line: ``cmake --install ... arg1 arg2`` :param subfolder: (Experimental): subfolder to be created inside the ``build_folder`` and the ``package_folder``. If not provided, files will be placed in the ``build_folder`` and ``package_folder`` root. :param stdout: Use it to redirect stdout to this stream :param stderr: Use it to redirect stderr to this stream """ self._conanfile.output.info("Running CMake.install()") package_folder = self._conanfile.package_folder build_folder = self._conanfile.build_folder if subfolder: package_folder = os.path.join(self._conanfile.package_folder, subfolder) build_folder = os.path.join(self._conanfile.build_folder, subfolder) mkdir(self._conanfile, package_folder) build_config = self._config_arg(build_type) pkg_folder = '"{}"'.format(package_folder.replace("\\", "/")) build_folder = '"{}"'.format(build_folder.replace("\\", "/")) arg_list = ["--install", build_folder, build_config, "--prefix", pkg_folder] if component: arg_list.extend(["--component", component]) arg_list.extend(self._compilation_verbosity_arg) deprecated_install_strip = self._conanfile.conf.get("tools.cmake:install_strip", check_type=bool) if deprecated_install_strip: self._conanfile.output.warning("The 'tools.cmake:install_strip' configuration is " "deprecated, use 'tools.build:install_strip' instead.", warn_tag="deprecated") do_strip = self._conanfile.conf.get("tools.build:install_strip", check_type=bool) if do_strip or deprecated_install_strip: arg_list.append("--strip") if cli_args: if "--install" in cli_args: raise ConanException("Do not pass '--install' argument to 'install()'") arg_list.extend(cli_args) arg_list = " ".join(filter(None, arg_list)) command = "%s %s" % (self._cmake_program, arg_list) self._conanfile.run(command, stdout=stdout, stderr=stderr) def test(self, build_type=None, target=None, cli_args=None, build_tool_args=None, env="", stdout=None, stderr=None): """ Equivalent to running cmake --build . --target=RUN_TESTS. :param build_type: Use it only to override the value defined in the ``settings.build_type``. It can fail if the build is single configuration (e.g. Unix Makefiles), as in that case the build type must be specified at configure time, not build time. :param target: Name of the build target to run, by default ``RUN_TESTS`` or ``test`` :param cli_args: Same as above ``build()``, a list of arguments ``[arg1, arg2, ...]`` to be passed as extra arguments for the underlying build system :param build_tool_args: Same as above ``build()`` :param stdout: Use it to redirect stdout to this stream :param stderr: Use it to redirect stderr to this stream """ if self._conanfile.conf.get("tools.build:skip_test", check_type=bool): return if not target: is_multi = is_multi_configuration(self._generator) is_ninja = "Ninja" in self._generator target = "RUN_TESTS" if is_multi and not is_ninja else "test" # CTest behavior controlled by CTEST_ env-vars should be directly defined in [buildenv] # The default for ``test()`` is both the buildenv and the runenv env = ["conanbuild", "conanrun"] if env == "" else env self._build(build_type=build_type, target=target, cli_args=cli_args, build_tool_args=build_tool_args, env=env, stdout=stdout, stderr=stderr) def ctest(self, cli_args=None, env="", stdout=None, stderr=None): """ Equivalent to running ctest ... :param cli_args: List of arguments ``[arg1, arg2, ...]`` to be passed as extra ctest command line arguments :param env: the environment files to activate, by default conanbuild + conanrun :param stdout: Use it to redirect stdout to this stream :param stderr: Use it to redirect stderr to this stream """ if self._conanfile.conf.get("tools.build:skip_test", check_type=bool): return arg_list = [] bt = self._conanfile.settings.get_safe("build_type") is_multi = is_multi_configuration(self._generator) if bt and is_multi: arg_list.append(f"--build-config {bt}") njobs = build_jobs(self._conanfile) if njobs: arg_list.append(f"--parallel {njobs}") arg_list.extend(cli_args or []) verbosity = self._conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) if verbosity: # https://cmake.org/cmake/help/latest/manual/ctest.1.html # Options such as --verbose, --extra-verbose, and --debug are # ignored if --quiet is specified. arg_list.append(f"--{verbosity}") extra_args = self._conanfile.conf.get("tools.cmake:ctest_args", check_type=list) if extra_args: arg_list.extend(extra_args) arg_list = " ".join(filter(None, arg_list)) command = f"ctest {arg_list}" env = ["conanbuild", "conanrun"] if env == "" else env self._conanfile.run(command, env=env, stdout=stdout, stderr=stderr) @property def _compilation_verbosity_arg(self): """ Controls build tool verbosity, that is, those controlled by -DCMAKE_VERBOSE_MAKEFILE See https://cmake.org/cmake/help/latest/manual/cmake.1.html#cmdoption-cmake-build-v """ verbosity = self._conanfile.conf.get("tools.compilation:verbosity", choices=("quiet", "verbose")) return ["--verbose"] if verbosity == "verbose" else [] @property def _cmake_log_levels_args(self): """ Controls CMake's own verbosity levels. See https://cmake.org/cmake/help/latest/manual/cmake.1.html#cmdoption-cmake-log-level :return: """ log_level = self._conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) if log_level == "quiet": log_level = "error" return ["--loglevel=" + log_level.upper()] if log_level is not None else [] ================================================ FILE: conan/tools/cmake/cmakeconfigdeps/__init__.py ================================================ ================================================ FILE: conan/tools/cmake/cmakeconfigdeps/cmakeconfigdeps.py ================================================ import glob import os import re import textwrap from jinja2 import Template from conan.api.output import Color, ConanOutput from conan.errors import ConanException from conan.internal import check_duplicated_generator from conan.internal.api.install.generators import relativize_path from conan.internal.model.dependencies import get_transitive_requires from conan.tools.cmake.cmakeconfigdeps.config import ConfigTemplate2 from conan.tools.cmake.cmakeconfigdeps.config_version import ConfigVersionTemplate2 from conan.tools.cmake.cmakeconfigdeps.target_configuration import TargetConfigurationTemplate2 from conan.tools.cmake.cmakeconfigdeps.targets import TargetsTemplate2 from conan.tools.files import save from conan.internal.util.files import load FIND_MODE_MODULE = "module" FIND_MODE_CONFIG = "config" FIND_MODE_NONE = "none" FIND_MODE_BOTH = "both" class CMakeConfigDeps: def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile self.configuration = str(self._conanfile.settings.build_type) # These are just for legacy compatibility, but not use at al self._build_context_activated = [] self._build_context_build_modules = [] self._build_context_suffix = {} # Enable/Disable checking if a component target exists or not self._check_components_exist = False self._properties = {} @property def build_context_activated(self): return self._build_context_activated @build_context_activated.setter def build_context_activated(self, value): self._conanfile.output.warning("CMakeConfigDeps.build_context_activated is deprecated, " "not used anymore", warn_tag="deprecated") self._build_context_activated = value @property def build_context_build_modules(self): return self._build_context_build_modules @build_context_build_modules.setter def build_context_build_modules(self, value): self._conanfile.output.warning("CMakeConfigDeps.build_context_build_modules is deprecated, " "not used anymore", warn_tag="deprecated") self._build_context_build_modules = value @property def build_context_suffix(self): return self._build_context_suffix @build_context_suffix.setter def build_context_suffix(self, value): self._conanfile.output.warning("CMakeConfigDeps.build_context_suffix is deprecated, " "not used anymore", warn_tag="deprecated") self._build_context_suffix = value @property def check_components_exist(self): return self._check_components_exist @check_components_exist.setter def check_components_exist(self, value): self._conanfile.output.warning("CMakeConfigDeps.check_components_exist is deprecated, " "not used anymore", warn_tag="deprecated") self._check_components_exist = value def generate(self): """ This method will save the generated files to the ``conanfile.generators_folder`` folder """ self._conanfile.output.warning("CMakeConfigDeps is experimental, and might get " "breaking changes in future releases", warn_tag="experimental") check_duplicated_generator(self, self._conanfile) # Current directory is the generators_folder generator_files = self._content() for generator_file, content in generator_files.items(): save(self._conanfile, generator_file, content) _PathGenerator(self, self._conanfile).generate() def _content(self): host_req = self._conanfile.dependencies.host build_req = self._conanfile.dependencies.direct_build test_req = self._conanfile.dependencies.test # Iterate all the transitive requires ret = {} direct_deps = [] for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): cmake_find_mode = self.get_property("cmake_find_mode", dep) cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG cmake_find_mode = cmake_find_mode.lower() if cmake_find_mode == FIND_MODE_NONE: continue if cmake_find_mode in (FIND_MODE_MODULE, FIND_MODE_BOTH): ConanOutput(self._conanfile.ref).warning("CMakeConfigDeps does not support " f"module find mode in {dep}.\n" f"Config mode will be used regardless.", # Should this be risk? warn_tag="deprecated") if require.direct: direct_deps.append((require, dep)) full_cpp_info = dep.cpp_info.deduce_full_cpp_info(dep) config = ConfigTemplate2(self, require, dep, full_cpp_info) ret[config.filename] = config.content() config_version = ConfigVersionTemplate2(self, dep) ret[config_version.filename] = config_version.content() targets = TargetsTemplate2(self, dep) ret[targets.filename] = targets.content() target_configuration = TargetConfigurationTemplate2(self, dep, require, full_cpp_info) ret[target_configuration.filename] = target_configuration.content() self._print_help(direct_deps) return ret def _print_help(self, direct_deps): if direct_deps: msg = ["CMakeDeps necessary find_package() and targets for your CMakeLists.txt"] link_targets = [] for (require, dep) in direct_deps: note = " # Optional. This is a tool-require, can't link its targets" \ if require.build else "" msg.append(f" find_package({self.get_cmake_filename(dep)}){note}") if not require.build and not dep.cpp_info.exe: target_name = self.get_property("cmake_target_name", dep) link_targets.append(target_name or f"{dep.ref.name}::{dep.ref.name}") if link_targets: msg.append(f" target_link_libraries(... {' '.join(link_targets)})") self._conanfile.output.info("\n".join(msg), fg=Color.CYAN) def set_property(self, dep, prop, value, build_context=False): """ Using this method you can overwrite the :ref:`property` values set by the Conan recipes from the consumer. :param dep: Name of the dependency to set the :ref:`property`. For components use the syntax: ``dep_name::component_name``. :param prop: Name of the :ref:`property`. :param value: Value of the property. Use ``None`` to invalidate any value set by the upstream recipe. :param build_context: Set to ``True`` if you want to set the property for a dependency that belongs to the build context (``False`` by default). """ build_suffix = "&build" if build_context else "" self._properties.setdefault(f"{dep}{build_suffix}", {}).update({prop: value}) def get_property(self, prop, dep, comp_name=None, check_type=None): dep_name = dep.ref.name # Find the requirement that points to this "dep". # TODO: It would probably be more explicit if it was an argument as "dep", but to keep # diff minimal require = next(iter(r for r, d in self._conanfile.dependencies.items() if d is dep)) build_suffix = "&build" if require.build else "" dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" try: value = self._properties[f"{dep_comp}{build_suffix}"][prop] if check_type is not None and not isinstance(value, check_type): raise ConanException(f'The expected type for {prop} is "{check_type.__name__}", ' f'but "{type(value).__name__}" was found') return value except KeyError: # Here we are not using the cpp_info = deduce_cpp_info(dep) because it is not # necessary for the properties if not comp_name: return dep.cpp_info.get_property(prop, check_type=check_type) comp = dep.cpp_info.components.get(comp_name) # it is a default dict if comp is not None: return comp.get_property(prop, check_type=check_type) def get_cmake_filename(self, dep): # Get the name of the file for the find_package(XXX) # This is used by CMakeDeps to determine: # - The filename to generate (XXX-config.cmake or FindXXX.cmake) # - The name of the defined XXX_DIR variables # - The name of transitive dependencies for calls to find_dependency ret = self.get_property("cmake_file_name", dep) return ret or dep.ref.name def _get_find_mode(self, dep): tmp = self.get_property("cmake_find_mode", dep) if tmp is None: return "config" return tmp.lower() def get_transitive_requires(self, conanfile): # Prepared to filter transitive tool-requires with visible=True return get_transitive_requires(self._conanfile, conanfile) # TODO: Repeated from CMakeToolchain blocks def _join_paths(conanfile, paths): paths = [p.replace('\\', '/').replace('$', '\\$').replace('"', '\\"') for p in paths] paths = [relativize_path(p, conanfile, "${CMAKE_CURRENT_LIST_DIR}") for p in paths] return " ".join([f'"{p}"' for p in paths]) class _PathGenerator: _conan_cmakedeps_paths = "conan_cmakedeps_paths.cmake" def __init__(self, cmakedeps, conanfile): self._conanfile = conanfile self._cmakedeps = cmakedeps def _get_cmake_paths(self, requirements, dirs_name): paths = {} cmake_vars = { "bindirs": "CMAKE_PROGRAM_PATH", "libdirs": "CMAKE_LIBRARY_PATH", "includedirs": "CMAKE_INCLUDE_PATH", "frameworkdirs": "CMAKE_FRAMEWORK_PATH", "builddirs": "CMAKE_MODULE_PATH" } for req, dep in requirements: cppinfo = dep.cpp_info.aggregated_components() cppinfo_dirs = getattr(cppinfo, dirs_name, []) if not cppinfo_dirs: continue previous = paths.get(req.ref.name) if previous: self._conanfile.output.info(f"There is already a '{req.ref}' package contributing" f" to {cmake_vars[dirs_name]}. Using the one" f" defined by the context={dep.context}.") paths[req.ref.name] = cppinfo_dirs return [d for dirs in paths.values() for d in dirs] def generate(self): template = textwrap.dedent("""\ set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) {% for pkg_name, folder in pkg_paths.items() %} set({{pkg_name}}_DIR "{{folder}}") {% endfor %} {% for pkg_name, folders in pkg_paths_multi.items() %} {% for folder in folders %} list(APPEND CONAN_{{pkg_name}}_DIR_MULTI "{{folder}}") {% endfor %} {% endfor %} {% if host_runtime_dirs %} set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} ) # Only for VS, needs CMake>=3.27 set(CMAKE_VS_DEBUGGER_ENVIRONMENT "PATH=${CONAN_RUNTIME_LIB_DIRS};%PATH%") {% endif %} {% if cmake_program_path %} list(PREPEND CMAKE_PROGRAM_PATH {{ cmake_program_path }}) {% endif %} {% if cmake_library_path %} list(PREPEND CMAKE_LIBRARY_PATH {{ cmake_library_path }}) {% endif %} {% if cmake_include_path %} list(PREPEND CMAKE_INCLUDE_PATH {{ cmake_include_path }}) {% endif %} {% if cmake_framework_path %} list(PREPEND CMAKE_FRAMEWORK_PATH {{ cmake_framework_path }}) {% endif %} # Definition of CMAKE_MODULE_PATH to be able to include(module) {% if cmake_module_path %} list(PREPEND CMAKE_MODULE_PATH {{ cmake_module_path }}) {% endif %} """) host_req = self._conanfile.dependencies.host build_req = self._conanfile.dependencies.direct_build test_req = self._conanfile.dependencies.test host_test_reqs = list(host_req.items()) + list(test_req.items()) all_reqs = host_test_reqs + list(build_req.items()) # gen_folder = self._conanfile.generators_folder.replace("\\", "/") # if not, test_cmake_add_subdirectory test fails # content.append('set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)') pkg_paths = {} pkg_paths_multi = {} if os.path.exists(self._conan_cmakedeps_paths): existing_toolchain = load(self._conan_cmakedeps_paths) pattern_paths = r"list\(APPEND CONAN_([A-Za-z0-9-_]*)_DIR_MULTI \"([^)]*)\"\)" variable_match = re.findall(pattern_paths, existing_toolchain) for (captured_name, captured_path) in variable_match: path_list = pkg_paths_multi.setdefault(captured_name, []) if captured_path not in path_list: path_list.append(captured_path) for req, dep in all_reqs: cmake_find_mode = self._cmakedeps.get_property("cmake_find_mode", dep) cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG cmake_find_mode = cmake_find_mode.lower() cmake_filename = self._cmakedeps.get_cmake_filename(dep) extra_variants = self._cmakedeps.get_property("cmake_file_name_variants", dep, check_type=list) or [] lowercase_variants = {variant.lower() for variant in extra_variants} if len(lowercase_variants) > 1: raise ConanException(f"'{dep.ref}' 'cmake_file_name_variants' property contains different words. " "They should be the same with different upper/lower cases only.") if lowercase_variants: if cmake_filename.lower() not in lowercase_variants: is_cmake_filename_defined = self._cmakedeps.get_property("cmake_file_name", dep) is not None if is_cmake_filename_defined: extra_variants = [] msg = (f"'{dep.ref}' 'cmake_file_name_variants' property contains names " f"with different casings than the defined name '{cmake_filename}'. " f"The specified 'cmake_file_name'='{cmake_filename}' property " f"will be used as the only name and the variants will be ignored.") self._conanfile.output.warning(msg) else: msg = (f"'{dep.ref}' 'cmake_file_name_variants' property contains entries " f"that differ from the default 'cmake_file_name'='{cmake_filename}'. " f"They should be the same with different upper/lower cases only.") raise ConanException(msg) pkg_names = set([cmake_filename] + extra_variants) # https://cmake.org/cmake/help/v3.22/guide/using-dependencies/index.html if cmake_find_mode == FIND_MODE_NONE: cps = glob.glob(os.path.join(dep.package_folder, f"**/{cmake_filename}.cps"), recursive=True) if cps: loc = os.path.dirname(os.path.join(dep.package_folder, cps[0])) loc = loc.replace("\\", "/") relative_path = relativize_path(loc, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for pkg_name in pkg_names: pkg_paths[pkg_name] = relative_path continue try: # This is irrespective of the components, it should be in the root cpp_info # To define the location of the pkg-config.cmake file build_dir = dep.cpp_info.builddirs[0] except IndexError: build_dir = dep.package_folder pkg_folder = build_dir.replace("\\", "/") if build_dir else None if pkg_folder: if any(os.path.isfile(os.path.join(pkg_folder, f + ext)) for f in pkg_names for ext in ("-config.cmake", "Config.cmake")): relative_path = relativize_path(pkg_folder, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for pkg_name in pkg_names: pkg_paths[pkg_name] = relative_path for pkg_name in pkg_names: existing_paths = pkg_paths_multi.setdefault(pkg_name, []) if pkg_folder not in existing_paths: existing_paths.append(pkg_folder) continue # If CMakeDeps generated, the folder is this one # content.append(f'set({pkg_name}_ROOT "{gen_folder}")') for pkg_name in pkg_names: pkg_paths[pkg_name] = "${CMAKE_CURRENT_LIST_DIR}" # CMAKE_PROGRAM_PATH | CMAKE_LIBRARY_PATH | CMAKE_INCLUDE_PATH cmake_program_path = self._get_cmake_paths([(req, dep) for req, dep in all_reqs if req.direct], "bindirs") cmake_library_path = self._get_cmake_paths(host_test_reqs, "libdirs") cmake_include_path = self._get_cmake_paths(host_test_reqs, "includedirs") cmake_framework_path = self._get_cmake_paths(host_test_reqs, "frameworkdirs") cmake_module_path = self._get_cmake_paths(all_reqs, "builddirs") context = {"host_runtime_dirs": self._get_host_runtime_dirs(), "pkg_paths": pkg_paths, "pkg_paths_multi": pkg_paths_multi, "cmake_program_path": _join_paths(self._conanfile, cmake_program_path), "cmake_library_path": _join_paths(self._conanfile, cmake_library_path), "cmake_include_path": _join_paths(self._conanfile, cmake_include_path), "cmake_framework_path": _join_paths(self._conanfile, cmake_framework_path), "cmake_module_path": _join_paths(self._conanfile, cmake_module_path) } content = Template(template, trim_blocks=True, lstrip_blocks=True).render(context) save(self._conanfile, self._conan_cmakedeps_paths, content) def _get_host_runtime_dirs(self): host_runtime_dirs = {} # Get the previous configuration if os.path.exists(self._conan_cmakedeps_paths): existing_toolchain = load(self._conan_cmakedeps_paths) pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" variable_match = re.search(pattern_lib_dirs, existing_toolchain) if variable_match: capture = variable_match.group(1) matches = re.findall(r'"\$<\$:([^>]*)>"', capture) for config, paths in matches: host_runtime_dirs.setdefault(config, []).append(paths) is_win = self._conanfile.settings.get_safe("os") == "Windows" host_req = self._conanfile.dependencies.host test_req = self._conanfile.dependencies.test for req in list(host_req.values()) + list(test_req.values()): config = req.settings.get_safe("build_type", self._cmakedeps.configuration) aggregated_cppinfo = req.cpp_info.aggregated_components() runtime_dirs = aggregated_cppinfo.bindirs if is_win else aggregated_cppinfo.libdirs for d in runtime_dirs: d = d.replace("\\", "/") d = relativize_path(d, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") existing = host_runtime_dirs.setdefault(config, []) if d not in existing: existing.append(d) return ' '.join(f'"$<$:{i}>"' for c, v in host_runtime_dirs.items() for i in v) ================================================ FILE: conan/tools/cmake/cmakeconfigdeps/config.py ================================================ import textwrap import jinja2 from jinja2 import Template from conan.tools.cmake.utils import parse_extra_variable, cmake_escape_value from conan.internal.api.install.generators import relativize_path class ConfigTemplate2: """ FooConfig.cmake foo-config.cmake """ def __init__(self, cmakedeps, require, conanfile, full_cpp_info): self._cmakedeps = cmakedeps self._require = require self._conanfile = conanfile self._full_cpp_info = full_cpp_info def content(self): t = Template(self._template, trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined) return t.render(self._context) @property def filename(self): f = self._cmakedeps.get_cmake_filename(self._conanfile) return f"{f}-config.cmake" if f == f.lower() else f"{f}Config.cmake" @property def _context(self): f = self._cmakedeps.get_cmake_filename(self._conanfile) targets_include = f"{f}Targets.cmake" pkg_name = self._conanfile.ref.name build_modules_paths = self._cmakedeps.get_property("cmake_build_modules", self._conanfile, check_type=list) or [] # FIXME: Proper escaping of paths for CMake and relativization # FIXME: build_module_paths coming from last config only build_modules_paths = [f.replace("\\", "/") for f in build_modules_paths] build_modules_paths = [relativize_path(p, self._cmakedeps._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for p in build_modules_paths] components = self._cmakedeps.get_property("cmake_components", self._conanfile, check_type=list) if components is None: # Lets compute the default components names components = [] # This assumes that cmake_components is only defined with not multi .libs=[lib1, lib2] for name in self._conanfile.cpp_info.components: if name.startswith("_"): # Skip private components continue comp_components = self._cmakedeps.get_property("cmake_components", self._conanfile, name, check_type=list) if comp_components: components.extend(comp_components) else: cmakename = self._cmakedeps.get_property("cmake_target_name", self._conanfile, name) if cmakename and "::" in cmakename: # Remove package namespace cmakename = cmakename.split("::", 1)[1] components.append(cmakename or name) components = " ".join(components) if components else "" result = {"filename": f, "components": components, "pkg_name": pkg_name, "targets_include_file": targets_include, "build_modules_paths": build_modules_paths} conf_extra_variables = self._conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables", default={}, check_type=dict) dep_extra_variables = self._cmakedeps.get_property("cmake_extra_variables", self._conanfile, check_type=dict) or {} # The configuration variables have precedence over the dependency ones extra_variables = {dep: value for dep, value in dep_extra_variables.items() if dep not in conf_extra_variables} parsed_extra_variables = {} for key, value in extra_variables.items(): parsed_extra_variables[key] = parse_extra_variable("cmake_extra_variables", key, value) result["extra_variables"] = parsed_extra_variables result.update(self._get_legacy_vars()) return result def _get_legacy_vars(self): # Auxiliary variables for legacy consumption and try_compile cases pkg_name = self._conanfile.ref.name prefixes = self._cmakedeps.get_property("cmake_additional_variables_prefixes", self._conanfile, check_type=list) or [] f = self._cmakedeps.get_cmake_filename(self._conanfile) prefixes = [f] + prefixes include_dirs = definitions = libraries = None if not self._require.build: # To add global variables for try_compile and legacy aggregated_cppinfo = self._full_cpp_info.aggregated_components() # FIXME: Proper escaping of paths for CMake incdirs = [i.replace("\\", "/") for i in aggregated_cppinfo.includedirs] incdirs = [relativize_path(i, self._cmakedeps._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for i in incdirs] include_dirs = ";".join(incdirs) definitions = ";".join("-D" + cmake_escape_value(d) for d in aggregated_cppinfo.defines) root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) libraries = root_target_name or f"{pkg_name}::{pkg_name}" return {"additional_variables_prefixes": prefixes, "version": self._conanfile.ref.version, "include_dirs": include_dirs, "definitions": definitions, "libraries": libraries} @property def _template(self): return textwrap.dedent("""\ # Requires CMake > 3.15 if(${CMAKE_VERSION} VERSION_LESS "3.15") message(FATAL_ERROR "The 'CMakeDeps' generator only works with CMake >= 3.15") endif() include(${CMAKE_CURRENT_LIST_DIR}/{{ targets_include_file }}) get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(NOT isMultiConfig AND NOT CMAKE_BUILD_TYPE) message(FATAL_ERROR "Please, set the CMAKE_BUILD_TYPE variable when calling to CMake " "adding the '-DCMAKE_BUILD_TYPE=' argument.") endif() {% if components %} set({{filename}}_PACKAGE_PROVIDED_COMPONENTS {{components}}) foreach(comp {%raw%}${{%endraw%}{{filename}}_FIND_COMPONENTS}) if(NOT ${comp} IN_LIST {{filename}}_PACKAGE_PROVIDED_COMPONENTS) if({%raw%}${{%endraw%}{{filename}}_FIND_REQUIRED_${comp}}) message(STATUS "Conan: Error: '{{pkg_name}}' required COMPONENT '${comp}' not found") set({{filename}}_FOUND FALSE) endif() endif() endforeach() {% endif %} ################# Global variables for try compile and legacy ############## {% for prefix in additional_variables_prefixes %} set({{ prefix }}_VERSION_STRING "{{ version }}") {% if include_dirs is not none %} set({{ prefix }}_INCLUDE_DIRS "{{ include_dirs }}" ) set({{ prefix }}_INCLUDE_DIR "{{ include_dirs }}" ) {% endif %} {% if libraries is not none %} set({{ prefix }}_LIBRARIES {{ libraries }} ) {% endif %} {% if definitions is not none %} set({{ prefix }}_DEFINITIONS "{{ definitions}}" ) {% endif %} {% endfor %} # build_modules_paths comes from last configuration only # Some build modules in ConanCenter use try_compile variables and legacy, so this # include() needs to happen after the above variables are defined {% for build_module in build_modules_paths %} message(STATUS "Conan: Including build module from '{{build_module}}'") include("{{ build_module }}") {% endfor %} # Definition of extra CMake variables from cmake_extra_variables {% for key, value in extra_variables.items() %} set({{ key }} {{ value }}) {% endfor %} """) ================================================ FILE: conan/tools/cmake/cmakeconfigdeps/config_version.py ================================================ import textwrap import jinja2 from jinja2 import Template from conan.errors import ConanException class ConfigVersionTemplate2: """ foo-config-version.cmake """ def __init__(self, cmakedeps, conanfile): self._cmakedeps = cmakedeps self._conanfile = conanfile def content(self): t = Template(self._template, trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined) return t.render(self._context) @property def filename(self): f = self._cmakedeps.get_cmake_filename(self._conanfile) return f"{f}-config-version.cmake" if f == f.lower() else f"{f}ConfigVersion.cmake" @property def _context(self): policy = self._cmakedeps.get_property("cmake_config_version_compat", self._conanfile) if policy is None: policy = "SameMajorVersion" if policy not in ("AnyNewerVersion", "SameMajorVersion", "SameMinorVersion", "ExactVersion"): raise ConanException(f"Unknown cmake_config_version_compat={policy} in {self._conanfile}") version = self._cmakedeps.get_property("system_package_version", self._conanfile) version = version or self._conanfile.ref.version return {"version": version, "policy": policy} @property def _template(self): # https://gitlab.kitware.com/cmake/cmake/blob/master/Modules/BasicConfigVersion-SameMajorVersion.cmake.in # This will be at XXX-config-version.cmake # AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion ret = textwrap.dedent("""\ set(PACKAGE_VERSION "{{ version }}") if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) set(PACKAGE_VERSION_COMPATIBLE FALSE) else() {% if policy == "AnyNewerVersion" %} set(PACKAGE_VERSION_COMPATIBLE TRUE) {% elif policy == "SameMajorVersion" %} if("{{ version }}" MATCHES "^([0-9]+)\\\\.") set(CVF_VERSION_MAJOR {{ '${CMAKE_MATCH_1}' }}) else() set(CVF_VERSION_MAJOR "{{ version }}") endif() if(PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() {% elif policy == "SameMinorVersion" %} if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)") set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") else() set(CVF_VERSION_MAJOR "{{ version }}") set(CVF_VERSION_MINOR "") endif() if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR)) set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() {% elif policy == "ExactVersion" %} if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") set(CVF_VERSION_MINOR "${CMAKE_MATCH_3}") else() set(CVF_VERSION_MAJOR "{{ version }}") set(CVF_VERSION_MINOR "") set(CVF_VERSION_PATCH "") endif() if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR) AND (PACKAGE_FIND_VERSION_PATCH STREQUAL CVF_VERSION_PATCH)) set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() {% endif %} if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) set(PACKAGE_VERSION_EXACT TRUE) endif() endif() """) return ret ================================================ FILE: conan/tools/cmake/cmakeconfigdeps/target_configuration.py ================================================ import os import textwrap import jinja2 from jinja2 import Template from conan.errors import ConanException from conan.internal.api.install.generators import relativize_path from conan.internal.model.pkg_type import PackageType from conan.internal.graph.graph import CONTEXT_BUILD, CONTEXT_HOST from conan.tools.cmake.utils import cmake_escape_value class TargetConfigurationTemplate2: """ FooTarget-release.cmake """ def __init__(self, cmakedeps, conanfile, require, full_cpp_info): self._cmakedeps = cmakedeps self._conanfile = conanfile # The dependency conanfile, not the consumer one self._require = require self._full_cpp_info = full_cpp_info def content(self): auto_link = self._cmakedeps.get_property("cmake_set_interface_link_directories", self._conanfile, check_type=bool) if auto_link: out = self._cmakedeps._conanfile.output # noqa out.warning("CMakeConfigDeps: cmake_set_interface_link_directories deprecated and " "invalid. The package 'package_info()' must correctly define the (CPS) " "information", warn_tag="deprecated") t = Template(self._template, trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined) return t.render(self._context) @property def filename(self): f = self._cmakedeps.get_cmake_filename(self._conanfile) # Fallback to consumer configuration if it doesn't have build_type config = self._conanfile.settings.get_safe("build_type", self._cmakedeps.configuration) config = (config or "none").lower() build = "Build" if self._conanfile.context == CONTEXT_BUILD else "" return f"{f}-Targets{build}-{config}.cmake" def _requires(self, info, components): result = {} requires = info.parsed_requires() pkg_name = self._conanfile.ref.name pkg_type = info.type assert isinstance(pkg_type, PackageType), f"Pkg type {pkg_type} {type(pkg_type)}" transitive_reqs = self._cmakedeps.get_transitive_requires(self._conanfile) if not requires and not components: # global cpp_info without components definition # require the pkgname::pkgname base (user defined) or INTERFACE base target for d in transitive_reqs.values(): if d.package_type is PackageType.APP: continue dep_target = self._cmakedeps.get_property("cmake_target_name", d) dep_target = dep_target or f"{d.ref.name}::{d.ref.name}" link_feature = self._cmakedeps.get_property("cmake_link_feature", d) link = not (pkg_type is PackageType.SHARED and d.package_type is PackageType.SHARED) result[dep_target] = { "link": link, "link_feature": link_feature } return result for required_pkg, required_comp in requires: if required_pkg is None: # Points to a component of same package dep_comp = components.get(required_comp) assert dep_comp, f"Component {required_comp} not found in {self._conanfile}" dep_target = self._cmakedeps.get_property("cmake_target_name", self._conanfile, required_comp) dep_target = dep_target or f"{pkg_name}::{required_comp}" link = not (pkg_type is PackageType.SHARED and dep_comp.type is PackageType.SHARED) link_feature = self._cmakedeps.get_property("cmake_link_feature", self._conanfile, required_comp) result[dep_target] = { "link": link, "link_feature": link_feature } else: # Different package try: dep = transitive_reqs[required_pkg] except KeyError: # The transitive dep might have been skipped pass else: # To check if the component exist, it is ok to use the standard cpp_info # No need to use the cpp_info = deduce_cpp_info(dep) dep_comp = dep.cpp_info.components.get(required_comp) if dep_comp is None: # It must be the interface pkgname::pkgname target if required_pkg != required_comp: msg = (f"{self._conanfile} recipe cpp_info did .requires to " f"'{required_pkg}::{required_comp}' but component " f"'{required_comp}' not found in {required_pkg}") raise ConanException(msg) if dep.package_type is PackageType.APP: continue # It doesn't make sense to link a package that is an App comp = None default_target = f"{dep.ref.name}::{dep.ref.name}" # replace_requires link = pkg_type is not PackageType.SHARED else: if dep_comp.type is PackageType.APP or dep_comp.exe: continue # It doesn't make sense to link a package that is an App comp = required_comp default_target = f"{required_pkg}::{required_comp}" link = not (pkg_type is PackageType.SHARED and dep_comp.type is PackageType.SHARED) dep_target = self._cmakedeps.get_property("cmake_target_name", dep, comp) dep_target = dep_target or default_target link_feature = self._cmakedeps.get_property("cmake_link_feature", dep, comp) result[dep_target] = { "link": link, "link_feature": link_feature } return result @property def _context(self): cpp_info = self._full_cpp_info assert isinstance(cpp_info.type, PackageType) pkg_name = self._conanfile.ref.name # fallback to consumer configuration if it doesn't have build_type config = self._conanfile.settings.get_safe("build_type", self._cmakedeps.configuration) config = config.upper() if config else None pkg_folder = self._conanfile.package_folder.replace("\\", "/") config_folder = f"_{config}" if config else "" build = "_BUILD" if self._conanfile.context == CONTEXT_BUILD else "" pkg_folder_var = f"{pkg_name}_PACKAGE_FOLDER{config_folder}{build}" libs = {} # The BUILD context does not generate libraries targets atm if not self._require.build: libs = self._get_libs(cpp_info, pkg_name, pkg_folder, pkg_folder_var) self._add_root_lib_target(libs, pkg_name, cpp_info) exes = self._get_exes(cpp_info, pkg_name, pkg_folder, pkg_folder_var) seen_aliases = set() root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) root_target_name = root_target_name or f"{pkg_name}::{pkg_name}" for lib in libs.values(): for alias in lib.get("cmake_target_aliases", []): if alias == root_target_name: raise ConanException(f"Can't define an alias '{alias}' for the " f"root target '{root_target_name}' in {self._conanfile}. " f"Changing the default target should be done with the " f"'cmake_target_name' property.") if alias in seen_aliases: raise ConanException(f"Alias '{alias}' already defined in {self._conanfile}. ") seen_aliases.add(alias) if alias in libs: raise ConanException(f"Alias '{alias}' already defined as a target in " f"{self._conanfile}. ") pkg_folder = relativize_path(pkg_folder, self._cmakedeps._conanfile, "${CMAKE_CURRENT_LIST_DIR}") dependencies = self._get_dependencies() return {"dependencies": dependencies, "pkg_folder": pkg_folder, "pkg_folder_var": pkg_folder_var, "config": config, "exes": exes, "libs": libs, "context": self._conanfile.context } def _get_libs(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var) -> dict: libs = {} if cpp_info.has_components: for name, component in cpp_info.components.items(): target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, name) target_name = target_name or f"{pkg_name}::{name}" target = self._get_cmake_lib(component, cpp_info.components, pkg_folder, pkg_folder_var, comp_name=name) if target is not None: cmake_target_aliases = self._get_aliases(name) target["cmake_target_aliases"] = cmake_target_aliases libs[target_name] = target else: target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) target_name = target_name or f"{pkg_name}::{pkg_name}" target = self._get_cmake_lib(cpp_info, None, pkg_folder, pkg_folder_var) if target is not None: cmake_target_aliases = self._get_aliases() target["cmake_target_aliases"] = cmake_target_aliases libs[target_name] = target return libs def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var, comp_name=None): if info.exe or not (info.package_framework or info.frameworks or info.includedirs or info.libs or info.system_libs or info.defines or info.requires): return includedirs = ";".join(self._path(i, pkg_folder, pkg_folder_var) for i in info.includedirs) if info.includedirs else "" requires = self._requires(info, components) assert isinstance(requires, dict) defines = ";".join(cmake_escape_value(f) for f in info.defines) # FIXME: Filter by lib traits!!!!! if not self._require.headers: # If not depending on headers, paths and includedirs = defines = None extra_libs = self._cmakedeps.get_property("cmake_extra_interface_libs", self._conanfile, comp_name=comp_name, check_type=list) or [] sources = [self._path(source, pkg_folder, pkg_folder_var) for source in info.sources] target = {"type": "INTERFACE", "comp_name": comp_name, "includedirs": includedirs, "defines": defines, "requires": requires, "cxxflags": ";".join(cmake_escape_value(f) for f in info.cxxflags), "cflags": ";".join(cmake_escape_value(f) for f in info.cflags), "sharedlinkflags": ";".join(cmake_escape_value(v) for v in info.sharedlinkflags), "exelinkflags": ";".join(cmake_escape_value(v) for v in info.exelinkflags), "system_libs": " ".join(info.system_libs + extra_libs), "sources": " ".join(sources) } # System frameworks (only Apple OS) if info.frameworks: target['frameworks'] = " ".join([f"-framework {frw}" for frw in info.frameworks]) # FIXME: We're ignoring this value at this moment. It relies on cmake_target_name or lib name # Revisit when cpp.exe value is used too. if info.package_framework: target["package_framework"] = {} lib_type = "SHARED" if info.type is PackageType.SHARED else \ "STATIC" if info.type is PackageType.STATIC else "STATIC" assert lib_type, f"Unknown package type {info.type}" assert info.location, f"cpp_info.location missing for framework {info.package_framework}" target["type"] = lib_type target["package_framework"]["location"] = self._path(info.location, pkg_folder, pkg_folder_var) target["includedirs"] = [] # empty as frameworks have their own way to inject headers # FIXME: This is not needed for CMake < 3.24. Remove it when Conan requires CMake >= 3.24 target["package_framework"]["frameworkdir"] = self._path(pkg_folder, pkg_folder, pkg_folder_var) if info.libs: if len(info.libs) != 1: raise ConanException(f"New CMakeDeps only allows 1 lib per component:\n" f"{self._conanfile}: {info.libs}") assert info.location, "info.location missing for .libs, it should have been deduced" location = self._path(info.location, pkg_folder, pkg_folder_var) link_location = self._path(info.link_location, pkg_folder, pkg_folder_var) \ if info.link_location else None lib_type = "SHARED" if info.type is PackageType.SHARED else \ "STATIC" if info.type is PackageType.STATIC else None assert lib_type, f"Unknown package type {info.type}" target["type"] = lib_type target["location"] = location target["link_location"] = link_location link_languages = info.languages or self._conanfile.languages or [] link_languages = ["CXX" if c == "C++" else c for c in link_languages] target["link_languages"] = link_languages return target def _get_aliases(self, comp_name=None): aliases = self._cmakedeps.get_property("cmake_target_aliases", self._conanfile, comp_name, check_type=list) or [] return aliases def _add_root_lib_target(self, libs, pkg_name, cpp_info): """ Add a new pkgname::pkgname INTERFACE target that depends on default_components or on all other library targets (not exes) It will not be added if there exists already a pkgname::pkgname target (Or an alias exists). """ root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) root_target_name = root_target_name or f"{pkg_name}::{pkg_name}" # TODO: What if an exe target is called like the pkg_name::pkg_name if libs and root_target_name not in libs: # Add a generic interface target for the package depending on the others if cpp_info.default_components is not None: all_requires = {} for defaultc in cpp_info.default_components: target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, defaultc) comp_name = target_name or f"{pkg_name}::{defaultc}" link_feature = self._cmakedeps.get_property("cmake_link_feature", self._conanfile, defaultc) all_requires[comp_name] = { "link": True, # It is an interface, full link "link_feature": link_feature } else: all_requires = {k: { "link": True, "link_feature": self._cmakedeps.get_property("cmake_link_feature", self._conanfile, v.get("comp_name")) } for k, v in libs.items()} # This target might have an alias, so we need to check it cmake_target_aliases = self._get_aliases() libs[root_target_name] = {"type": "INTERFACE", "requires": all_requires, "cmake_target_aliases": cmake_target_aliases} def _get_exes(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var): exes = {} if cpp_info.has_components: for name, comp in cpp_info.components.items(): if comp.exe or comp.type is PackageType.APP: target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, name) target = target_name or f"{pkg_name}::{name}" exe_location = self._path(comp.location, pkg_folder, pkg_folder_var) exes[target] = exe_location else: if cpp_info.exe: target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) target = target_name or f"{pkg_name}::{pkg_name}" exe_location = self._path(cpp_info.location, pkg_folder, pkg_folder_var) exes[target] = exe_location return exes def _get_dependencies(self): """ transitive dependencies Filenames for find_dependency() """ # Build requires are already filtered by the get_transitive_requires transitive_reqs = self._cmakedeps.get_transitive_requires(self._conanfile) # FIXME: Hardcoded CONFIG ret = {self._cmakedeps.get_cmake_filename(r): "CONFIG" for r in transitive_reqs.values()} extra_mods = self._cmakedeps.get_property("cmake_extra_dependencies", self._conanfile, check_type=list) or [] ret.update({extra_mod: "" for extra_mod in extra_mods}) return ret @staticmethod def _path(p, pkg_folder, pkg_folder_var): def escape(p_): return p_.replace("$", "\\$").replace('"', '\\"') p = p.replace("\\", "/") if os.path.isabs(p): if p.startswith(pkg_folder): rel = p[len(pkg_folder):].lstrip("/") return f"${{{pkg_folder_var}}}/{escape(rel)}" return escape(p) return f"${{{pkg_folder_var}}}/{escape(p)}" @property def _template(self): # TODO: CMake 3.24: Apple Frameworks: https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#genex:LINK_LIBRARY # TODO: Check why not set_property instead of target_link_libraries return textwrap.dedent("""\ {%- macro config_wrapper(config, value) -%} {% if config -%} $<$:{{value}}> {%- else -%} {{value}} {%- endif %} {%- endmacro -%} set({{pkg_folder_var}} "{{pkg_folder}}") # Dependencies finding include(CMakeFindDependencyMacro) {% for dep, dep_find_mode in dependencies.items() %} if(NOT {{dep}}_FOUND) find_dependency({{dep}} REQUIRED {{dep_find_mode}}) endif() {% endfor %} ################# Libs information ############## {% for lib, lib_info in libs.items() %} #################### {{lib}} #################### if(NOT TARGET {{ lib }}) message(STATUS "Conan: Target declared imported {{lib_info["type"]}} library '{{lib}}'") add_library({{lib}} {{lib_info["type"]}} IMPORTED) endif() {% for alias in lib_info.get("cmake_target_aliases", []) %} if(NOT TARGET {{alias}}) message(STATUS "Conan: Target declared alias '{{alias}}' for '{{lib}}'") add_library({{alias}} ALIAS {{lib}}) endif() {% endfor %} {% if lib_info.get("includedirs") %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES {{config_wrapper(config, lib_info["includedirs"])}}) {% endif %} {% if lib_info.get("defines") %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS "{{config_wrapper(config, lib_info["defines"])}}") {% endif %} {% if lib_info.get("cxxflags") %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS "$<$:{{config_wrapper(config, lib_info["cxxflags"])}}>") {% endif %} {% if lib_info.get("cflags") %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS "$<$:{{config_wrapper(config, lib_info["cflags"])}}>") {% endif %} {% if lib_info.get("sharedlinkflags") %} {% set linkflags = config_wrapper(config, lib_info["sharedlinkflags"]) %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_OPTIONS "$<$,SHARED_LIBRARY>:{{linkflags}}>" "$<$,MODULE_LIBRARY>:{{linkflags}}>") {% endif %} {% if lib_info.get("exelinkflags") %} {% set exeflags = config_wrapper(config, lib_info["exelinkflags"]) %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_OPTIONS "$<$,EXECUTABLE>:{{exeflags}}>") {% endif %} {% if lib_info.get("link_languages") %} get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES) if("CXX" IN_LIST _languages) list(APPEND _languages "C") endif() if("CUDA" IN_LIST _languages) list(APPEND _languages "C" "CXX") endif() {% for lang in lib_info["link_languages"] %} if(NOT "{{lang}}" IN_LIST _languages) message(SEND_ERROR "Target {{lib}} has {{lang}} linkage but {{lang}} not enabled in project()") endif() set_property(TARGET {{lib}} APPEND PROPERTY IMPORTED_LINK_INTERFACE_LANGUAGES_{{config}} {{lang}}) {% endfor %} {% endif %} {% if lib_info.get("location") %} set_property(TARGET {{lib}} APPEND PROPERTY IMPORTED_CONFIGURATIONS {{config}}) set_target_properties({{lib}} PROPERTIES IMPORTED_LOCATION_{{config}} "{{lib_info["location"]}}") {% elif lib_info.get("type") == "INTERFACE" %} set_property(TARGET {{lib}} APPEND PROPERTY IMPORTED_CONFIGURATIONS {{config}}) {% endif %} {% if lib_info.get("link_location") %} set_target_properties({{lib}} PROPERTIES IMPORTED_IMPLIB_{{config}} "{{lib_info["link_location"]}}") {% endif %} {% if lib_info.get("requires") %} # Information of transitive dependencies {% for require_target, link_info in lib_info["requires"].items() %} # Requirement {{lib}} -> {{require_target}} (Full link: {{link_info["link"]}}) {% if link_info["link"] %} {% if link_info["link_feature"] %} # Link feature: {{link_info["link_feature"]}} if(CMAKE_VERSION VERSION_LESS "3.24") message(FATAL_ERROR "The 'CMakeConfigDeps' generator LINK_FEATURE property only works with CMake >= 3.24") endif() {% endif %} # set property allows to append, and lib_info[requires] will iterate set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES {% if link_info["link_feature"] %} "$") {% else %} "{{config_wrapper(config, require_target)}}") {% endif %} {% else %} if(CMAKE_VERSION VERSION_LESS "3.27") message(FATAL_ERROR "The 'CMakeConfigDeps' generator COMPILE_ONLY expression only works with CMake >= 3.27") endif() # If the headers trait is not there, this will do nothing target_link_libraries({{lib}} INTERFACE $ ) set_property(TARGET {{lib}} APPEND PROPERTY IMPORTED_LINK_DEPENDENT_LIBRARIES_{{config}} {{require_target}}) {% endif %} {% endfor %} {% endif %} {% if lib_info.get("system_libs") %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES {{config_wrapper(config, lib_info["system_libs"])}}) {% endif %} {% if lib_info.get("frameworks") %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "{{config_wrapper(config, lib_info["frameworks"])}}") {% endif %} {% if lib_info.get("package_framework") %} set_target_properties({{lib}} PROPERTIES IMPORTED_LOCATION_{{config}} "{{lib_info["package_framework"]["location"]}}" FRAMEWORK TRUE) if(CMAKE_VERSION VERSION_LESS "3.24") set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS $<$:-F{{lib_info["package_framework"]["frameworkdir"]}}>) set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS $<$:-F{{lib_info["package_framework"]["frameworkdir"]}}>) endif() {% endif %} {% if lib_info.get("sources") %} set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_SOURCES {{config_wrapper(config, lib_info["sources"] )}}) {% endif %} {% endfor %} ################# Exes information ############## {% for exe, location in exes.items() %} #################### {{exe}} #################### if(NOT TARGET {{ exe }}) message(STATUS "Conan: Target declared imported executable '{{exe}}' {{context}}") add_executable({{exe}} IMPORTED) else() get_property(_context TARGET {{exe}} PROPERTY CONAN_CONTEXT) if(NOT $${_context} STREQUAL "{{context}}") message(STATUS "Conan: Exe {{exe}} was already defined in ${_context}") get_property(_configurations TARGET {{exe}} PROPERTY IMPORTED_CONFIGURATIONS) message(STATUS "Conan: Exe {{exe}} defined configurations: ${_configurations}") foreach(_config ${_configurations}) set_property(TARGET {{exe}} PROPERTY IMPORTED_LOCATION_${_config}) endforeach() set_property(TARGET {{exe}} PROPERTY IMPORTED_CONFIGURATIONS) endif() endif() set_property(TARGET {{exe}} APPEND PROPERTY IMPORTED_CONFIGURATIONS {{config}}) set_target_properties({{exe}} PROPERTIES IMPORTED_LOCATION_{{config}} "{{location}}") set_property(TARGET {{exe}} PROPERTY CONAN_CONTEXT "{{context}}") {% endfor %} """) ================================================ FILE: conan/tools/cmake/cmakeconfigdeps/targets.py ================================================ import textwrap import jinja2 from jinja2 import Template class TargetsTemplate2: """ FooTargets.cmake """ def __init__(self, cmakedeps, conanfile): self._cmakedeps = cmakedeps self._conanfile = conanfile def content(self): t = Template(self._template, trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined) return t.render(self._context) @property def filename(self): f = self._cmakedeps.get_cmake_filename(self._conanfile) return f"{f}Targets.cmake" @property def _context(self): filename = self._cmakedeps.get_cmake_filename(self._conanfile) ret = {"ref": str(self._conanfile.ref), "filename": filename} return ret @property def _template(self): return textwrap.dedent("""\ include_guard() message(STATUS "Conan: Configuring Targets for {{ ref }}") # Load information for each installed configuration. file(GLOB _target_files "${CMAKE_CURRENT_LIST_DIR}/{{filename}}-Targets-*.cmake") foreach(_target_file IN LISTS _target_files) include("${_target_file}") endforeach() file(GLOB _build_files "${CMAKE_CURRENT_LIST_DIR}/{{filename}}-TargetsBuild-*.cmake") foreach(_build_file IN LISTS _build_files) include("${_build_file}") endforeach() """) ================================================ FILE: conan/tools/cmake/cmakedeps/__init__.py ================================================ FIND_MODE_MODULE = "module" FIND_MODE_CONFIG = "config" FIND_MODE_NONE = "none" FIND_MODE_BOTH = "both" ================================================ FILE: conan/tools/cmake/cmakedeps/cmakedeps.py ================================================ import textwrap import jinja2 from jinja2 import Template from conan.api.output import Color from conan.internal import check_duplicated_generator from conan.tools.cmake.cmakedeps import FIND_MODE_CONFIG, FIND_MODE_NONE, FIND_MODE_BOTH, \ FIND_MODE_MODULE from conan.tools.cmake.cmakedeps.templates.config import ConfigTemplate from conan.tools.cmake.cmakedeps.templates.config_version import ConfigVersionTemplate from conan.tools.cmake.cmakedeps.templates.macros import MacrosTemplate from conan.tools.cmake.cmakedeps.templates.target_configuration import TargetConfigurationTemplate from conan.tools.cmake.cmakedeps.templates.target_data import ConfigDataTemplate from conan.tools.cmake.cmakedeps.templates.targets import TargetsTemplate from conan.tools.files import save from conan.errors import ConanException from conan.internal.model.dependencies import get_transitive_requires class CMakeDeps: def __init__(self, conanfile): self._conanfile = conanfile self.arch = self._conanfile.settings.get_safe("arch") self.configuration = str(self._conanfile.settings.build_type) # Activate the build config files for the specified libraries self.build_context_activated = [] # By default, the build modules are generated for host context only self.build_context_build_modules = [] # If specified, the files/targets/variables for the build context will be renamed appending # a suffix. It is necessary in case of same require and build_require and will cause an error self.build_context_suffix = {} # Enable/Disable checking if a component target exists or not self.check_components_exist = False self._properties = {} def generate(self): """ This method will save the generated files to the ``conanfile.generators_folder`` folder """ check_duplicated_generator(self, self._conanfile) # Current directory is the generators_folder generator_files = self.content for generator_file, content in generator_files.items(): save(self._conanfile, generator_file, content) self.generate_aggregator() @property def content(self): macros = MacrosTemplate() ret = {macros.filename: macros.render()} host_req = self._conanfile.dependencies.host build_req = self._conanfile.dependencies.direct_build test_req = self._conanfile.dependencies.test # Check if the same package is at host and build and the same time activated_br = {r.ref.name for r in build_req.values() if r.ref.name in self.build_context_activated} common_names = {r.ref.name for r in host_req.values()}.intersection(activated_br) for common_name in common_names: suffix = self.build_context_suffix.get(common_name) if not suffix: raise ConanException("The package '{}' exists both as 'require' and as " "'build require'. You need to specify a suffix using the " "'build_context_suffix' attribute at the CMakeDeps " "generator.".format(common_name)) # Iterate all the transitive requires direct_configs = [] for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): # Require is not used at the moment, but its information could be used, # and will be used in Conan 2.0 # Filter the build_requires not activated with cmakedeps.build_context_activated if require.build and dep.ref.name not in self.build_context_activated: continue cmake_find_mode = self.get_property("cmake_find_mode", dep) cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG cmake_find_mode = cmake_find_mode.lower() # Skip from the requirement if cmake_find_mode == FIND_MODE_NONE: # Skip the generation of config files for this node, it will be located externally continue if cmake_find_mode in (FIND_MODE_CONFIG, FIND_MODE_BOTH): self._generate_files(require, dep, ret, find_module_mode=False) if cmake_find_mode in (FIND_MODE_MODULE, FIND_MODE_BOTH): self._generate_files(require, dep, ret, find_module_mode=True) if require.direct: # aggregate config information for user convenience find_module_mode = True if cmake_find_mode == FIND_MODE_MODULE else False config = ConfigTemplate(self, require, dep, find_module_mode) direct_configs.append(config) if direct_configs: # Some helpful verbose messages about generated files msg = ["CMakeDeps necessary find_package() and targets for your CMakeLists.txt"] for config in direct_configs: msg.append(f" find_package({config.file_name})") targets = ' '.join(c.root_target_name for c in direct_configs) msg.append(f" target_link_libraries(... {targets})") if self._conanfile._conan_is_consumer: # noqa self._conanfile.output.info("\n".join(msg), fg=Color.CYAN) else: self._conanfile.output.verbose("\n".join(msg)) return ret def _generate_files(self, require, dep, ret, find_module_mode): if not find_module_mode: config_version = ConfigVersionTemplate(self, require, dep) ret[config_version.filename] = config_version.render() data_target = ConfigDataTemplate(self, require, dep, find_module_mode) data_content = data_target.render() ret[data_target.filename] = data_content target_configuration = TargetConfigurationTemplate(self, require, dep, find_module_mode) ret[target_configuration.filename] = target_configuration.render() targets = TargetsTemplate(self, require, dep, find_module_mode) ret[targets.filename] = targets.render() config = ConfigTemplate(self, require, dep, find_module_mode) # Only the latest configuration BUILD_MODULES and additional_variables will be used # in multi-config they will be overwritten by the latest install ret[config.filename] = config.render() def set_property(self, dep, prop, value, build_context=False): """ Using this method you can overwrite the :ref:`property` values set by the Conan recipes from the consumer. This can be done for ``cmake_file_name``, ``cmake_target_name``, ``cmake_find_mode``, ``cmake_module_file_name``, ``cmake_module_target_name``, ``cmake_additional_variables_prefixes``, ``cmake_config_version_compat``, ``system_package_version``, ``cmake_build_modules``, ``nosoname``, ``cmake_target_aliases`` and ``cmake_extra_variables``. :param dep: Name of the dependency to set the :ref:`property`. For components use the syntax: ``dep_name::component_name``. :param prop: Name of the :ref:`property`. :param value: Value of the property. Use ``None`` to invalidate any value set by the upstream recipe. :param build_context: Set to ``True`` if you want to set the property for a dependency that belongs to the build context (``False`` by default). """ build_suffix = "&build" if build_context else "" self._properties.setdefault(f"{dep}{build_suffix}", {}).update({prop: value}) def get_property(self, prop, dep, comp_name=None, check_type=None): dep_name = dep.ref.name build_suffix = "&build" if str( dep_name) in self.build_context_activated and dep.context == "build" else "" dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" try: value = self._properties[f"{dep_comp}{build_suffix}"][prop] if check_type is not None and not isinstance(value, check_type): raise ConanException( f'The expected type for {prop} is "{check_type.__name__}", but "{type(value).__name__}" was found') return value except KeyError: return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \ else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type) def get_cmake_package_name(self, dep, module_mode=None): """Get the name of the file for the ``find_package(XXX)`` call""" # This is used by CMakeDeps to determine: # - The filename to generate (XXX-config.cmake or FindXXX.cmake) # - The name of the defined XXX_DIR variables # - The name of transitive dependencies for calls to find_dependency if module_mode and self.get_find_mode(dep) in [FIND_MODE_MODULE, FIND_MODE_BOTH]: ret = self.get_property("cmake_module_file_name", dep) if ret: return ret ret = self.get_property("cmake_file_name", dep) return ret or dep.ref.name def get_find_mode(self, dep): """ :param dep: requirement :return: One of ``"none"``, ``"config"``, ``"module"`` or ``"both"``. Defaults to ``"config"`` when not set """ tmp = self.get_property("cmake_find_mode", dep) if tmp is None: return "config" return tmp.lower() def generate_aggregator(self): host = self._conanfile.dependencies.host build_req = self._conanfile.dependencies.direct_build test_req = self._conanfile.dependencies.test configs = [] for require, dep in list(host.items()) + list(build_req.items()) + list(test_req.items()): if not require.direct: continue if require.build and dep.ref.name not in self.build_context_activated: continue cmake_find_mode = self.get_property("cmake_find_mode", dep) cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG cmake_find_mode = cmake_find_mode.lower() find_module_mode = True if cmake_find_mode == FIND_MODE_MODULE else False config = ConfigTemplate(self, require, dep, find_module_mode) configs.append(config) template = textwrap.dedent("""\ message(STATUS "Conan: Using CMakeDeps conandeps_legacy.cmake aggregator via include()") message(STATUS "Conan: It is recommended to use explicit find_package() per dependency instead") {% for config in configs %} find_package({{config.file_name}}) {% endfor %} set(CONANDEPS_LEGACY {% for t in configs %} {{t.root_target_name}} {% endfor %}) """) template = Template(template, trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined) conandeps = template.render({"configs": configs}) save(self._conanfile, "conandeps_legacy.cmake", conandeps) def get_transitive_requires(self, conanfile): # Prepared to filter transitive tool-requires with visible=True return get_transitive_requires(self._conanfile, conanfile) ================================================ FILE: conan/tools/cmake/cmakedeps/templates/__init__.py ================================================ import jinja2 from jinja2 import Template from conan.errors import ConanException class CMakeDepsFileTemplate: def __init__(self, cmakedeps, require, conanfile, generating_module=False): self.cmakedeps = cmakedeps self.require = require self.conanfile = conanfile self.generating_module = generating_module @property def pkg_name(self): return self.conanfile.ref.name + self.suffix @property def root_target_name(self): return self.get_root_target_name(self.conanfile, self.suffix) @property def file_name(self): return self.cmakedeps.get_cmake_package_name(self.conanfile, module_mode=self.generating_module) + self.suffix @property def suffix(self): if not self.require.build: return "" return self.cmakedeps.build_context_suffix.get(self.conanfile.ref.name, "") def render(self): try: context = self.context except Exception as e: raise ConanException("error generating context for '{}': {}".format(self.conanfile, e)) # Cache the template instance as a class attribute to greatly speed up the rendering # NOTE: this assumes that self.template always returns the same string template_instance = getattr(type(self), "template_instance", None) if template_instance is None: template_instance = Template(self.template, trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined) setattr(type(self), "template_instance", template_instance) return template_instance.render(context) def context(self): raise NotImplementedError() @property def template(self): raise NotImplementedError() @property def filename(self): raise NotImplementedError() @property def configuration(self): return self.cmakedeps.configuration @property def arch(self): return self.cmakedeps.arch @property def config_suffix(self): return "_{}".format(self.configuration.upper()) if self.configuration else "" @staticmethod def _get_target_default_name(req, component_name="", suffix=""): return "{name}{suffix}::{cname}{suffix}".format(cname=component_name or req.ref.name, name=req.ref.name, suffix=suffix) def get_root_target_name(self, req, suffix=""): if self.generating_module: ret = self.cmakedeps.get_property("cmake_module_target_name", req) if ret: return ret ret = self.cmakedeps.get_property("cmake_target_name", req) return ret or self._get_target_default_name(req, suffix=suffix) def get_component_alias(self, req, comp_name): if comp_name not in req.cpp_info.components: # foo::foo might be referencing the root cppinfo if req.ref.name == comp_name: return self.get_root_target_name(req) raise ConanException("Component '{name}::{cname}' not found in '{name}' " "package requirement".format(name=req.ref.name, cname=comp_name)) if self.generating_module: ret = self.cmakedeps.get_property("cmake_module_target_name", req, comp_name=comp_name) if ret: return ret ret = self.cmakedeps.get_property("cmake_target_name", req, comp_name=comp_name) # If we don't specify the `cmake_target_name` property for the component it will # fallback to the pkg_name::comp_name, it wont use the root cpp_info cmake_target_name # property because that is also an absolute name (Greetings::Greetings), it is not a namespace # and we don't want to split and do tricks. return ret or self._get_target_default_name(req, component_name=comp_name) ================================================ FILE: conan/tools/cmake/cmakedeps/templates/config.py ================================================ import textwrap from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate """ FooConfig.cmake foo-config.cmake """ class ConfigTemplate(CMakeDepsFileTemplate): @property def filename(self): if self.generating_module: return "Find{}.cmake".format(self.file_name) else: if self.file_name == self.file_name.lower(): return "{}-config.cmake".format(self.file_name) else: return "{}Config.cmake".format(self.file_name) @property def additional_variables_prefixes(self): prefix_list = ( self.cmakedeps.get_property("cmake_additional_variables_prefixes", self.conanfile, check_type=list) or []) return list(set([self.file_name] + prefix_list)) @property def parsed_extra_variables(self): # Reading configuration from "cmake_extra_variables" property from conan.tools.cmake.utils import parse_extra_variable conf_extra_variables = self.conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables", default={}, check_type=dict) dep_extra_variables = self.cmakedeps.get_property("cmake_extra_variables", self.conanfile, check_type=dict) or {} # The configuration variables have precedence over the dependency ones extra_variables = {dep: value for dep, value in dep_extra_variables.items() if dep not in conf_extra_variables} parsed_extra_variables = {} for key, value in extra_variables.items(): parsed_extra_variables[key] = parse_extra_variable("cmake_extra_variables", key, value) return parsed_extra_variables @property def context(self): targets_include = "" if not self.generating_module else "module-" targets_include += "{}Targets.cmake".format(self.file_name) return {"is_module": self.generating_module, "version": self.conanfile.ref.version, "file_name": self.file_name, "additional_variables_prefixes": self.additional_variables_prefixes, "extra_variables": self.parsed_extra_variables, "pkg_name": self.pkg_name, "config_suffix": self.config_suffix, "check_components_exist": self.cmakedeps.check_components_exist, "targets_include_file": targets_include} @property def template(self): return textwrap.dedent("""\ {%- macro pkg_var(pkg_name, var, config_suffix) -%} {{'${'+pkg_name+'_'+var+config_suffix+'}'}} {%- endmacro -%} ########## MACROS ########################################################################### ############################################################################################# # Requires CMake > 3.15 if(${CMAKE_VERSION} VERSION_LESS "3.15") message(FATAL_ERROR "The 'CMakeDeps' generator only works with CMake >= 3.15") endif() if({{ file_name }}_FIND_QUIETLY) set({{ file_name }}_MESSAGE_MODE VERBOSE) else() set({{ file_name }}_MESSAGE_MODE STATUS) endif() include(${CMAKE_CURRENT_LIST_DIR}/cmakedeps_macros.cmake) include(${CMAKE_CURRENT_LIST_DIR}/{{ targets_include_file }}) include(CMakeFindDependencyMacro) check_build_type_defined() foreach(_DEPENDENCY {{ pkg_var(pkg_name, 'FIND_DEPENDENCY_NAMES', '') }} ) # Check that we have not already called a find_package with the transitive dependency if(NOT {{ '${_DEPENDENCY}' }}_FOUND) find_dependency({{ '${_DEPENDENCY}' }} REQUIRED ${${_DEPENDENCY}_FIND_MODE}) endif() endforeach() {% for prefix in additional_variables_prefixes %} set({{ prefix }}_VERSION_STRING "{{ version }}") set({{ prefix }}_INCLUDE_DIRS {{ pkg_var(pkg_name, 'INCLUDE_DIRS', config_suffix) }} ) set({{ prefix }}_INCLUDE_DIR {{ pkg_var(pkg_name, 'INCLUDE_DIRS', config_suffix) }} ) set({{ prefix }}_LIBRARIES {{ pkg_var(pkg_name, 'LIBRARIES', config_suffix) }} ) set({{ prefix }}_DEFINITIONS {{ pkg_var(pkg_name, 'DEFINITIONS', config_suffix) }} ) {% endfor %} # Definition of extra CMake variables from cmake_extra_variables {% for key, value in extra_variables.items() %} set({{ key }} {{ value }}) {% endfor %} # Only the last installed configuration BUILD_MODULES are included to avoid the collision foreach(_BUILD_MODULE {{ pkg_var(pkg_name, 'BUILD_MODULES_PATHS', config_suffix) }} ) message({% raw %}${{% endraw %}{{ file_name }}_MESSAGE_MODE} "Conan: Including build module from '${_BUILD_MODULE}'") include({{ '${_BUILD_MODULE}' }}) endforeach() {% if check_components_exist %} # Check that the specified components in the find_package(Foo COMPONENTS x y z) are there # This is the variable filled by CMake with the requested components in find_package if({{ file_name }}_FIND_COMPONENTS) foreach(_FIND_COMPONENT {{ pkg_var(file_name, 'FIND_COMPONENTS', '') }}) if (TARGET ${_FIND_COMPONENT}) message({% raw %}${{% endraw %}{{ file_name }}_MESSAGE_MODE} "Conan: Component '${_FIND_COMPONENT}' found in package '{{ pkg_name }}'") else() message(FATAL_ERROR "Conan: Component '${_FIND_COMPONENT}' NOT found in package '{{ pkg_name }}'") endif() endforeach() endif() {% endif %} {% if is_module %} include(FindPackageHandleStandardArgs) set({{ file_name }}_FOUND 1) set({{ file_name }}_VERSION "{{ version }}") find_package_handle_standard_args({{ file_name }} REQUIRED_VARS {{ file_name }}_VERSION VERSION_VAR {{ file_name }}_VERSION) mark_as_advanced({{ file_name }}_FOUND {{ file_name }}_VERSION) {% for prefix in additional_variables_prefixes %} set({{ prefix }}_FOUND 1) set({{ prefix }}_VERSION "{{ version }}") mark_as_advanced({{ prefix }}_FOUND {{ prefix }}_VERSION) {% endfor %} {% endif %} """) ================================================ FILE: conan/tools/cmake/cmakedeps/templates/config_version.py ================================================ import textwrap from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate from conan.errors import ConanException """ foo-config-version.cmake """ class ConfigVersionTemplate(CMakeDepsFileTemplate): @property def filename(self): if self.file_name == self.file_name.lower(): return "{}-config-version.cmake".format(self.file_name) else: return "{}ConfigVersion.cmake".format(self.file_name) @property def context(self): policy = self.cmakedeps.get_property("cmake_config_version_compat", self.conanfile) if policy is None: policy = "SameMajorVersion" if policy not in ("AnyNewerVersion", "SameMajorVersion", "SameMinorVersion", "ExactVersion"): raise ConanException(f"Unknown cmake_config_version_compat={policy} in {self.conanfile}") version = self.cmakedeps.get_property("system_package_version", self.conanfile) version = version or self.conanfile.ref.version return {"version": version, "policy": policy} @property def template(self): # https://gitlab.kitware.com/cmake/cmake/blob/master/Modules/BasicConfigVersion-SameMajorVersion.cmake.in # This will be at XXX-config-version.cmake # AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion ret = textwrap.dedent("""\ set(PACKAGE_VERSION "{{ version }}") if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) set(PACKAGE_VERSION_COMPATIBLE FALSE) else() {% if policy == "AnyNewerVersion" %} set(PACKAGE_VERSION_COMPATIBLE TRUE) {% elif policy == "SameMajorVersion" %} if("{{ version }}" MATCHES "^([0-9]+)\\\\.") set(CVF_VERSION_MAJOR {{ '${CMAKE_MATCH_1}' }}) else() set(CVF_VERSION_MAJOR "{{ version }}") endif() if(PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() {% elif policy == "SameMinorVersion" %} if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)") set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") else() set(CVF_VERSION_MAJOR "{{ version }}") set(CVF_VERSION_MINOR "") endif() if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR)) set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() {% elif policy == "ExactVersion" %} if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") set(CVF_VERSION_MINOR "${CMAKE_MATCH_3}") else() set(CVF_VERSION_MAJOR "{{ version }}") set(CVF_VERSION_MINOR "") set(CVF_VERSION_PATCH "") endif() if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR) AND (PACKAGE_FIND_VERSION_PATCH STREQUAL CVF_VERSION_PATCH)) set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() {% endif %} if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) set(PACKAGE_VERSION_EXACT TRUE) endif() endif() """) return ret ================================================ FILE: conan/tools/cmake/cmakedeps/templates/macros.py ================================================ import textwrap from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate """ cmakedeps_macros.cmake """ class MacrosTemplate(CMakeDepsFileTemplate): """cmakedeps_macros.cmake""" def __init__(self): super(MacrosTemplate, self).__init__(cmakedeps=None, require=None, conanfile=None) @property def filename(self): return "cmakedeps_macros.cmake" @property def context(self): return {} @property def template(self): return textwrap.dedent(""" macro(conan_find_apple_frameworks FRAMEWORKS_FOUND FRAMEWORKS FRAMEWORKS_DIRS) if(APPLE) foreach(_FRAMEWORK ${FRAMEWORKS}) # https://cmake.org/pipermail/cmake-developers/2017-August/030199.html find_library(CONAN_FRAMEWORK_${_FRAMEWORK}_FOUND NAMES ${_FRAMEWORK} PATHS ${FRAMEWORKS_DIRS} CMAKE_FIND_ROOT_PATH_BOTH) if(CONAN_FRAMEWORK_${_FRAMEWORK}_FOUND) list(APPEND ${FRAMEWORKS_FOUND} ${CONAN_FRAMEWORK_${_FRAMEWORK}_FOUND}) message(VERBOSE "Framework found! ${FRAMEWORKS_FOUND}") else() message(FATAL_ERROR "Framework library ${_FRAMEWORK} not found in paths: ${FRAMEWORKS_DIRS}") endif() endforeach() endif() endmacro() function(conan_package_library_targets libraries package_libdir package_bindir library_type is_host_windows deps_target out_libraries_target config_suffix package_name no_soname_mode) set(_out_libraries_target "") foreach(_LIBRARY_NAME ${libraries}) if(CMAKE_SYSTEM_NAME MATCHES "Windows" AND NOT DEFINED MINGW AND CMAKE_VERSION VERSION_LESS "3.29") # Backport logic from https://github.com/Kitware/CMake/commit/c6efbd78d86798573654d1a791f76de0e71bd93f # which is only needed on versions older than 3.29 # this allows finding static library files created by meson # We are not affected by PATH-derived folders, because we call find_library with NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH set(_original_find_library_suffixes "${CMAKE_FIND_LIBRARY_SUFFIXES}") set(_original_find_library_prefixes "${CMAKE_FIND_LIBRARY_PREFIXES}") set(CMAKE_FIND_LIBRARY_PREFIXES "" "lib") set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll.lib" ".lib" ".a") endif() find_library(CONAN_FOUND_LIBRARY NAMES ${_LIBRARY_NAME} PATHS ${package_libdir} NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) if(DEFINED _original_find_library_suffixes) set(CMAKE_FIND_LIBRARY_SUFFIXES "${_original_find_library_suffixes}") set(CMAKE_FIND_LIBRARY_PREFIXES "${_original_find_library_prefixes}") unset(_original_find_library_suffixes) unset(_original_find_library_prefixes) endif() if(CONAN_FOUND_LIBRARY) message(VERBOSE "Conan: Library ${_LIBRARY_NAME} found ${CONAN_FOUND_LIBRARY}") # Create a micro-target for each lib/a found # Allow only some characters for the target name string(REGEX REPLACE "[^A-Za-z0-9.+_-]" "_" _LIBRARY_NAME ${_LIBRARY_NAME}) set(_LIB_NAME CONAN_LIB::${package_name}_${_LIBRARY_NAME}${config_suffix}) if(is_host_windows AND library_type STREQUAL "SHARED") # Store and reset the variable, so it doesn't leak set(_OLD_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) set(CMAKE_FIND_LIBRARY_SUFFIXES .dll ${CMAKE_FIND_LIBRARY_SUFFIXES}) find_library(CONAN_SHARED_FOUND_LIBRARY NAMES ${_LIBRARY_NAME} PATHS ${package_bindir} NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) set(CMAKE_FIND_LIBRARY_SUFFIXES ${_OLD_CMAKE_FIND_LIBRARY_SUFFIXES}) if(NOT CONAN_SHARED_FOUND_LIBRARY) message(DEBUG "DLL library not found, creating UNKNOWN IMPORTED target, searched for: ${_LIBRARY_NAME}") if(NOT TARGET ${_LIB_NAME}) add_library(${_LIB_NAME} UNKNOWN IMPORTED) endif() set_target_properties(${_LIB_NAME} PROPERTIES IMPORTED_LOCATION${config_suffix} ${CONAN_FOUND_LIBRARY}) else() if(NOT TARGET ${_LIB_NAME}) add_library(${_LIB_NAME} SHARED IMPORTED) endif() set_target_properties(${_LIB_NAME} PROPERTIES IMPORTED_LOCATION${config_suffix} ${CONAN_SHARED_FOUND_LIBRARY}) set_target_properties(${_LIB_NAME} PROPERTIES IMPORTED_IMPLIB${config_suffix} ${CONAN_FOUND_LIBRARY}) message(DEBUG "Found DLL and STATIC at ${CONAN_SHARED_FOUND_LIBRARY}, ${CONAN_FOUND_LIBRARY}") endif() unset(CONAN_SHARED_FOUND_LIBRARY CACHE) else() if(NOT TARGET ${_LIB_NAME}) # library_type can be STATIC, still UNKNOWN (if no package type available in the recipe) or SHARED (but no windows) add_library(${_LIB_NAME} ${library_type} IMPORTED) endif() message(DEBUG "Created target ${_LIB_NAME} ${library_type} IMPORTED") set_target_properties(${_LIB_NAME} PROPERTIES IMPORTED_LOCATION${config_suffix} ${CONAN_FOUND_LIBRARY} IMPORTED_NO_SONAME ${no_soname_mode}) endif() list(APPEND _out_libraries_target ${_LIB_NAME}) message(VERBOSE "Conan: Found: ${CONAN_FOUND_LIBRARY}") else() message(FATAL_ERROR "Library '${_LIBRARY_NAME}' not found in package. If '${_LIBRARY_NAME}' is a system library, declare it with 'cpp_info.system_libs' property") endif() unset(CONAN_FOUND_LIBRARY CACHE) endforeach() # Add the dependencies target for all the imported libraries foreach(_T ${_out_libraries_target}) set_property(TARGET ${_T} APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${deps_target}) endforeach() set(${out_libraries_target} ${_out_libraries_target} PARENT_SCOPE) endfunction() macro(check_build_type_defined) # Check that the -DCMAKE_BUILD_TYPE argument is always present get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(NOT isMultiConfig AND NOT CMAKE_BUILD_TYPE) message(FATAL_ERROR "Please, set the CMAKE_BUILD_TYPE variable when calling to CMake " "adding the '-DCMAKE_BUILD_TYPE=' argument.") endif() endmacro() """) ================================================ FILE: conan/tools/cmake/cmakedeps/templates/target_configuration.py ================================================ import textwrap from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate """ FooTarget-release.cmake """ class TargetConfigurationTemplate(CMakeDepsFileTemplate): @property def filename(self): name = "" if not self.generating_module else "module-" name += "{}-Target-{}.cmake".format(self.file_name, self.cmakedeps.configuration.lower()) return name @property def context(self): deps_targets_names = self.get_deps_targets_names() \ if not self.require.build else [] components_targets_names = self.get_declared_components_targets_names() components_names = [(components_target_name.replace("::", "_"), components_target_name) for components_target_name in components_targets_names] is_win = self.conanfile.settings.get_safe("os") == "Windows" auto_link = self.cmakedeps.get_property("cmake_set_interface_link_directories", self.conanfile, check_type=bool) if auto_link: out = self.cmakedeps._conanfile.output # noqa out.warning("CMakeDeps: cmake_set_interface_link_directories is legacy, not necessary", warn_tag="deprecated") return {"pkg_name": self.pkg_name, "root_target_name": self.root_target_name, "config_suffix": self.config_suffix, "config": self.configuration.upper(), "deps_targets_names": ";".join(deps_targets_names), "components_names": components_names, "configuration": self.cmakedeps.configuration, "set_interface_link_directories": auto_link and is_win} @property def template(self): return textwrap.dedent("""\ # Avoid multiple calls to find_package to append duplicated properties to the targets include_guard() {%- macro comp_var(pkg_name, comp_name, var, config_suffix) -%} {{'${'+pkg_name+'_'+comp_name+'_'+var+config_suffix+'}'}} {%- endmacro -%} {%- macro pkg_var(pkg_name, var, config_suffix) -%} {{'${'+pkg_name+'_'+var+config_suffix+'}'}} {%- endmacro -%} ########### VARIABLES ####################################################################### ############################################################################################# set({{ pkg_name }}_FRAMEWORKS_FOUND{{ config_suffix }} "") # Will be filled later conan_find_apple_frameworks({{ pkg_name }}_FRAMEWORKS_FOUND{{ config_suffix }} "{{ pkg_var(pkg_name, 'FRAMEWORKS', config_suffix) }}" "{{ pkg_var(pkg_name, 'FRAMEWORK_DIRS', config_suffix) }}") set({{ pkg_name }}_LIBRARIES_TARGETS "") # Will be filled later ######## Create an interface target to contain all the dependencies (frameworks, system and conan deps) if(NOT TARGET {{ pkg_name+'_DEPS_TARGET'}}) add_library({{ pkg_name+'_DEPS_TARGET'}} INTERFACE IMPORTED) endif() set_property(TARGET {{ pkg_name + '_DEPS_TARGET'}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES $<$:{{ pkg_var(pkg_name, 'FRAMEWORKS_FOUND', config_suffix) }}> $<$:{{ pkg_var(pkg_name, 'SYSTEM_LIBS', config_suffix) }}> $<$:{{ deps_targets_names }}>) ####### Find the libraries declared in cpp_info.libs, create an IMPORTED target for each one and link the ####### {{pkg_name}}_DEPS_TARGET to all of them conan_package_library_targets("{{ pkg_var(pkg_name, 'LIBS', config_suffix) }}" # libraries "{{ pkg_var(pkg_name, 'LIB_DIRS', config_suffix) }}" # package_libdir "{{ pkg_var(pkg_name, 'BIN_DIRS', config_suffix) }}" # package_bindir "{{ pkg_var(pkg_name, 'LIBRARY_TYPE', config_suffix) }}" "{{ pkg_var(pkg_name, 'IS_HOST_WINDOWS', config_suffix) }}" {{ pkg_name + '_DEPS_TARGET'}} {{ pkg_name }}_LIBRARIES_TARGETS # out_libraries_targets "{{ config_suffix }}" "{{ pkg_name }}" # package_name "{{ pkg_var(pkg_name, 'NO_SONAME_MODE', config_suffix) }}") # soname # FIXME: What is the result of this for multi-config? All configs adding themselves to path? set(CMAKE_MODULE_PATH {{ pkg_var(pkg_name, 'BUILD_DIRS', config_suffix) }} {{ '${' }}CMAKE_MODULE_PATH}) {% if not components_names %} ########## GLOBAL TARGET PROPERTIES {{ configuration }} ######################################## set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES $<$:{{ pkg_var(pkg_name, 'OBJECTS', config_suffix) }}> $<$:{{ pkg_var(pkg_name, 'LIBRARIES_TARGETS', '') }}> ) if("{{ pkg_var(pkg_name, 'LIBS', config_suffix) }}" STREQUAL "") # If the package is not declaring any "cpp_info.libs" the package deps, system libs, # frameworks etc are not linked to the imported targets and we need to do it to the # global target set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES {{pkg_name}}_DEPS_TARGET) endif() set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_LINK_OPTIONS $<$:{{ pkg_var(pkg_name, 'LINKER_FLAGS', config_suffix) }}>) set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES $<$:{{ pkg_var(pkg_name, 'INCLUDE_DIRS', config_suffix) }}>) # Necessary to find LINK shared libraries in Linux set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_LINK_DIRECTORIES $<$:{{ pkg_var(pkg_name, 'LIB_DIRS', config_suffix) }}>) set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS $<$:{{ pkg_var(pkg_name, 'COMPILE_DEFINITIONS', config_suffix) }}>) set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS $<$:{{ pkg_var(pkg_name, 'COMPILE_OPTIONS', config_suffix) }}>) {%- else %} ########## COMPONENTS TARGET PROPERTIES {{ configuration }} ######################################## {%- for comp_variable_name, comp_target_name in components_names %} ########## COMPONENT {{ comp_target_name }} ############# set({{ pkg_name }}_{{ comp_variable_name }}_FRAMEWORKS_FOUND{{ config_suffix }} "") conan_find_apple_frameworks({{ pkg_name }}_{{ comp_variable_name }}_FRAMEWORKS_FOUND{{ config_suffix }} "{{ comp_var(pkg_name, comp_variable_name, 'FRAMEWORKS', config_suffix) }}" "{{ comp_var(pkg_name, comp_variable_name, 'FRAMEWORK_DIRS', config_suffix) }}") set({{ pkg_name }}_{{ comp_variable_name }}_LIBRARIES_TARGETS "") ######## Create an interface target to contain all the dependencies (frameworks, system and conan deps) if(NOT TARGET {{ pkg_name + '_' + comp_variable_name + '_DEPS_TARGET'}}) add_library({{ pkg_name + '_' + comp_variable_name + '_DEPS_TARGET'}} INTERFACE IMPORTED) endif() set_property(TARGET {{ pkg_name + '_' + comp_variable_name + '_DEPS_TARGET'}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES $<$:{{ comp_var(pkg_name, comp_variable_name, 'FRAMEWORKS_FOUND', config_suffix) }}> $<$:{{ comp_var(pkg_name, comp_variable_name, 'SYSTEM_LIBS', config_suffix) }}> $<$:{{ comp_var(pkg_name, comp_variable_name, 'DEPENDENCIES', config_suffix) }}> ) ####### Find the libraries declared in cpp_info.component["xxx"].libs, ####### create an IMPORTED target for each one and link the '{{pkg_name}}_{{comp_variable_name}}_DEPS_TARGET' to all of them conan_package_library_targets("{{ comp_var(pkg_name, comp_variable_name, 'LIBS', config_suffix) }}" "{{ comp_var(pkg_name, comp_variable_name, 'LIB_DIRS', config_suffix) }}" "{{ comp_var(pkg_name, comp_variable_name, 'BIN_DIRS', config_suffix) }}" # package_bindir "{{ comp_var(pkg_name, comp_variable_name, 'LIBRARY_TYPE', config_suffix) }}" "{{ comp_var(pkg_name, comp_variable_name, 'IS_HOST_WINDOWS', config_suffix) }}" {{ pkg_name + '_' + comp_variable_name + '_DEPS_TARGET'}} {{ pkg_name + '_' + comp_variable_name + '_LIBRARIES_TARGETS'}} "{{ config_suffix }}" "{{ pkg_name }}_{{ comp_variable_name }}" "{{ comp_var(pkg_name, comp_variable_name, 'NO_SONAME_MODE', config_suffix) }}") ########## TARGET PROPERTIES ##################################### set_property(TARGET {{comp_target_name}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES $<$:{{ comp_var(pkg_name, comp_variable_name, 'OBJECTS', config_suffix) }}> $<$:{{ comp_var(pkg_name, comp_variable_name, 'LIBRARIES_TARGETS', '') }}> ) if("{{ comp_var(pkg_name, comp_variable_name, 'LIBS', config_suffix) }}" STREQUAL "") # If the component is not declaring any "cpp_info.components['foo'].libs" the system, frameworks etc are not # linked to the imported targets and we need to do it to the global target set_property(TARGET {{comp_target_name}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES {{pkg_name}}_{{comp_variable_name}}_DEPS_TARGET) endif() set_property(TARGET {{ comp_target_name }} APPEND PROPERTY INTERFACE_LINK_OPTIONS $<$:{{ comp_var(pkg_name, comp_variable_name, 'LINKER_FLAGS', config_suffix) }}>) set_property(TARGET {{ comp_target_name }} APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES $<$:{{ comp_var(pkg_name, comp_variable_name, 'INCLUDE_DIRS', config_suffix) }}>) set_property(TARGET {{ comp_target_name }} APPEND PROPERTY INTERFACE_LINK_DIRECTORIES $<$:{{ comp_var(pkg_name, comp_variable_name, 'LIB_DIRS', config_suffix) }}>) set_property(TARGET {{ comp_target_name }} APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS $<$:{{ comp_var(pkg_name, comp_variable_name, 'COMPILE_DEFINITIONS', config_suffix) }}>) set_property(TARGET {{ comp_target_name }} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS $<$:{{ comp_var(pkg_name, comp_variable_name, 'COMPILE_OPTIONS', config_suffix) }}>) {%endfor %} ########## AGGREGATED GLOBAL TARGET WITH THE COMPONENTS ##################### {%- for comp_variable_name, comp_target_name in components_names %} set_property(TARGET {{root_target_name}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES {{ comp_target_name }}) {%- endfor %} {%- endif %} ########## For the modules (FindXXX) set({{ pkg_name }}_LIBRARIES{{ config_suffix }} {{root_target_name}}) """) def get_declared_components_targets_names(self): """Returns a list of component_name""" ret = [] sorted_comps = self.conanfile.cpp_info.get_sorted_components() for comp_name, comp in sorted_comps.items(): ret.append(self.get_component_alias(self.conanfile, comp_name)) ret.reverse() return ret def get_deps_targets_names(self): """ - [{foo}::{bar}, ] of the required """ ret = [] # Get a list of dependencies target names # Declared cppinfo.requires or .components[].requires transitive_reqs = self.cmakedeps.get_transitive_requires(self.conanfile) if self.conanfile.cpp_info.required_components: for dep_name, component_name in self.conanfile.cpp_info.required_components: try: # if not dep_name, it is internal, from current self.conanfile req = transitive_reqs[dep_name] if dep_name is not None else self.conanfile except KeyError: # if it raises it means the required component is not in the direct_host # dependencies, maybe it has been filtered out by traits => Skip pass else: component_name = self.get_component_alias(req, component_name) ret.append(component_name) elif transitive_reqs: # Regular external "conanfile.requires" declared, not cpp_info requires ret = [self.get_root_target_name(r) for r in transitive_reqs.values()] return ret ================================================ FILE: conan/tools/cmake/cmakedeps/templates/target_data.py ================================================ import os import textwrap from conan.tools.cmake.cmakedeps import FIND_MODE_NONE, FIND_MODE_CONFIG, FIND_MODE_MODULE, \ FIND_MODE_BOTH from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate from conan.errors import ConanException from conan.internal.api.install.generators import relativize_path from conan.tools.cmake.utils import cmake_escape_value """ foo-release-x86_64-data.cmake """ class ConfigDataTemplate(CMakeDepsFileTemplate): @property def filename(self): data_fname = "" if not self.generating_module else "module-" data_fname += "{}-{}".format(self.file_name, self.configuration.lower()) if self.arch: data_fname += "-{}".format(self.arch) data_fname += "-data.cmake" # https://github.com/conan-io/conan/issues/17009 return data_fname.replace("|", "_") @property def _build_modules_activated(self): if self.require.build: return self.conanfile.ref.name in self.cmakedeps.build_context_build_modules else: return self.conanfile.ref.name not in self.cmakedeps.build_context_build_modules @property def context(self): global_cpp = self._get_global_cpp_cmake() if not self._build_modules_activated: global_cpp.build_modules_paths = "" components = self._get_required_components_cpp() # using the target names to name components, may change in the future? components_names = " ".join([components_target_name for components_target_name, _ in reversed(components)]) components_cpp = [(cmake_target_name.replace("::", "_"), cmake_target_name, cpp) for cmake_target_name, cpp in components] # For the build requires, we don't care about the transitive (only runtime for the br) # so as the xxx-conf.cmake files won't be generated, don't include them as find_dependency # This is because in Conan 2.0 model, only the pure tools like CMake will be build_requires # for example a framework test won't be a build require but a "test/not public" require. dependency_filenames = self._get_dependency_filenames() # Get the nodes that have the property cmake_find_mode=None (no files to generate) dependency_find_modes = self._get_dependencies_find_modes() # Make the root_folder relative to the generated xxx-data.cmake file root_folder = self._root_folder root_folder = root_folder.replace('\\', '/').replace('$', '\\$').replace('"', '\\"') # it is the relative path of the caller, not the dependency root_folder = relativize_path(root_folder, self.cmakedeps._conanfile, "${CMAKE_CURRENT_LIST_DIR}") return {"global_cpp": global_cpp, "has_components": self.conanfile.cpp_info.has_components, "pkg_name": self.pkg_name, "file_name": self.file_name, "package_folder": root_folder, "config_suffix": self.config_suffix, "components_names": components_names, "components_cpp": components_cpp, "dependency_filenames": " ".join(dependency_filenames), "dependency_find_modes": dependency_find_modes} @property def cmake_package_type(self): return {"shared-library": "SHARED", "static-library": "STATIC"}.get(str(self.conanfile.package_type), "UNKNOWN") @property def is_host_windows(self): # to account for all WindowsStore, WindowsCE and Windows OS in settings return "Windows" in self.conanfile.settings.get_safe("os", "") @property def template(self): # This will be at: XXX-release-data.cmake ret = textwrap.dedent("""\ {%- macro comp_var(pkg_name, comp_name, var, config_suffix) -%} {{'${'+pkg_name+'_'+comp_name+'_'+var+config_suffix+'}'}} {%- endmacro -%} {%- macro pkg_var(pkg_name, var, config_suffix) -%} {{'${'+pkg_name+'_'+var+config_suffix+'}'}} {%- endmacro -%} ########### AGGREGATED COMPONENTS AND DEPENDENCIES FOR THE MULTI CONFIG ##################### ############################################################################################# {% if components_names %} list(APPEND {{ pkg_name }}_COMPONENT_NAMES {{ components_names }}) list(REMOVE_DUPLICATES {{ pkg_name }}_COMPONENT_NAMES) {% else %} set({{ pkg_name }}_COMPONENT_NAMES "") {% endif %} if(DEFINED {{ pkg_name }}_FIND_DEPENDENCY_NAMES) list(APPEND {{ pkg_name }}_FIND_DEPENDENCY_NAMES {{ dependency_filenames }}) list(REMOVE_DUPLICATES {{ pkg_name }}_FIND_DEPENDENCY_NAMES) else() set({{ pkg_name }}_FIND_DEPENDENCY_NAMES {{ dependency_filenames }}) endif() {% for dep_name, mode in dependency_find_modes.items() %} set({{ dep_name }}_FIND_MODE "{{ mode }}") {% endfor %} ########### VARIABLES ####################################################################### ############################################################################################# set({{ pkg_name }}_PACKAGE_FOLDER{{ config_suffix }} "{{ package_folder }}") set({{ pkg_name }}_BUILD_MODULES_PATHS{{ config_suffix }} {{ global_cpp.build_modules_paths }}) set({{ pkg_name }}_INCLUDE_DIRS{{ config_suffix }} {{ global_cpp.include_paths }}) set({{ pkg_name }}_RES_DIRS{{ config_suffix }} {{ global_cpp.res_paths }}) set({{ pkg_name }}_DEFINITIONS{{ config_suffix }} {{ global_cpp.defines }}) set({{ pkg_name }}_SHARED_LINK_FLAGS{{ config_suffix }} {{ global_cpp.sharedlinkflags_list }}) set({{ pkg_name }}_EXE_LINK_FLAGS{{ config_suffix }} {{ global_cpp.exelinkflags_list }}) set({{ pkg_name }}_OBJECTS{{ config_suffix }} {{ global_cpp.objects_list }}) set({{ pkg_name }}_COMPILE_DEFINITIONS{{ config_suffix }} {{ global_cpp.compile_definitions }}) set({{ pkg_name }}_COMPILE_OPTIONS_C{{ config_suffix }} {{ global_cpp.cflags_list }}) set({{ pkg_name }}_COMPILE_OPTIONS_CXX{{ config_suffix }} {{ global_cpp.cxxflags_list}}) set({{ pkg_name }}_LIB_DIRS{{ config_suffix }} {{ global_cpp.lib_paths }}) set({{ pkg_name }}_BIN_DIRS{{ config_suffix }} {{ global_cpp.bin_paths }}) set({{ pkg_name }}_LIBRARY_TYPE{{ config_suffix }} {{ global_cpp.library_type }}) set({{ pkg_name }}_IS_HOST_WINDOWS{{ config_suffix }} {{ global_cpp.is_host_windows }}) set({{ pkg_name }}_LIBS{{ config_suffix }} {{ global_cpp.libs }}) set({{ pkg_name }}_SYSTEM_LIBS{{ config_suffix }} {{ global_cpp.system_libs }}) set({{ pkg_name }}_FRAMEWORK_DIRS{{ config_suffix }} {{ global_cpp.framework_paths }}) set({{ pkg_name }}_FRAMEWORKS{{ config_suffix }} {{ global_cpp.frameworks }}) set({{ pkg_name }}_BUILD_DIRS{{ config_suffix }} {{ global_cpp.build_paths }}) set({{ pkg_name }}_NO_SONAME_MODE{{ config_suffix }} {{ global_cpp.no_soname }}) # COMPOUND VARIABLES set({{ pkg_name }}_COMPILE_OPTIONS{{ config_suffix }} "$<$:{{ pkg_var(pkg_name, 'COMPILE_OPTIONS_CXX', config_suffix) }}>" "$<$:{{ pkg_var(pkg_name, 'COMPILE_OPTIONS_C', config_suffix) }}>") set({{ pkg_name }}_LINKER_FLAGS{{ config_suffix }} "$<$,SHARED_LIBRARY>:{{ pkg_var(pkg_name, 'SHARED_LINK_FLAGS', config_suffix) }}>" "$<$,MODULE_LIBRARY>:{{ pkg_var(pkg_name, 'SHARED_LINK_FLAGS', config_suffix) }}>" "$<$,EXECUTABLE>:{{ pkg_var(pkg_name, 'EXE_LINK_FLAGS', config_suffix) }}>") set({{ pkg_name }}_COMPONENTS{{ config_suffix }} {{ components_names }}) {%- for comp_variable_name, comp_target_name, cpp in components_cpp %} ########### COMPONENT {{ comp_target_name }} VARIABLES ############################################ set({{ pkg_name }}_{{ comp_variable_name }}_INCLUDE_DIRS{{ config_suffix }} {{ cpp.include_paths }}) set({{ pkg_name }}_{{ comp_variable_name }}_LIB_DIRS{{ config_suffix }} {{ cpp.lib_paths }}) set({{ pkg_name }}_{{ comp_variable_name }}_BIN_DIRS{{ config_suffix }} {{ cpp.bin_paths }}) set({{ pkg_name }}_{{ comp_variable_name }}_LIBRARY_TYPE{{ config_suffix }} {{ cpp.library_type }}) set({{ pkg_name }}_{{ comp_variable_name }}_IS_HOST_WINDOWS{{ config_suffix }} {{ cpp.is_host_windows }}) set({{ pkg_name }}_{{ comp_variable_name }}_RES_DIRS{{ config_suffix }} {{ cpp.res_paths }}) set({{ pkg_name }}_{{ comp_variable_name }}_DEFINITIONS{{ config_suffix }} {{ cpp.defines }}) set({{ pkg_name }}_{{ comp_variable_name }}_OBJECTS{{ config_suffix }} {{ cpp.objects_list }}) set({{ pkg_name }}_{{ comp_variable_name }}_COMPILE_DEFINITIONS{{ config_suffix }} {{ cpp.compile_definitions }}) set({{ pkg_name }}_{{ comp_variable_name }}_COMPILE_OPTIONS_C{{ config_suffix }} "{{ cpp.cflags_list }}") set({{ pkg_name }}_{{ comp_variable_name }}_COMPILE_OPTIONS_CXX{{ config_suffix }} "{{ cpp.cxxflags_list }}") set({{ pkg_name }}_{{ comp_variable_name }}_LIBS{{ config_suffix }} {{ cpp.libs }}) set({{ pkg_name }}_{{ comp_variable_name }}_SYSTEM_LIBS{{ config_suffix }} {{ cpp.system_libs }}) set({{ pkg_name }}_{{ comp_variable_name }}_FRAMEWORK_DIRS{{ config_suffix }} {{ cpp.framework_paths }}) set({{ pkg_name }}_{{ comp_variable_name }}_FRAMEWORKS{{ config_suffix }} {{ cpp.frameworks }}) set({{ pkg_name }}_{{ comp_variable_name }}_DEPENDENCIES{{ config_suffix }} {{ cpp.public_deps }}) set({{ pkg_name }}_{{ comp_variable_name }}_SHARED_LINK_FLAGS{{ config_suffix }} {{ cpp.sharedlinkflags_list }}) set({{ pkg_name }}_{{ comp_variable_name }}_EXE_LINK_FLAGS{{ config_suffix }} {{ cpp.exelinkflags_list }}) set({{ pkg_name }}_{{ comp_variable_name }}_NO_SONAME_MODE{{ config_suffix }} {{ cpp.no_soname }}) # COMPOUND VARIABLES set({{ pkg_name }}_{{ comp_variable_name }}_LINKER_FLAGS{{ config_suffix }} $<$,SHARED_LIBRARY>:{{ comp_var(pkg_name, comp_variable_name, 'SHARED_LINK_FLAGS', config_suffix) }}> $<$,MODULE_LIBRARY>:{{ comp_var(pkg_name, comp_variable_name, 'SHARED_LINK_FLAGS', config_suffix) }}> $<$,EXECUTABLE>:{{ comp_var(pkg_name, comp_variable_name, 'EXE_LINK_FLAGS', config_suffix) }}> ) set({{ pkg_name }}_{{ comp_variable_name }}_COMPILE_OPTIONS{{ config_suffix }} "$<$:{{ comp_var(pkg_name, comp_variable_name, 'COMPILE_OPTIONS_CXX', config_suffix) }}>" "$<$:{{ comp_var(pkg_name, comp_variable_name, 'COMPILE_OPTIONS_C', config_suffix) }}>") {%- endfor %} """) return ret def _get_global_cpp_cmake(self): global_cppinfo = self.conanfile.cpp_info.aggregated_components() pfolder_var_name = "{}_PACKAGE_FOLDER{}".format(self.pkg_name, self.config_suffix) return _TargetDataContext(global_cppinfo, pfolder_var_name, self._root_folder, self.require, self.cmake_package_type, self.is_host_windows, self.conanfile, self.cmakedeps) @property def _root_folder(self): return self.conanfile.recipe_folder if self.conanfile.package_folder is None \ else self.conanfile.package_folder def _get_required_components_cpp(self): """Returns a list of (component_name, DepsCppCMake)""" ret = [] sorted_comps = self.conanfile.cpp_info.get_sorted_components() pfolder_var_name = "{}_PACKAGE_FOLDER{}".format(self.pkg_name, self.config_suffix) transitive_requires = self.cmakedeps.get_transitive_requires(self.conanfile) for comp_name, comp in sorted_comps.items(): deps_cpp_cmake = _TargetDataContext(comp, pfolder_var_name, self._root_folder, self.require, self.cmake_package_type, self.is_host_windows, self.conanfile, self.cmakedeps, comp_name) public_comp_deps = [] for required_pkg, required_comp in comp.parsed_requires(): if required_pkg is not None: # Points to a component of a different package try: # Make sure the declared dependency is at least in the recipe requires self.conanfile.dependencies[required_pkg] except KeyError: raise ConanException(f"{self.conanfile}: component '{comp_name}' required " f"'{required_pkg}::{required_comp}', " f"but '{required_pkg}' is not a direct dependency") try: req = transitive_requires[required_pkg] except KeyError: # The transitive dep might have been skipped pass else: public_comp_deps.append(self.get_component_alias(req, required_comp)) else: # Points to a component of same package public_comp_deps.append(self.get_component_alias(self.conanfile, required_comp)) deps_cpp_cmake.public_deps = " ".join(public_comp_deps) component_target_name = self.get_component_alias(self.conanfile, comp_name) ret.append((component_target_name, deps_cpp_cmake)) ret.reverse() return ret def _get_dependency_filenames(self): if self.require.build: return [] transitive_reqs = self.cmakedeps.get_transitive_requires(self.conanfile) # Previously it was filtering here components, but not clear why the file dependency # should be skipped if components are not being required, why would it declare a # dependency to it? ret = [self.cmakedeps.get_cmake_package_name(r, self.generating_module) for r in transitive_reqs.values()] return ret def _get_dependencies_find_modes(self): ret = {} if self.require.build: return ret deps = self.cmakedeps.get_transitive_requires(self.conanfile) for dep in deps.values(): dep_file_name = self.cmakedeps.get_cmake_package_name(dep, self.generating_module) find_mode = self.cmakedeps.get_find_mode(dep) default_value = "NO_MODULE" if not self.generating_module else "MODULE" values = { FIND_MODE_NONE: "", FIND_MODE_CONFIG: "NO_MODULE", FIND_MODE_MODULE: "MODULE", # When the dependency is "both" or not defined, we use the one is forced # by self.find_module_mode (creating modules files-> modules, config -> config) FIND_MODE_BOTH: default_value, None: default_value} ret[dep_file_name] = values[find_mode] return ret class _TargetDataContext: def __init__(self, cpp_info, pfolder_var_name, package_folder, require, library_type, is_host_windows, conanfile, cmakedeps, comp_name=None): def join_paths(paths): """ Paths are doubled quoted, and escaped (but spaces) e.g: set(LIBFOO_INCLUDE_DIRS "/path/to/included/dir" "/path/to/included/dir2") """ ret = [] for p in paths: assert os.path.isabs(p), "{} is not absolute".format(p) # Trying to use a ${mypkg_PACKAGE_FOLDER}/include path instead of full if p.startswith(package_folder): # Prepend the {{ pkg_name }}_PACKAGE_FOLDER{{ config_suffix }} rel = p[len(package_folder):] rel = rel.replace('\\', '/').replace('$', '\\$').replace('"', '\\"').lstrip("/") norm_path = ("${%s}/%s" % (pfolder_var_name, rel)) else: norm_path = p.replace('\\', '/').replace('$', '\\$').replace('"', '\\"') ret.append('"{}"'.format(norm_path)) return "\n\t\t\t".join(ret) def join_flags(separator, values): # Flags have to be escaped ret = separator.join(cmake_escape_value(v) for v in values) return ret def join_defines(values, prefix=""): # Defines have to be escaped, included spaces return "\n\t\t\t".join('"%s%s"' % (prefix, cmake_escape_value(v)) for v in values) self.include_paths = join_paths(cpp_info.includedirs) self.lib_paths = join_paths(cpp_info.libdirs) self.res_paths = join_paths(cpp_info.resdirs) self.bin_paths = join_paths(cpp_info.bindirs) self.build_paths = join_paths(cpp_info.builddirs) self.framework_paths = join_paths(cpp_info.frameworkdirs) self.libs = join_flags(" ", cpp_info.libs) self.system_libs = join_flags(" ", cpp_info.system_libs) self.frameworks = join_flags(" ", cpp_info.frameworks) self.defines = join_defines(cpp_info.defines, "-D") self.compile_definitions = join_defines(cpp_info.defines) self.library_type = library_type self.is_host_windows = "1" if is_host_windows else "0" # For modern CMake targets we need to prepare a list to not # loose the elements in the list by replacing " " with ";". Example "-framework Foundation" # Issue: #1251 self.cxxflags_list = join_flags(";", cpp_info.cxxflags) self.cflags_list = join_flags(";", cpp_info.cflags) # linker flags without magic: trying to mess with - and / => # https://github.com/conan-io/conan/issues/8811 # frameworks should be declared with cppinfo.frameworks not "-framework Foundation" self.sharedlinkflags_list = '"{}"'.format(join_flags(";", cpp_info.sharedlinkflags)) \ if cpp_info.sharedlinkflags else '' self.exelinkflags_list = '"{}"'.format(join_flags(";", cpp_info.exelinkflags)) \ if cpp_info.exelinkflags else '' self.objects_list = join_paths(cpp_info.objects) # traits logic if require and not require.headers: self.include_paths = "" if require and not require.libs: # self.lib_paths = "" IMPORTANT! LINKERS IN LINUX FOR SHARED MIGHT NEED IT EVEN IF # NOT REALLY LINKING LIB self.libs = "" if cpp_info.frameworkdirs: # Only invalidate for in-package frameworks # FIXME: The mix of in-package frameworks + system ones is broken self.frameworks = "" if require and not require.libs and not require.headers: self.defines = "" self.compile_definitions = "" self.cxxflags_list = "" self.cflags_list = "" self.sharedlinkflags_list = "" self.exelinkflags_list = "" self.objects_list = "" if require and not require.run: self.bin_paths = "" build_modules = cmakedeps.get_property("cmake_build_modules", conanfile, check_type=list) or [] self.build_modules_paths = join_paths(build_modules) # SONAME flag only makes sense for SHARED libraries nosoname = cmakedeps.get_property("nosoname", conanfile, comp_name, check_type=bool) self.no_soname = str((nosoname if self.library_type == "SHARED" else False) or False).upper() ================================================ FILE: conan/tools/cmake/cmakedeps/templates/targets.py ================================================ import textwrap from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate """ FooTargets.cmake """ class TargetsTemplate(CMakeDepsFileTemplate): @property def filename(self): name = "" if not self.generating_module else "module-" name += self.file_name + "Targets.cmake" return name @property def context(self): data_pattern = "${CMAKE_CURRENT_LIST_DIR}/" if not self.generating_module else "${CMAKE_CURRENT_LIST_DIR}/module-" data_pattern += "{}-*-data.cmake".format(self.file_name) target_pattern = "" if not self.generating_module else "module-" target_pattern += "{}-Target-*.cmake".format(self.file_name) cmake_target_aliases = self.cmakedeps.get_property("cmake_target_aliases", self.conanfile, check_type=list) or dict() target = self.root_target_name cmake_target_aliases = {alias: target for alias in cmake_target_aliases} cmake_component_target_aliases = dict() for comp_name in self.conanfile.cpp_info.components: if comp_name is not None: aliases = self.cmakedeps.get_property("cmake_target_aliases", self.conanfile, comp_name=comp_name, check_type=list) or dict() target = self.get_component_alias(self.conanfile, comp_name) cmake_component_target_aliases[comp_name] = {alias: target for alias in aliases} ret = {"pkg_name": self.pkg_name, "root_target_name": self.root_target_name, "file_name": self.file_name, "data_pattern": data_pattern, "target_pattern": target_pattern, "cmake_target_aliases": cmake_target_aliases, "cmake_component_target_aliases": cmake_component_target_aliases} return ret @property def template(self): return textwrap.dedent("""\ # Load the debug and release variables file(GLOB DATA_FILES "{{data_pattern}}") foreach(f ${DATA_FILES}) include(${f}) endforeach() # Create the targets for all the components foreach(_COMPONENT {{ '${' + pkg_name + '_COMPONENT_NAMES' + '}' }} ) if(NOT TARGET ${_COMPONENT}) add_library(${_COMPONENT} INTERFACE IMPORTED) message({% raw %}${{% endraw %}{{ file_name }}_MESSAGE_MODE} "Conan: Component target declared '${_COMPONENT}'") endif() endforeach() if(NOT TARGET {{ root_target_name }}) add_library({{ root_target_name }} INTERFACE IMPORTED) message({% raw %}${{% endraw %}{{ file_name }}_MESSAGE_MODE} "Conan: Target declared '{{ root_target_name }}'") endif() {%- for alias, target in cmake_target_aliases.items() %} if(NOT TARGET {{alias}}) add_library({{alias}} INTERFACE IMPORTED) set_property(TARGET {{ alias }} PROPERTY INTERFACE_LINK_LIBRARIES {{target}}) endif() {%- endfor %} {%- for comp_name, component_aliases in cmake_component_target_aliases.items() %} {%- for alias, target in component_aliases.items() %} if(NOT TARGET {{alias}}) add_library({{alias}} INTERFACE IMPORTED) set_property(TARGET {{ alias }} PROPERTY INTERFACE_LINK_LIBRARIES {{target}}) endif() {%- endfor %} {%- endfor %} # Load the debug and release library finders file(GLOB CONFIG_FILES "${CMAKE_CURRENT_LIST_DIR}/{{ target_pattern }}") foreach(f ${CONFIG_FILES}) include(${f}) endforeach() """) ================================================ FILE: conan/tools/cmake/layout.py ================================================ import os import tempfile from conan.internal.graph.graph import RECIPE_CONSUMER, RECIPE_EDITABLE from conan.errors import ConanException def cmake_layout(conanfile, generator=None, src_folder=".", build_folder="build"): """ :param conanfile: The current recipe object. Always use ``self``. :param generator: Allow defining the CMake generator. In most cases it doesn't need to be passed, as it will get the value from the configuration ``tools.cmake.cmaketoolchain:generator``, or it will automatically deduce the generator from the ``settings`` :param src_folder: Value for ``conanfile.folders.source``, change it if your source code (and CMakeLists.txt) is in a subfolder. :param build_folder: Specify the name of the "base" build folder. The default is "build", but if that folder name is used by the project, a different one can be defined """ gen = conanfile.conf.get("tools.cmake.cmaketoolchain:generator", default=generator) if gen: multi = "Visual" in gen or "Xcode" in gen or "Multi-Config" in gen else: compiler = conanfile.settings.get_safe("compiler") if compiler == "msvc": multi = True else: multi = False subproject = conanfile.folders.subproject conanfile.folders.source = src_folder if not subproject else os.path.join(subproject, src_folder) try: build_type = str(conanfile.settings.build_type) except ConanException: raise ConanException("'build_type' setting not defined, it is necessary for cmake_layout()") try: # TODO: Refactor this repeated pattern to deduce "is-consumer" if conanfile._conan_node.recipe in (RECIPE_CONSUMER, RECIPE_EDITABLE): folder = "test_folder" if conanfile.tested_reference_str else "build_folder" build_folder = conanfile.conf.get(f"tools.cmake.cmake_layout:{folder}") or build_folder if build_folder == "$TMP" and folder == "test_folder": build_folder = tempfile.mkdtemp() except AttributeError: pass build_folder = build_folder if not subproject else os.path.join(subproject, build_folder) config_build_folder, user_defined_build = get_build_folder_custom_vars(conanfile) if config_build_folder: build_folder = os.path.join(build_folder, config_build_folder) if not multi and not user_defined_build: build_folder = os.path.join(build_folder, build_type) conanfile.folders.build = build_folder conanfile.folders.generators = os.path.join(conanfile.folders.build, "generators") conanfile.cpp.source.includedirs = ["include"] if multi: conanfile.cpp.build.libdirs = ["{}".format(build_type)] conanfile.cpp.build.bindirs = ["{}".format(build_type)] else: conanfile.cpp.build.libdirs = ["."] conanfile.cpp.build.bindirs = ["."] def get_build_folder_custom_vars(conanfile): conanfile_vars = conanfile.folders.build_folder_vars build_vars = conanfile.conf.get("tools.cmake.cmake_layout:build_folder_vars", check_type=list) if conanfile.tested_reference_str: if build_vars is None: # The user can define conf build_folder_vars = [] for no vars build_vars = conanfile_vars or \ ["settings.compiler", "settings.compiler.version", "settings.arch", "settings.compiler.cppstd", "settings.build_type", "options.shared"] else: try: is_consumer = conanfile._conan_node.recipe in (RECIPE_CONSUMER, RECIPE_EDITABLE) except AttributeError: is_consumer = False if is_consumer: if build_vars is None: build_vars = conanfile_vars or [] else: build_vars = conanfile_vars or [] ret = [] for s in build_vars: group, var = s.split(".", 1) tmp = None if group == "settings": tmp = conanfile.settings.get_safe(var) if tmp and var == "arch": # handle Apple multi-arch/universal binaries tmp = tmp.replace("|", "_") elif group == "options": value = conanfile.options.get_safe(var) if value is not None: if var == "shared": tmp = "shared" if value else "static" else: tmp = "{}_{}".format(var, value) elif group == "self": tmp = getattr(conanfile, var, None) elif group == "const": tmp = var else: raise ConanException("Invalid 'tools.cmake.cmake_layout:build_folder_vars' value, it has" f" to start with 'settings.', 'options.', 'self.' or 'const.': {s}") if tmp: ret.append(tmp.lower()) user_defined_build = "settings.build_type" in build_vars return "-".join(ret), user_defined_build ================================================ FILE: conan/tools/cmake/presets.py ================================================ import json import os import platform import textwrap from conan.api.output import ConanOutput, Color from conan.tools.cmake.layout import get_build_folder_custom_vars from conan.tools.cmake.toolchain.blocks import GenericSystemBlock from conan.tools.cmake.utils import is_multi_configuration from conan.tools.build import build_jobs from conan.tools.microsoft import is_msvc from conan.internal.graph.graph import RECIPE_CONSUMER from conan.errors import ConanException from conan.internal.util.files import save, load def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables, user_presets_path=None, preset_prefix=None, buildenv=None, runenv=None, cmake_executable=None, absolute_paths=None): preset_path, preset_data = _CMakePresets.generate(conanfile, toolchain_file, generator, cache_variables, preset_prefix, buildenv, runenv, cmake_executable, absolute_paths) _IncludingPresets.generate(conanfile, preset_path, user_presets_path, preset_prefix, preset_data, absolute_paths) class _CMakePresets: """ Conan generated main CMakePresets.json inside the generators_folder """ @staticmethod def generate(conanfile, toolchain_file, generator, cache_variables, preset_prefix, buildenv, runenv, cmake_executable, absolute_paths): toolchain_file = os.path.abspath(os.path.join(conanfile.generators_folder, toolchain_file)) if not absolute_paths: try: # Make it relative to the build dir if possible toolchain_file = os.path.relpath(toolchain_file, conanfile.build_folder) except ValueError: pass cache_variables = cache_variables or {} if platform.system() == "Windows" and generator == "MinGW Makefiles": if "CMAKE_SH" not in cache_variables: cache_variables["CMAKE_SH"] = "CMAKE_SH-NOTFOUND" cmake_make_program = conanfile.conf.get("tools.gnu:make_program", default=cache_variables.get("CMAKE_MAKE_PROGRAM")) if cmake_make_program: cmake_make_program = cmake_make_program.replace("\\", "/") cache_variables["CMAKE_MAKE_PROGRAM"] = cmake_make_program if "CMAKE_POLICY_DEFAULT_CMP0091" not in cache_variables: cache_variables["CMAKE_POLICY_DEFAULT_CMP0091"] = "NEW" if "BUILD_TESTING" not in cache_variables: if conanfile.conf.get("tools.build:skip_test", check_type=bool): cache_variables["BUILD_TESTING"] = "OFF" preset_path = os.path.join(conanfile.generators_folder, "CMakePresets.json") multiconfig = is_multi_configuration(generator) if os.path.exists(preset_path): data = json.loads(load(preset_path)) if "conan" not in data.get("vendor", {}): # The file is not ours, we cannot overwrite it raise ConanException("Existing CMakePresets.json not generated by Conan cannot be " "overwritten.\nUse --output-folder or define a 'layout' to " "avoid collision with your CMakePresets.json") if os.path.exists(preset_path) and multiconfig: data = json.loads(load(preset_path)) build_preset = _CMakePresets._build_preset_fields(conanfile, multiconfig, preset_prefix) test_preset = _CMakePresets._test_preset_fields(conanfile, multiconfig, preset_prefix, runenv) _CMakePresets._insert_preset(data, "buildPresets", build_preset) _CMakePresets._insert_preset(data, "testPresets", test_preset) configure_preset = _CMakePresets._configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig, preset_prefix, buildenv, cmake_executable) # Conan generated presets should have only 1 configurePreset, no more, overwrite it data["configurePresets"] = [configure_preset] else: data = _CMakePresets._contents(conanfile, toolchain_file, cache_variables, generator, preset_prefix, buildenv, runenv, cmake_executable) preset_content = json.dumps(data, indent=4) save(preset_path, preset_content) ConanOutput(str(conanfile)).info(f"CMakeToolchain generated: {preset_path}") return preset_path, data @staticmethod def _insert_preset(data, preset_type, preset): presets = data.setdefault(preset_type, []) preset_name = preset["name"] positions = [index for index, p in enumerate(presets) if p["name"] == preset_name] if positions: data[preset_type][positions[0]] = preset else: data[preset_type].append(preset) @staticmethod def _contents(conanfile, toolchain_file, cache_variables, generator, preset_prefix, buildenv, runenv, cmake_executable): """ Contents for the CMakePresets.json It uses schema version 3 unless it is forced to 2 """ multiconfig = is_multi_configuration(generator) conf = _CMakePresets._configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig, preset_prefix, buildenv, cmake_executable) build = _CMakePresets._build_preset_fields(conanfile, multiconfig, preset_prefix) test = _CMakePresets._test_preset_fields(conanfile, multiconfig, preset_prefix, runenv) ret = {"version": 3, "vendor": {"conan": {}}, "cmakeMinimumRequired": {"major": 3, "minor": 15, "patch": 0}, "configurePresets": [conf], "buildPresets": [build], "testPresets": [test] } return ret @staticmethod def _configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig, preset_prefix, buildenv, cmake_executable): build_type = conanfile.settings.get_safe("build_type") name = _CMakePresets._configure_preset_name(conanfile, multiconfig) if preset_prefix: name = f"{preset_prefix}-{name}" if not multiconfig and build_type: cache_variables["CMAKE_BUILD_TYPE"] = build_type ret = { "name": name, "displayName": "'{}' config".format(name), "description": "'{}' configure using '{}' generator".format(name, generator), "generator": generator, "cacheVariables": cache_variables, } if buildenv: ret["environment"] = buildenv if cmake_executable: ret["cmakeExecutable"] = cmake_executable if is_msvc(conanfile): # We can force the generator Visual even if it is Ninja, to define the toolset toolset = GenericSystemBlock.get_toolset("Visual", conanfile) # It seems "external" strategy is enough, as it is defined by toolchain if toolset: ret["toolset"] = { "value": toolset, "strategy": "external" } arch = GenericSystemBlock.get_generator_platform("Visual", conanfile) # https://learn.microsoft.com/en-us/cpp/build/cmake-presets-vs if generator and "Ninja" in generator and arch == "Win32": arch = "x86" # for command line, it is not Win32, it is x86 if arch: ret["architecture"] = { "value": arch, "strategy": "external" } # Second attempt at https://github.com/conan-io/conan/issues/13136 # for cmake-tools to activate environment. Similar to CompilersBlock # Only for cl/clang-cl, other compilers such clang can be breaking (Android) compilers_by_conf = conanfile.conf.get("tools.build:compiler_executables", default={}) compiler = conanfile.settings.get_safe("compiler") default_cl = "cl" if compiler == "msvc" and "Ninja" in str(generator) else None for lang in ("c", "cpp"): comp = compilers_by_conf.get(lang, default_cl) if comp and os.path.basename(comp) in ("cl", "cl.exe", "clang-cl", "clang-cl.exe"): lang = {"c": "C", "cpp": "CXX"}[lang] ret["cacheVariables"][f"CMAKE_{lang}_COMPILER"] = comp.replace("\\", "/") ret["toolchainFile"] = toolchain_file if conanfile.build_folder: # If we are installing a ref: "conan install ", we don't have build_folder, because # we don't even have a conanfile with a `layout()` to determine the build folder. # If we install a local conanfile "conan install ." with a layout(), it will be available ret["binaryDir"] = conanfile.build_folder def _format_val(val): return f'"{val}"' if type(val) is str and " " in val else f"{val}" try: is_consumer = conanfile._conan_node.recipe == RECIPE_CONSUMER and \ conanfile.tested_reference_str is None except: is_consumer = False if is_consumer: # https://github.com/conan-io/conan/pull/12034#issuecomment-1253776285 vars_tip = " ".join([f"-D{k}={_format_val(v)}" for k, v in cache_variables.items()]) tc_tip = f"-DCMAKE_TOOLCHAIN_FILE=/{toolchain_file} " \ if "CMAKE_TOOLCHAIN_FILE" not in vars_tip else "" msg = textwrap.dedent(f"""\ CMakeToolchain: Preset '{name}' added to CMakePresets.json. (cmake>=3.23) cmake --preset {name} (cmake<3.23) cmake -G {_format_val(generator)} {tc_tip} {vars_tip}""") conanfile.output.info(msg, fg=Color.CYAN) return ret @staticmethod def _common_preset_fields(conanfile, multiconfig, preset_prefix): build_type = conanfile.settings.get_safe("build_type") configure_preset_name = _CMakePresets._configure_preset_name(conanfile, multiconfig) build_preset_name = _CMakePresets._build_and_test_preset_name(conanfile) if preset_prefix: configure_preset_name = f"{preset_prefix}-{configure_preset_name}" build_preset_name = f"{preset_prefix}-{build_preset_name}" ret = {"name": build_preset_name, "configurePreset": configure_preset_name} if multiconfig: ret["configuration"] = build_type return ret @staticmethod def _build_preset_fields(conanfile, multiconfig, preset_prefix): ret = _CMakePresets._common_preset_fields(conanfile, multiconfig, preset_prefix) build_preset_jobs = build_jobs(conanfile) if build_preset_jobs: ret["jobs"] = build_preset_jobs return ret @staticmethod def _test_preset_fields(conanfile, multiconfig, preset_prefix, runenv): ret = _CMakePresets._common_preset_fields(conanfile, multiconfig, preset_prefix) build_preset_jobs = build_jobs(conanfile) if build_preset_jobs: ret.setdefault("execution", {})["jobs"] = build_preset_jobs if runenv: ret["environment"] = runenv return ret @staticmethod def _build_and_test_preset_name(conanfile): build_type = conanfile.settings.get_safe("build_type") custom_conf, user_defined_build = get_build_folder_custom_vars(conanfile) if user_defined_build: return custom_conf if custom_conf: if build_type: return "{}-{}".format(custom_conf, build_type.lower()) else: return custom_conf return build_type.lower() if build_type else "default" @staticmethod def _configure_preset_name(conanfile, multiconfig): build_type = conanfile.settings.get_safe("build_type") custom_conf, user_defined_build = get_build_folder_custom_vars(conanfile) if user_defined_build: return custom_conf if multiconfig or not build_type: return "default" if not custom_conf else custom_conf if custom_conf: return "{}-{}".format(custom_conf, str(build_type).lower()) else: return str(build_type).lower() class _IncludingPresets: """ CMakeUserPresets or ConanPresets.json that include the main generated CMakePresets """ @staticmethod def generate(conanfile, preset_path, user_presets_path, preset_prefix, preset_data, absolute_paths): if not user_presets_path: return # If generators folder is the same as source folder, do not create the user presets # we already have the CMakePresets.json right there if not (conanfile.source_folder and conanfile.source_folder != conanfile.generators_folder): return is_default = user_presets_path == "CMakeUserPresets.json" user_presets_path = os.path.join(conanfile.source_folder, user_presets_path) if os.path.isdir(user_presets_path): # Allows user to specify only the folder output_dir = user_presets_path user_presets_path = os.path.join(user_presets_path, "CMakeUserPresets.json") else: output_dir = os.path.dirname(user_presets_path) if is_default and not os.path.exists(os.path.join(output_dir, "CMakeLists.txt")): return # It uses schema version 4 unless it is forced to 2 inherited_user = {} if os.path.basename(user_presets_path) != "CMakeUserPresets.json": inherited_user = _IncludingPresets._collect_user_inherits(output_dir, preset_prefix) if not os.path.exists(user_presets_path): data = {"version": 4, "vendor": {"conan": dict()}} else: data = json.loads(load(user_presets_path)) if "conan" not in data.get("vendor", {}): # The file is not ours, we cannot overwrite it return if not absolute_paths: try: # Make it relative to the CMakeUserPresets.json if possible preset_path = os.path.relpath(preset_path, output_dir) # If we don't normalize, path will be removed in Linux shared folders # https://github.com/conan-io/conan/issues/18434 preset_path = preset_path.replace("\\", "/") except ValueError: pass data = _IncludingPresets._append_user_preset_path(data, preset_path, output_dir) if inherited_user: data = _IncludingPresets._update_stubs(data, inherited_user, output_dir, absolute_paths) data = json.dumps(data, indent=4) ConanOutput(str(conanfile)).info(f"CMakeToolchain generated: {user_presets_path}") save(user_presets_path, data) @staticmethod def _update_stubs(data, inherited_user, output_dir, absolute_paths): """ Set configurePresets/buildPresets/testPresets to stubs for conan-* presets that the user inherits but that don't have a real preset of the same type in the includes. """ real_preset_names_by_type = { "configurePresets": set(), "buildPresets": set(), "testPresets": set(), } for inc in data.get("include", []): inc_path = os.path.join(output_dir, inc) if not absolute_paths else inc assert os.path.exists(inc_path), f"Presets include must point to an existing file: '{inc_path}'" try: inc_json = json.loads(load(inc_path)) except Exception: continue for preset_type in ("configurePresets", "buildPresets", "testPresets"): for p in inc_json.get(preset_type, []): name = p.get("name") if name: real_preset_names_by_type[preset_type].add(name) for preset_type in ("configurePresets", "buildPresets", "testPresets"): real_names = real_preset_names_by_type[preset_type] stubs = [] for name in inherited_user.get(preset_type, []): if name not in real_names: stub = {"name": name} if preset_type in ("buildPresets", "testPresets"): stub["configurePreset"] = name stubs.append(stub) data[preset_type] = stubs return data @staticmethod def _collect_user_inherits(output_dir, preset_prefix): # Collect all the existing targets in the user files, to create empty conan- presets # so things doesn't break for multi-platform, when inherits don't exist collected_targets = {} types = "configurePresets", "buildPresets", "testPresets" for file in ("CMakePresets.json", "CMakeUserPresets.json"): user_file = os.path.join(output_dir, file) if os.path.exists(user_file): user_json = json.loads(load(user_file)) for preset_type in types: for preset in user_json.get(preset_type, []): inherits = preset.get("inherits", []) if isinstance(inherits, str): inherits = [inherits] inherits = [i for i in inherits if i.startswith(preset_prefix)] if inherits: existing = collected_targets.setdefault(preset_type, []) for i in inherits: if i not in existing: existing.append(i) return collected_targets @staticmethod def _append_user_preset_path(data, preset_path, output_dir): """ - Appends a 'include' to preset_path if the schema supports it. - Otherwise it merges to "data" all the configurePresets, buildPresets etc from the read preset_path. """ if "include" not in data: data["include"] = [] # Clear the folders that have been deleted data["include"] = [i for i in data.get("include", []) if os.path.exists(os.path.join(output_dir, i))] if preset_path not in data["include"]: data["include"].append(preset_path) return data def load_cmake_presets(folder): try: tmp = load(os.path.join(folder, "CMakePresets.json")) except FileNotFoundError: # Issue: https://github.com/conan-io/conan/issues/12896 raise ConanException(f"CMakePresets.json was not found in {folder} folder. Check that you " f"are using CMakeToolchain as generator to ensure its correct " f"initialization.") return json.loads(tmp) ================================================ FILE: conan/tools/cmake/toolchain/__init__.py ================================================ CONAN_TOOLCHAIN_FILENAME = "conan_toolchain.cmake" ================================================ FILE: conan/tools/cmake/toolchain/blocks.py ================================================ import os import re import textwrap from collections import OrderedDict from jinja2 import Template from conan.internal.internal_tools import universal_arch_separator, is_universal_arch from conan.tools.apple.apple import get_apple_sdk_fullname, _to_apple_arch from conan.tools.android.utils import android_abi from conan.tools.apple.apple import is_apple_os, to_apple_arch from conan.tools.build import build_jobs from conan.tools.build.flags import architecture_flag, architecture_link_flag, libcxx_flags, threads_flags from conan.tools.build.cross_building import cross_building from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME from conan.tools.cmake.utils import is_multi_configuration, cmake_escape_value from conan.tools.intel import IntelCC from conan.tools.microsoft.visual import msvc_version_to_toolset_version, msvc_platform_from_arch from conan.internal.api.install.generators import relativize_path from conan.internal.subsystems import deduce_subsystem, WINDOWS from conan.errors import ConanException from conan.internal.model.version import Version from conan.internal.util.files import load class Block: def __init__(self, conanfile, toolchain, name): self._conanfile = conanfile self._toolchain = toolchain self._context_values = None self._name = name @property def values(self): if self._context_values is None: self._context_values = self.context() return self._context_values @values.setter def values(self, context_values): self._context_values = context_values def get_rendered_content(self): context = self.values if context is None: return template = f"########## '{self._name}' block #############\n" + self.template + "\n\n" template = Template(template, trim_blocks=True, lstrip_blocks=True) return template.render(**context) def context(self): return {} @property def template(self): raise NotImplementedError() class VSRuntimeBlock(Block): template = textwrap.dedent("""\ # Definition of VS runtime CMAKE_MSVC_RUNTIME_LIBRARY, from settings build_type, # compiler.runtime, compiler.runtime_type {% set genexpr = namespace(str='') %} {% for config, value in vs_runtimes.items() %} {% set genexpr.str = genexpr.str + '$<$:' + value|string + '>' %} {% endfor %} cmake_policy(GET CMP0091 POLICY_CMP0091) if(NOT "${POLICY_CMP0091}" STREQUAL NEW) message(FATAL_ERROR "The CMake policy CMP0091 must be NEW, but is '${POLICY_CMP0091}'") endif() message(STATUS "Conan toolchain: Setting CMAKE_MSVC_RUNTIME_LIBRARY={{ genexpr.str }}") set(CMAKE_MSVC_RUNTIME_LIBRARY "{{ genexpr.str }}") """) def context(self): # Parsing existing toolchain file to get existing configured runtimes settings = self._conanfile.settings if settings.get_safe("os") != "Windows": return compiler = settings.get_safe("compiler") if compiler not in ("msvc", "clang", "intel-cc"): return runtime = settings.get_safe("compiler.runtime") if runtime is None: return config_dict = {} if os.path.exists(CONAN_TOOLCHAIN_FILENAME): existing_include = load(CONAN_TOOLCHAIN_FILENAME) msvc_runtime_value = re.search(r"set\(CMAKE_MSVC_RUNTIME_LIBRARY \"([^)]*)\"\)", existing_include) if msvc_runtime_value: capture = msvc_runtime_value.group(1) matches = re.findall(r"\$<\$:([A-Za-z]*)>", capture) config_dict = dict(matches) build_type = settings.get_safe("build_type") # FIXME: change for configuration if build_type is None: return None if compiler == "msvc" or compiler == "intel-cc" or compiler == "clang": runtime_type = settings.get_safe("compiler.runtime_type") rt = "MultiThreadedDebug" if runtime_type == "Debug" else "MultiThreaded" if runtime != "static": rt += "DLL" config_dict[build_type] = rt # If clang is being used the CMake check of compiler will try to create a simple # test application, and will fail because the Debug runtime is not there if compiler == "clang": if config_dict.get("Debug") is None: clang_rt = "MultiThreadedDebug" + ("DLL" if runtime != "static" else "") config_dict["Debug"] = clang_rt return {"vs_runtimes": config_dict} class VSDebuggerEnvironment(Block): template = textwrap.dedent("""\ # Definition of CMAKE_VS_DEBUGGER_ENVIRONMENT from "bindirs" folders of dependencies # for execution of applications with shared libraries within the VS IDE {% if vs_debugger_path %} # if the file exists it will be loaded by FindFiles block and the variable defined there if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") # This variable requires CMake>=3.27 to work set(CMAKE_VS_DEBUGGER_ENVIRONMENT "{{ vs_debugger_path }}") endif() {% endif %} """) def context(self): os_ = self._conanfile.settings.get_safe("os") build_type = self._conanfile.settings.get_safe("build_type") if (os_ and "Windows" not in os_) or not build_type: return None if "Visual" not in self._toolchain.generator: return None config_dict = {} if os.path.exists(CONAN_TOOLCHAIN_FILENAME): existing_include = load(CONAN_TOOLCHAIN_FILENAME) pattern = r"set\(CMAKE_VS_DEBUGGER_ENVIRONMENT \"PATH=([^)]*);%PATH%\"\)" vs_debugger_environment = re.search(pattern, existing_include) if vs_debugger_environment: capture = vs_debugger_environment.group(1) matches = re.findall(r"\$<\$:([^>]*)>", capture) config_dict = dict(matches) host_deps = self._conanfile.dependencies.host.values() test_deps = self._conanfile.dependencies.test.values() bin_dirs = [p for dep in host_deps for p in dep.cpp_info.aggregated_components().bindirs] test_bindirs = [p for dep in test_deps for p in dep.cpp_info.aggregated_components().bindirs] bin_dirs.extend(test_bindirs) bin_dirs = [relativize_path(p, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for p in bin_dirs] bin_dirs = [p.replace("\\", "/") for p in bin_dirs] bin_dirs = ";".join(bin_dirs) if bin_dirs else None if bin_dirs: config_dict[build_type] = bin_dirs if not config_dict: return None vs_debugger_path = "" for config, value in config_dict.items(): vs_debugger_path += f"$<$:{value}>" vs_debugger_path = f"PATH={vs_debugger_path};%PATH%" return {"vs_debugger_path": vs_debugger_path} class FPicBlock(Block): template = textwrap.dedent("""\ # Defining CMAKE_POSITION_INDEPENDENT_CODE for static libraries when necessary {% if fpic %} message(STATUS "Conan toolchain: Setting CMAKE_POSITION_INDEPENDENT_CODE={{ fpic }} (options.fPIC)") set(CMAKE_POSITION_INDEPENDENT_CODE {{ fpic }} CACHE BOOL "Position independent code") {% endif %} """) def context(self): fpic = self._conanfile.options.get_safe("fPIC") if fpic is None: return None os_ = self._conanfile.settings.get_safe("os") if os_ and "Windows" in os_: self._conanfile.output.warning("Toolchain: Ignoring fPIC option defined for Windows") return None return {"fpic": "ON" if fpic else "OFF"} class GLibCXXBlock(Block): template = textwrap.dedent("""\ # Definition of libcxx from 'compiler.libcxx' setting, defining the # right CXX_FLAGS for that libcxx {% if set_libcxx %} message(STATUS "Conan toolchain: Defining libcxx as C++ flags: {{ set_libcxx }}") string(APPEND CONAN_CXX_FLAGS " {{ set_libcxx }}") {% endif %} {% if glibcxx %} message(STATUS "Conan toolchain: Adding glibcxx compile definition: {{ glibcxx }}") add_compile_definitions({{ glibcxx }}) {% endif %} """) def context(self): libcxx, stdlib11 = libcxx_flags(self._conanfile) return {"set_libcxx": libcxx, "glibcxx": stdlib11} class SkipRPath(Block): template = textwrap.dedent("""\ # Defining CMAKE_SKIP_RPATH {% if skip_rpath %} set(CMAKE_SKIP_RPATH 1 CACHE BOOL "rpaths" FORCE) # Policy CMP0068 # We want the old behavior, in CMake >= 3.9 CMAKE_SKIP_RPATH won't affect install_name in OSX set(CMAKE_INSTALL_NAME_DIR "") {% endif %} """) skip_rpath = False def context(self): return {"skip_rpath": self.skip_rpath} class ArchitectureBlock(Block): template = textwrap.dedent("""\ {% if arch_flag %} # Define C++ flags, C flags and linker flags from 'settings.arch' message(STATUS "Conan toolchain: Defining architecture flag: {{ arch_flag }}") string(APPEND CONAN_CXX_FLAGS " {{ arch_flag }}") string(APPEND CONAN_C_FLAGS " {{ arch_flag }}") string(APPEND CONAN_SHARED_LINKER_FLAGS " {{ arch_flag }}") string(APPEND CONAN_EXE_LINKER_FLAGS " {{ arch_flag }}") {% endif %} {% if arch_link_flag %} message(STATUS "Conan toolchain: Defining architecture linker flag: {{ arch_link_flag }}") string(APPEND CONAN_SHARED_LINKER_FLAGS " {{ arch_link_flag }}") string(APPEND CONAN_EXE_LINKER_FLAGS " {{ arch_link_flag }}") {% endif %} {% if thread_flags_list %} # Define C++ flags, C flags and linker flags from 'compiler.threads' message(STATUS "Conan toolchain: Defining thread flags: {{ thread_flags_list }}") string(APPEND CONAN_CXX_FLAGS " {{ thread_flags_list }}") string(APPEND CONAN_C_FLAGS " {{ thread_flags_list }}") string(APPEND CONAN_SHARED_LINKER_FLAGS " {{ thread_flags_list }}") string(APPEND CONAN_EXE_LINKER_FLAGS " {{ thread_flags_list }}") {% endif %} """) def context(self): arch_flag = architecture_flag(self._conanfile) arch_link_flag = architecture_link_flag(self._conanfile) thread_flags_list = " ".join(threads_flags(self._conanfile)) if not arch_flag and not arch_link_flag and not thread_flags_list: return return {"arch_flag": arch_flag, "arch_link_flag": arch_link_flag, "thread_flags_list": thread_flags_list} class RpathLinkFlagsBlock(Block): template = textwrap.dedent("""\ # Pass -rpath-link pointing to all directories with runtime libraries {% if rpath_link_flags %} string(APPEND CONAN_EXE_LINKER_FLAGS " {{ rpath_link_flags }}") string(APPEND CONAN_SHARED_LINKER_FLAGS " {{ rpath_link_flags }}") {% endif %} """) def context(self): add_rpath_link = self._toolchain.add_rpath_link or self._conanfile.conf.get("tools.build:add_rpath_link", check_type=bool) if add_rpath_link: runtime_dirs = [] host_req = self._conanfile.dependencies.filter({"build": False}).values() for req in host_req: cppinfo = req.cpp_info.aggregated_components() runtime_dirs.extend(cppinfo.libdirs) # surround each dir with escaped quotes, to avoid problems with spaces in paths rpath_link_flags = " ".join([f'-Wl,-rpath-link=\\"{d}\\"' for d in runtime_dirs]) if runtime_dirs else None else: rpath_link_flags = None return {"rpath_link_flags": rpath_link_flags} class LinkerScriptsBlock(Block): template = textwrap.dedent("""\ # Add linker flags from tools.build:linker_scripts conf message(STATUS "Conan toolchain: Defining linker script flag: {{ linker_script_flags }}") string(APPEND CONAN_EXE_LINKER_FLAGS " {{ linker_script_flags }}") """) def context(self): linker_scripts = self._conanfile.conf.get( "tools.build:linker_scripts", check_type=list, default=[]) if not linker_scripts: return linker_scripts = [linker_script.replace('\\', '/') for linker_script in linker_scripts] linker_scripts = [relativize_path(p, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for p in linker_scripts] linker_script_flags = [r'-T\"' + linker_script + r'\"' for linker_script in linker_scripts] return {"linker_script_flags": " ".join(linker_script_flags)} class CppStdBlock(Block): template = textwrap.dedent("""\ # Define the C++ and C standards from 'compiler.cppstd' and 'compiler.cstd' function(conan_modify_std_watch variable access value current_list_file stack) set(conan_watched_std_variable "{{ cppstd }}") if (${variable} STREQUAL "CMAKE_C_STANDARD") set(conan_watched_std_variable "{{ cstd }}") endif() if ("${access}" STREQUAL "MODIFIED_ACCESS" AND NOT "${value}" STREQUAL "${conan_watched_std_variable}") message(STATUS "Warning: Standard ${variable} value defined in conan_toolchain.cmake to ${conan_watched_std_variable} has been modified to ${value} by ${current_list_file}") endif() unset(conan_watched_std_variable) endfunction() {% if cppstd %} message(STATUS "Conan toolchain: C++ Standard {{ cppstd }} with extensions {{ cppstd_extensions }}") set(CMAKE_CXX_STANDARD {{ cppstd }}) set(CMAKE_CXX_EXTENSIONS {{ cppstd_extensions }}) set(CMAKE_CXX_STANDARD_REQUIRED ON) variable_watch(CMAKE_CXX_STANDARD conan_modify_std_watch) {% endif %} {% if cstd %} message(STATUS "Conan toolchain: C Standard {{ cstd }} with extensions {{ cstd_extensions }}") set(CMAKE_C_STANDARD {{ cstd }}) set(CMAKE_C_EXTENSIONS {{ cstd_extensions }}) set(CMAKE_C_STANDARD_REQUIRED ON) variable_watch(CMAKE_C_STANDARD conan_modify_std_watch) {% endif %} """) def context(self): compiler_cppstd = self._conanfile.settings.get_safe("compiler.cppstd") compiler_cstd = self._conanfile.settings.get_safe("compiler.cstd") result = {} if compiler_cppstd is not None: if compiler_cppstd.startswith("gnu"): result["cppstd"] = compiler_cppstd[3:] result["cppstd_extensions"] = "ON" else: result["cppstd"] = compiler_cppstd result["cppstd_extensions"] = "OFF" if compiler_cstd is not None: if compiler_cstd.startswith("gnu"): result["cstd"] = compiler_cstd[3:] result["cstd_extensions"] = "ON" else: result["cstd"] = compiler_cstd result["cstd_extensions"] = "OFF" return result or None class SharedLibBock(Block): template = textwrap.dedent("""\ # Define BUILD_SHARED_LIBS for shared libraries message(STATUS "Conan toolchain: Setting BUILD_SHARED_LIBS = {{ shared_libs }}") set(BUILD_SHARED_LIBS {{ shared_libs }} CACHE BOOL "Build shared libraries") """) def context(self): try: shared_libs = "ON" if self._conanfile.options.shared else "OFF" return {"shared_libs": shared_libs} except ConanException: return None class ParallelBlock(Block): template = textwrap.dedent("""\ # Define VS paralell build /MP flags string(APPEND CONAN_CXX_FLAGS " /MP{{ parallel }}") string(APPEND CONAN_C_FLAGS " /MP{{ parallel }}") """) def context(self): # TODO: Check this conf compiler = self._conanfile.settings.get_safe("compiler") if compiler != "msvc" or "Visual" not in self._toolchain.generator: return jobs = build_jobs(self._conanfile) if jobs: return {"parallel": jobs} class AndroidSystemBlock(Block): template = textwrap.dedent("""\ # Define Android variables ANDROID_PLATFORM, ANDROID_STL, ANDROID_ABI, etc # and include(.../android.toolchain.cmake) from NDK toolchain file # New Android toolchain definitions message(STATUS "Conan toolchain: Setting Android platform: {{ android_platform }}") set(ANDROID_PLATFORM {{ android_platform }}) {% if android_stl %} message(STATUS "Conan toolchain: Setting Android stl: {{ android_stl }}") set(ANDROID_STL {{ android_stl }}) {% endif %} message(STATUS "Conan toolchain: Setting Android abi: {{ android_abi }}") set(ANDROID_ABI {{ android_abi }}) {% if android_use_legacy_toolchain_file %} set(ANDROID_USE_LEGACY_TOOLCHAIN_FILE {{ android_use_legacy_toolchain_file }}) {% endif %} include("{{ android_ndk_path }}/build/cmake/android.toolchain.cmake") """) def context(self): os_ = self._conanfile.settings.get_safe("os") if os_ != "Android": return # TODO: only 'c++_shared' y 'c++_static' supported? # https://developer.android.com/ndk/guides/cpp-support libcxx_str = self._conanfile.settings.get_safe("compiler.libcxx") android_ndk_path = self._conanfile.conf.get("tools.android:ndk_path") if not android_ndk_path: raise ConanException('CMakeToolchain needs tools.android:ndk_path configuration defined') android_ndk_path = android_ndk_path.replace("\\", "/") android_ndk_path = relativize_path(android_ndk_path, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") use_cmake_legacy_toolchain = self._conanfile.conf.get("tools.android:cmake_legacy_toolchain", check_type=bool) if use_cmake_legacy_toolchain is not None: use_cmake_legacy_toolchain = "ON" if use_cmake_legacy_toolchain else "OFF" ctxt_toolchain = { 'android_platform': 'android-' + str(self._conanfile.settings.os.api_level), 'android_abi': android_abi(self._conanfile), 'android_stl': libcxx_str, 'android_ndk_path': android_ndk_path, 'android_use_legacy_toolchain_file': use_cmake_legacy_toolchain, } return ctxt_toolchain class AppleSystemBlock(Block): template = textwrap.dedent("""\ # Define Apple architectures, sysroot, deployment target, bitcode, etc # Set the architectures for which to build. set(CMAKE_OSX_ARCHITECTURES {{ cmake_osx_architectures }} CACHE STRING "" FORCE) # Setting CMAKE_OSX_SYSROOT SDK, when using Xcode generator the name is enough # but full path is necessary for others set(CMAKE_OSX_SYSROOT {{ cmake_osx_sysroot }} CACHE STRING "" FORCE) {% if cmake_osx_deployment_target is defined %} # Setting CMAKE_OSX_DEPLOYMENT_TARGET if "os.version" is defined by the used conan profile set(CMAKE_OSX_DEPLOYMENT_TARGET "{{ cmake_osx_deployment_target }}" CACHE STRING "") {% endif %} set(BITCODE "") set(FOBJC_ARC "") set(VISIBILITY "") {% if enable_bitcode %} # Bitcode ON set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "YES") set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode") {% if enable_bitcode_marker %} set(BITCODE "-fembed-bitcode-marker") {% else %} set(BITCODE "-fembed-bitcode") {% endif %} {% elif enable_bitcode is not none %} # Bitcode OFF set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO") {% endif %} {% if enable_arc %} # ARC ON set(FOBJC_ARC "-fobjc-arc") set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "YES") {% elif enable_arc is not none %} # ARC OFF set(FOBJC_ARC "-fno-objc-arc") set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "NO") {% endif %} {% if enable_visibility %} # Visibility ON set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "NO") set(VISIBILITY "-fvisibility=default") {% elif enable_visibility is not none %} # Visibility OFF set(VISIBILITY "-fvisibility=hidden -fvisibility-inlines-hidden") set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "YES") {% endif %} #Check if Xcode generator is used, since that will handle these flags automagically if(CMAKE_GENERATOR MATCHES "Xcode") message(DEBUG "Not setting any manual command-line buildflags, since Xcode is selected as generator.") else() string(APPEND CONAN_C_FLAGS " ${BITCODE} ${VISIBILITY}") string(APPEND CONAN_CXX_FLAGS " ${BITCODE} ${VISIBILITY}") # Objective-C/C++ specific flags string(APPEND CONAN_OBJC_FLAGS " ${BITCODE} ${VISIBILITY} ${FOBJC_ARC}") string(APPEND CONAN_OBJCXX_FLAGS " ${BITCODE} ${VISIBILITY} ${FOBJC_ARC}") endif() """) def context(self): if not is_apple_os(self._conanfile): return None def to_apple_archs(conanfile): f"""converts conan-style architectures into Apple-style archs to be used by CMake also supports multiple architectures separated by '{universal_arch_separator}'""" arch_ = conanfile.settings.get_safe("arch") if conanfile else None if arch_ is not None: return ";".join([_to_apple_arch(arch, default=arch) for arch in arch_.split(universal_arch_separator)]) # check valid combinations of architecture - os ? # for iOS a FAT library valid for simulator and device can be generated # if multiple archs are specified "-DCMAKE_OSX_ARCHITECTURES=armv7;armv7s;arm64;i386;x86_64" host_architecture = to_apple_archs(self._conanfile) host_os_version = self._conanfile.settings.get_safe("os.version") host_sdk_name = self._conanfile.conf.get("tools.apple:sdk_path") or get_apple_sdk_fullname(self._conanfile) is_debug = self._conanfile.settings.get_safe('build_type') == "Debug" # Reading some configurations to enable or disable some Xcode toolchain flags and variables # Issue related: https://github.com/conan-io/conan/issues/9448 # Based on https://github.com/leetal/ios-cmake repository enable_bitcode = self._conanfile.conf.get("tools.apple:enable_bitcode", check_type=bool) enable_arc = self._conanfile.conf.get("tools.apple:enable_arc", check_type=bool) enable_visibility = self._conanfile.conf.get("tools.apple:enable_visibility", check_type=bool) ctxt_toolchain = { "enable_bitcode": enable_bitcode, "enable_bitcode_marker": all([enable_bitcode, is_debug]), "enable_arc": enable_arc, "enable_visibility": enable_visibility } if host_sdk_name: host_sdk_name = relativize_path(host_sdk_name, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") ctxt_toolchain["cmake_osx_sysroot"] = host_sdk_name # this is used to initialize the OSX_ARCHITECTURES property on each target as it is created if host_architecture: ctxt_toolchain["cmake_osx_architectures"] = host_architecture if host_os_version: # https://cmake.org/cmake/help/latest/variable/CMAKE_OSX_DEPLOYMENT_TARGET.html # Despite the OSX part in the variable name(s) they apply also to other SDKs than # macOS like iOS, tvOS, watchOS or visionOS. ctxt_toolchain["cmake_osx_deployment_target"] = host_os_version return ctxt_toolchain class FindFiles(Block): template = textwrap.dedent("""\ # Define paths to find packages, programs, libraries, etc. if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") message(STATUS "Conan toolchain: Including CMakeDeps generated conan_cmakedeps_paths.cmake") include("${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") else() {% if find_package_prefer_config %} set(CMAKE_FIND_PACKAGE_PREFER_CONFIG {{ find_package_prefer_config }}) {% endif %} # Definition of CMAKE_MODULE_PATH {% if build_paths %} list(PREPEND CMAKE_MODULE_PATH {{ build_paths }}) {% endif %} {% if generators_folder %} # the generators folder (where conan generates files, like this toolchain) list(PREPEND CMAKE_MODULE_PATH {{ generators_folder }}) {% endif %} # Definition of CMAKE_PREFIX_PATH, CMAKE_XXXXX_PATH {% if build_paths %} # The explicitly defined "builddirs" of "host" context dependencies must be in PREFIX_PATH list(PREPEND CMAKE_PREFIX_PATH {{ build_paths }}) {% endif %} {% if generators_folder %} # The Conan local "generators" folder, where this toolchain is saved. list(PREPEND CMAKE_PREFIX_PATH {{ generators_folder }} ) {% endif %} {% if cmake_program_path %} list(PREPEND CMAKE_PROGRAM_PATH {{ cmake_program_path }}) {% endif %} {% if cmake_library_path %} list(PREPEND CMAKE_LIBRARY_PATH {{ cmake_library_path }}) {% endif %} {% if is_apple and cmake_framework_path %} list(PREPEND CMAKE_FRAMEWORK_PATH {{ cmake_framework_path }}) {% endif %} {% if cmake_include_path %} list(PREPEND CMAKE_INCLUDE_PATH {{ cmake_include_path }}) {% endif %} {% if host_runtime_dirs %} set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} ) {% endif %} {% if cross_building %} if(NOT DEFINED CMAKE_FIND_ROOT_PATH_MODE_PACKAGE OR CMAKE_FIND_ROOT_PATH_MODE_PACKAGE STREQUAL "ONLY") set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE "BOTH") endif() if(NOT DEFINED CMAKE_FIND_ROOT_PATH_MODE_PROGRAM OR CMAKE_FIND_ROOT_PATH_MODE_PROGRAM STREQUAL "ONLY") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM "BOTH") endif() if(NOT DEFINED CMAKE_FIND_ROOT_PATH_MODE_LIBRARY OR CMAKE_FIND_ROOT_PATH_MODE_LIBRARY STREQUAL "ONLY") set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY "BOTH") endif() {% if is_apple %} if(NOT DEFINED CMAKE_FIND_ROOT_PATH_MODE_FRAMEWORK OR CMAKE_FIND_ROOT_PATH_MODE_FRAMEWORK STREQUAL "ONLY") set(CMAKE_FIND_ROOT_PATH_MODE_FRAMEWORK "BOTH") endif() {% endif %} if(NOT DEFINED CMAKE_FIND_ROOT_PATH_MODE_INCLUDE OR CMAKE_FIND_ROOT_PATH_MODE_INCLUDE STREQUAL "ONLY") set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE "BOTH") endif() {% endif %} endif() """) def _runtime_dirs_value(self, dirs): if is_multi_configuration(self._toolchain.generator): return ' '.join(f'"$<$:{i}>"' for c, v in dirs.items() for i in v) else: return ' '.join(f'"{item}"' for _, items in dirs.items() for item in items) def _get_host_runtime_dirs(self, host_req): settings = self._conanfile.settings host_runtime_dirs = {} is_win = self._conanfile.settings.get_safe("os") == "Windows" # Get the previous configuration if is_multi_configuration(self._toolchain.generator) and os.path.exists(CONAN_TOOLCHAIN_FILENAME): existing_toolchain = load(CONAN_TOOLCHAIN_FILENAME) pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" variable_match = re.search(pattern_lib_dirs, existing_toolchain) if variable_match: capture = variable_match.group(1) matches = re.findall(r'"\$<\$:([^>]*)>"', capture) host_runtime_dirs = {} for k, v in matches: host_runtime_dirs.setdefault(k, []).append(v) # Calculate the dirs for the current build_type runtime_dirs = [] for req in host_req: cppinfo = req.cpp_info.aggregated_components() runtime_dirs.extend(cppinfo.bindirs if is_win else cppinfo.libdirs) build_type = settings.get_safe("build_type") host_runtime_dirs[build_type] = [s.replace("\\", "/") for s in runtime_dirs] return host_runtime_dirs def _join_paths(self, paths): paths = [p.replace('\\', '/').replace('$', '\\$').replace('"', '\\"') for p in paths] paths = [relativize_path(p, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for p in paths] return " ".join([f'"{p}"' for p in paths]) def context(self): # To find the generated cmake_find_package finders # TODO: Change this for parameterized output location of CMakeDeps find_package_prefer_config = "ON" # assume ON by default if not specified in conf prefer_config = self._conanfile.conf.get("tools.cmake.cmaketoolchain:find_package_prefer_config", check_type=bool) if prefer_config is False: find_package_prefer_config = "OFF" is_apple_ = is_apple_os(self._conanfile) # Read information from host context # TODO: Add here in 2.0 the "skip": False trait host_req = self._conanfile.dependencies.filter({"build": False}).values() build_paths = [] host_lib_paths = [] host_runtime_dirs = self._get_host_runtime_dirs(host_req) host_framework_paths = [] host_include_paths = [] for req in host_req: cppinfo = req.cpp_info.aggregated_components() build_paths.extend(cppinfo.builddirs) host_lib_paths.extend(cppinfo.libdirs) if is_apple_: host_framework_paths.extend(cppinfo.frameworkdirs) host_include_paths.extend(cppinfo.includedirs) # Read information from build context build_req = self._conanfile.dependencies.build.values() build_bin_paths = [] for req in build_req: cppinfo = req.cpp_info.aggregated_components() build_paths.extend(cppinfo.builddirs) build_bin_paths.extend(cppinfo.bindirs) return { "find_package_prefer_config": find_package_prefer_config, "generators_folder": "${CMAKE_CURRENT_LIST_DIR}", "build_paths": self._join_paths(build_paths), "cmake_program_path": self._join_paths(build_bin_paths), "cmake_library_path": self._join_paths(host_lib_paths), "cmake_framework_path": self._join_paths(host_framework_paths), "cmake_include_path": self._join_paths(host_include_paths), "is_apple": is_apple_, "cross_building": cross_building(self._conanfile), "host_runtime_dirs": self._runtime_dirs_value(host_runtime_dirs) } class PkgConfigBlock(Block): template = textwrap.dedent("""\ # Define pkg-config from 'tools.gnu:pkg_config' executable and paths {% if pkg_config %} set(PKG_CONFIG_EXECUTABLE {{ pkg_config }} CACHE FILEPATH "pkg-config executable") {% endif %} {% if pkg_config_path %} if (DEFINED ENV{PKG_CONFIG_PATH}) set(ENV{PKG_CONFIG_PATH} "{{ pkg_config_path }}$ENV{PKG_CONFIG_PATH}") else() set(ENV{PKG_CONFIG_PATH} "{{ pkg_config_path }}") endif() {% endif %} """) def context(self): pkg_config = self._conanfile.conf.get("tools.gnu:pkg_config", check_type=str) if pkg_config: pkg_config = pkg_config.replace("\\", "/") subsystem = deduce_subsystem(self._conanfile, "build") pathsep = ":" if subsystem != WINDOWS else ";" pkg_config_path = "${CMAKE_CURRENT_LIST_DIR}" + pathsep return {"pkg_config": pkg_config, "pkg_config_path": pkg_config_path} class UserToolchain(Block): template = textwrap.dedent("""\ # Include one or more CMake user toolchain from tools.cmake.cmaketoolchain:user_toolchain {% for user_toolchain in paths %} message(STATUS "Conan toolchain: Including user_toolchain: {{user_toolchain}}") include("{{user_toolchain}}") {% endfor %} """) def context(self): # This is global [conf] injection of extra toolchain files user_toolchain = self._conanfile.conf.get("tools.cmake.cmaketoolchain:user_toolchain", default=[], check_type=list) paths = [relativize_path(p, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") for p in user_toolchain] paths = [p.replace("\\", "/") for p in paths] return {"paths": paths} class ExtraFlagsBlock(Block): """This block is adding flags directly from user [conf] section""" _template = textwrap.dedent("""\ # Include extra C++, C and linker flags from configuration tools.build:flags # and from CMakeToolchain.extra__flags # Conan conf flags start: {{config}} {% if cxxflags %} string(APPEND CONAN_CXX_FLAGS{{suffix}} "{% for cxxflag in cxxflags %} {{ cxxflag }}{% endfor %}") {% endif %} {% if cflags %} string(APPEND CONAN_C_FLAGS{{suffix}} "{% for cflag in cflags %} {{ cflag }}{% endfor %}") {% endif %} {% if sharedlinkflags %} string(APPEND CONAN_SHARED_LINKER_FLAGS{{suffix}} "{% for sharedlinkflag in sharedlinkflags %} {{ sharedlinkflag }}{% endfor %}") {% endif %} {% if exelinkflags %} string(APPEND CONAN_EXE_LINKER_FLAGS{{suffix}} "{% for exelinkflag in exelinkflags %} {{ exelinkflag }}{% endfor %}") {% endif %} {% if rcflags %} string(APPEND CONAN_RC_FLAGS{{suffix}} "{% for rcflag in rcflags %} {{ rcflag }}{% endfor %}") {% endif %} {% if defines %} {% if config %} {% for define in defines %} add_compile_definitions("$<$:{{ define }}>") {% endfor %} {% else %} add_compile_definitions({% for define in defines %} "{{ define }}"{% endfor %}) {% endif %} {% endif %} # Conan conf flags end """) @property def template(self): if not is_multi_configuration(self._toolchain.generator): return self._template sections = {} if os.path.exists(CONAN_TOOLCHAIN_FILENAME): existing_toolchain = load(CONAN_TOOLCHAIN_FILENAME) lines = existing_toolchain.splitlines() current_section = None for line in lines: if line.startswith("# Conan conf flags start: "): section_name = line.split(":", 1)[1].strip() current_section = [line] sections[section_name] = current_section elif line == "# Conan conf flags end": current_section.append(line) current_section = None elif current_section is not None: current_section.append(line) sections.pop("", None) # Just in case it had a single config before config = self._conanfile.settings.get_safe("build_type") for k, v in sections.items(): if k != config: v.insert(0, "{% raw %}") v.append("{% endraw %}") sections[config] = [self._template] sections = ["\n".join(lines) for lines in sections.values()] sections = "\n".join(sections) return sections def context(self): # Now, it's time to get all the flags defined by the user cxxflags = self._toolchain.extra_cxxflags + self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) cflags = self._toolchain.extra_cflags + self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list) sharedlinkflags = self._toolchain.extra_sharedlinkflags + self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) exelinkflags = self._toolchain.extra_exelinkflags + self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list) rcflags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) defines = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list) # See https://github.com/conan-io/conan/issues/13374 android_ndk_path = self._conanfile.conf.get("tools.android:ndk_path") android_legacy_toolchain = self._conanfile.conf.get("tools.android:cmake_legacy_toolchain", check_type=bool) if android_ndk_path and (cxxflags or cflags) and android_legacy_toolchain is not False: self._conanfile.output.warning("tools.build:cxxflags or cflags are defined, but Android NDK toolchain may be overriding " "the values. Consider setting tools.android:cmake_legacy_toolchain to False.") config = "" suffix = "" if is_multi_configuration(self._toolchain.generator): config = self._conanfile.settings.get_safe("build_type") suffix = f"_{config.upper()}" if config else "" return { "config": config, "suffix": suffix, "cxxflags": cxxflags, "cflags": cflags, "sharedlinkflags": sharedlinkflags, "exelinkflags": exelinkflags, "rcflags": rcflags, "defines": [define.replace('"', '\\"') for define in defines], } class CMakeFlagsInitBlock(Block): template = textwrap.dedent("""\ # Define CMAKE__FLAGS from CONAN__FLAGS foreach(config IN LISTS CMAKE_CONFIGURATION_TYPES) string(TOUPPER ${config} config) if(DEFINED CONAN_CXX_FLAGS_${config}) string(APPEND CMAKE_CXX_FLAGS_${config}_INIT " ${CONAN_CXX_FLAGS_${config}}") endif() if(DEFINED CONAN_C_FLAGS_${config}) string(APPEND CMAKE_C_FLAGS_${config}_INIT " ${CONAN_C_FLAGS_${config}}") endif() if(DEFINED CONAN_SHARED_LINKER_FLAGS_${config}) string(APPEND CMAKE_SHARED_LINKER_FLAGS_${config}_INIT " ${CONAN_SHARED_LINKER_FLAGS_${config}}") endif() if(DEFINED CONAN_EXE_LINKER_FLAGS_${config}) string(APPEND CMAKE_EXE_LINKER_FLAGS_${config}_INIT " ${CONAN_EXE_LINKER_FLAGS_${config}}") endif() if(DEFINED CONAN_RC_FLAGS_${config}) string(APPEND CMAKE_RC_FLAGS_${config}_INIT " ${CONAN_RC_FLAGS_${config}}") endif() endforeach() if(DEFINED CONAN_CXX_FLAGS) string(APPEND CMAKE_CXX_FLAGS_INIT " ${CONAN_CXX_FLAGS}") endif() if(DEFINED CONAN_C_FLAGS) string(APPEND CMAKE_C_FLAGS_INIT " ${CONAN_C_FLAGS}") endif() if(DEFINED CONAN_SHARED_LINKER_FLAGS) string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT " ${CONAN_SHARED_LINKER_FLAGS}") endif() if(DEFINED CONAN_EXE_LINKER_FLAGS) string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT " ${CONAN_EXE_LINKER_FLAGS}") endif() if(DEFINED CONAN_RC_FLAGS) string(APPEND CMAKE_RC_FLAGS_INIT " ${CONAN_RC_FLAGS}") endif() if(DEFINED CONAN_OBJCXX_FLAGS) string(APPEND CMAKE_OBJCXX_FLAGS_INIT " ${CONAN_OBJCXX_FLAGS}") endif() if(DEFINED CONAN_OBJC_FLAGS) string(APPEND CMAKE_OBJC_FLAGS_INIT " ${CONAN_OBJC_FLAGS}") endif() """) class TryCompileBlock(Block): template = textwrap.dedent("""\ # Blocks after this one will not be added when running CMake try/checks {% if config %} if(NOT DEFINED CMAKE_TRY_COMPILE_CONFIGURATION) # to allow user command line override set(CMAKE_TRY_COMPILE_CONFIGURATION {{config}}) endif() {% endif %} get_property( _CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE ) if(_CMAKE_IN_TRY_COMPILE) message(STATUS "Running toolchain IN_TRY_COMPILE") return() endif() """) def context(self): # Only for well known CMake configurations, but not for custom ones # Revert of https://github.com/conan-io/conan/pull/18559, even if it was correct, there are # legacy code using check_function_exists that breaks in CMake with MSVC, see # https://github.com/conan-io/conan/issues/18689 # TODO: Resume this effort when other try_compile things are sorted out # bt = self._conanfile.settings.get_safe("build_type") # config = bt if bt in ["Debug", "Release", "RelWithDebInfo", "MinSizeRel"] else None config = None # Keep it defined but as `None` in case some user already customized it return {"config": config} class CompilersBlock(Block): template = textwrap.dedent(r""" {% for lang, compiler_path in compilers.items() %} set(CMAKE_{{ lang }}_COMPILER "{{ compiler_path|replace('\\', '/') }}") {% endfor %} """) def context(self): # Reading configuration from "tools.build:compiler_executables" -> {"C": "/usr/bin/gcc"} compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={}, check_type=dict) # Map the possible languages compilers = {} # Allowed variables (and _LAUNCHER) compilers_mapping = {"c": "C", "cuda": "CUDA", "cpp": "CXX", "objc": "OBJC", "objcpp": "OBJCXX", "rc": "RC", 'fortran': "Fortran", 'asm': "ASM", "hip": "HIP", "ispc": "ISPC"} for comp, lang in compilers_mapping.items(): # To set CMAKE__COMPILER if comp in compilers_by_conf: compilers[lang] = compilers_by_conf[comp] compiler = self._conanfile.settings.get_safe("compiler") if compiler == "msvc" and "Ninja" in str(self._toolchain.generator): # None of them defined, if one is defined by user, user should define the other too if "c" not in compilers_by_conf and "cpp" not in compilers_by_conf: compilers["C"] = "cl" compilers["CXX"] = "cl" return {"compilers": compilers} class GenericSystemBlock(Block): template = textwrap.dedent("""\ # Definition of system, platform and toolset {% if cmake_sysroot %} set(CMAKE_SYSROOT {{ cmake_sysroot }}) {% endif %} {% if cmake_system_name %} # Cross building if(NOT DEFINED CMAKE_SYSTEM_NAME) # It might have been defined by a user toolchain set(CMAKE_SYSTEM_NAME {{ cmake_system_name }}) endif() {% endif %} {% if cmake_system_version %} if(NOT DEFINED CMAKE_SYSTEM_VERSION) # It might have been defined by a user toolchain set(CMAKE_SYSTEM_VERSION {{ cmake_system_version }}) endif() {% endif %} {% if cmake_system_processor %} if(NOT DEFINED CMAKE_SYSTEM_PROCESSOR) # It might have been defined by a user toolchain set(CMAKE_SYSTEM_PROCESSOR {{ cmake_system_processor }}) endif() {% endif %} {% if generator_platform and not winsdk_version %} set(CMAKE_GENERATOR_PLATFORM "{{ generator_platform }}" CACHE STRING "" FORCE) {% elif winsdk_version %} if(POLICY CMP0149) cmake_policy(GET CMP0149 _POLICY_WINSDK_VERSION) endif() if(_POLICY_WINSDK_VERSION STREQUAL "NEW") message(STATUS "Conan toolchain: CMAKE_GENERATOR_PLATFORM={{gen_platform_sdk_version}}") set(CMAKE_GENERATOR_PLATFORM "{{ gen_platform_sdk_version }}" CACHE STRING "" FORCE) else() # winsdk_version will be taken from above CMAKE_SYSTEM_VERSION message(STATUS "Conan toolchain: CMAKE_GENERATOR_PLATFORM={{generator_platform}}") set(CMAKE_GENERATOR_PLATFORM "{{ generator_platform }}" CACHE STRING "" FORCE) endif() {% endif %} {% if toolset %} message(STATUS "Conan toolchain: CMAKE_GENERATOR_TOOLSET={{ toolset }}") set(CMAKE_GENERATOR_TOOLSET "{{ toolset }}" CACHE STRING "" FORCE) {% endif %} """) @staticmethod def get_toolset(generator, conanfile): toolset = None if generator is None or ("Visual" not in generator and "Xcode" not in generator): return None settings = conanfile.settings compiler = settings.get_safe("compiler") if compiler == "intel-cc": return IntelCC(conanfile).ms_toolset elif compiler == "msvc": toolset = settings.get_safe("compiler.toolset") if toolset is None: compiler_version = str(settings.compiler.version) msvc_update = conanfile.conf.get("tools.microsoft:msvc_update") compiler_update = msvc_update or settings.get_safe("compiler.update") toolset = msvc_version_to_toolset_version(compiler_version) if compiler_update is not None: # It is full one(19.28), not generic 19.2X # The equivalent of compiler 19.26 is toolset 14.26 toolset += ",version=14.{}{}".format(compiler_version[-1], compiler_update) elif compiler == "clang": if generator and "Visual" in generator: if any(f"Visual Studio {v}" in generator for v in ("16", "17", "18")): toolset = "ClangCL" else: raise ConanException("CMakeToolchain with compiler=clang and a CMake " "'Visual Studio' generator requires VS16, VS17 or VS18") toolset_arch = conanfile.conf.get("tools.cmake.cmaketoolchain:toolset_arch") if toolset_arch is not None: toolset_arch = "host={}".format(toolset_arch) toolset = toolset_arch if toolset is None else "{},{}".format(toolset, toolset_arch) toolset_cuda = conanfile.conf.get("tools.cmake.cmaketoolchain:toolset_cuda") if toolset_cuda is not None: toolset_cuda = relativize_path(toolset_cuda, conanfile, "${CMAKE_CURRENT_LIST_DIR}") toolset_cuda = f"cuda={toolset_cuda}" toolset = toolset_cuda if toolset is None else f"{toolset},{toolset_cuda}" return toolset @staticmethod def get_generator_platform(generator, conanfile): settings = conanfile.settings # Returns the generator platform to be used by CMake compiler = settings.get_safe("compiler") arch = settings.get_safe("arch") if settings.get_safe("os") == "WindowsCE": return settings.get_safe("os.platform") if compiler in ("msvc", "clang") and generator and "Visual" in generator: return msvc_platform_from_arch(arch) return None def _get_generic_system_name(self): os_host = self._conanfile.settings.get_safe("os") os_build = self._conanfile.settings_build.get_safe("os") arch_host = self._conanfile.settings.get_safe("arch") arch_build = self._conanfile.settings_build.get_safe("arch") cmake_system_name_map = {"Neutrino": "QNX", "": "Generic", "baremetal": "Generic", None: "Generic"} if os_host != os_build: # os_host would be 'baremetal' for tricore, but it's ideal to use the Generic-ELF # system name instead of just "Generic" because it matches how Aurix Dev Studio # generated makefiles behave by generating binaries with the '.elf' extension. if arch_host in ['tc131', 'tc16', 'tc161', 'tc162', 'tc18']: return "Generic-ELF" return cmake_system_name_map.get(os_host, os_host) elif arch_host is not None and arch_host != arch_build: if not ((arch_build == "x86_64") and (arch_host == "x86") or (arch_build == "sparcv9") and (arch_host == "sparc") or (arch_build == "ppc64") and (arch_host == "ppc32")): return cmake_system_name_map.get(os_host, os_host) def _is_apple_cross_building(self): if is_universal_arch(self._conanfile.settings.get_safe("arch"), self._conanfile.settings.possible_values().get("arch")): return False os_host = self._conanfile.settings.get_safe("os") arch_host = self._conanfile.settings.get_safe("arch") arch_build = self._conanfile.settings_build.get_safe("arch") os_build = self._conanfile.settings_build.get_safe("os") return os_host in ('iOS', 'watchOS', 'tvOS', 'visionOS') or ( os_host == 'Macos' and (arch_host != arch_build or os_build != os_host)) @staticmethod def _get_darwin_version(os_name, os_version): # version mapping from https://en.wikipedia.org/wiki/Darwin_(operating_system) # but a more detailed version can be found in https://theapplewiki.com/wiki/Kernel version_mapping = { "Macos": { "10.6": "10", "10.7": "11", "10.8": "12", "10.9": "13", "10.10": "14", "10.11": "15", "10.12": "16", "10.13": "17", "10.14": "18", "10.15": "19", "11": "20", "12": "21", "13": "22", "14": "23", "15": "24" }, "iOS": { "7": "14", "8": "14", "9": "15", "10": "16", "11": "17", "12": "18", "13": "19", "14": "20", "15": "21", "16": "22", "17": "23", "18": "24" }, "watchOS": { "4": "17", "5": "18", "6": "19", "7": "20", "8": "21", "9": "22", "10": "23", "11": "24" }, "tvOS": { "11": "17", "12": "18", "13": "19", "14": "20", "15": "21", "16": "22", "17": "23", "18": "24" }, "visionOS": { "1": "23", "2": "24" } } os_version = Version(os_version).major if os_name != "Macos" or (os_name == "Macos" and Version( os_version) >= Version("11")) else os_version return version_mapping.get(os_name, {}).get(str(os_version)) def _get_cross_build(self): system_name = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_name") system_version = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_version") system_processor = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_processor") # try to detect automatically if not is_universal_arch(self._conanfile.settings.get_safe("arch"), self._conanfile.settings.possible_values().get("arch")): os_host = self._conanfile.settings.get_safe("os") os_host_version = self._conanfile.settings.get_safe("os.version") arch_host = self._conanfile.settings.get_safe("arch") if arch_host == "armv8": arch_host = {"Windows": "ARM64", "Macos": "arm64"}.get(os_host, "aarch64") if system_name is None: # Try to deduce _system_version = None _system_processor = None if self._is_apple_cross_building(): # cross-build in Macos also for M1 system_name = {'Macos': 'Darwin'}.get(os_host, os_host) # CMAKE_SYSTEM_VERSION for Apple sets the Darwin version, not the os version _system_version = self._get_darwin_version(os_host, os_host_version) _system_processor = to_apple_arch(self._conanfile) elif os_host != 'Android': system_name = self._get_generic_system_name() if arch_host in ['tc131', 'tc16', 'tc161', 'tc162', 'tc18']: _system_processor = "tricore" else: _system_processor = arch_host _system_version = os_host_version if system_name is not None and system_version is None: system_version = _system_version if system_name is not None and system_processor is None: system_processor = _system_processor return system_name, system_version, system_processor def _get_winsdk_version(self, system_version, generator_platform): compiler = self._conanfile.settings.get_safe("compiler") if compiler not in ("msvc", "clang") or "Visual" not in str(self._toolchain.generator): # Ninja will get it from VCVars, not from toolchain return system_version, None, None winsdk_version = self._conanfile.conf.get("tools.microsoft:winsdk_version", check_type=str) if winsdk_version: if system_version: self._conanfile.output.warning("Both cmake_system_version and winsdk_version confs" " defined, prioritizing winsdk_version") system_version = winsdk_version elif "Windows" in self._conanfile.settings.get_safe("os", ""): winsdk_version = self._conanfile.settings.get_safe("os.version") if system_version: if winsdk_version: self._conanfile.output.warning("Both cmake_system_version conf and os.version" " defined, prioritizing cmake_system_version") winsdk_version = system_version gen_platform_sdk_version = [generator_platform, f"version={winsdk_version}" if winsdk_version else None] gen_platform_sdk_version = ",".join(d for d in gen_platform_sdk_version if d) return system_version, winsdk_version, gen_platform_sdk_version def context(self): generator = self._toolchain.generator generator_platform = self.get_generator_platform(generator, self._conanfile) toolset = self.get_toolset(generator, self._conanfile) system_name, system_version, system_processor = self._get_cross_build() # This is handled by the tools.apple:sdk_path and CMAKE_OSX_SYSROOT in Apple cmake_sysroot = self._conanfile.conf.get("tools.build:sysroot") cmake_sysroot = cmake_sysroot.replace("\\", "/") if cmake_sysroot is not None else None if cmake_sysroot is not None: cmake_sysroot = relativize_path(cmake_sysroot, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}") result = self._get_winsdk_version(system_version, generator_platform) system_version, winsdk_version, gen_platform_sdk_version = result return {"toolset": toolset, "generator_platform": generator_platform, "cmake_system_name": system_name, "cmake_system_version": system_version, "cmake_system_processor": system_processor, "cmake_sysroot": cmake_sysroot, "winsdk_version": winsdk_version, "gen_platform_sdk_version": gen_platform_sdk_version} class ExtraVariablesBlock(Block): template = textwrap.dedent("""\ # Definition of extra CMake variables from tools.cmake.cmaketoolchain:extra_variables {% if extra_variables %} {% for key, value in extra_variables.items() %} set({{ key }} {{ value }}) {% endfor %} {% endif %} """) def context(self): from conan.tools.cmake.utils import parse_extra_variable # Reading configuration from "tools.cmake.cmaketoolchain:extra_variables" extra_variables = self._conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables", default={}, check_type=dict) compilation_verbosity = self._conanfile.conf.get("tools.compilation:verbosity", choices=("quiet", "verbose")) build_verbosity = self._conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) if build_verbosity == "quiet": build_verbosity = "error" if compilation_verbosity == "verbose": extra_variables.setdefault("CMAKE_VERBOSE_MAKEFILE", {"cache": True, "type": "BOOL", "value": "ON"}) if build_verbosity: extra_variables.setdefault("CMAKE_MESSAGE_LOG_LEVEL", {"cache": True, "type": "STRING", "value": build_verbosity.upper()}) parsed_extra_variables = {} for key, value in extra_variables.items(): parsed_extra_variables[key] = parse_extra_variable("tools.cmake.cmaketoolchain:extra_variables", key, value) return {"extra_variables": parsed_extra_variables} class OutputDirsBlock(Block): @property def template(self): return textwrap.dedent("""\ # Definition of CMAKE_INSTALL_XXX folders # Ensure export(PACKAGE) honors CMAKE_EXPORT_PACKAGE_REGISTRY even if the # project sets cmake_minimum_required() lower than 3.15. cmake_policy(SET CMP0090 NEW) if(NOT DEFINED CMAKE_EXPORT_PACKAGE_REGISTRY) set(CMAKE_EXPORT_PACKAGE_REGISTRY OFF) endif() {% if package_folder %} set(CMAKE_INSTALL_PREFIX "{{package_folder}}") {% endif %} {% if default_bin %} set(CMAKE_INSTALL_BINDIR "{{default_bin}}") set(CMAKE_INSTALL_SBINDIR "{{default_bin}}") set(CMAKE_INSTALL_LIBEXECDIR "{{default_bin}}") {% endif %} {% if default_lib %} set(CMAKE_INSTALL_LIBDIR "{{default_lib}}") {% endif %} {% if default_include %} set(CMAKE_INSTALL_INCLUDEDIR "{{default_include}}") set(CMAKE_INSTALL_OLDINCLUDEDIR "{{default_include}}") {% endif %} {% if default_res %} set(CMAKE_INSTALL_DATAROOTDIR "{{default_res}}") {% endif %} """) def _get_cpp_info_value(self, name): # Why not taking cpp.build? because this variables are used by the "cmake install" # that correspond to the package folder (even if the root is the build directory) elements = getattr(self._conanfile.cpp.package, name) return elements[0] if elements else None def context(self): pf = self._conanfile.package_folder return {"package_folder": pf.replace("\\", "/") if pf else None, "default_bin": self._get_cpp_info_value("bindirs"), "default_lib": self._get_cpp_info_value("libdirs"), "default_include": self._get_cpp_info_value("includedirs"), "default_res": self._get_cpp_info_value("resdirs")} class VariablesBlock(Block): @property def template(self): return textwrap.dedent("""\ # Definition of CMake variables from CMakeToolchain.variables values {% macro iterate_configs(var_config, action) %} {% for it, values in var_config.items() %} {% set genexpr = namespace(str='') %} {% for conf, value in values -%} set(CONAN_DEF_{{ conf }}{{ it }} "{{ value }}") {% endfor %} {% for conf, value in values -%} {% set genexpr.str = genexpr.str + '$,${CONAN_DEF_' + conf|string + it|string + '},' %} {% if loop.last %}{% set genexpr.str = genexpr.str + '""' -%}{%- endif -%} {% endfor %} {% for i in range(values|count) %}{% set genexpr.str = genexpr.str + '>' %} {% endfor %} set({{ it }} {{ genexpr.str }} CACHE STRING "Variable {{ it }} conan-toolchain defined") {% endfor %} {% endmacro %} # Variables {% for it, value in variables.items() %} {% if value is boolean %} set({{ it }} {{ "ON" if value else "OFF"}} CACHE BOOL "Variable {{ it }} conan-toolchain defined") {% else %} set({{ it }} "{{ value }}" CACHE STRING "Variable {{ it }} conan-toolchain defined") {% endif %} {% endfor %} # Variables per configuration {{ iterate_configs(variables_config, action='set') }} """) def context(self): return {"variables": self._toolchain.variables, "variables_config": self._toolchain.variables.configuration_types} class PreprocessorBlock(Block): @property def template(self): return textwrap.dedent("""\ # Preprocessor definitions from CMakeToolchain.preprocessor_definitions values {% for it, value in preprocessor_definitions.items() %} {% if value is none %} add_compile_definitions("{{ it }}") {% else %} add_compile_definitions("{{ it }}={{ value }}") {% endif %} {% endfor %} # Preprocessor definitions per configuration {% for name, values in preprocessor_definitions_config.items() %} {%- for (conf, value) in values %} {% if value is none %} set(CONAN_DEF_{{conf}}_{{name}} "{{name}}") {% else %} set(CONAN_DEF_{{conf}}_{{name}} "{{name}}={{value}}") {% endif %} {% endfor %} add_compile_definitions( {%- for (conf, value) in values %} $<$:${CONAN_DEF_{{conf}}_{{name}}}> {%- endfor -%}) {% endfor %} """) def context(self): return {"preprocessor_definitions": self._toolchain.preprocessor_definitions, "preprocessor_definitions_config": self._toolchain.preprocessor_definitions.configuration_types} class ToolchainBlocks: def __init__(self, conanfile, toolchain, items=None): self._blocks = OrderedDict() self._conanfile = conanfile self._toolchain = toolchain if items: for name, block in items: self._blocks[name] = block(conanfile, toolchain, name) def keys(self): return self._blocks.keys() def items(self): return self._blocks.items() def remove(self, name, *args): del self._blocks[name] for arg in args: del self._blocks[arg] def select(self, name, *args): """ keep the blocks provided as arguments, remove the others, except pre-existing "variables" and "preprocessor", to not break behavior """ self._conanfile.output.warning("CMakeToolchain.select is deprecated. Use blocks.enabled()" " instead", warn_tag="deprecated") to_keep = [name] + list(args) + ["variables", "preprocessor"] self._blocks = OrderedDict((k, v) for k, v in self._blocks.items() if k in to_keep) def enabled(self, name, *args): """ keep the blocks provided as arguments, remove the others """ to_keep = [name] + list(args) self._blocks = OrderedDict((k, v) for k, v in self._blocks.items() if k in to_keep) def __setitem__(self, name, block_type): # Create a new class inheriting Block with the elements of the provided one block_type = type('proxyUserBlock', (Block,), dict(block_type.__dict__)) self._blocks[name] = block_type(self._conanfile, self._toolchain, name) def __getitem__(self, name): return self._blocks[name] def process_blocks(self): blocks = self._conanfile.conf.get("tools.cmake.cmaketoolchain:enabled_blocks", check_type=list) if blocks is not None: try: new_blocks = OrderedDict((b, self._blocks[b]) for b in blocks) except KeyError as e: raise ConanException(f"Block {e} defined in tools.cmake.cmaketoolchain" f":enabled_blocks doesn't exist in {list(self._blocks.keys())}") self._blocks = new_blocks result = [] for b in self._blocks.values(): content = b.get_rendered_content() if content: result.append(content) return result ================================================ FILE: conan/tools/cmake/toolchain/toolchain.py ================================================ import os import textwrap from collections import OrderedDict from jinja2 import Template from conan.api.output import ConanOutput from conan.internal import check_duplicated_generator from conan.tools.build import use_win_mingw from conan.tools.cmake.presets import write_cmake_presets from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME from conan.tools.cmake.toolchain.blocks import (ExtraVariablesBlock, ToolchainBlocks, UserToolchain, GenericSystemBlock, AndroidSystemBlock, AppleSystemBlock, FPicBlock, ArchitectureBlock, GLibCXXBlock, VSRuntimeBlock, CppStdBlock, ParallelBlock, CMakeFlagsInitBlock, TryCompileBlock, FindFiles, PkgConfigBlock, SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock, CompilersBlock, LinkerScriptsBlock, VSDebuggerEnvironment, VariablesBlock, PreprocessorBlock, RpathLinkFlagsBlock) from conan.tools.cmake.utils import is_multi_configuration from conan.tools.env import VirtualBuildEnv, VirtualRunEnv from conan.tools.intel import IntelCC from conan.tools.microsoft import VCVars from conan.tools.microsoft.visual import vs_ide_version from conan.errors import ConanException from conan.internal.model.options import _PackageOption from conan.internal.graph.graph import RECIPE_CONSUMER, RECIPE_EDITABLE from conan.internal.util.files import save class Variables(OrderedDict): _configuration_types = None # Needed for py27 to avoid infinite recursion def __init__(self): super(Variables, self).__init__() self._configuration_types = {} def __getattribute__(self, config): try: return super(Variables, self).__getattribute__(config) except AttributeError: return self._configuration_types.setdefault(config, OrderedDict()) @property def configuration_types(self): # Reverse index for the configuration_types variables ret = OrderedDict() for conf, definitions in self._configuration_types.items(): for k, v in definitions.items(): ret.setdefault(k, []).append((conf, v)) return ret def quote_preprocessor_strings(self): for key, var in self.items(): if isinstance(var, str): self[key] = str(var).replace('"', '\\"') for config, data in self._configuration_types.items(): for key, var in data.items(): if isinstance(var, str): data[key] = str(var).replace('"', '\\"') class CMakeToolchain: filename = CONAN_TOOLCHAIN_FILENAME _template = textwrap.dedent("""\ # Conan automatically generated toolchain file # DO NOT EDIT MANUALLY, it will be overwritten # Avoid including toolchain file several times (bad if appending to variables like # CMAKE_CXX_FLAGS. See https://github.com/android/ndk/issues/323 include_guard() message(STATUS "Using Conan toolchain: ${CMAKE_CURRENT_LIST_FILE}") if(${CMAKE_VERSION} VERSION_LESS "3.15") message(FATAL_ERROR "The 'CMakeToolchain' generator only works with CMake >= 3.15") endif() {% for conan_block in conan_blocks %} {{ conan_block }} {% endfor %} if(CMAKE_POLICY_DEFAULT_CMP0091) # Avoid unused and not-initialized warnings endif() """) def __init__(self, conanfile, generator=None): self._conanfile = conanfile self.generator = self._get_generator(generator) self.variables = Variables() # This doesn't support multi-config, they go to the same configPreset common in multi-config self.cache_variables = {} self.preprocessor_definitions = Variables() self.extra_cxxflags = [] self.extra_cflags = [] self.extra_sharedlinkflags = [] self.extra_exelinkflags = [] self.add_rpath_link = False self.blocks = ToolchainBlocks(self._conanfile, self, [("user_toolchain", UserToolchain), ("generic_system", GenericSystemBlock), ("compilers", CompilersBlock), ("android_system", AndroidSystemBlock), ("apple_system", AppleSystemBlock), ("fpic", FPicBlock), ("arch_flags", ArchitectureBlock), ("linker_scripts", LinkerScriptsBlock), ("rpath_link_flags", RpathLinkFlagsBlock), ("libcxx", GLibCXXBlock), ("vs_runtime", VSRuntimeBlock), ("vs_debugger_environment", VSDebuggerEnvironment), ("cppstd", CppStdBlock), ("parallel", ParallelBlock), ("extra_flags", ExtraFlagsBlock), ("cmake_flags_init", CMakeFlagsInitBlock), ("extra_variables", ExtraVariablesBlock), ("try_compile", TryCompileBlock), ("find_paths", FindFiles), ("pkg_config", PkgConfigBlock), ("rpath", SkipRPath), ("shared", SharedLibBock), ("output_dirs", OutputDirsBlock), ("variables", VariablesBlock), ("preprocessor", PreprocessorBlock)]) # Set the CMAKE_MODULE_PATH and CMAKE_PREFIX_PATH to the deps .builddirs self.find_builddirs = True self.user_presets_path = "CMakeUserPresets.json" self.presets_prefix = "conan" self.presets_build_environment = None self.presets_run_environment = None self.absolute_paths = False # By default use relative paths to toolchain and presets def _context(self): """ Returns dict, the context for the template """ self.preprocessor_definitions.quote_preprocessor_strings() blocks = self.blocks.process_blocks() ctxt_toolchain = { "conan_blocks": blocks } return ctxt_toolchain @property def content(self): context = self._context() content = Template(self._template, trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True).render(**context) return content @property def is_multi_configuration(self): return is_multi_configuration(self.generator) def _find_cmake_exe(self): for req in self._conanfile.dependencies.direct_build.values(): if req.ref.name == "cmake": for bindir in req.cpp_info.bindirs: cmake_path = os.path.join(bindir, "cmake") cmake_exe_path = os.path.join(bindir, "cmake.exe") if os.path.exists(cmake_path): return cmake_path elif os.path.exists(cmake_exe_path): return cmake_exe_path def generate(self): """ This method will save the generated files to the conanfile.generators_folder """ check_duplicated_generator(self, self._conanfile) toolchain_file = self._conanfile.conf.get("tools.cmake.cmaketoolchain:toolchain_file") if toolchain_file is None: # The main toolchain file generated only if user dont define toolchain_file = self.filename save(os.path.join(self._conanfile.generators_folder, toolchain_file), self.content) ConanOutput(str(self._conanfile)).info(f"CMakeToolchain generated: {toolchain_file}") # If we're using Intel oneAPI, we need to generate the environment file and run it if self._conanfile.settings.get_safe("compiler") == "intel-cc": IntelCC(self._conanfile).generate() # Generators like Ninja or NMake requires an active vcvars elif self.generator is not None and "Visual" not in self.generator: VCVars(self._conanfile).generate() cache_variables = {} for name, value in self.cache_variables.items(): if isinstance(value, bool): cache_variables[name] = "ON" if value else "OFF" elif isinstance(value, _PackageOption): if str(value).lower() in ["true", "false", "none"]: cache_variables[name] = "ON" if bool(value) else "OFF" elif str(value).isdigit(): cache_variables[name] = int(value) else: cache_variables[name] = str(value) else: cache_variables[name] = value buildenv, runenv, cmake_executable = None, None, None if self._conanfile.conf.get("tools.cmake.cmaketoolchain:presets_environment", default="", check_type=str, choices=("disabled", "")) != "disabled": build_env = self.presets_build_environment.vars(self._conanfile) \ if self.presets_build_environment \ else VirtualBuildEnv(self._conanfile, auto_generate=True).vars() run_env = self.presets_run_environment.vars(self._conanfile) \ if self.presets_run_environment \ else VirtualRunEnv(self._conanfile, auto_generate=True).vars() buildenv = {name: value for name, value in build_env.items(variable_reference="$penv{{{name}}}")} runenv = {name: value for name, value in run_env.items(variable_reference="$penv{{{name}}}")} cmake_executable = self._conanfile.conf.get("tools.cmake:cmake_program", None) cmake_executable = cmake_executable or self._find_cmake_exe() user_presets = self.user_presets_path try: # TODO: Refactor this repeated pattern to deduce "is-consumer" # The user conf user_presets ONLY applies to dev space, not in the cache if self._conanfile._conan_node.recipe in (RECIPE_CONSUMER, RECIPE_EDITABLE): user_presets = self._conanfile.conf.get("tools.cmake.cmaketoolchain:user_presets", default=self.user_presets_path) except AttributeError: pass write_cmake_presets(self._conanfile, toolchain_file, self.generator, cache_variables, user_presets, self.presets_prefix, buildenv, runenv, cmake_executable, self.absolute_paths) def _get_generator(self, recipe_generator): # Returns the name of the generator to be used by CMake conanfile = self._conanfile # Downstream consumer always higher priority generator_conf = conanfile.conf.get("tools.cmake.cmaketoolchain:generator") if generator_conf: return generator_conf # second priority: the recipe one: if recipe_generator: return recipe_generator # if not defined, deduce automatically the default one compiler = conanfile.settings.get_safe("compiler") compiler_version = conanfile.settings.get_safe("compiler.version") cmake_years = {'8': '8 2005', '9': '9 2008', '10': '10 2010', '11': '11 2012', '12': '12 2013', '14': '14 2015', '15': '15 2017', '16': '16 2019', '17': '17 2022', '18': '18 2026'} if compiler == "msvc": if compiler_version is None: raise ConanException("compiler.version must be defined") vs_version = vs_ide_version(self._conanfile) return "Visual Studio %s" % cmake_years[vs_version] if use_win_mingw(conanfile): return "MinGW Makefiles" return "Unix Makefiles" ================================================ FILE: conan/tools/cmake/utils.py ================================================ from conan.errors import ConanException def is_multi_configuration(generator): if not generator: return False return "Visual" in generator or "Xcode" in generator or "Multi-Config" in generator def parse_extra_variable(source, key, value): CMAKE_CACHE_TYPES = ["BOOL", "FILEPATH", "PATH", "STRING", "INTERNAL"] if isinstance(value, str): return f"\"{value}\"" elif isinstance(value, (int, float)): return value elif isinstance(value, dict): var_value = parse_extra_variable(source, key, value.get("value")) is_force = value.get("force") if is_force: if not isinstance(is_force, bool): raise ConanException(f'{source} "{key}" "force" must be a boolean') is_cache = value.get("cache") if is_cache: if not isinstance(is_cache, bool): raise ConanException(f'{source} "{key}" "cache" must be a boolean') var_type = value.get("type") if not var_type: raise ConanException(f'{source} "{key}" needs "type" defined for cache variable') if var_type not in CMAKE_CACHE_TYPES: raise ConanException(f'{source} "{key}" invalid type "{var_type}" for cache variable.' f' Possible types: {", ".join(CMAKE_CACHE_TYPES)}') # Set docstring as variable name if not defined docstring = value.get("docstring") or key force_str = " FORCE" if is_force else "" # Support python < 3.11 return f"{var_value} CACHE {var_type} \"{docstring}\"{force_str}" else: if is_force: raise ConanException(f'{source} "{key}" "force" is only allowed for cache variables') return var_value raise ConanException(f'{source} "{key}" has invalid type. Allowed types: str, int, float, dict,' f' got {type(value)}') def cmake_escape_value(v): return v.replace('\\', '\\\\').replace('$', '\\$').replace('"', '\\"') ================================================ FILE: conan/tools/cps/__init__.py ================================================ from conan.tools.cps.cps_deps import CPSDeps ================================================ FILE: conan/tools/cps/cps_deps.py ================================================ from conan.cps.cps import CPS from conan.tools.files import save import json import os class CPSDeps: def __init__(self, conanfile): self._conanfile = conanfile def _config_name(self): build_vars = ["settings.compiler", "settings.compiler.version", "settings.arch", "settings.compiler.cppstd", "settings.build_type", "options.shared"] ret = [] for s in build_vars: group, var = s.split(".", 1) tmp = None if group == "settings": tmp = self._conanfile.settings.get_safe(var) elif group == "options": value = self._conanfile.options.get_safe(var) if value is not None: if var == "shared": tmp = "shared" if value else "static" else: tmp = "{}_{}".format(var, value) if tmp: ret.append(tmp.lower()) return "-".join(ret) def generate(self): cps_folder = os.path.join(self._conanfile.folders.base_build, "build", "cps") config_name = self._config_name() folder = os.path.join(cps_folder, config_name, "cps") # CMake wants a "/cps/" folder self._conanfile.output.info(f"[CPSDeps] folder {cps_folder}") deps = self._conanfile.dependencies.host.items() mapping = {} for _, dep in deps: self._conanfile.output.info(f"[CPSDeps]: dep {dep.ref.name}") cps_in_package = os.path.join(dep.package_folder, f"{dep.ref.name}.cps") if os.path.exists(cps_in_package): mapping[dep.ref.name] = cps_in_package continue cps = CPS.from_conan(dep) output_file = cps.save(folder) mapping[dep.ref.name] = output_file name = f"cpsmap-{config_name}.json" self._conanfile.output.info(f"Generating CPS mapping file: {name}") save(self._conanfile, os.path.join(cps_folder, name), json.dumps(mapping, indent=2)) ================================================ FILE: conan/tools/env/__init__.py ================================================ from conan.tools.env.environment import Environment, create_env_script, register_env_script from conan.tools.env.virtualbuildenv import VirtualBuildEnv from conan.tools.env.virtualrunenv import VirtualRunEnv ================================================ FILE: conan/tools/env/environment.py ================================================ import os import textwrap from shlex import quote from collections import OrderedDict from contextlib import contextmanager from conan.api.output import ConanOutput from conan.internal.subsystems import deduce_subsystem, WINDOWS, subsystem_path from conan.errors import ConanException from conan.internal.model.recipe_ref import ref_matches from conan.internal.util.files import save class _EnvVarPlaceHolder: pass def environment_wrap_command(conanfile, env_filenames, env_folder, cmd, subsystem=None, accepted_extensions=None): if not env_filenames: return cmd filenames = [env_filenames] if not isinstance(env_filenames, list) else env_filenames bats, shs, ps1s = [], [], [] accept = accepted_extensions or ("ps1", "bat", "sh") # TODO: This implemantation is dirty, improve it for f in filenames: f = f if os.path.isabs(f) else os.path.join(env_folder, f) if f.lower().endswith(".sh"): if os.path.isfile(f) and "sh" in accept: f = subsystem_path(subsystem, f) shs.append(f) elif f.lower().endswith(".bat"): if os.path.isfile(f) and "bat" in accept: bats.append(f) elif f.lower().endswith(".ps1") and "ps1" in accept: if os.path.isfile(f): ps1s.append(f) else: # Simple name like "conanrunenv" path_bat = "{}.bat".format(f) path_sh = "{}.sh".format(f) path_ps1 = "{}.ps1".format(f) if os.path.isfile(path_bat) and "bat" in accept: bats.append(path_bat) if os.path.isfile(path_ps1) and "ps1" in accept: ps1s.append(path_ps1) if os.path.isfile(path_sh) and "sh" in accept: path_sh = subsystem_path(subsystem, path_sh) shs.append(path_sh) if bool(bats + ps1s) + bool(shs) > 1: raise ConanException("Cannot wrap command with different envs," "{} - {}".format(bats+ps1s, shs)) powershell = conanfile.conf.get("tools.env.virtualenv:powershell") or "powershell.exe" powershell = "powershell.exe" if powershell is True else powershell if bats: launchers = " && ".join('"{}"'.format(b) for b in bats) if ps1s: ps1_launchers = f'{powershell} -Command "' + " ; ".join('&\'{}\''.format(f) for f in ps1s) + '"' cmd = cmd.replace('"', r'\"') return '{} && {} ; cmd /c "{}"'.format(launchers, ps1_launchers, cmd) else: return '{} && {}'.format(launchers, cmd) elif shs: launchers = " && ".join('. "{}"'.format(f) for f in shs) return '{} && {}'.format(launchers, cmd) elif ps1s: ps1_launchers = f'{powershell} -Command "' + " ; ".join('&\'{}\''.format(f) for f in ps1s) + '"' cmd = cmd.replace('"', r'\"') return '{} ; cmd /c "{}"'.format(ps1_launchers, cmd) else: return cmd class _EnvValue: def __init__(self, name, value=None, separator=" ", path=False): self._name = name self._values = [] if value is None else value if isinstance(value, list) else [value] self._path = path self._sep = separator def __bool__(self): return bool(self._values) # Empty means unset def dumps(self): result = [] path = "(path)" if self._path else "" sep = f"(sep={self._sep})" if self._sep != " " and not self._path else "" if not self._values: # Empty means unset result.append("{}=!".format(self._name)) elif _EnvVarPlaceHolder in self._values: index = self._values.index(_EnvVarPlaceHolder) for v in reversed(self._values[:index]): # Reverse to prepend result.append("{}=+{}{}{}".format(self._name, path, sep, v)) for v in self._values[index+1:]: result.append("{}+={}{}{}".format(self._name, path, sep, v)) else: append = "" for v in self._values: result.append("{}{}={}{}{}".format(self._name, append, path, sep, v)) append = "+" return "\n".join(result) def copy(self): return _EnvValue(self._name, self._values, self._sep, self._path) @property def is_path(self): return self._path def remove(self, value): self._values.remove(value) def append(self, value, separator=None): if separator is not None: self._sep = separator if isinstance(value, list): self._values.extend(value) else: self._values.append(value) def prepend(self, value, separator=None): if separator is not None: self._sep = separator if isinstance(value, list): self._values = value + self._values else: self._values.insert(0, value) def compose_env_value(self, other): """ :type other: _EnvValue """ try: index = self._values.index(_EnvVarPlaceHolder) except ValueError: # It doesn't have placeholder pass else: new_value = self._values[:] # do a copy new_value[index:index + 1] = other._values # replace the placeholder self._values = new_value def get_str(self, placeholder, subsystem, pathsep, root_path=None, script_path=None): """ :param subsystem: :param placeholder: a OS dependant string pattern of the previous env-var value like $PATH, %PATH%, et :param pathsep: The path separator, typically ; or : :param root_path: To do a relativize of paths, the base root path to be replaced :param script_path: the replacement instead of the script path :return: a string representation of the env-var value, including the $NAME-like placeholder """ values = [] for v in self._values: if v is _EnvVarPlaceHolder: if placeholder: values.append(placeholder.format(name=self._name)) else: if self._path: v = subsystem_path(subsystem, v) if root_path is not None: if v.startswith(root_path): # relativize v = v.replace(root_path, script_path, 1) elif os.sep == "\\": # Just in case user specified C:/path/to/somewhere r = root_path.replace("\\", "/") if v.startswith(r): v = v.replace(r, script_path.replace("\\", "/")) values.append(v) if self._path: return pathsep.join(values) return self._sep.join(values) def get_value(self, subsystem, pathsep): previous_value = os.getenv(self._name) return self.get_str(previous_value, subsystem, pathsep) def deploy_base_folder(self, package_folder, deploy_folder): """Make the path relative to the deploy_folder""" if not self._path: return for i, v in enumerate(self._values): if v is _EnvVarPlaceHolder: continue rel_path = os.path.relpath(v, package_folder) if rel_path.startswith(".."): # If it is pointing to a folder outside of the package, then do not relocate continue self._values[i] = os.path.join(deploy_folder, rel_path) def set_relative_base_folder(self, folder): if not self._path: return self._values = [os.path.join(folder, v) if v != _EnvVarPlaceHolder else v for v in self._values] class Environment: """ Generic class that helps to define modifications to the environment variables. """ def __init__(self): # It being ordered allows for Windows case-insensitive composition self._values = OrderedDict() # {var_name: [] of values, including separators} def __bool__(self): return bool(self._values) def copy(self): e = Environment() for k, v in self._values.items(): e._values[k] = v.copy() return e def __repr__(self): return repr(self._values) def dumps(self): """ :return: A string with a profile-like original definition, not the full environment values """ return "\n".join([v.dumps() for v in reversed(self._values.values())]) def define(self, name, value, separator=" "): """ Define `name` environment variable with value `value` :param name: Name of the variable :param value: Value that the environment variable will take :param separator: The character to separate appended or prepended values """ self._values[name] = _EnvValue(name, value, separator, path=False) def define_path(self, name, value): self._values[name] = _EnvValue(name, value, path=True) def unset(self, name): """ clears the variable, equivalent to a unset or set XXX= :param name: Name of the variable to unset """ self._values[name] = _EnvValue(name, None) def append(self, name, value, separator=None): """ Append the `value` to an environment variable `name` :param name: Name of the variable to append a new value :param value: New value :param separator: The character to separate the appended value with the previous value. By default it will use a blank space. """ self._values.setdefault(name, _EnvValue(name, _EnvVarPlaceHolder)).append(value, separator) def append_path(self, name, value): """ Similar to "append" method but indicating that the variable is a filesystem path. It will automatically handle the path separators depending on the operating system. :param name: Name of the variable to append a new value :param value: New value """ self._values.setdefault(name, _EnvValue(name, _EnvVarPlaceHolder, path=True)).append(value) def prepend(self, name, value, separator=None): """ Prepend the `value` to an environment variable `name` :param name: Name of the variable to prepend a new value :param value: New value :param separator: The character to separate the prepended value with the previous value """ self._values.setdefault(name, _EnvValue(name, _EnvVarPlaceHolder)).prepend(value, separator) def prepend_path(self, name, value): """ Similar to "prepend" method but indicating that the variable is a filesystem path. It will automatically handle the path separators depending on the operating system. :param name: Name of the variable to prepend a new value :param value: New value """ self._values.setdefault(name, _EnvValue(name, _EnvVarPlaceHolder, path=True)).prepend(value) def remove(self, name, value): """ Removes the `value` from the variable `name`. :param name: Name of the variable :param value: Value to be removed. """ self._values[name].remove(value) def compose_env(self, other): """ Compose an Environment object with another one. ``self`` has precedence, the "other" will add/append if possible and not conflicting, but ``self`` mandates what to do. If ``self`` has ``define()``, without placeholder, that will remain. :param other: the "other" Environment :type other: class:`Environment` """ for k, v in other._values.items(): existing = self._values.get(k) if existing is None: self._values[k] = v.copy() else: existing.compose_env_value(v) return self def __eq__(self, other): """ :param other: the "other" environment :type other: class:`Environment` """ return other._values == self._values def vars(self, conanfile, scope="build"): """ :param conanfile: Instance of a conanfile, usually ``self`` in a recipe :param scope: Determine the scope of the declared variables. :return: An EnvVars object from the current Environment object """ return EnvVars(conanfile, self._values, scope) def deploy_base_folder(self, package_folder, deploy_folder): """Make the paths relative to the deploy_folder""" for varvalues in self._values.values(): varvalues.deploy_base_folder(package_folder, deploy_folder) def set_relative_base_folder(self, folder): for v in self._values.values(): v.set_relative_base_folder(folder) class EnvVars: """ Represents an instance of environment variables for a given system. It is obtained from the generic Environment class. """ def __init__(self, conanfile, values, scope): self._values = values # {var_name: _EnvValue}, just a reference to the Environment self._conanfile = conanfile self._scope = scope self._subsystem = deduce_subsystem(conanfile, scope) self._deactivation_mode = conanfile.conf.get("tools.env:deactivation_mode", default=None, check_type=str) @property def _pathsep(self): return ":" if self._subsystem != WINDOWS else ";" def __getitem__(self, name): return self._values[name].get_value(self._subsystem, self._pathsep) def keys(self): return self._values.keys() def get(self, name, default=None, variable_reference=None): """ get the value of a env-var :param name: The name of the environment variable. :param default: The returned value if the variable doesn't exist, by default None. :param variable_reference: if specified, use a variable reference instead of the pre-existing value of environment variable, where {name} can be used to refer to the name of the variable. """ v = self._values.get(name) if v is None: return default if variable_reference: return v.get_str(variable_reference, self._subsystem, self._pathsep) else: return v.get_value(self._subsystem, self._pathsep) def items(self, variable_reference=None): """returns {str: str} (varname: value) :param variable_reference: if specified, use a variable reference instead of the pre-existing value of environment variable, where {name} can be used to refer to the name of the variable. """ if variable_reference: return {k: v.get_str(variable_reference, self._subsystem, self._pathsep) for k, v in self._values.items()}.items() else: return {k: v.get_value(self._subsystem, self._pathsep) for k, v in self._values.items()}.items() @contextmanager def apply(self): """ Context manager to apply the declared variables to the current ``os.environ`` restoring the original environment when the context ends. """ apply_vars = self.items() old_env = dict(os.environ) os.environ.update(apply_vars) try: yield finally: os.environ.clear() os.environ.update(old_env) def save_dotenv(self, file_location): result = [] for varname, varvalues in self._values.items(): value = varvalues.get_value(subsystem=self._subsystem, pathsep=self._pathsep) result.append('{}="{}"'.format(varname, value)) content = "\n".join(result) save(file_location, content) def save_bat(self, file_location, generate_deactivate=True): _, filename = os.path.split(file_location) deactivate_file = "deactivate_{}".format(filename) is_function = self._deactivation_mode == "function" deactivates_variable = f"_CONAN_{self._scope}_DEACTIVATES_DIR" dest_variable = f"%{deactivates_variable}%" if is_function else "%~dp0" function_preamble = textwrap.dedent(f""" set "local_defined=0" if defined {deactivates_variable} goto skip_deactivate_variable set "local_defined=1" set "{deactivates_variable}=%TEMP%\\conan_{self._scope}_%RANDOM%" mkdir "%{deactivates_variable}%" set "PATH=%{deactivates_variable}%;%PATH%" :skip_deactivate_variable """) if is_function else "" function_epilogue = textwrap.dedent(f""" if %local_defined% == 0 goto end echo set "PATH=%%PATH:%{deactivates_variable}%;=%%" >> "{dest_variable}/{deactivate_file}" echo set "{deactivates_variable}=">> "{dest_variable}/{deactivate_file}" :end """) if is_function else "" variables = " ".join(self._values.keys()) deactivate = textwrap.dedent(f"""\ @echo off {function_preamble} setlocal echo @echo off > "{dest_variable}/{deactivate_file}" echo echo Restoring environment for {filename} >> "{dest_variable}/{deactivate_file}" for %%v in ({variables}) do ( set foundenvvar= for /f "delims== tokens=1,2" %%a in ('set') do ( if /I "%%a" == "%%v" ( echo set "%%a=%%b">> "{dest_variable}/{deactivate_file}" set foundenvvar=1 ) ) if not defined foundenvvar ( echo set %%v=>> "{dest_variable}/{deactivate_file}" ) ) endlocal {function_epilogue} """) capture = textwrap.dedent("""\ @echo off chcp 65001 > nul {deactivate} """).format(deactivate=deactivate if generate_deactivate else "") result = [capture] abs_base_path, new_path = _relativize_paths(self._conanfile, "%~dp0") for varname, varvalues in self._values.items(): value = varvalues.get_str("%{name}%", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) no_value = varvalues.get_str("", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) if value != no_value: set_value = textwrap.dedent(f"""\ if defined {varname} ( set "{varname}={value}" ) else ( set "{varname}={no_value}" )""") else: set_value = f'set "{varname}={value}"' result.append(set_value) content = "\n".join(result) # It is very important to save it correctly with utf-8, the Conan util save() is broken os.makedirs(os.path.dirname(os.path.abspath(file_location)), exist_ok=True) with open(file_location, "w", encoding="utf-8") as f: f.write(content) def save_ps1(self, file_location, generate_deactivate=True): _, filename = os.path.split(file_location) result = [] if generate_deactivate: result.append(_ps1_deactivate_contents(self._deactivation_mode, self._values, filename)) abs_base_path, new_path = _relativize_paths(self._conanfile, "$PSScriptRoot") for varname, varvalues in self._values.items(): value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) no_value = varvalues.get_str("", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) if generate_deactivate and self._deactivation_mode == "function": # Check environment variable existence before saving value result.append( f'if ($env:{varname}) {{ $env:{_old_env_prefix(filename)}_{varname} = $env:{varname} }}' ) if varvalues: value = value.replace('"', '`"') # escape quotes no_value = no_value.replace('"', '`"') # escape quotes if value != no_value: set_value = textwrap.dedent(f"""\ if ($env:{varname}) {{ $env:{varname}="{value}" }} else {{ $env:{varname}="{no_value}" }} """) else: set_value = f'$env:{varname}="{value}"' result.append(set_value) else: result.append('if (Test-Path env:{0}) {{ Remove-Item env:{0} }}'.format(varname)) content = "\n".join(result) # It is very important to save it correctly with utf-16, the Conan util save() is broken # and powershell uses utf-16 files!!! os.makedirs(os.path.dirname(os.path.abspath(file_location)), exist_ok=True) with open(file_location, "w", encoding="utf-16") as f: f.write(content) def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) result = [] if generate_deactivate: result.append(_sh_deactivate_contents(self._deactivation_mode, self._values, filename)) abs_base_path, new_path = _relativize_paths(self._conanfile, "$script_folder") for varname, varvalues in self._values.items(): value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) placeholder = f"${varname}" sep = self._pathsep if varvalues._path else varvalues._sep # noqa if value.endswith(sep + placeholder): value = value.replace(sep + placeholder, f"${{{varname}:+{sep}${varname}}}", 1) elif (placeholder + sep) in value: value = value.replace(placeholder + sep, f"${{{varname}:-}}${{{varname}:+{sep}}}", 1) value = value.replace('"', '\\"') if generate_deactivate and self._deactivation_mode == "function": # Check environment variable existence before saving value result.append( f'if [ -n "${{{varname}+x}}" ]; then ' f'export {_old_env_prefix(filename)}_{varname}="${{{varname}}}"; ' f'fi;' ) if varvalues: result.append(f'export {varname}="{value}"') else: result.append(f'unset {varname}') content = "\n".join(result) content = f'script_folder="{os.path.abspath(filepath)}"\n' + content save(file_location, content) def save_script(self, filename): """ Saves a script file (bat, sh, ps1) with a launcher to set the environment. If the conf "tools.env.virtualenv:powershell" is not an empty string it will generate powershell launchers if Windows. :param filename: Name of the file to generate. If the extension is provided, it will generate the launcher script for that extension, otherwise the format will be deduced checking if we are running inside Windows (checking also the subsystem) or not. """ name, ext = os.path.splitext(filename) if ext: is_bat = ext == ".bat" is_ps1 = ext == ".ps1" else: # Need to deduce it automatically is_bat = self._subsystem == WINDOWS try: is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=bool) if is_ps1 is not None: ConanOutput().warning( "Boolean values for 'tools.env.virtualenv:powershell' are deprecated. " "Please specify 'powershell.exe' or 'pwsh' instead, appending arguments if needed " "(for example: 'powershell.exe -argument'). " "To unset this configuration, use `tools.env.virtualenv:powershell=!`, which matches " "the previous 'False' behavior.", warn_tag="deprecated" ) except ConanException: is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=str) if is_ps1: filename = filename + ".ps1" is_bat = False else: filename = filename + (".bat" if is_bat else ".sh") path = os.path.join(self._conanfile.generators_folder, filename) if is_bat: self.save_bat(path) elif is_ps1: self.save_ps1(path) else: self.save_sh(path) if self._conanfile.conf.get("tools.env:dotenv", check_type=bool): bt = self._conanfile.settings.get_safe("build_type") arch = self._conanfile.settings.get_safe("arch") name = name.replace(bt.lower(), bt) if bt else name name = name.replace(arch.lower(), arch) if arch else name ConanOutput().warning(f"Creating dotenv file: {name}.env\n" "Files generated with absolute paths, not interpolated.\n" "When https://github.com/microsoft/vscode-cpptools/issues/13781 " "solved, it will get interpolation", warn_tag="experimental") self.save_dotenv(f"{name}.env") if self._scope: register_env_script(self._conanfile, path, self._scope) def _deactivate_func_name(filename): return os.path.splitext(os.path.basename(filename))[0].replace("-", "_") def _old_env_prefix(filename): return f"_CONAN_OLD_{_deactivate_func_name(filename).upper()}" def _ps1_deactivate_contents(deactivation_mode, values, filename): vars_list = ", ".join(f'"{v}"' for v in values.keys()) if deactivation_mode == "function": var_prefix = _old_env_prefix(filename) func_name = _deactivate_func_name(filename) return textwrap.dedent(f"""\ function global:deactivate_{func_name} {{ Write-Host "Restoring environment" foreach ($v in @({vars_list})) {{ $oldVarName = "{var_prefix}_$v" $oldValue = Get-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue if (Test-Path env:$oldValue) {{ Remove-Item -Path "Env:$v" -ErrorAction SilentlyContinue }} else {{ Set-Item -Path "Env:$v" -Value $oldValue.Value }} Remove-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue }} Remove-Item -Path function:deactivate_{func_name} -ErrorAction SilentlyContinue }} """) deactivate_file = "deactivate_{}".format(filename) return textwrap.dedent(f"""\ Push-Location $PSScriptRoot "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}" $vars = (Get-ChildItem env:*).name $updated_vars = @({vars_list}) foreach ($var in $updated_vars) {{ if ($var -in $vars) {{ $var_value = (Get-ChildItem env:$var).value Add-Content "{deactivate_file}" "`n`$env:$var = `"$var_value`"" }} else {{ Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}" }} }} Pop-Location """) def _sh_deactivate_contents(deactivation_mode, values, filename): vars_list = " ".join(quote(v) for v in values.keys()) if deactivation_mode == "function": func_name = _deactivate_func_name(filename) return textwrap.dedent(f"""\ # sh-like function to restore environment deactivate_{func_name} () {{ echo "Restoring environment" for v in {vars_list}; do old_var="{_old_env_prefix(filename)}_${{v}}" # Use eval for indirect expansion (POSIX safe) eval "is_set=\\${{${{old_var}}+x}}" if [ -n "${{is_set}}" ]; then eval "old_value=\\${{${{old_var}}}}" eval "export ${{v}}=\\${{old_value}}" else unset "${{v}}" fi unset "${{old_var}}" done unset -f deactivate_{func_name} }} """) deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) return textwrap.dedent(f"""\ echo "echo Restoring environment" > "{deactivate_file}" for v in {vars_list} do is_defined="true" value=$(printenv $v) || is_defined="" || true if [ -n "$value" ] || [ -n "$is_defined" ] then echo export "$v='$value'" >> "{deactivate_file}" else echo unset $v >> "{deactivate_file}" fi done """) class ProfileEnvironment: def __init__(self): self._environments = OrderedDict() def __repr__(self): return repr(self._environments) def __bool__(self): return bool(self._environments) def get_profile_env(self, ref, is_consumer=False): """ computes package-specific Environment it is only called when conanfile.buildenv is called the last one found in the profile file has top priority """ result = Environment() for pattern, env in self._environments.items(): if pattern is None or ref_matches(ref, pattern, is_consumer): # Latest declared has priority, copy() necessary to not destroy data result = env.copy().compose_env(result) return result def update_profile_env(self, other): """ :type other: ProfileEnvironment :param other: The argument profile has priority/precedence over the current one. """ for pattern, environment in other._environments.items(): existing = self._environments.get(pattern) if existing is not None: self._environments[pattern] = environment.compose_env(existing) else: self._environments[pattern] = environment def dumps(self): result = [] for pattern, env in self._environments.items(): if pattern is None: result.append(env.dumps()) else: result.append("\n".join("{}:{}".format(pattern, line) if line else "" for line in env.dumps().splitlines())) if result: result.append("") return "\n".join(result) @staticmethod def loads(text): result = ProfileEnvironment() for line in text.splitlines(): line = line.strip() if not line or line.startswith("#"): continue for op, method in (("+=", "append"), ("=+", "prepend"), ("=!", "unset"), ("=", "define")): tokens = line.split(op, 1) if len(tokens) != 2: continue pattern_name, value = tokens pattern_name = pattern_name.split(":", 1) if len(pattern_name) == 2: pattern, name = pattern_name else: pattern, name = None, pattern_name[0] # strip whitespaces before/after = # values are not strip() unless they are a path, to preserve potential whitespaces name = name.strip() # When loading from profile file, latest line has priority env = Environment() if method == "unset": env.unset(name) elif value.strip().startswith("(sep="): value = value.strip() sep = value[5] value = value[7:] if value.strip().startswith("(path)"): msg = f"Cannot use (sep) and (path) qualifiers simultaneously: {line}" raise ConanException(msg) getattr(env, method)(name, value, separator=sep) else: if value.strip().startswith("(path)"): value = value.strip() value = value[6:] method = method + "_path" getattr(env, method)(name, value) existing = result._environments.get(pattern) if existing is None: result._environments[pattern] = env else: result._environments[pattern] = env.compose_env(existing) break else: raise ConanException("Bad env definition: {}".format(line)) return result def create_env_script(conanfile, content, filename, scope="build"): """ Create a file with any content which will be registered as a new script for the defined "scope". Args: conanfile: The Conanfile instance. content (str): The content of the script to write into the file. filename (str): The name of the file to be created in the generators folder. scope (str): The scope or environment group for which the script will be registered. """ path = os.path.join(conanfile.generators_folder, filename) save(path, content) if scope: register_env_script(conanfile, path, scope) def register_env_script(conanfile, env_script_path, scope="build"): """ Add the "env_script_path" to the current list of registered scripts for defined "scope" These will be mapped to files: - conan{group}.bat|sh = calls env_script_path1,... env_script_pathN Args: conanfile: The Conanfile instance. env_script_path (str): The full path of the script to register. scope (str): The scope ('build' or 'host') for which the script will be registered. """ existing = conanfile.env_scripts.setdefault(scope, []) if env_script_path not in existing: existing.append(env_script_path) def generate_aggregated_env(conanfile): def deactivates(filenames): # FIXME: Probably the order needs to be reversed result = [] for s in reversed(filenames): folder, f = os.path.split(s) result.append(os.path.join(folder, "deactivate_{}".format(f))) return result def deactivate_function_names(filenames): return [os.path.splitext(os.path.basename(s))[0].replace("-", "_") for s in reversed(filenames)] deactivation_mode = conanfile.conf.get("tools.env:deactivation_mode", default=None, check_type=str) generated = [] for group, env_scripts in conanfile.env_scripts.items(): subsystem = deduce_subsystem(conanfile, group) bats = [] shs = [] ps1s = [] for env_script in env_scripts: path = os.path.join(conanfile.generators_folder, env_script) # Only the .bat and .ps1 are made relative to current script if env_script.endswith(".bat"): path = os.path.relpath(path, conanfile.generators_folder) bats.append("%~dp0/"+path) elif env_script.endswith(".sh"): shs.append(subsystem_path(subsystem, path)) elif env_script.endswith(".ps1"): path = os.path.relpath(path, conanfile.generators_folder) # This $PSScriptRoot uses the current script directory ps1s.append("$PSScriptRoot/"+path) if shs: def sh_content(files): content = ". " + " && . ".join('"{}"'.format(s) for s in files) if deactivation_mode == "function": content += f"\n\ndeactivate_conan{group}() {{\n" for deactivate_name in deactivate_function_names(shs): content += f" deactivate_{deactivate_name}\n" content += f" unset -f deactivate_conan{group}\n}}\n" return content filename = "conan{}.sh".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), sh_content(shs)) if not deactivation_mode: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), sh_content(deactivates(shs))) if bats: filename = f"conan{group}.bat" deactivate_filename = f"deactivate_{filename}" def bat_content(files): content = ["@echo off"] if deactivation_mode == "function": from conan.tools.microsoft.visual import CONAN_VCVARS deactivates_var = f"_CONAN_{group}_DEACTIVATES_DIR" content += [ f'set "{deactivates_var}=%TEMP%\\conan_{group}_%RANDOM%"', f'mkdir "%{deactivates_var}%"' ] # TODO: Find a better way to get rid of vcvars deactivation f = [f for f in files if f != f"%~dp0/{CONAN_VCVARS}.bat"] deactivate_filenames = [f.replace("%~dp0\\", "") for f in deactivates(f)] content += [f'set PATH=%{deactivates_var}%;%PATH%'] content += [f'echo @echo off > "%{deactivates_var}%\\{deactivate_filename}"'] content += [f'echo call "{b}" >> "%{deactivates_var}%\\{deactivate_filename}"' for b in deactivate_filenames] # See https://ss64.com/nt/syntax-replace.html for the syntax below to remove # the deactivation path from PATH when the deactivation script is called content += [f'echo set "PATH=%%PATH:%{deactivates_var}%;=%%" >> ' f'"%{deactivates_var}%\\{deactivate_filename}"'] content += [f'echo set "{deactivates_var}=" >> ' f'"%{deactivates_var}%\\{deactivate_filename}"'] content += [f'call "{b}"' for b in files] return "\r\n".join(content) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), bat_content(bats)) if not deactivation_mode: save(os.path.join(conanfile.generators_folder, deactivate_filename), bat_content(deactivates(bats))) if ps1s: def ps1_content(files): content = "\r\n".join(['& "{}"'.format(b) for b in files]) if deactivation_mode == "function": content += f"\n\nfunction global:deactivate_conan{group} {{\n" for deactivate_name in deactivate_function_names(ps1s): content += f" deactivate_{deactivate_name}\n" content += (f" Remove-Item -Path function:deactivate_conan{group} " "-ErrorAction SilentlyContinue" "\n}\n") return content filename = "conan{}.ps1".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), ps1_content(ps1s)) if not deactivation_mode: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), ps1_content(deactivates(ps1s))) if generated: conanfile.output.highlight("Generating aggregated env files") conanfile.output.info(f"Generated aggregated env files: {generated}") def _relativize_paths(conanfile, placeholder): abs_base_path = conanfile.folders._base_generators # noqa if not abs_base_path or not os.path.isabs(abs_base_path): return None, None abs_base_path = os.path.join(abs_base_path, "") # For the trailing / to dissambiguate matches generators_folder = conanfile.generators_folder try: rel_path = os.path.relpath(abs_base_path, generators_folder) except ValueError: # In case the unit in Windows is different, path cannot be made relative return None, None new_path = placeholder if rel_path == "." else os.path.join(placeholder, rel_path) new_path = os.path.join(new_path, "") # For the trailing / to dissambiguate matches return abs_base_path, new_path ================================================ FILE: conan/tools/env/virtualbuildenv.py ================================================ from conan.internal import check_duplicated_generator from conan.tools.env import Environment from conan.tools.env.virtualrunenv import runenv_from_cpp_info class VirtualBuildEnv: """ Calculates the environment variables of the build time context and produces a conanbuildenv .bat or .sh script """ def __init__(self, conanfile, auto_generate=False): self._buildenv = None self._conanfile = conanfile if not auto_generate: self._conanfile.virtualbuildenv = False self.basename = "conanbuildenv" self.configuration = None self.arch = None @property def _filename(self): if not self.configuration: # TODO: Make this use the settings_build configuration = self._conanfile.settings.get_safe("build_type") configuration = configuration.lower() if configuration else None else: configuration = self.configuration if not self.arch: arch = self._conanfile.settings.get_safe("arch") arch = arch.lower() if arch else None else: arch = self.arch f = self.basename if configuration: f += "-" + configuration.replace(".", "_") if arch: f += "-" + arch.replace(".", "_").replace("|", "_") return f def environment(self): """ Returns an ``Environment`` object containing the environment variables of the build context. :return: an ``Environment`` object instance containing the obtained variables. """ if self._buildenv is None: self._buildenv = Environment() else: return self._buildenv # Top priority: profile profile_env = self._conanfile.buildenv self._buildenv.compose_env(profile_env) build_requires = self._conanfile.dependencies.build.topological_sort for require, build_require in reversed(build_requires.items()): if require.direct: # Only buildenv_info from direct deps is propagated # higher priority, explicit buildenv_info if build_require.buildenv_info: self._buildenv.compose_env(build_require.buildenv_info) # Lower priority, the runenv of all transitive "requires" of the build requires if build_require.runenv_info: self._buildenv.compose_env(build_require.runenv_info) # Then the implicit os_name = self._conanfile.settings_build.get_safe("os") self._buildenv.compose_env(runenv_from_cpp_info(build_require, os_name)) # Requires in host context can also bring some direct buildenv_info host_requires = self._conanfile.dependencies.host.topological_sort for require in reversed(host_requires.values()): if require.buildenv_info: self._buildenv.compose_env(require.buildenv_info) return self._buildenv def vars(self, scope="build"): """ :param scope: Scope to be used. :return: An ``EnvVars`` instance containing the computed environment variables. """ return self.environment().vars(self._conanfile, scope=scope) def generate(self, scope="build"): """ Produces the launcher scripts activating the variables for the build context. :param scope: Scope to be used. """ check_duplicated_generator(self, self._conanfile) build_env = self.environment() build_env.vars(self._conanfile, scope=scope).save_script(self._filename) ================================================ FILE: conan/tools/env/virtualrunenv.py ================================================ import os from conan.internal import check_duplicated_generator from conan.tools.env import Environment def runenv_from_cpp_info(dep, os_name): """ return an Environment deducing the runtime information from a cpp_info """ dyn_runenv = Environment() cpp_info = dep.cpp_info.aggregated_components() def _prepend_path(envvar, paths): existing = [p for p in paths if os.path.exists(p)] if paths else None if existing: dyn_runenv.prepend_path(envvar, existing) _prepend_path("PATH", cpp_info.bindirs) # If it is a build_require this will be the build-os, otherwise it will be the host-os if os_name and not os_name.startswith("Windows"): _prepend_path("LD_LIBRARY_PATH", cpp_info.libdirs) _prepend_path("DYLD_LIBRARY_PATH", cpp_info.libdirs) _prepend_path("DYLD_FRAMEWORK_PATH", cpp_info.frameworkdirs) return dyn_runenv class VirtualRunEnv: """ Calculates the environment variables of the runtime context and produces a conanrunenv .bat or .sh script """ def __init__(self, conanfile, auto_generate=False): """ :param conanfile: The current recipe object. Always use ``self``. """ self._runenv = None self._conanfile = conanfile if not auto_generate: self._conanfile.virtualrunenv = False self.basename = "conanrunenv" self.configuration = conanfile.settings.get_safe("build_type") if self.configuration: self.configuration = self.configuration.lower() self.arch = conanfile.settings.get_safe("arch") if self.arch: self.arch = self.arch.lower() @property def _filename(self): f = self.basename if self.configuration: f += "-" + self.configuration.replace(".", "_") if self.arch: f += "-" + self.arch.replace(".", "_").replace("|", "_") return f def environment(self): """ Returns an ``Environment`` object containing the environment variables of the run context. :return: an ``Environment`` object instance containing the obtained variables. """ if self._runenv is None: self._runenv = Environment() else: return self._runenv # Top priority: profile profile_env = self._conanfile.runenv self._runenv.compose_env(profile_env) host_req = self._conanfile.dependencies.host test_req = self._conanfile.dependencies.test for require, dep in list(host_req.items()) + list(test_req.items()): if dep.runenv_info: self._runenv.compose_env(dep.runenv_info) if require.run: # Only if the require is run (shared or application to be run) _os = self._conanfile.settings.get_safe("os") self._runenv.compose_env(runenv_from_cpp_info(dep, _os)) return self._runenv def vars(self, scope="run"): """ :param scope: Scope to be used. :return: An ``EnvVars`` instance containing the computed environment variables. """ return self.environment().vars(self._conanfile, scope=scope) def generate(self, scope="run"): """ Produces the launcher scripts activating the variables for the run context. :param scope: Scope to be used. """ check_duplicated_generator(self, self._conanfile) run_env = self.environment() run_env.vars(self._conanfile, scope=scope).save_script(self._filename) ================================================ FILE: conan/tools/files/__init__.py ================================================ from conan.tools.files.files import load, save, mkdir, rmdir, rm, ftp_download, download, get, \ rename, chdir, unzip, replace_in_file, collect_libs, check_md5, check_sha1, check_sha256, \ move_folder_contents, chmod from conan.tools.files.patches import patch, apply_conandata_patches, export_conandata_patches from conan.tools.files.symlinks import symlinks from conan.tools.files.copy_pattern import copy from conan.tools.files.conandata import update_conandata, trim_conandata ================================================ FILE: conan/tools/files/conandata.py ================================================ import os import yaml from conan.errors import ConanException from conan.internal.util.files import load, save def update_conandata(conanfile, data): """ Tool to modify the ``conandata.yml`` once it is exported. It can be used, for example: - To add additional data like the "commit" and "url" for the scm. - To modify the contents cleaning the data that belong to other versions (different from the exported) to avoid changing the recipe revision when the changed data doesn't belong to the current version. :param conanfile: The current recipe object. Always use ``self``. :param data: (Required) A dictionary (can be nested), of values to update """ if not hasattr(conanfile, "export_folder") or conanfile.export_folder is None: raise ConanException("The 'update_conandata()' can only be used in the 'export()' method") path = os.path.join(conanfile.export_folder, "conandata.yml") if os.path.exists(path): conandata = load(path) conandata = yaml.safe_load(conandata) else: # File doesn't exist, create it conandata = {} def recursive_dict_update(d, u): for k, v in u.items(): if isinstance(v, dict): d[k] = recursive_dict_update(d.get(k, {}), v) else: d[k] = v return d recursive_dict_update(conandata, data) new_content = yaml.safe_dump(conandata) save(path, new_content) def trim_conandata(conanfile, raise_if_missing=True): """ Tool to modify the ``conandata.yml`` once it is exported, to limit it to the current version only """ if not hasattr(conanfile, "export_folder") or conanfile.export_folder is None: raise ConanException("The 'trim_conandata()' tool can only be used in " "the 'export()' method or 'post_export()' hook") path = os.path.join(conanfile.export_folder, "conandata.yml") if not os.path.exists(path): if raise_if_missing: raise ConanException("conandata.yml file doesn't exist") else: conanfile.output.warning("conandata.yml file doesn't exist") return conandata = load(path) conandata = yaml.safe_load(conandata) version = str(conanfile.version) result = {} for k, v in conandata.items(): if k == "scm" or not isinstance(v, dict): result[k] = v continue # to allow user extra conandata, common to all versions version_data = v.get(version) if version_data is not None: result[k] = {version: version_data} # Update the internal conanfile data too conanfile.conan_data = result new_conandata_yml = yaml.safe_dump(result, default_flow_style=False) save(path, new_conandata_yml) ================================================ FILE: conan/tools/files/copy_pattern.py ================================================ import filecmp import fnmatch import os import shutil from conan.errors import ConanException from conan.internal.util.files import mkdir def copy(conanfile, pattern, src, dst, keep_path=True, excludes=None, ignore_case=True, overwrite_equal=False): """ Copy the files matching the pattern (fnmatch) at the src folder to a dst folder. :param conanfile: The current recipe object. Always use ``self``. :param pattern: (Required) An fnmatch file pattern of the files that should be copied. It must not start with ``..`` relative path or an exception will be raised. :param src: (Required) Source folder in which those files will be searched. This folder will be stripped from the dst parameter. E.g., lib/Debug/x86. :param dst: (Required) Destination local folder. It must be different from src value or an exception will be raised. :param keep_path: (Optional, defaulted to ``True``) Means if you want to keep the relative path when you copy the files from the src folder to the dst one. :param excludes: (Optional, defaulted to ``None``) A tuple/list of fnmatch patterns or even a single one to be excluded from the copy. :param ignore_case: (Optional, defaulted to ``True``) If enabled, it will do a case-insensitive pattern matching. will do a case-insensitive pattern matching when ``True`` :param overwrite_equal: (Optional, default ``False``). If the file to be copied already exists in the destination folder, only really copy it if it seems different (different size, different modification time) :return: list of copied files """ if src == dst: raise ConanException("copy() 'src' and 'dst' arguments must have different values") if pattern.startswith(".."): raise ConanException("copy() it is not possible to use relative patterns starting with '..'") if src is None: raise ConanException("copy() received 'src=None' argument") # This is necessary to add the trailing / so it is not reported as symlink src = os.path.join(src, "") excluded_folder = dst files_to_copy, files_symlinked_to_folders = _filter_files(src, pattern, excludes, ignore_case, excluded_folder) copied_files = _copy_files(files_to_copy, src, dst, keep_path, overwrite_equal) copied_files.extend(_copy_files_symlinked_to_folders(files_symlinked_to_folders, src, dst)) if conanfile: # Some usages still pass None copied = '\n '.join(files_to_copy) conanfile.output.debug(f"copy(pattern={pattern}) copied {len(copied_files)} files\n" f" from {src}\n" f" to {dst}\n" f" Files:\n {copied}") return copied_files def _filter_files(src, pattern, excludes, ignore_case, excluded_folder): """ return a list of the files matching the patterns The list will be relative path names wrt to the root src folder """ filenames = [] files_symlinked_to_folders = [] pattern = pattern.lower() if ignore_case else pattern if excludes: if not isinstance(excludes, (tuple, list)): excludes = (excludes, ) if ignore_case: excludes = [e.lower() for e in excludes] else: excludes = [] for root, subfolders, files in os.walk(src): if root == excluded_folder: subfolders[:] = [] continue # Check if any of the subfolders is a symlink for subfolder in subfolders: if os.path.islink(os.path.join(root, subfolder)): relative_path = os.path.relpath(os.path.join(root, subfolder), src) compare_relative_path = relative_path.lower() if ignore_case else relative_path if fnmatch.fnmatch(os.path.normpath(compare_relative_path), pattern): files_symlinked_to_folders.append(relative_path) relative_path = os.path.relpath(root, src) compare_relative_path = relative_path.lower() if ignore_case else relative_path # Don't try to exclude the start folder, it conflicts with excluding names starting with dots if not compare_relative_path == ".": for exclude in excludes: if fnmatch.fnmatch(compare_relative_path, exclude): subfolders[:] = [] files = [] break for f in files: relative_name = os.path.normpath(os.path.join(relative_path, f)) filenames.append(relative_name) if ignore_case: files_to_copy = [n for n in filenames if fnmatch.fnmatch(os.path.normpath(n.lower()), pattern)] else: files_to_copy = [n for n in filenames if fnmatch.fnmatchcase(os.path.normpath(n), pattern)] for exclude in excludes: if ignore_case: files_to_copy = [f for f in files_to_copy if not fnmatch.fnmatch(f.lower(), exclude)] files_symlinked_to_folders =\ [f for f in files_symlinked_to_folders if not fnmatch.fnmatch(f.lower(), exclude)] else: files_to_copy = [f for f in files_to_copy if not fnmatch.fnmatchcase(f, exclude)] files_symlinked_to_folders =\ [f for f in files_symlinked_to_folders if not fnmatch.fnmatchcase(f, exclude)] return files_to_copy, files_symlinked_to_folders def _copy_files(files, src, dst, keep_path, overwrite_equal): """ executes a multiple file copy from [(src_file, dst_file), (..)] managing symlinks if necessary """ copied_files = [] for filename in files: abs_src_name = os.path.join(src, filename) filename = filename if keep_path else os.path.basename(filename) abs_dst_name = os.path.normpath(os.path.join(dst, filename)) parent_folder = os.path.dirname(abs_dst_name) if parent_folder: # There are cases where this folder will be empty for relative paths os.makedirs(parent_folder, exist_ok=True) if os.path.islink(abs_src_name): linkto = os.readlink(abs_src_name) try: os.remove(abs_dst_name) except OSError: pass os.symlink(linkto, abs_dst_name) else: # Avoid the copy if the file exists and has the exact same signature (size + mod time) if overwrite_equal or not os.path.exists(abs_dst_name) \ or not filecmp.cmp(abs_src_name, abs_dst_name): shutil.copy2(abs_src_name, abs_dst_name) copied_files.append(abs_dst_name) return copied_files def _copy_files_symlinked_to_folders(files_symlinked_to_folders, src, dst): """Copy the files that are symlinks to folders from src to dst. The files are already filtered with the specified pattern""" copied_files = [] for relative_path in files_symlinked_to_folders: abs_path = os.path.join(src, relative_path) symlink_path = os.path.join(dst, relative_path) # We create the same symlink in dst, no matter if it is absolute or relative link_dst = os.readlink(abs_path) # This could be perfectly broken # Create the parent directory that will contain the symlink file mkdir(os.path.dirname(symlink_path)) # If the symlink is already there, remove it (multiple copy(*.h) copy(*.dll)) if os.path.islink(symlink_path): os.unlink(symlink_path) os.symlink(link_dst, symlink_path) copied_files.append(symlink_path) return copied_files ================================================ FILE: conan/tools/files/files.py ================================================ import gzip import os import stat import platform import shutil import subprocess from typing import Optional from contextlib import contextmanager from fnmatch import fnmatch from shutil import which from conan.internal.rest.caching_file_downloader import SourcesCachingDownloader from conan.errors import ConanException from conan.internal.rest.file_uploader import FileProgress from conan.internal.util.files import rmdir as _internal_rmdir, human_size, check_with_algorithm_sum def load(conanfile, path, encoding="utf-8"): """ Utility function to load files in one line. It will manage the open and close of the file, and load binary encodings. Returns the content of the file. :param conanfile: The current recipe object. Always use ``self``. :param path: Path to the file to read :param encoding: (Optional, Defaulted to ``utf-8``): Specifies the input file text encoding. :return: The contents of the file """ with open(path, "r", encoding=encoding, newline="") as handle: tmp = handle.read() return tmp def save(conanfile, path, content, append=False, encoding="utf-8"): """ Utility function to save files in one line. It will manage the open and close of the file and creating directories if necessary. :param conanfile: The current recipe object. Always use ``self``. :param path: Path of the file to be created. :param content: Content (str or bytes) to be write to the file. :param append: (Optional, Defaulted to False): If ``True`` the contents will be appended to the existing one. :param encoding: (Optional, Defaulted to utf-8): Specifies the output file text encoding. """ dir_path = os.path.dirname(path) if dir_path: os.makedirs(dir_path, exist_ok=True) with open(path, "a" if append else "w", encoding=encoding, newline="") as handle: handle.write(content) def mkdir(conanfile, path): """ Utility functions to create a directory. The existence of the specified directory is checked, so mkdir() will do nothing if the directory already exists. :param conanfile: The current recipe object. Always use ``self``. :param path: Path to the folder to be created. """ if os.path.exists(path): return os.makedirs(path) def rmdir(conanfile, path): _internal_rmdir(path) def rm(conanfile, pattern, folder, recursive=False, excludes=None): """ Utility functions to remove files matching a ``pattern`` in a ``folder``. :param conanfile: The current recipe object. Always use ``self``. :param pattern: Pattern that the files to be removed have to match (fnmatch). :param folder: Folder to search/remove the files. :param recursive: If ``recursive`` is specified it will search in the subfolders. :param excludes: (Optional, defaulted to None) A tuple/list of fnmatch patterns or even a single one to be excluded from the remove pattern. """ if excludes and not isinstance(excludes, (tuple, list)): excludes = (excludes,) elif not excludes: excludes = [] for root, _, filenames in os.walk(folder): for filename in filenames: if fnmatch(filename, pattern) and not any(fnmatch(filename, it) for it in excludes): fullname = os.path.join(root, filename) os.unlink(fullname) if not recursive: break def get(conanfile, url, md5=None, sha1=None, sha256=None, destination=".", filename="", keep_permissions=False, pattern=None, verify=True, retry=None, retry_wait=None, auth=None, headers=None, strip_root=False, extract_filter=None, excludes=None): """ High level download and decompressing of a tgz, zip or other compressed format file. Just a high level wrapper for download, unzip, and remove the temporary zip file once unzipped. You can pass hash checking parameters: ``md5``, ``sha1``, ``sha256``. All the specified algorithms will be checked. If any of them doesn't match, it will raise a ``ConanException``. :param conanfile: The current recipe object. Always use ``self``. :param destination: (Optional defaulted to ``.``) Destination folder :param filename: (Optional defaulted to '') If provided, the saved file will have the specified name, otherwise it is deduced from the URL :param url: forwarded to ``tools.file.download()``. :param md5: forwarded to ``tools.file.download()``. :param sha1: forwarded to ``tools.file.download()``. :param sha256: forwarded to ``tools.file.download()``. :param keep_permissions: forwarded to ``tools.file.unzip()``. :param pattern: forwarded to ``tools.file.unzip()``. :param verify: forwarded to ``tools.file.download()``. :param retry: forwarded to ``tools.file.download()``. :param retry_wait: S forwarded to ``tools.file.download()``. :param auth: forwarded to ``tools.file.download()``. :param headers: forwarded to ``tools.file.download()``. :param strip_root: forwarded to ``tools.file.unzip()``. :param extract_filter: forwarded to ``tools.file.unzip()``. :param excludes: forwarded to ``tools.file.unzip()``. """ if not filename: # deduce filename from the URL url_base = url[0] if isinstance(url, (list, tuple)) else url if "?" in url_base or "=" in url_base: raise ConanException("Cannot deduce file name from the url: '{}'. Use 'filename' " "parameter.".format(url_base)) filename = os.path.basename(url_base) download(conanfile, url, filename, verify=verify, retry=retry, retry_wait=retry_wait, auth=auth, headers=headers, md5=md5, sha1=sha1, sha256=sha256) unzip(conanfile, filename, destination=destination, keep_permissions=keep_permissions, pattern=pattern, strip_root=strip_root, extract_filter=extract_filter, excludes=excludes) os.unlink(filename) def ftp_download(conanfile, host, filename, login='', password='', secure=False): """ Ftp download of a file. Retrieves a file from an FTP server. :param conanfile: The current recipe object. Always use ``self``. :param host: IP or host of the FTP server. :param filename: Path to the file to be downloaded. :param login: Authentication login. :param password: Authentication password. :param secure: Set to True to use FTP over TLS/SSL (FTPS). Defaults to False for regular FTP. """ # TODO: Check if we want to join this method with download() one, based on ftp:// protocol # this has been requested by some users, but the hash is a bit divergent import ftplib ftp = None try: if secure: ftp = ftplib.FTP_TLS(host) ftp.prot_p() else: ftp = ftplib.FTP(host) ftp.login(login, password) filepath, filename = os.path.split(filename) if filepath: ftp.cwd(filepath) with open(filename, 'wb') as f: ftp.retrbinary('RETR ' + filename, f.write) except Exception as e: try: os.unlink(filename) except OSError: pass raise ConanException("Error in FTP download from %s\n%s" % (host, str(e))) finally: if ftp: ftp.quit() def download(conanfile, url, filename, verify=True, retry=None, retry_wait=None, auth=None, headers=None, md5=None, sha1=None, sha256=None): """ Retrieves a file from a given URL into a file with a given filename. It uses certificates from a list of known verifiers for https downloads, but this can be optionally disabled. You can pass hash checking parameters: ``md5``, ``sha1``, ``sha256``. All the specified algorithms will be checked. If any of them doesn’t match, the downloaded file will be removed and it will raise a ``ConanException``. :param conanfile: The current recipe object. Always use ``self``. :param url: URL to download. It can be a list, which only the first one will be downloaded, and the follow URLs will be used as mirror in case of download error. Files accessible in the local filesystem can be referenced with a URL starting with ``file:///`` followed by an absolute path to a file (where the third ``/`` implies ``localhost``). :param filename: Name of the file to be created in the local storage :param verify: When False, disables https certificate validation :param retry: Number of retries in case of failure. Default is overridden by "tools.files.download:retry" conf :param retry_wait: Seconds to wait between download attempts. Default is overriden by "tools.files.download:retry_wait" conf. :param auth: A tuple of user and password to use HTTPBasic authentication :param headers: A dictionary with additional headers :param md5: MD5 hash code to check the downloaded file :param sha1: SHA-1 hash code to check the downloaded file :param sha256: SHA-256 hash code to check the downloaded file """ config = conanfile.conf retry = retry if retry is not None else 2 retry = config.get("tools.files.download:retry", check_type=int, default=retry) retry_wait = retry_wait if retry_wait is not None else 5 retry_wait = config.get("tools.files.download:retry_wait", check_type=int, default=retry_wait) verify = config.get("tools.files.download:verify", check_type=bool, default=verify) filename = os.path.abspath(filename) downloader = SourcesCachingDownloader(conanfile) downloader.download(url, filename, retry, retry_wait, verify, auth, headers, md5, sha1, sha256) def rename(conanfile, src, dst): """ Utility functions to rename a file or folder src to dst with retrying. ``os.rename()`` frequently raises “Access is denied” exception on Windows. This function renames file or folder using robocopy to avoid the exception on Windows. :param conanfile: The current recipe object. Always use ``self``. :param src: Path to be renamed. :param dst: Path to be renamed to. """ # FIXME: This function has been copied from legacy. Needs to fix: which() # call and wrap subprocess call. if os.path.exists(dst): raise ConanException("rename {} to {} failed, dst exists.".format(src, dst)) if platform.system() == "Windows" and which("robocopy") and os.path.isdir(src): # /move Moves files and directories, and deletes them from the source after they are copied. # /e Copies subdirectories. Note that this option includes empty directories. # /ndl Specifies that directory names are not to be logged. # /nfl Specifies that file names are not to be logged. process = subprocess.Popen(["robocopy", "/move", "/e", "/ndl", "/nfl", src, dst], stdout=subprocess.PIPE) process.communicate() if process.returncode > 7: # https://ss64.com/nt/robocopy-exit.html raise ConanException("rename {} to {} failed.".format(src, dst)) else: try: os.rename(src, dst) except Exception as err: raise ConanException("rename {} to {} failed: {}".format(src, dst, err)) @contextmanager def chdir(conanfile, newdir): """ This is a context manager that allows to temporary change the current directory in your conanfile :param conanfile: The current recipe object. Always use ``self``. :param newdir: Directory path name to change the current directory. """ old_path = os.getcwd() os.chdir(newdir) try: yield finally: os.chdir(old_path) def chmod(conanfile, path: str, read: Optional[bool] = None, write: Optional[bool] = None, execute: Optional[bool] = None, recursive: bool = False): """Change file or directory permissions cross-platform. .. versionadded:: 2.15 This function is a simple wrapper around the chmod Unix command, but it is cross-platform supported. It is indicated to use it instead of os.stat + os.chmod, as it only changes the permissions of the directory or file for the owner and avoids issues with the umask. On Windows is limited to changing write permission only. Parameters ---------- conanfile : object The current recipe object. Always use ``self``. path : str Path to the file or directory whose permissions will be changed. read : bool, optional If ``True``, the file or directory will be given read permissions for owner user. If ``False``, the read permission will be removed. If ``None``, the read permission will be left unchanged. Defaults to None. write : bool, optional If ``True``, the file or directory will be given write permissions for owner user. If ``False``, the write permission will be removed. If ``None``, the file or directory will not be changed. Defaults to None. execute : bool, optional If ``True``, the file or directory will be given execute permissions for owner user. If ``False``, the execution permission will be removed. If ``None``, the file or directory will not be changed. Defaults to None. recursive : bool If ``True``, the permissions will be applied recursively to all files and directories inside the specified directory. If ``False``, only the specified file or directory will be changed. Defaults to False. Returns ------- None Examples -------- .. code-block:: python :caption: Add execution permission to a packaged bash script from conan.tools.files import chmod chmod(self, os.path.join(self.package_folder, "bin", "script.sh"), execute=True) """ if read is None and write is None and execute is None: raise ConanException("Could not change permission: At least one of the permissions should be set.") if not os.path.exists(path): raise ConanException(f"Could not change permission: Path \"{path}\" does not exist.") def _change_permission(it_path:str): mode = os.stat(it_path).st_mode permissions = [ (read, stat.S_IRUSR), (write, stat.S_IWUSR), (execute, stat.S_IXUSR) ] for enabled, mask in permissions: if enabled is None: continue elif enabled: mode |= mask else: mode &= ~mask os.chmod(it_path, mode) if recursive: for root, _, files in os.walk(path): for file in files: _change_permission(os.path.join(root, file)) else: _change_permission(path) def unzip(conanfile, filename, destination=".", keep_permissions=False, pattern=None, strip_root=False, extract_filter=None, excludes=None): """ Extract different compressed formats :param conanfile: The current recipe object. Always use ``self``. :param filename: Path to the compressed file. :param destination: (Optional, Defaulted to ``.``) Destination folder (or file for .gz files) :param keep_permissions: (Optional, Defaulted to ``False``) Keep the zip permissions. WARNING: Can be dangerous if the zip was not created in a NIX system, the bits could produce undefined permission schema. Use this option only if you are sure that the zip was created correctly. :param pattern: (Optional, Defaulted to ``None``) Extract only paths matching the pattern. This should be a Unix shell-style wildcard, see fnmatch documentation for more details. :param strip_root: (Optional, Defaulted to False) If True, and all the unzipped contents are in a single folder it will flat the folder moving all the contents to the parent folder. :param extract_filter: (Optional, defaulted to None). When extracting a tar file, use the tar extracting filters define by Python in https://docs.python.org/3/library/tarfile.html :param excludes: (Optional, defaulted to None). When extracting a file, exclude paths matching any of the patterns. This should be a Unix shell-style wildcard, see fnmatch documentation for more details. """ output = conanfile.output extract_filter = conanfile.conf.get("tools.files.unzip:filter") or extract_filter output.info(f"Uncompressing {filename} to {destination}") if (filename.endswith(".tar.gz") or filename.endswith(".tgz") or filename.endswith(".tbz2") or filename.endswith(".tar.bz2") or filename.endswith(".tar")): return untargz(filename, destination, pattern, strip_root, extract_filter, excludes=excludes) if filename.endswith(".gz"): target_name = filename[:-3] if destination == "." else destination target_dir = os.path.dirname(target_name) if target_dir: os.makedirs(target_dir, exist_ok=True) with gzip.open(filename, 'rb') as fin: with open(target_name, "wb") as fout: shutil.copyfileobj(fin, fout) return if filename.endswith(".tar.xz") or filename.endswith(".txz"): return untargz(filename, destination, pattern, strip_root, extract_filter, excludes=excludes) import zipfile full_path = os.path.normpath(os.path.join(os.getcwd(), destination)) with FileProgress(filename, msg="Unzipping", mode="r") as file, zipfile.ZipFile(file) as z: zip_info = z.infolist() if pattern: zip_info = [zi for zi in zip_info if fnmatch(zi.filename, pattern)] if excludes: zip_info = [zi for zi in zip_info if not any(fnmatch(zi.filename, pat) for pat in excludes)] if strip_root: names = [zi.filename.replace("\\", "/") for zi in zip_info] common_folder = os.path.commonprefix(names).split("/", 1)[0] if not common_folder and len(names) > 1: raise ConanException("The zip file contains more than 1 folder in the root") if len(names) == 1 and len(names[0].split("/", 1)) == 1: raise ConanException("The zip file contains a file in the root") # Remove the directory entry if present # Note: The "zip" format contains the "/" at the end if it is a directory zip_info = [m for m in zip_info if m.filename != (common_folder + "/")] for member in zip_info: name = member.filename.replace("\\", "/") member.filename = name.split("/", 1)[1] uncompress_size = sum((file_.file_size for file_ in zip_info)) if uncompress_size > 100000: output.info("Unzipping %s, this can take a while" % human_size(uncompress_size)) else: output.info("Unzipping %s" % human_size(uncompress_size)) extracted_size = 0 if platform.system() == "Windows": for file_ in zip_info: extracted_size += file_.file_size try: z.extract(file_, full_path) except Exception as e: output.error(f"Error extract {file_.filename}\n{str(e)}", error_type="exception") else: # duplicated for, to avoid a platform check for each zipped file for file_ in zip_info: extracted_size += file_.file_size try: z.extract(file_, full_path) if keep_permissions: # Could be dangerous if the ZIP has been created in a non nix system # https://bugs.python.org/issue15795 perm = file_.external_attr >> 16 & 0xFFF os.chmod(os.path.join(full_path, file_.filename), perm) except Exception as e: output.error(f"Error extract {file_.filename}\n{str(e)}", error_type="exception") output.writeln("") def untargz(filename, destination=".", pattern=None, strip_root=False, extract_filter=None, excludes=None): # NOT EXPOSED at `conan.tools.files` but used in tests import tarfile with tarfile.TarFile.open(filename, mode='r:*') as tarredgzippedFile: f = getattr(tarfile, f"{extract_filter}_filter", None) if extract_filter else None tarredgzippedFile.extraction_filter = f or (lambda member_, _: member_) # https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry # File I/O functions in the Windows API convert "/" to "\" as part of converting # the name to an NT-style name, except when using the "\\?\" prefix using_long_path_prefix = destination.startswith("\\\\?\\") if not pattern and not excludes and not strip_root and not using_long_path_prefix: tarredgzippedFile.extractall(destination) else: common_folder = None members = [] for member in tarredgzippedFile: if pattern and not fnmatch(member.name, pattern): continue # Skip files that don’t match the pattern if excludes and any(fnmatch(member.name, pat) for pat in excludes): continue # Skip files that match the excludes if strip_root: name = member.name.replace("\\", "/") if not common_folder: splits = name.split("/", 1) # First case for a plain folder in the root if member.isdir() or len(splits) > 1: common_folder = splits[0] # Find the root folder else: raise ConanException("Can't untar a tgz containing files in the root with strip_root enabled") if not name.startswith(common_folder): raise ConanException("The tgz file contains more than 1 folder in the root") # Adjust the member's name for extraction member.name = name[len(common_folder) + 1:] member.path = member.name if member.linkpath and member.linkpath.startswith(common_folder): # https://github.com/conan-io/conan/issues/11065 member.linkpath = member.linkpath[len(common_folder) + 1:].replace("\\", "/") member.linkname = member.linkpath if using_long_path_prefix: member.name = member.name.replace("/", "\\") # Let's gather each member members.append(member) tarredgzippedFile.extractall(destination, members=members) def check_sha1(conanfile, file_path, signature): """ Check that the specified ``SHA-1`` hash of the ``file_path`` matches the actual hash. If doesn’t match it will raise a ``ConanException``. :param conanfile: Conanfile object. :param file_path: Path of the file to check. :param signature: Expected SHA-1 hash. """ check_with_algorithm_sum("sha1", file_path, signature) def check_md5(conanfile, file_path, signature): """ Check that the specified ``MD5`` hash of the ``file_path`` matches the actual hash. If doesn’t match it will raise a ``ConanException``. :param conanfile: The current recipe object. Always use ``self``. :param file_path: Path of the file to check. :param signature: Expected MD5 hash. """ check_with_algorithm_sum("md5", file_path, signature) def check_sha256(conanfile, file_path, signature): """ Check that the specified ``SHA-256`` hash of the ``file_path`` matches the actual hash. If doesn’t match it will raise a ``ConanException``. :param conanfile: Conanfile object. :param file_path: Path of the file to check. :param signature: Expected SHA-256 hash. """ check_with_algorithm_sum("sha256", file_path, signature) def replace_in_file(conanfile, file_path, search, replace, strict=True, encoding="utf-8"): """ Replace a string ``search`` in the contents of the file ``file_path`` with the string replace. :param conanfile: The current recipe object. Always use ``self``. :param file_path: File path of the file to perform the replacing. :param search: String you want to be replaced. :param replace: String to replace the searched string. :param strict: (Optional, Defaulted to ``True``) If ``True``, it raises an error if the searched string is not found, so nothing is actually replaced. :param encoding: (Optional, Defaulted to utf-8): Specifies the input and output files text encoding. :return: ``True`` if the pattern was found, ``False`` otherwise if `strict` is ``False``. """ output = conanfile.output content = load(conanfile, file_path, encoding=encoding) if -1 == content.find(search): message = "replace_in_file didn't find pattern '%s' in '%s' file." % (search, file_path) if strict: raise ConanException(message) else: output.warning(message) return False content = content.replace(search, replace) save(conanfile, file_path, content, encoding=encoding) return True def collect_libs(conanfile, folder=None): """ Returns a sorted list of library names from the libraries (files with extensions *.so*, *.lib*, *.a* and *.dylib*) located inside the ``conanfile.cpp_info.libdirs`` (by default) or the **folder** directory relative to the package folder. Useful to collect not inter-dependent libraries or with complex names like ``libmylib-x86-debug-en.lib``. For UNIX libraries staring with **lib**, like *libmath.a*, this tool will collect the library name **math**. :param conanfile: The current recipe object. Always use ``self``. :param folder: (Optional, Defaulted to ``None``): String indicating the subfolder name inside ``conanfile.package_folder`` where the library files are. :return: A list with the library names """ if not conanfile.package_folder: return [] if folder: lib_folders = [os.path.join(conanfile.package_folder, folder)] else: lib_folders = [os.path.join(conanfile.package_folder, folder) for folder in conanfile.cpp_info.libdirs] ref_libs = {} for lib_folder in lib_folders: if not os.path.exists(lib_folder): conanfile.output.warning("Lib folder doesn't exist, can't collect libraries: " "{0}".format(lib_folder)) continue # In case of symlinks, only keep shortest file name in the same "group" files = os.listdir(lib_folder) for f in files: name, ext = os.path.splitext(f) if ext in (".so", ".lib", ".a", ".dylib", ".bc"): real_lib = os.path.basename(os.path.realpath(os.path.join(lib_folder, f))) if real_lib not in ref_libs or len(f) < len(ref_libs[real_lib]): ref_libs[real_lib] = f result = [] for f in ref_libs.values(): name, ext = os.path.splitext(f) if ext != ".lib" and name.startswith("lib"): name = name[3:] result.append(name) result.sort() return result def move_folder_contents(conanfile, src_folder, dst_folder): """ replaces the dst_folder contents with the contents of the src_folder, which can be a child folder of dst_folder. This is used in the SCM monorepo flow, when it is necessary to use one subproject subfolder to replace the whole cloned git repo /base-folder /base-folder /pkg (src folder) /other/ /other/ /pkg/ /pkg/ /siblings """ # Remove potential "siblings" folders not wanted src_folder_name = os.path.basename(src_folder) for f in os.listdir(dst_folder): if f != src_folder_name: # FIXME: Only works for 1st level subfolder dst = os.path.join(dst_folder, f) if os.path.isfile(dst): os.remove(dst) else: _internal_rmdir(dst) # Move all the contents for f in os.listdir(src_folder): src = os.path.join(src_folder, f) dst = os.path.join(dst_folder, f) if not os.path.exists(dst): shutil.move(src, dst_folder) else: for sub_src in os.listdir(src): shutil.move(os.path.join(src, sub_src), dst) _internal_rmdir(src) try: os.rmdir(src_folder) except OSError: pass ================================================ FILE: conan/tools/files/patches.py ================================================ import logging import os import shutil import patch_ng import yaml from conan.errors import ConanException from conan.internal.paths import DATA_YML from conan.internal.util.files import mkdir, load, save class PatchLogHandler(logging.Handler): def __init__(self, scoped_output, patch_file): logging.Handler.__init__(self, logging.DEBUG) self._scoped_output = scoped_output self.patchname = patch_file or "patch_ng" def emit(self, record): logstr = self.format(record) if record.levelno == logging.WARN: self._scoped_output.warning("%s: %s" % (self.patchname, logstr)) else: self._scoped_output.info("%s: %s" % (self.patchname, logstr)) def patch(conanfile, base_path=None, patch_file=None, patch_string=None, strip=0, fuzz=False, **kwargs): """ Applies a diff from file (patch_file) or string (patch_string) in the conanfile.source_folder directory. The folder containing the sources can be customized with the self.folders attribute in the layout(self) method. :param conanfile: the current recipe, always pass 'self' :param base_path: The path is a relative path to conanfile.export_sources_folder unless an absolute path is provided. :param patch_file: Patch file that should be applied. The path is relative to the conanfile.source_folder unless an absolute path is provided. :param patch_string: Patch string that should be applied. :param strip: Number of folders to be stripped from the path. :param fuzz: Should accept fuzzy patches. :param kwargs: Extra parameters that can be added and will contribute to output information """ patch_type = kwargs.get('patch_type') or ("file" if patch_file else "string") patch_description = kwargs.get('patch_description') if patch_type or patch_description: patch_type_str = ' ({})'.format(patch_type) if patch_type else '' patch_description_str = ': {}'.format(patch_description) if patch_description else '' conanfile.output.info('Apply patch{}{}'.format(patch_type_str, patch_description_str)) patchlog = logging.getLogger("patch_ng") patchlog.handlers = [] patchlog.addHandler(PatchLogHandler(conanfile.output, patch_file)) if patch_file: # trick *1: patch_file path could be absolute (e.g. conanfile.build_folder), in that case # the join does nothing and works. patch_path = os.path.join(conanfile.export_sources_folder, patch_file) patchset = patch_ng.fromfile(patch_path) else: patchset = patch_ng.fromstring(patch_string.encode()) if not patchset: raise ConanException("Failed to parse patch: %s" % (patch_file if patch_file else "string")) # trick *1 root = os.path.join(conanfile.source_folder, base_path) if base_path else conanfile.source_folder if not patchset.apply(strip=strip, root=root, fuzz=fuzz): raise ConanException("Failed to apply patch: %s" % patch_file) def apply_conandata_patches(conanfile): """ Applies patches stored in ``conanfile.conan_data`` (read from ``conandata.yml`` file). It will apply all the patches under ``patches`` entry that matches the given ``conanfile.version``. If versions are not defined in ``conandata.yml`` it will apply all the patches directly under ``patches`` keyword. The key entries will be passed as kwargs to the ``patch`` function. """ if conanfile.conan_data is None: raise ConanException("conandata.yml not defined") patches = conanfile.conan_data.get('patches') if patches is None: conanfile.output.info("apply_conandata_patches(): No patches defined in conandata") return if isinstance(patches, dict): assert conanfile.version, "Can only be applied if conanfile.version is already defined" entries = patches.get(str(conanfile.version), []) if entries is None: conanfile.output.warning(f"apply_conandata_patches(): No patches defined for version {conanfile.version} in conandata.yml") return elif isinstance(patches, list): entries = patches else: raise ConanException("conandata.yml 'patches' should be a list or a dict {version: list}") for it in entries: if "patch_file" in it: # The patch files are located in the root src entry = it.copy() patch_file = entry.pop("patch_file") patch_file_path = os.path.join(conanfile.export_sources_folder, patch_file) if "patch_description" not in entry: entry["patch_description"] = patch_file patch(conanfile, patch_file=patch_file_path, **entry) elif "patch_string" in it: patch(conanfile, **it) elif "patch_user" in it: pass # This will be managed directly by users, can be a command or a script execution else: raise ConanException("The 'conandata.yml' file needs a 'patch_file' or 'patch_string'" " entry for every patch to be applied") def export_conandata_patches(conanfile): """ Exports patches stored in 'conanfile.conan_data' (read from 'conandata.yml' file). It will export all the patches under 'patches' entry that matches the given 'conanfile.version'. If versions are not defined in 'conandata.yml' it will export all the patches directly under 'patches' keyword. """ if conanfile.conan_data is None: raise ConanException("conandata.yml not defined") conanfile_patches = conanfile.conan_data.get('patches') def _handle_patches(patches, patches_folder): if patches is None: conanfile.output.info("export_conandata_patches(): No patches defined in conandata") return if isinstance(patches, dict): assert conanfile.version, "Can only be exported if conanfile.version is already defined" entries = patches.get(conanfile.version, []) if entries is None: conanfile.output.warning("export_conandata_patches(): No patches defined for " f"version {conanfile.version} in conandata.yml") return elif isinstance(patches, list): entries = patches else: raise ConanException("conandata.yml 'patches' should be a list or a dict " "{version: list}") for it in entries: patch_file = it.get("patch_file") if patch_file: src = os.path.join(patches_folder, patch_file) dst = os.path.join(conanfile.export_sources_folder, patch_file) if not os.path.exists(src): raise ConanException(f"Patch file does not exist: '{src}'") mkdir(os.path.dirname(dst)) shutil.copy2(src, dst) return entries _handle_patches(conanfile_patches, conanfile.recipe_folder) extra_path = conanfile.conf.get("core.sources.patch:extra_path") if extra_path: if not os.path.isdir(extra_path): raise ConanException(f"Patches extra path '{extra_path}' does not exist") pkg_path = os.path.join(extra_path, conanfile.name) if not os.path.isdir(pkg_path): return data_path = os.path.join(pkg_path, DATA_YML) try: data = yaml.safe_load(load(data_path)) except Exception as e: raise ConanException("Invalid yml format at {}: {}".format(data_path, e)) data = data or {} conanfile.output.info(f"Applying extra patches 'core.sources.patch:extra_path': {data_path}") new_patches = _handle_patches(data.get('patches'), pkg_path) # Update the CONANDATA.YML conanfile_patches = conanfile_patches or {} conanfile_patches.setdefault(conanfile.version, []).extend(new_patches) conanfile.conan_data['patches'] = conanfile_patches # Saving in the EXPORT folder conanfile_data_path = os.path.join(conanfile.export_folder, DATA_YML) new_conandata_yml = yaml.safe_dump(conanfile.conan_data, default_flow_style=False) save(conanfile_data_path, new_conandata_yml) ================================================ FILE: conan/tools/files/symlinks/__init__.py ================================================ from conan.tools.files.symlinks.symlinks import absolute_to_relative_symlinks, \ remove_external_symlinks, remove_broken_symlinks, get_symlinks ================================================ FILE: conan/tools/files/symlinks/symlinks.py ================================================ import os def get_symlinks(base_folder): """Return the absolute path to the symlink files in base_folder""" for (root, dirnames, filenames) in os.walk(base_folder): for el in filenames + dirnames: fullpath = os.path.join(root, el) if os.path.islink(fullpath): yield fullpath def _path_inside(base, folder): base = os.path.abspath(base) folder = os.path.abspath(folder) return os.path.commonprefix([base, folder]) == base def absolute_to_relative_symlinks(conanfile, base_folder): """ Convert the symlinks with absolute paths into relative ones if they are pointing to a file or directory inside the ``base_folder``. Any absolute symlink pointing outside the ``base_folder`` will be ignored. :param conanfile: The current recipe object. Always use ``self``. :param base_folder: Folder to be scanned. """ for fullpath in get_symlinks(base_folder): link_target = os.readlink(fullpath) if not os.path.isabs(link_target): continue folder_of_symlink = os.path.dirname(fullpath) if _path_inside(base_folder, link_target): os.unlink(fullpath) new_link = os.path.relpath(link_target, folder_of_symlink) os.symlink(new_link, fullpath) def remove_external_symlinks(conanfile, base_folder): """ Remove the symlinks to files that point outside the ``base_folder``, no matter if relative or absolute. :param conanfile: The current recipe object. Always use ``self``. :param base_folder: Folder to be scanned. """ for fullpath in get_symlinks(base_folder): link_target = os.readlink(fullpath) if not os.path.isabs(link_target): link_target = os.path.join(os.path.dirname(fullpath), link_target) if not _path_inside(base_folder, link_target): os.unlink(fullpath) def remove_broken_symlinks(conanfile, base_folder=None): """ Remove the broken symlinks, no matter if relative or absolute. :param conanfile: The current recipe object. Always use ``self``. :param base_folder: Folder to be scanned. """ for fullpath in get_symlinks(base_folder): link_target = os.readlink(fullpath) if not os.path.isabs(link_target): link_target = os.path.join(os.path.dirname(fullpath), link_target) if not os.path.exists(link_target): os.unlink(fullpath) ================================================ FILE: conan/tools/gnu/__init__.py ================================================ from conan.tools.gnu.autotools import Autotools from conan.tools.gnu.autotoolstoolchain import AutotoolsToolchain from conan.tools.gnu.gnutoolchain import GnuToolchain from conan.tools.gnu.autotoolsdeps import AutotoolsDeps from conan.tools.gnu.pkgconfig import PkgConfig from conan.tools.gnu.pkgconfigdeps import PkgConfigDeps from conan.tools.gnu.makedeps import MakeDeps ================================================ FILE: conan/tools/gnu/autotools.py ================================================ import os import re from conan.tools.build import build_jobs, cmd_args_to_string, load_toolchain_args from conan.internal.subsystems import subsystem_path, deduce_subsystem from conan.tools.files import chdir from conan.tools.microsoft import unix_path def join_arguments(args): return " ".join(filter(None, args)) class Autotools: def __init__(self, conanfile, namespace=None): """ :param conanfile: The current recipe object. Always use ``self``. :param namespace: this argument avoids collisions when you have multiple toolchain calls in the same recipe. By setting this argument, the *conanbuild.conf* file used to pass information to the toolchain will be named as: *_conanbuild.conf*. The default value is ``None`` meaning that the name of the generated file is *conanbuild.conf*. This namespace must be also set with the same value in the constructor of the AutotoolsToolchain so that it reads the information from the proper file. """ self._conanfile = conanfile toolchain_file_content = load_toolchain_args(self._conanfile.generators_folder, namespace=namespace) self._configure_args = toolchain_file_content.get("configure_args") self._make_args = toolchain_file_content.get("make_args") self._autoreconf_args = toolchain_file_content.get("autoreconf_args") def configure(self, build_script_folder=None, args=None): """ Call the configure script. :param args: List of arguments to use for the ``configure`` call. :param build_script_folder: Subfolder where the `configure` script is located. If not specified conanfile.source_folder is used. """ # http://jingfenghanmax.blogspot.com.es/2010/09/configure-with-host-target-and-build.html # https://gcc.gnu.org/onlinedocs/gccint/Configure-Terms.html script_folder = os.path.join(self._conanfile.source_folder, build_script_folder) \ if build_script_folder else self._conanfile.source_folder configure_args = [] configure_args.extend(args or []) self._configure_args = "{} {}".format(self._configure_args, cmd_args_to_string(configure_args)) configure_cmd = "{}/configure".format(script_folder) subsystem = deduce_subsystem(self._conanfile, scope="build") configure_cmd = subsystem_path(subsystem, configure_cmd) cmd = '"{}" {}'.format(configure_cmd, self._configure_args) self._conanfile.run(cmd) def make(self, target=None, args=None, makefile=None): """ Call the make program. :param target: (Optional, Defaulted to ``None``): Choose which target to build. This allows building of e.g., docs, shared libraries or install for some AutoTools projects :param args: (Optional, Defaulted to ``None``): List of arguments to use for the ``make`` call. :param makefile: (Optional, Defaulted to ``None``): Allow specifying a custom makefile to use instead of default "Makefile" """ make_program = self._conanfile.conf.get("tools.gnu:make_program", default="mingw32-make" if self._use_win_mingw() else "make") subsystem = deduce_subsystem(self._conanfile, scope="build") make_program = subsystem_path(subsystem, make_program) str_args = self._make_args str_extra_args = " ".join(args) if args is not None else "" jobs = "" jobs_already_passed = re.search(r"(^-j\d+)|(\W-j\d+\s*)", join_arguments([str_args, str_extra_args])) if not jobs_already_passed and "nmake" not in make_program.lower(): njobs = build_jobs(self._conanfile) if njobs: jobs = "-j{}".format(njobs) str_makefile = f"--file={makefile}" if makefile else None command = join_arguments([make_program, str_makefile, target, str_args, str_extra_args, jobs]) self._conanfile.run(command) def install(self, args=None, target=None, makefile=None): """ This is just an "alias" of ``self.make(target="install")`` or ``self.make(target="install-strip")`` :param args: (Optional, Defaulted to ``None``): List of arguments to use for the ``make`` call. By default an argument ``DESTDIR=unix_path(self.package_folder)`` is added to the call if the passed value is ``None``. See more information about :ref:`tools.microsoft.unix_path() function` :param target: (Optional, Defaulted to ``None``): Choose which target to install. :param makefile: (Optional, Defaulted to ``None``): Allow specifying a custom makefile to use instead of default "Makefile" """ if target is None: target = "install" do_strip = self._conanfile.conf.get("tools.build:install_strip", check_type=bool) if do_strip: target += "-strip" args = args if args else [] str_args = " ".join(args) if "DESTDIR=" not in str_args: args.insert(0, "DESTDIR={}".format(unix_path(self._conanfile, self._conanfile.package_folder))) self.make(target=target, args=args, makefile=makefile) def autoreconf(self, build_script_folder=None, args=None): """ Call ``autoreconf`` :param args: (Optional, Defaulted to ``None``): List of arguments to use for the ``autoreconf`` call. :param build_script_folder: Subfolder where the `configure` script is located. If not specified conanfile.source_folder is used. """ script_folder = os.path.join(self._conanfile.source_folder, build_script_folder) \ if build_script_folder else self._conanfile.source_folder args = args or [] command = join_arguments(["autoreconf", self._autoreconf_args, cmd_args_to_string(args)]) with chdir(self, script_folder): self._conanfile.run(command) def _use_win_mingw(self): os_build = self._conanfile.settings_build.get_safe('os') if os_build == "Windows": compiler = self._conanfile.settings.get_safe("compiler") sub = self._conanfile.settings.get_safe("os.subsystem") if sub in ("cygwin", "msys2", "msys") or compiler == "qcc": return False else: if self._conanfile.win_bash: return False return True return False ================================================ FILE: conan/tools/gnu/autotoolsdeps.py ================================================ from conan.internal import check_duplicated_generator from conan.tools import CppInfo from conan.tools.env import Environment from conan.tools.gnu.gnudeps_flags import GnuDepsFlags class AutotoolsDeps: def __init__(self, conanfile): self._conanfile = conanfile self._environment = None self._ordered_deps = None @property def ordered_deps(self): if self._ordered_deps is None: deps = self._conanfile.dependencies.host.topological_sort self._ordered_deps = [dep for dep in reversed(deps.values())] return self._ordered_deps def _get_cpp_info(self): ret = CppInfo(self._conanfile) for dep in self.ordered_deps: dep_cppinfo = dep.cpp_info.aggregated_components() # In case we have components, aggregate them, we do not support isolated # "targets" with autotools ret.merge(dep_cppinfo) return ret def _rpaths_flags(self): flags = [] for dep in self.ordered_deps: flags.extend(["-Wl,-rpath -Wl,{}".format(libdir) for libdir in dep.cpp_info.libdirs if dep.options.get_safe("shared", False)]) return flags @property def environment(self): """ :return: An ``Environment`` object containing the computed variables. If you need to modify some of the computed values you can access to the ``environment`` object. """ if self._environment is None: flags = GnuDepsFlags(self._conanfile, self._get_cpp_info()) # cpp_flags cpp_flags = [] cpp_flags.extend(flags.include_paths) cpp_flags.extend(flags.defines) # Ldflags ldflags = flags.sharedlinkflags ldflags.extend(flags.exelinkflags) ldflags.extend(flags.frameworks) ldflags.extend(flags.framework_paths) ldflags.extend(flags.lib_paths) # set the rpath in Macos so that the library are found in the configure step if self._conanfile.settings.get_safe("os") == "Macos": ldflags.extend(self._rpaths_flags()) # libs libs = flags.libs libs.extend(flags.system_libs) # cflags cflags = flags.cflags cxxflags = flags.cxxflags env = Environment() env.append("CPPFLAGS", cpp_flags) env.append("LIBS", libs) env.append("LDFLAGS", ldflags) env.append("CXXFLAGS", cxxflags) env.append("CFLAGS", cflags) self._environment = env return self._environment def vars(self, scope="build"): return self.environment.vars(self._conanfile, scope=scope) def generate(self, scope="build"): check_duplicated_generator(self, self._conanfile) self.vars(scope).save_script("conanautotoolsdeps") ================================================ FILE: conan/tools/gnu/autotoolstoolchain.py ================================================ import os from conan.errors import ConanException from conan.internal import check_duplicated_generator from conan.internal.internal_tools import is_universal_arch from conan.tools.apple.apple import is_apple_os, resolve_apple_flags, apple_extra_flags from conan.tools.build import cmd_args_to_string, save_toolchain_args from conan.tools.build.cross_building import cross_building from conan.tools.build.flags import architecture_flag, architecture_link_flag, build_type_flags, cppstd_flag, \ build_type_link_flags, libcxx_flags, cstd_flag, llvm_clang_front, threads_flags from conan.tools.env import Environment, VirtualBuildEnv from conan.tools.gnu.get_gnu_triplet import _get_gnu_triplet from conan.tools.microsoft import VCVars, msvc_runtime_flag, unix_path, check_min_vs, is_msvc from conan.internal.model.pkg_type import PackageType class AutotoolsToolchain: def __init__(self, conanfile, namespace=None, prefix="/"): """ :param conanfile: The current recipe object. Always use ``self``. :param namespace: This argument avoids collisions when you have multiple toolchain calls in the same recipe. By setting this argument, the *conanbuild.conf* file used to pass information to the build helper will be named as *_conanbuild.conf*. The default value is ``None`` meaning that the name of the generated file is *conanbuild.conf*. This namespace must be also set with the same value in the constructor of the Autotools build helper so that it reads the information from the proper file. :param prefix: Folder to use for ``--prefix`` argument ("/" by default). """ self._conanfile = conanfile self._namespace = namespace self._prefix = prefix # Flags self.extra_cxxflags = [] self.extra_cflags = [] self.extra_ldflags = [] self.extra_defines = [] # Defines self.ndebug = None build_type = self._conanfile.settings.get_safe("build_type") if build_type in ['Release', 'RelWithDebInfo', 'MinSizeRel']: self.ndebug = "NDEBUG" # TODO: This is also covering compilers like Visual Studio, necessary to test it (&remove?) self.build_type_flags = build_type_flags(self._conanfile) self.build_type_link_flags = build_type_link_flags(self._conanfile.settings) self.cppstd = cppstd_flag(self._conanfile) self.cstd = cstd_flag(self._conanfile) self.arch_flag = architecture_flag(self._conanfile) self.arch_ld_flag = architecture_link_flag(self._conanfile) self.threads_flags = threads_flags(self._conanfile) self.libcxx, self.gcc_cxx11_abi = libcxx_flags(self._conanfile) self.fpic = self._conanfile.options.get_safe("fPIC") self.msvc_runtime_flag = self._get_msvc_runtime_flag() self.msvc_extra_flags = self._msvc_extra_flags() self.msvc_runtime_link_flags = [] if llvm_clang_front(self._conanfile) == "clang": self.msvc_runtime_link_flags = ["-fuse-ld=lld-link"] self._is_universal_arch = is_universal_arch(conanfile.settings.get_safe("arch"), conanfile.settings.possible_values().get("arch")) if self._is_universal_arch and not is_apple_os(self._conanfile): arch_str = conanfile.settings.get_safe('arch') raise ConanException(f"Universal arch '{arch_str}' is only supported in Apple OSes") # Cross build triplets self._host = self._conanfile.conf.get("tools.gnu:host_triplet") self._build = self._conanfile.conf.get("tools.gnu:build_triplet") self._target = None self.android_cross_flags = {} self._is_cross_building = not self._is_universal_arch and cross_building(self._conanfile) if self._is_cross_building: compiler = self._conanfile.settings.get_safe("compiler") # If cross-building and tools.android:ndk_path is defined, let's try to guess the Android # cross-building flags self.android_cross_flags = self._resolve_android_cross_compilation() # If it's not defined the triplet if not self._host: os_host = conanfile.settings.get_safe("os") arch_host = conanfile.settings.get_safe("arch") self._host = _get_gnu_triplet(os_host, arch_host, compiler=compiler)["triplet"] # Build triplet if not self._build: os_build = conanfile.settings_build.get_safe('os') arch_build = conanfile.settings_build.get_safe('arch') self._build = _get_gnu_triplet(os_build, arch_build, compiler=compiler)["triplet"] sysroot = self._conanfile.conf.get("tools.build:sysroot") if sysroot: root = sysroot.replace("\\", "/") compiler = self._conanfile.settings.get_safe("compiler") self.sysroot_flag = f"--sysroot {root}" if compiler != "qcc" else f"-Wc,-isysroot,{root}" else: self.sysroot_flag = None extra_configure_args = self._conanfile.conf.get("tools.gnu:extra_configure_args", check_type=list, default=[]) self.configure_args = (self._default_configure_shared_flags() + self._default_configure_install_flags() + self._get_triplets() + extra_configure_args) self.autoreconf_args = self._default_autoreconf_flags() self.make_args = [] # Apple stuff is_cross_building_osx = (self._is_cross_building and conanfile.settings_build.get_safe('os') == "Macos" and is_apple_os(conanfile) and not self._is_universal_arch) min_flag, arch_flags, isysroot_flag = ( resolve_apple_flags(conanfile, is_cross_building=is_cross_building_osx, is_universal=self._is_universal_arch) ) # https://man.archlinux.org/man/clang.1.en#Target_Selection_Options self.apple_arch_flag = arch_flags # -isysroot makes all includes for your library relative to the build directory self.apple_isysroot_flag = isysroot_flag self.apple_min_version_flag = min_flag self.apple_extra_flags = apple_extra_flags(self._conanfile) def _resolve_android_cross_compilation(self): # Issue related: https://github.com/conan-io/conan/issues/13443 ret = {} if not self._is_cross_building or not self._conanfile.settings.get_safe("os") == "Android": return ret # Setting host if it was not already defined yet arch = self._conanfile.settings.get_safe("arch") android_target = {'armv7': 'armv7a-linux-androideabi', 'armv8': 'aarch64-linux-android', 'x86': 'i686-linux-android', 'x86_64': 'x86_64-linux-android'}.get(arch) self._host = self._host or android_target # Automatic guessing made by Conan (need the NDK path variable defined) conan_vars = {} ndk_path = self._conanfile.conf.get("tools.android:ndk_path", check_type=str) if ndk_path: if self._conanfile.conf.get("tools.build:compiler_executables"): self._conanfile.output.warning("tools.build:compiler_executables conf has no effect" " when tools.android:ndk_path is defined too.") os_build = self._conanfile.settings_build.get_safe("os") ndk_os_folder = { 'Macos': 'darwin', 'iOS': 'darwin', 'watchOS': 'darwin', 'tvOS': 'darwin', 'visionOS': 'darwin', 'FreeBSD': 'linux', 'Linux': 'linux', 'Windows': 'windows', 'WindowsCE': 'windows', 'WindowsStore': 'windows' }.get(os_build, "linux") ext = ".cmd" if os_build == "Windows" else "" ndk_bin = os.path.join(ndk_path, "toolchains", "llvm", "prebuilt", f"{ndk_os_folder}-x86_64", "bin") android_api_level = self._conanfile.settings.get_safe("os.api_level") conan_vars = { "CC": os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang{ext}"), "CXX": os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang++{ext}"), "LD": os.path.join(ndk_bin, "ld"), "STRIP": os.path.join(ndk_bin, "llvm-strip"), "RANLIB": os.path.join(ndk_bin, "llvm-ranlib"), "AS": os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang{ext}"), "AR": os.path.join(ndk_bin, "llvm-ar"), "ADDR2LINE": os.path.join(ndk_bin, "llvm-addr2line"), "NM": os.path.join(ndk_bin, "llvm-nm"), "OBJCOPY": os.path.join(ndk_bin, "llvm-objcopy"), "OBJDUMP": os.path.join(ndk_bin, "llvm-objdump"), "READELF": os.path.join(ndk_bin, "llvm-readelf"), "ELFEDIT": os.path.join(ndk_bin, "llvm-elfedit") } build_env = VirtualBuildEnv(self._conanfile, auto_generate=True).vars() for var_name, var_path in conan_vars.items(): # User variables have more priority than Conan ones, so if it was defined within # the build env then do nothing if build_env.get(var_name) is None: ret[var_name] = var_path return ret def _get_msvc_runtime_flag(self): if llvm_clang_front(self._conanfile) == "clang": if self._conanfile.settings.compiler.runtime == "dynamic": runtime_type = self._conanfile.settings.get_safe("compiler.runtime_type") library = "msvcrtd" if runtime_type == "Debug" else "msvcrt" # The -D_DEBUG is important to link with the Debug MSVCP140D.dll debug = "-D_DEBUG " if runtime_type == "Debug" else "" return f"{debug}-D_DLL -D_MT -Xclang --dependent-lib={library}" return "" # By default it already link statically flag = msvc_runtime_flag(self._conanfile) if flag: flag = "-{}".format(flag) return flag def _msvc_extra_flags(self): if is_msvc(self._conanfile) and check_min_vs(self._conanfile, "180", raise_invalid=False): return ["-FS"] return [] def _add_msvc_flags(self, flags): # This is to avoid potential duplicate with users recipes -FS (already some in ConanCenter) return [f for f in self.msvc_extra_flags if f not in flags] @staticmethod def _filter_list_empty_fields(v): return list(filter(bool, v)) @property def cxxflags(self): fpic = "-fPIC" if self.fpic else None ret = [self.libcxx, self.cppstd, self.arch_flag, fpic, self.msvc_runtime_flag, self.sysroot_flag] + self.threads_flags apple_flags = [self.apple_isysroot_flag, self.apple_arch_flag, self.apple_min_version_flag] apple_flags += self.apple_extra_flags conf_flags = self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) vs_flag = self._add_msvc_flags(self.extra_cxxflags) ret = ret + self.build_type_flags + apple_flags + self.extra_cxxflags + vs_flag + conf_flags return self._filter_list_empty_fields(ret) @property def cflags(self): fpic = "-fPIC" if self.fpic else None ret = [self.cstd, self.arch_flag, fpic, self.msvc_runtime_flag, self.sysroot_flag] + self.threads_flags apple_flags = [self.apple_isysroot_flag, self.apple_arch_flag, self.apple_min_version_flag] apple_flags += self.apple_extra_flags conf_flags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list) vs_flag = self._add_msvc_flags(self.extra_cflags) ret = ret + self.build_type_flags + apple_flags + self.extra_cflags + vs_flag + conf_flags return self._filter_list_empty_fields(ret) @property def ldflags(self): ret = [self.arch_flag, self.sysroot_flag, self.arch_ld_flag] + self.threads_flags apple_flags = [self.apple_isysroot_flag, self.apple_arch_flag, self.apple_min_version_flag] apple_flags += self.apple_extra_flags conf_flags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) conf_flags.extend(self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list)) linker_scripts = self._conanfile.conf.get("tools.build:linker_scripts", default=[], check_type=list) conf_flags.extend(["-T'" + linker_script + "'" for linker_script in linker_scripts]) ret = ret + self.build_type_link_flags + apple_flags + self.extra_ldflags + conf_flags ret = ret + self.msvc_runtime_link_flags return self._filter_list_empty_fields(ret) @property def defines(self): conf_flags = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list) ret = [self.ndebug, self.gcc_cxx11_abi] + self.extra_defines + conf_flags return self._filter_list_empty_fields(ret) @property def rcflags(self): conf_flags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) return self._filter_list_empty_fields(conf_flags) def _include_obj_arc_flags(self, env): enable_arc = self._conanfile.conf.get("tools.apple:enable_arc", check_type=bool) fobj_arc = "" if enable_arc: fobj_arc = "-fobjc-arc" if enable_arc is False: fobj_arc = "-fno-objc-arc" if fobj_arc: env.append('OBJCFLAGS', [fobj_arc]) env.append('OBJCXXFLAGS', [fobj_arc]) def environment(self): env = Environment() # Setting Android cross-compilation flags (if exist) if self.android_cross_flags: for env_var, env_value in self.android_cross_flags.items(): unix_env_value = unix_path(self._conanfile, env_value) env.define(env_var, unix_env_value) else: # Setting user custom compiler executables flags compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={}, check_type=dict) if compilers_by_conf: compilers_mapping = {"c": "CC", "cpp": "CXX", "cuda": "NVCC", "fortran": "FC", "rc": "RC"} for comp, env_var in compilers_mapping.items(): if comp in compilers_by_conf: compiler = compilers_by_conf[comp] # https://github.com/conan-io/conan/issues/13780 compiler = unix_path(self._conanfile, compiler) env.define(env_var, compiler) compiler_setting = self._conanfile.settings.get_safe("compiler") if compiler_setting == "msvc": # None of them defined, if one is defined by user, user should define the other too if "c" not in compilers_by_conf and "cpp" not in compilers_by_conf: env.define("CC", "cl") env.define("CXX", "cl") env.append("CPPFLAGS", ["-D{}".format(d) for d in self.defines]) env.append("CXXFLAGS", self.cxxflags) env.append("CFLAGS", self.cflags) env.append("LDFLAGS", self.ldflags) if self.rcflags: env.append("RCFLAGS", self.rcflags) env.prepend_path("PKG_CONFIG_PATH", self._conanfile.generators_folder) # Objective C/C++ self._include_obj_arc_flags(env) # Issue related: https://github.com/conan-io/conan/issues/15486 if self._is_cross_building and self._conanfile.conf_build: compilers_build_mapping = ( self._conanfile.conf_build.get("tools.build:compiler_executables", default={}, check_type=dict) ) if "c" in compilers_build_mapping: env.define("CC_FOR_BUILD", compilers_build_mapping["c"]) if "cpp" in compilers_build_mapping: env.define("CXX_FOR_BUILD", compilers_build_mapping["cpp"]) return env def vars(self): return self.environment().vars(self._conanfile, scope="build") def generate(self, env=None, scope="build"): check_duplicated_generator(self, self._conanfile) env = env or self.environment() env = env.vars(self._conanfile, scope=scope) env.save_script("conanautotoolstoolchain") self.generate_args() VCVars(self._conanfile).generate(scope=scope) def _default_configure_shared_flags(self): args = [] # Just add these flags if there's a shared option defined (never add to exe's) try: if self._conanfile.package_type is PackageType.SHARED: args.extend(["--enable-shared", "--disable-static"]) elif self._conanfile.package_type is PackageType.STATIC: args.extend(["--disable-shared", "--enable-static"]) except ConanException: pass return args def _default_configure_install_flags(self): configure_install_flags = [] def _get_argument(argument_name, cppinfo_name): elements = getattr(self._conanfile.cpp.package, cppinfo_name) return "--{}=${{prefix}}/{}".format(argument_name, elements[0]) if elements else "" # If someone want arguments but not the defaults can pass them in args manually configure_install_flags.extend([f"--prefix={self._prefix}", _get_argument("bindir", "bindirs"), _get_argument("sbindir", "bindirs"), _get_argument("libdir", "libdirs"), _get_argument("includedir", "includedirs"), _get_argument("oldincludedir", "includedirs"), _get_argument("datarootdir", "resdirs")]) return [el for el in configure_install_flags if el] @staticmethod def _default_autoreconf_flags(): return ["--force", "--install"] def _get_triplets(self): triplets = [] for flag, value in (("--host=", self._host), ("--build=", self._build), ("--target=", self._target)): if value: triplets.append(f'{flag}{value}') return triplets def update_configure_args(self, updated_flags): """ Helper to update/prune flags from ``self.configure_args``. :param updated_flags: ``dict`` with arguments as keys and their argument values. Notice that if argument value is ``None``, this one will be pruned. """ self._update_flags("configure_args", updated_flags) def update_make_args(self, updated_flags): """ Helper to update/prune arguments from ``self.make_args``. :param updated_flags: ``dict`` with arguments as keys and their argument values. Notice that if argument value is ``None``, this one will be pruned. """ self._update_flags("make_args", updated_flags) def update_autoreconf_args(self, updated_flags): """ Helper to update/prune arguments from ``self.autoreconf_args``. :param updated_flags: ``dict`` with arguments as keys and their argument values. Notice that if argument value is ``None``, this one will be pruned. """ self._update_flags("autoreconf_args", updated_flags) # FIXME: Remove all these update_xxxx whenever xxxx_args are dicts or new ones replace them def _update_flags(self, attr_name, updated_flags): def _list_to_dict(flags): ret = {} for flag in flags: # Only splitting if "=" is there option = flag.split("=", 1) if len(option) == 2: ret[option[0]] = option[1] else: ret[option[0]] = "" return ret def _dict_to_list(flags): return [f"{k}={v}" if v else k for k, v in flags.items() if v is not None] self_args = getattr(self, attr_name) # FIXME: if xxxxx_args -> dict-type at some point, all these lines could be removed options = _list_to_dict(self_args) # Add/update/remove the current xxxxx_args with the new flags given options.update(updated_flags) # Update the current ones setattr(self, attr_name, _dict_to_list(options)) def generate_args(self): args = {"configure_args": cmd_args_to_string(self.configure_args), "make_args": cmd_args_to_string(self.make_args), "autoreconf_args": cmd_args_to_string(self.autoreconf_args)} save_toolchain_args(args, namespace=self._namespace) ================================================ FILE: conan/tools/gnu/get_gnu_triplet.py ================================================ from conan.errors import ConanException def _get_gnu_arch(os_, arch): # Calculate the arch machine = {"x86": "i686", "x86_64": "x86_64", "armv8": "aarch64", "armv8_32": "aarch64", # https://wiki.linaro.org/Platform/arm64-ilp32 "armv8.3": "aarch64", "asm.js": "asmjs", "wasm": "wasm32", "wasm64": "wasm64", }.get(arch, None) if not machine: # https://wiki.debian.org/Multiarch/Tuples if os_ == "AIX": if "ppc32" in arch: machine = "rs6000" elif "ppc64" in arch: machine = "powerpc" elif "arm" in arch: machine = "arm" elif "ppc32be" in arch: machine = "powerpcbe" elif "ppc64le" in arch: machine = "powerpc64le" elif "ppc64" in arch: machine = "powerpc64" elif "ppc32" in arch: machine = "powerpc" elif "mips64" in arch: machine = "mips64" elif "mips" in arch: machine = "mips" elif "sparcv9" in arch: machine = "sparc64" elif "sparc" in arch: machine = "sparc" elif "s390x" in arch: machine = "s390x-ibm" elif "s390" in arch: machine = "s390-ibm" elif "sh4" in arch: machine = "sh4" elif "e2k" in arch: # https://lists.gnu.org/archive/html/config-patches/2015-03/msg00000.html machine = "e2k-unknown" elif "riscv64" in arch: machine = 'riscv64' elif 'riscv32' in arch: machine = "riscv32" if machine is None: raise ConanException("Unknown '%s' machine, Conan doesn't know how to " "translate it to the GNU triplet, please report at " " https://github.com/conan-io/conan/issues" % arch) return machine def _get_gnu_os(os_, arch, compiler=None): # Calculate the OS if compiler == "gcc": windows_op = "w64-mingw32" else: windows_op = "unknown-windows" op_system = {"Windows": windows_op, "Linux": "linux-gnu", "Darwin": "apple-darwin", "Android": "linux-android", "Macos": "apple-darwin", "iOS": "apple-ios", "watchOS": "apple-watchos", "tvOS": "apple-tvos", "visionOS": "apple-xros", # NOTE: it technically must be "asmjs-unknown-emscripten" or # "wasm32-unknown-emscripten", but it's not recognized by old config.sub versions "Emscripten": "local-emscripten", "AIX": "ibm-aix", "Neutrino": "nto-qnx"}.get(os_, os_.lower()) if os_ in ("Linux", "Android"): if "arm" in arch and "armv8" not in arch: op_system += "eabi" if (arch == "armv5hf" or arch == "armv7hf") and os_ == "Linux": op_system += "hf" if arch == "armv8_32" and os_ == "Linux": op_system += "_ilp32" # https://wiki.linaro.org/Platform/arm64-ilp32 return op_system def _get_gnu_triplet(os_, arch, compiler=None): """ Returns string with -- triplet ( can be omitted in practice) :param os_: os to be used to create the triplet :param arch: arch to be used to create the triplet :param compiler: compiler used to create the triplet (only needed fo windows) """ if os_ == "Windows" and compiler is None: raise ConanException("'compiler' parameter for 'get_gnu_triplet()' is not specified and " "needed for os=Windows") machine = _get_gnu_arch(os_, arch) op_system = _get_gnu_os(os_, arch, compiler=compiler) return { 'machine': machine, 'system': op_system, 'triplet': f"{machine}-{op_system}" } ================================================ FILE: conan/tools/gnu/gnudeps_flags.py ================================================ """ This is a helper class which offers a lot of useful methods and attributes """ # FIXME: only for tools.gnu? perhaps it should be a global module from conan.tools.apple.apple import is_apple_os from conan.tools.microsoft import is_msvc from conan.internal.subsystems import subsystem_path, deduce_subsystem class GnuDepsFlags: def __init__(self, conanfile, cpp_info): self._conanfile = conanfile self._subsystem = deduce_subsystem(conanfile, scope="build") # From cppinfo, calculated flags self.include_paths = self._format_include_paths(cpp_info.includedirs) self.lib_paths = self._format_library_paths(cpp_info.libdirs) self.defines = self._format_defines(cpp_info.defines) self.libs = self._format_libraries(cpp_info.libs) self.frameworks = self._format_frameworks(cpp_info.frameworks) self.framework_paths = self._format_frameworks(cpp_info.frameworkdirs, is_path=True) # Direct flags self.cxxflags = cpp_info.cxxflags or [] self.cflags = cpp_info.cflags or [] self.sharedlinkflags = cpp_info.sharedlinkflags or [] self.exelinkflags = cpp_info.exelinkflags or [] self.system_libs = self._format_libraries(cpp_info.system_libs) # Not used? # self.bin_paths # self.build_paths # self.src_paths _GCC_LIKE = ['clang', 'apple-clang', 'gcc'] @staticmethod def _format_defines(defines): return ["-D%s" % define for define in defines] if defines else [] def _format_frameworks(self, frameworks, is_path=False): """ returns an appropriate compiler flags to link with Apple Frameworks or an empty array, if Apple Frameworks aren't supported by the given compiler """ os_ = self._conanfile.settings.get_safe("os") if not frameworks or not is_apple_os(self._conanfile): return [] compiler = self._conanfile.settings.get_safe("compiler") if str(compiler) not in self._GCC_LIKE: return [] if is_path: return ["-F\"%s\"" % self._adjust_path(framework_path) for framework_path in frameworks] else: return ["-framework %s" % framework for framework in frameworks] def _format_include_paths(self, include_paths): if not include_paths: return [] pattern = "/I%s" if is_msvc(self._conanfile) else "-I%s" return [pattern % (self._adjust_path(include_path)) for include_path in include_paths if include_path] def _format_library_paths(self, library_paths): if not library_paths: return [] pattern = "/LIBPATH:%s" if is_msvc(self._conanfile) else "-L%s" return [pattern % self._adjust_path(library_path) for library_path in library_paths if library_path] def _format_libraries(self, libraries): if not libraries: return [] result = [] is_visual = is_msvc(self._conanfile) for library in libraries: if is_visual: if not library.endswith(".lib"): library += ".lib" result.append(library) else: result.append("-l%s" % library) return result def _adjust_path(self, path): if is_msvc(self._conanfile): path = path.replace('/', '\\') else: path = path.replace('\\', '/') path = subsystem_path(self._subsystem, path) return '"%s"' % path if ' ' in path else path ================================================ FILE: conan/tools/gnu/gnutoolchain.py ================================================ import os from conan.errors import ConanException from conan.internal import check_duplicated_generator from conan.internal.internal_tools import is_universal_arch from conan.tools.apple.apple import is_apple_os, resolve_apple_flags, apple_extra_flags from conan.tools.build import cmd_args_to_string, save_toolchain_args from conan.tools.build.cross_building import cross_building from conan.tools.build.flags import architecture_flag, architecture_link_flag, build_type_flags, cppstd_flag, \ build_type_link_flags, \ libcxx_flags, llvm_clang_front, threads_flags from conan.tools.env import Environment, VirtualBuildEnv from conan.tools.gnu.get_gnu_triplet import _get_gnu_triplet from conan.tools.microsoft import VCVars, msvc_runtime_flag, unix_path, check_min_vs, is_msvc from conan.internal.model.pkg_type import PackageType class GnuToolchain: """ GnuToolchain generator. Note: it's based on legacy AutotoolsToolchain but with a more modern and usable UX """ script_name = "conangnutoolchain" def __init__(self, conanfile, namespace=None, prefix="/"): """ :param conanfile: The current recipe object. Always use ``self``. :param namespace: This argument avoids collisions when you have multiple toolchain calls in the same recipe. By setting this argument, the *conanbuild.conf* file used to pass information to the build helper will be named as *_conanbuild.conf*. The default value is ``None`` meaning that the name of the generated file is *conanbuild.conf*. This namespace must be also set with the same value in the constructor of the Autotools build helper so that it reads the information from the proper file. :param prefix: Folder to use for ``--prefix`` argument ("/" by default). """ self._conanfile = conanfile self._namespace = namespace self._is_apple_system = is_apple_os(self._conanfile) self._prefix = prefix # Extra flags self.extra_cxxflags = [] self.extra_cflags = [] self.extra_ldflags = [] self.extra_defines = [] # Extra environment definitions self.extra_env = Environment() # Defines self.ndebug = None build_type = self._conanfile.settings.get_safe("build_type") if build_type in ['Release', 'RelWithDebInfo', 'MinSizeRel']: self.ndebug = "NDEBUG" # TODO: This is also covering compilers like Visual Studio, necessary to test it (&remove?) self.build_type_flags = build_type_flags(self._conanfile) self.build_type_link_flags = build_type_link_flags(self._conanfile.settings) self.cppstd = cppstd_flag(self._conanfile) self.arch_flag = architecture_flag(self._conanfile) self.arch_ld_flag = architecture_link_flag(self._conanfile) self.threads_flags = threads_flags(self._conanfile) self.libcxx, self.gcc_cxx11_abi = libcxx_flags(self._conanfile) self.fpic = self._conanfile.options.get_safe("fPIC") self.msvc_runtime_flag = self._get_msvc_runtime_flag() self.msvc_extra_flags = self._msvc_extra_flags() self.msvc_runtime_link_flags = [] if llvm_clang_front(self._conanfile) == "clang": self.msvc_runtime_link_flags = ["-fuse-ld=lld-link"] self._is_universal_arch = is_universal_arch(conanfile.settings.get_safe("arch"), conanfile.settings.possible_values().get("arch")) if self._is_universal_arch and not is_apple_os(self._conanfile): arch_str = conanfile.settings.get_safe('arch') raise ConanException(f"Universal arch '{arch_str}' is only supported in Apple OSes") extra_configure_args = self._conanfile.conf.get("tools.gnu:extra_configure_args", check_type=list, default=[]) extra_configure_args = {it: None for it in extra_configure_args} # Host/Build triplets self.triplets_info = { "host": {"triplet": self._conanfile.conf.get("tools.gnu:host_triplet")}, "build": {"triplet": self._conanfile.conf.get("tools.gnu:build_triplet")} } self._is_cross_building = not self._is_universal_arch and cross_building(self._conanfile) if self._is_cross_building: compiler = self._conanfile.settings.get_safe("compiler") # Host triplet if not self.triplets_info["host"]["triplet"]: os_host = conanfile.settings.get_safe("os") arch_host = conanfile.settings.get_safe("arch") self.triplets_info["host"] = _get_gnu_triplet(os_host, arch_host, compiler=compiler) # Build triplet if not self.triplets_info["build"]["triplet"]: os_build = conanfile.settings_build.get_safe('os') arch_build = conanfile.settings_build.get_safe('arch') self.triplets_info["build"] = _get_gnu_triplet(os_build, arch_build, compiler=compiler) sysroot = self._conanfile.conf.get("tools.build:sysroot") if sysroot: root = sysroot.replace("\\", "/") compiler = self._conanfile.settings.get_safe("compiler") self.sysroot_flag = f"--sysroot {root}" if compiler != "qcc" else f"-Wc,-isysroot,{root}" else: self.sysroot_flag = None self.configure_args = {} self.autoreconf_args = {"--force": None, "--install": None} self.make_args = {} # Initializing configure arguments: triplets, shared flags, dirs flags, etc. self.configure_args.update(self._get_default_configure_shared_flags()) self.configure_args.update(self._get_default_configure_install_flags()) self.configure_args.update(self._get_default_triplets()) self.configure_args.update(extra_configure_args) # Apple stuff is_cross_building_osx = (self._is_cross_building and conanfile.settings_build.get_safe('os') == "Macos" and is_apple_os(conanfile) and not self._is_universal_arch) min_flag, arch_flags, isysroot_flag = ( resolve_apple_flags(conanfile, is_cross_building=is_cross_building_osx, is_universal=self._is_universal_arch) ) # https://man.archlinux.org/man/clang.1.en#Target_Selection_Options self.apple_arch_flag = arch_flags # -isysroot makes all includes for your library relative to the build directory self.apple_isysroot_flag = isysroot_flag self.apple_min_version_flag = min_flag self.apple_extra_flags = apple_extra_flags(conanfile) # Default initial environment flags self._initialize_default_extra_env() def yes_no(self, option_name, default=None, negated=False): """ Simple wrapper to return "yes" or "no" depending on whether option_name is evaluated as True or False. :param option_name: option name. :param default: Default value to return. :param negated: Negates the option value if True. :return: "yes" or "no" depending on whether option_name is True or False. """ option_value = bool(self._conanfile.options.get_safe(option_name, default=default)) option_value = not option_value if negated else option_value return "yes" if option_value else "no" def _resolve_android_cross_compilation(self): # Issue related: https://github.com/conan-io/conan/issues/13443 ret = {} if not self._is_cross_building or not self._conanfile.settings.get_safe("os") == "Android": return ret # Setting host if it was not already defined yet arch = self._conanfile.settings.get_safe("arch") android_target = {'armv7': 'armv7a-linux-androideabi', 'armv8': 'aarch64-linux-android', 'x86': 'i686-linux-android', 'x86_64': 'x86_64-linux-android'}.get(arch) if self.triplets_info["host"]["triplet"] is None: self.triplets_info["host"]["triplet"] = android_target # Automatic guessing made by Conan (need the NDK path variable defined) conan_vars = {} ndk_path = self._conanfile.conf.get("tools.android:ndk_path", check_type=str) if ndk_path: os_build = self._conanfile.settings_build.get_safe("os") ndk_os_folder = { 'Macos': 'darwin', 'iOS': 'darwin', 'watchOS': 'darwin', 'tvOS': 'darwin', 'visionOS': 'darwin', 'FreeBSD': 'linux', 'Linux': 'linux', 'Windows': 'windows', 'WindowsCE': 'windows', 'WindowsStore': 'windows' }.get(os_build, "linux") ext = ".cmd" if os_build == "Windows" else "" ndk_bin = os.path.join(ndk_path, "toolchains", "llvm", "prebuilt", f"{ndk_os_folder}-x86_64", "bin") android_api_level = self._conanfile.settings.get_safe("os.api_level") conan_vars = { "CC": os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang{ext}"), "CXX": os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang++{ext}"), "LD": os.path.join(ndk_bin, "ld"), "STRIP": os.path.join(ndk_bin, "llvm-strip"), "RANLIB": os.path.join(ndk_bin, "llvm-ranlib"), "AS": os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang{ext}"), "AR": os.path.join(ndk_bin, "llvm-ar"), "ADDR2LINE": os.path.join(ndk_bin, "llvm-addr2line"), "NM": os.path.join(ndk_bin, "llvm-nm"), "OBJCOPY": os.path.join(ndk_bin, "llvm-objcopy"), "OBJDUMP": os.path.join(ndk_bin, "llvm-objdump"), "READELF": os.path.join(ndk_bin, "llvm-readelf"), "ELFEDIT": os.path.join(ndk_bin, "llvm-elfedit") } build_env = VirtualBuildEnv(self._conanfile, auto_generate=True).vars() for var_name, var_path in conan_vars.items(): # User variables have more priority than Conan ones, so if it was defined within # the build env then do nothing if build_env.get(var_name) is None: ret[var_name] = var_path return ret def _resolve_compilers_mapping_variables(self): ret = {} # Configuration map compilers_mapping = {"c": "CC", "cpp": "CXX", "cuda": "NVCC", "fortran": "FC", "rc": "RC", "nm": "NM", "ranlib": "RANLIB", "objdump": "OBJDUMP", "strip": "STRIP"} # Compiler definitions by conf compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={}, check_type=dict) if compilers_by_conf: for comp, env_var in compilers_mapping.items(): if comp in compilers_by_conf: compiler = compilers_by_conf[comp] # https://github.com/conan-io/conan/issues/13780 compiler = unix_path(self._conanfile, compiler) ret[env_var] = compiler # User/tools ones have precedence return ret def _initialize_default_extra_env(self): """Initialize the default environment variables.""" # If it's an Android cross-compilation extra_env_vars = self._resolve_android_cross_compilation() if not extra_env_vars: # Normally, these are the most common default flags used by MSVC in Windows if is_msvc(self._conanfile): extra_env_vars = {"CC": "cl -nologo", "CXX": "cl -nologo", "LD": "link -nologo", "AR": "lib", "NM": "dumpbin -symbols", "OBJDUMP": ":", "RANLIB": ":", "STRIP": ":"} extra_env_vars.update(self._resolve_compilers_mapping_variables()) # Issue related: https://github.com/conan-io/conan/issues/15486 if self._is_cross_building and self._conanfile.conf_build: compilers_build_mapping = ( self._conanfile.conf_build.get("tools.build:compiler_executables", default={}, check_type=dict) ) if "c" in compilers_build_mapping: extra_env_vars["CC_FOR_BUILD"] = compilers_build_mapping["c"] if "cpp" in compilers_build_mapping: extra_env_vars["CXX_FOR_BUILD"] = compilers_build_mapping["cpp"] # Update the extra_env attribute with all the compiler values for env_var, env_value in extra_env_vars.items(): self.extra_env.define(env_var, env_value) def _get_msvc_runtime_flag(self): if llvm_clang_front(self._conanfile) == "clang": if self._conanfile.settings.compiler.runtime == "dynamic": runtime_type = self._conanfile.settings.get_safe("compiler.runtime_type") library = "msvcrtd" if runtime_type == "Debug" else "msvcrt" debug = "-D_DEBUG " if runtime_type == "Debug" else "" return f"{debug}-D_DLL -D_MT -Xclang --dependent-lib={library}" return "" # By default it already link statically flag = msvc_runtime_flag(self._conanfile) return f"-{flag}" if flag else "" def _msvc_extra_flags(self): if is_msvc(self._conanfile) and check_min_vs(self._conanfile, "180", raise_invalid=False): return ["-FS"] return [] def _add_msvc_flags(self, flags): # This is to avoid potential duplicate with users recipes -FS (alreday some in ConanCenter) return [f for f in self.msvc_extra_flags if f not in flags] @staticmethod def _filter_list_empty_fields(v): return list(filter(bool, v)) @staticmethod def _dict_to_list(flags): return [f"{k}={v}" if v is not None else k for k, v in flags.items()] @property def cxxflags(self): fpic = "-fPIC" if self.fpic else None ret = [self.libcxx, self.cppstd, self.arch_flag, fpic, self.msvc_runtime_flag, self.sysroot_flag] + self.threads_flags apple_flags = [self.apple_isysroot_flag, self.apple_arch_flag, self.apple_min_version_flag] apple_flags += self.apple_extra_flags conf_flags = self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) vs_flag = self._add_msvc_flags(self.extra_cxxflags) ret = ret + self.build_type_flags + apple_flags + self.extra_cxxflags + vs_flag + conf_flags return self._filter_list_empty_fields(ret) @property def cflags(self): fpic = "-fPIC" if self.fpic else None ret = [self.arch_flag, fpic, self.msvc_runtime_flag, self.sysroot_flag] + self.threads_flags apple_flags = [self.apple_isysroot_flag, self.apple_arch_flag, self.apple_min_version_flag] apple_flags += self.apple_extra_flags conf_flags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list) vs_flag = self._add_msvc_flags(self.extra_cflags) ret = ret + self.build_type_flags + apple_flags + self.extra_cflags + vs_flag + conf_flags return self._filter_list_empty_fields(ret) @property def ldflags(self): ret = [self.arch_flag, self.sysroot_flag, self.arch_ld_flag] + self.threads_flags apple_flags = [self.apple_isysroot_flag, self.apple_arch_flag, self.apple_min_version_flag] apple_flags += self.apple_extra_flags conf_flags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) conf_flags.extend(self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list)) linker_scripts = self._conanfile.conf.get("tools.build:linker_scripts", default=[], check_type=list) conf_flags.extend(["-T'" + linker_script + "'" for linker_script in linker_scripts]) ret = ret + self.build_type_link_flags + apple_flags + self.extra_ldflags + conf_flags ret = ret + self.msvc_runtime_link_flags return self._filter_list_empty_fields(ret) @property def defines(self): conf_flags = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list) ret = [self.ndebug, self.gcc_cxx11_abi] + self.extra_defines + conf_flags return self._filter_list_empty_fields(ret) @property def rcflags(self): conf_flags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) return self._filter_list_empty_fields(conf_flags) def _get_default_configure_shared_flags(self): args = {} # Just add these flags if there's a shared option defined (never add to exe's) if self._conanfile.package_type is PackageType.SHARED: args = {"--enable-shared": None, "--disable-static": None} elif self._conanfile.package_type is PackageType.STATIC: args = {"--disable-shared": None, "--enable-static": None} return args def _get_default_configure_install_flags(self): configure_install_flags = {"--prefix": self._prefix} # If someone want arguments but not the defaults can pass them in args manually for flag_name, cppinfo_name in [("bindir", "bindirs"), ("sbindir", "bindirs"), ("libdir", "libdirs"), ("includedir", "includedirs"), ("oldincludedir", "includedirs"), ("datarootdir", "resdirs")]: elements = getattr(self._conanfile.cpp.package, cppinfo_name) cppinfo_value = f"${{prefix}}/{elements[0]}" if elements else None if cppinfo_value: configure_install_flags[f"--{flag_name}"] = cppinfo_value return configure_install_flags def _get_default_triplets(self): triplets = {} for context, info in self.triplets_info.items(): if info.get("triplet") is not None: triplets[f"--{context}"] = info["triplet"] return triplets def _include_obj_arc_flags(self, env): enable_arc = self._conanfile.conf.get("tools.apple:enable_arc", check_type=bool) fobj_arc = "" if enable_arc: fobj_arc = "-fobjc-arc" if enable_arc is False: fobj_arc = "-fno-objc-arc" if fobj_arc: env.append('OBJCFLAGS', [fobj_arc]) env.append('OBJCXXFLAGS', [fobj_arc]) @property def _environment(self): env = Environment() # Flags and defines env.append("CPPFLAGS", ["-D{}".format(d) for d in self.defines]) env.append("CXXFLAGS", self.cxxflags) env.append("CFLAGS", self.cflags) env.append("LDFLAGS", self.ldflags) if self.rcflags: env.append("RCFLAGS", self.rcflags) env.prepend_path("PKG_CONFIG_PATH", self._conanfile.generators_folder) # Objective C/C++ self._include_obj_arc_flags(env) # Let's compose with user extra env variables defined (user ones have precedence) return self.extra_env.compose_env(env) def generate(self): check_duplicated_generator(self, self._conanfile) # Composing both environments. User extra_env definitions has precedence env_vars = self._environment.vars(self._conanfile) env_vars.save_script(GnuToolchain.script_name) # Converts all the arguments into strings args = { "configure_args": cmd_args_to_string(self._dict_to_list(self.configure_args)), "make_args": cmd_args_to_string(self._dict_to_list(self.make_args)), "autoreconf_args": cmd_args_to_string(self._dict_to_list(self.autoreconf_args)) } save_toolchain_args(args, namespace=self._namespace) VCVars(self._conanfile).generate() ================================================ FILE: conan/tools/gnu/makedeps.py ================================================ """Makefile generator for Conan dependencies This generator creates a Makefile (conandeps.mk) with variables for each dependency and consider their components. To simplify its usage, it also creates global variables with aggregated values from all dependencies. This generator does not work like a toolchain, it does not include settings. For better customization, it allows appending prefixes as flags variables: - CONAN_LIB_FLAG: Add a prefix to all libs variables, e.g. -l - CONAN_DEFINE_FLAG: Add a prefix to all defines variables, e.g. -D - CONAN_SYSTEM_LIB_FLAG: Add a prefix to all system_libs variables, e.g. -l - CONAN_INCLUDE_DIR_FLAG: Add a prefix to all include dirs variables, e.g. -I - CONAN_LIB_DIR_FLAG: Add a prefix to all lib dirs variables, e.g. -L - CONAN_BIN_DIR_FLAG: Add a prefix to all bin dirs variables, e.g. -L The conandeps.mk file layout is as follows: - CONAN_DEPS: list all transitive and direct dependencies names without version (e.g. zlib) - Iterate over each dependency and its components: - Prints name, version, root folder, regular folders, libs and flags - Components are rootified to avoid repeating same prefix twice for the root folder - Components libs, folder and flags are solved as variables to avoid repeating same name twice - Aggregated global variables for simplification, sum all dependencies to common variables (e.g. CONAN_INCLUDE_DIRS) """ import os import re import textwrap from jinja2 import Template, StrictUndefined from typing import Optional from conan.api.output import ConanOutput from conan.internal import check_duplicated_generator from conan.tools.files import save CONAN_MAKEFILE_FILENAME = "conandeps.mk" def _get_formatted_dirs(folders: list, prefix_path_: str, name: str) -> list: """ Format the directories to be used in the makefile, adding the prefix path if needed :param folders: list of directories :param prefix_path_: prefix path :param name: component name :return: list of formatted directories """ ret = [] for directory in folders: if directory.startswith("$(CONAN"): # already a variable ret.append(directory) continue directory = os.path.normpath(directory).replace("\\", "/") prefix = "" if not os.path.isabs(directory): prefix = f"$(CONAN_ROOT_{name})/" elif directory.startswith(prefix_path_): prefix = f"$(CONAN_ROOT_{name})/" directory = os.path.relpath(directory, prefix_path_).replace("\\", "/") ret.append(f"{prefix}{directory}") return ret def _makefy(name: str) -> str: """ Convert a name to Make-variable-friendly syntax :param name: The name to be converted :return: Safe makefile variable, not including bad characters that are not parsed correctly """ return re.sub(r'[^0-9A-Z_]', '_', name.upper()) def _makefy_properties(properties: Optional[dict]) -> dict: """ Convert property dictionary keys to Make-variable-friendly syntax :param properties: The property dictionary to be converted (None is also accepted) :return: Modified property dictionary with keys not including bad characters that are not parsed correctly """ return {_makefy(name): value for name, value in properties.items()} if properties else {} def _check_property_value(name, value, output): if "\n" in value: output.warning(f"Skipping propery '{name}' because it contains newline") return False else: return True def _filter_properties(properties: Optional[dict], output) -> dict: """ Filter out properties whose values contain newlines, because they would break the generated makefile :param properties: A property dictionary (None is also accepted) :return: A property dictionary without the properties containing newlines """ return {name: value for name, value in properties.items() if _check_property_value(name, value, output)} if properties else {} def _conan_prefix_flag(variable: str) -> str: """ Return a global flag to be used as prefix to any value in the makefile """ return f"$(CONAN_{variable.upper()}_FLAG)" if variable else "" def _common_cppinfo_variables() -> dict: """ Regular cppinfo variables exported by any Conanfile and their Makefile prefixes """ return { "objects": None, "libs": "lib", "defines": "define", "cflags": None, "cxxflags": None, "sharedlinkflags": None, "exelinkflags": None, "frameworks": None, "requires": None, "system_libs": "system_lib", } def _common_cppinfo_dirs() -> dict: """ Regular cppinfo folders exported by any Conanfile and their Makefile prefixes """ return { "includedirs": "include_dir", "libdirs": "lib_dir", "bindirs": "bin_dir", "srcdirs": None, "builddirs": None, "resdirs": None, "frameworkdirs": None, } def _jinja_format_list_values() -> str: """ Template method to format a list of values in a Makefile, - Empty variables are not exposed in the Makefile - Single value variables are exposed in a single line - Multiple value variables are exposed in multiple lines with a tabulation e.g. define_variable_value("FOO", ["single_value"]) output: FOO = single_value define_variable_value("BAR", ["value1", "value2"]) output: BAR = \ value1 \ value2 """ return textwrap.dedent("""\ {%- macro define_variable_value_safe(var, object, attribute) -%} {%- if attribute in object -%} {{ define_variable_value("{}".format(var), object[attribute]) }} {%- endif -%} {%- endmacro %} {%- macro define_multiple_variable_value(var, values) -%} {% for property_name, value in values.items() %} {{ var }}_{{ property_name }} = {{ value }} {% endfor %} {%- endmacro %} {%- macro define_variable_value(var, values) -%} {%- if values is not none -%} {%- if values|length > 0 -%} {{ var }} = {{ format_list_values(values) }} {%- endif -%} {%- endif -%} {%- endmacro %} {%- macro format_list_values(values) -%} {% if values|length == 1 %} {{ values[0] }} {% elif values|length > 1 %} \\ {% for value in values[:-1] %} \t{{ value }} \\ {% endfor %} \t{{ values|last }} {% endif %} {%- endmacro %} """) class MakeInfo: """ Store temporary information about each dependency """ def __init__(self, name: str, dirs: list, flags: list): """ :param name: Dependency or component raw name :param dirs: cpp_info folders supported by the dependency :param flags: cpp_info variables supported by the dependency """ self._name = name self._dirs = dirs self._flags = flags @property def name(self) -> str: return self._name @property def dirs(self) -> list: """ :return: List of cpp_info folders supported by the dependency without duplicates """ return list(set(self._dirs)) @property def flags(self) -> list: """ :return: List of cpp_info variables supported by the dependency without duplicates """ return list(set(self._flags)) def dirs_append(self, directory: str): """ Add a new cpp_info folder to the dependency """ self._dirs.append(directory) def flags_append(self, flag: str): """ Add a new cpp_info variable to the dependency """ self._flags.append(flag) class GlobalContentGenerator: """ Generates the formatted content for global variables (e.g. CONAN_DEPS, CONAN_LIBS) """ template = textwrap.dedent("""\ # Aggregated global variables {{ define_variable_value("CONAN_INCLUDE_DIRS", deps_cpp_info_dirs.include_dirs) -}} {{- define_variable_value("CONAN_LIB_DIRS", deps_cpp_info_dirs.lib_dirs) -}} {{- define_variable_value("CONAN_BIN_DIRS", deps_cpp_info_dirs.bin_dirs) -}} {{- define_variable_value("CONAN_SRC_DIRS", deps_cpp_info_dirs.src_dirs) -}} {{- define_variable_value("CONAN_BUILD_DIRS", deps_cpp_info_dirs.build_dirs) -}} {{- define_variable_value("CONAN_RES_DIRS", deps_cpp_info_dirs.res_dirs) -}} {{- define_variable_value("CONAN_FRAMEWORK_DIRS", deps_cpp_info_dirs.framework_dirs) -}} {{- define_variable_value("CONAN_OBJECTS", deps_cpp_info_flags.objects) -}} {{- define_variable_value("CONAN_LIBS", deps_cpp_info_flags.libs) -}} {{- define_variable_value("CONAN_DEFINES", deps_cpp_info_flags.defines) -}} {{- define_variable_value("CONAN_CFLAGS", deps_cpp_info_flags.cflags) -}} {{- define_variable_value("CONAN_CXXFLAGS", deps_cpp_info_flags.cxxflags) -}} {{- define_variable_value("CONAN_SHAREDLINKFLAGS", deps_cpp_info_flags.sharedlinkflags) -}} {{- define_variable_value("CONAN_EXELINKFLAGS", deps_cpp_info_flags.exelinkflags) -}} {{- define_variable_value("CONAN_FRAMEWORKS", deps_cpp_info_flags.frameworks) -}} {{- define_variable_value("CONAN_REQUIRES", deps_cpp_info_flags.requires) -}} {{- define_variable_value("CONAN_SYSTEM_LIBS", deps_cpp_info_flags.system_libs) -}} """) template_deps = textwrap.dedent("""\ {{ define_variable_value("CONAN_DEPS", deps) }} """) def content(self, deps_cpp_info_dirs: dict, deps_cpp_info_flags: dict) -> str: """ Generate content for Cppinfo variables (e.g. CONAN_LIBS, CONAN_INCLUDE_DIRS) :param deps_cpp_info_dirs: Formatted dependencies folders :param deps_cpp_info_flags: Formatted dependencies variables """ context = {"deps_cpp_info_dirs": deps_cpp_info_dirs, "deps_cpp_info_flags": deps_cpp_info_flags} template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) return template.render(context) def deps_content(self, dependencies_names: list) -> str: """ Generate content for CONAN_DEPS (e.g. CONAN_DEPS = zlib, openssl) :param dependencies_names: Non-formatted dependencies names """ context = {"deps": dependencies_names} template = Template(_jinja_format_list_values() + self.template_deps, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) return template.render(context) class GlobalGenerator: """ Process all collected dependencies and parse to generate global content """ def __init__(self, conanfile, make_infos): self._conanfile = conanfile self._make_infos = make_infos def _get_dependency_dirs(self) -> dict: """ List regular directories from cpp_info and format them to be used in the makefile """ dirs = {} for var in _common_cppinfo_dirs(): key = var.replace("dirs", "_dirs") dirs[key] = [f"$(CONAN_{key.upper()}_{_makefy(makeinfo.name)})" for makeinfo in self._make_infos if var in makeinfo.dirs] return dirs def _get_dependency_flags(self) -> dict: """ List common variables from cpp_info and format them to be used in the makefile """ flags = {} for var in _common_cppinfo_variables(): key = var.replace("dirs", "_dirs") flags[key] = [f"$(CONAN_{key.upper()}_{_makefy(makeinfo.name)})" for makeinfo in self._make_infos if var in makeinfo.flags] return flags def generate(self) -> str: """ Process folder and variables for a dependency and generates its Makefile content """ glob_content_gen = GlobalContentGenerator() dirs = self._get_dependency_dirs() flags = self._get_dependency_flags() return glob_content_gen.content(dirs, flags) def deps_generate(self) -> str: """ Process dependencies names and generates its Makefile content. It should be added as first variable in the Makefile. """ dependencies = [makeinfo.name for makeinfo in self._make_infos if makeinfo.name != self._conanfile.name] glob_content_gen = GlobalContentGenerator() return glob_content_gen.deps_content(dependencies) class DepComponentContentGenerator: """ Generates Makefile content for each dependency component """ template = textwrap.dedent("""\ # {{ dep.ref.name }}::{{ comp_name }} {{ define_variable_value_safe("CONAN_INCLUDE_DIRS_{}_{}".format(dep_name, name), cpp_info_dirs, 'include_dirs') -}} {{- define_variable_value_safe("CONAN_LIB_DIRS_{}_{}".format(dep_name, name), cpp_info_dirs, 'lib_dirs') -}} {{- define_variable_value_safe("CONAN_BIN_DIRS_{}_{}".format(dep_name, name), cpp_info_dirs, 'bin_dirs') -}} {{- define_variable_value_safe("CONAN_SRC_DIRS_{}_{}".format(dep_name, name), cpp_info_dirs, 'src_dirs') -}} {{- define_variable_value_safe("CONAN_BUILD_DIRS_{}_{}".format(dep_name, name), cpp_info_dirs, 'build_dirs') -}} {{- define_variable_value_safe("CONAN_RES_DIRS_{}_{}".format(dep_name, name), cpp_info_dirs, 'res_dirs') -}} {{- define_variable_value_safe("CONAN_FRAMEWORK_DIRS_{}_{}".format(dep_name, name), cpp_info_dirs, 'framework_dirs') -}} {{- define_variable_value_safe("CONAN_OBJECTS_{}_{}".format(dep_name, name), cpp_info_flags, 'objects') -}} {{- define_variable_value_safe("CONAN_LIBS_{}_{}".format(dep_name, name), cpp_info_flags, 'libs') -}} {{- define_variable_value_safe("CONAN_DEFINES_{}_{}".format(dep_name, name), cpp_info_flags, 'defines') -}} {{- define_variable_value_safe("CONAN_CFLAGS_{}_{}".format(dep_name, name), cpp_info_flags, 'cflags') -}} {{- define_variable_value_safe("CONAN_CXXFLAGS_{}_{}".format(dep_name, name), cpp_info_flags, 'cxxflags') -}} {{- define_variable_value_safe("CONAN_SHAREDLINKFLAGS_{}_{}".format(dep_name, name), cpp_info_flags, 'sharedlinkflags') -}} {{- define_variable_value_safe("CONAN_EXELINKFLAGS_{}_{}".format(dep_name, name), cpp_info_flags, 'exelinkflags') -}} {{- define_variable_value_safe("CONAN_FRAMEWORKS_{}_{}".format(dep_name, name), cpp_info_flags, 'frameworks') -}} {{- define_variable_value_safe("CONAN_REQUIRES_{}_{}".format(dep_name, name), cpp_info_flags, 'requires') -}} {{- define_variable_value_safe("CONAN_SYSTEM_LIBS_{}_{}".format(dep_name, name), cpp_info_flags, 'system_libs') -}} {{- define_multiple_variable_value("CONAN_PROPERTY_{}_{}".format(dep_name, name), properties) -}} """) def __init__(self, dependency, component_name: str, dirs: dict, flags: dict, output): """ :param dependency: The dependency object that owns the component :param component_name: component raw name e.g. poco::poco_json :param dirs: The component cpp_info folders :param flags: The component cpp_info variables """ self._dep = dependency self._name = component_name self._dirs = dirs or {} self._flags = flags or {} self._output = output def content(self) -> str: """ Format template and generate Makefile component """ context = { "dep": self._dep, "comp_name": self._name, "dep_name": _makefy(self._dep.ref.name), "name": _makefy(self._name), "cpp_info_dirs": self._dirs, "cpp_info_flags": self._flags, "properties": _makefy_properties(_filter_properties(self._dep.cpp_info.components[self._name]._properties, self._output)), } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) return template.render(context) class DepContentGenerator: """ Generates Makefile content for a dependency """ template = textwrap.dedent("""\ # {{ dep.ref }}{% if not req.direct %} (indirect dependency){% endif +%} CONAN_NAME_{{ name }} = {{ dep.ref.name }} CONAN_VERSION_{{ name }} = {{ dep.ref.version }} CONAN_REFERENCE_{{ name }} = {{ dep.ref }} CONAN_ROOT_{{ name }} = {{ root }} {{ define_variable_value("CONAN_SYSROOT_{}".format(name), sysroot) -}} {{- define_variable_value_safe("CONAN_INCLUDE_DIRS_{}".format(name), cpp_info_dirs, 'include_dirs') -}} {{- define_variable_value_safe("CONAN_LIB_DIRS_{}".format(name), cpp_info_dirs, 'lib_dirs') -}} {{- define_variable_value_safe("CONAN_BIN_DIRS_{}".format(name), cpp_info_dirs, 'bin_dirs') -}} {{- define_variable_value_safe("CONAN_SRC_DIRS_{}".format(name), cpp_info_dirs, 'src_dirs') -}} {{- define_variable_value_safe("CONAN_BUILD_DIRS_{}".format(name), cpp_info_dirs, 'build_dirs') -}} {{- define_variable_value_safe("CONAN_RES_DIRS_{}".format(name), cpp_info_dirs, 'res_dirs') -}} {{- define_variable_value_safe("CONAN_FRAMEWORK_DIRS_{}".format(name), cpp_info_dirs, 'framework_dirs') -}} {{- define_variable_value_safe("CONAN_OBJECTS_{}".format(name), cpp_info_flags, 'objects') -}} {{- define_variable_value_safe("CONAN_LIBS_{}".format(name), cpp_info_flags, 'libs') -}} {{- define_variable_value_safe("CONAN_DEFINES_{}".format(name), cpp_info_flags, 'defines') -}} {{- define_variable_value_safe("CONAN_CFLAGS_{}".format(name), cpp_info_flags, 'cflags') -}} {{- define_variable_value_safe("CONAN_CXXFLAGS_{}".format(name), cpp_info_flags, 'cxxflags') -}} {{- define_variable_value_safe("CONAN_SHAREDLINKFLAGS_{}".format(name), cpp_info_flags, 'sharedlinkflags') -}} {{- define_variable_value_safe("CONAN_EXELINKFLAGS_{}".format(name), cpp_info_flags, 'exelinkflags') -}} {{- define_variable_value_safe("CONAN_FRAMEWORKS_{}".format(name), cpp_info_flags, 'frameworks') -}} {{- define_variable_value_safe("CONAN_REQUIRES_{}".format(name), cpp_info_flags, 'requires') -}} {{- define_variable_value_safe("CONAN_SYSTEM_LIBS_{}".format(name), cpp_info_flags, 'system_libs') -}} {{- define_variable_value("CONAN_COMPONENTS_{}".format(name), components) -}} {{- define_multiple_variable_value("CONAN_PROPERTY_{}".format(name), properties) -}} """) def __init__(self, dependency, require, root: str, sysroot, dirs: dict, flags: dict, output): self._dep = dependency self._req = require self._root = root self._sysroot = sysroot self._dirs = dirs or {} self._flags = flags or {} self._output = output def content(self) -> str: """ Parse dependency variables and generate its Makefile content """ context = { "dep": self._dep, "req": self._req, "name": _makefy(self._dep.ref.name), "root": self._root, "sysroot": self._sysroot, "components": list(self._dep.cpp_info.get_sorted_components().keys()), "cpp_info_dirs": self._dirs, "cpp_info_flags": self._flags, "properties": _makefy_properties(_filter_properties(self._dep.cpp_info._properties, self._output)), } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) return template.render(context) class DepComponentGenerator: """ Generates Makefile content for a dependency component """ def __init__(self, dependency, makeinfo: MakeInfo, component_name: str, component, root: str, output): """ :param dependency: The dependency object that owns the component :param makeinfo: Makeinfo to store component variables :param component_name: The component raw name e.g. poco::poco_json :param component: The component object to obtain cpp_info variables :param root: The dependency root folder """ self._dep = dependency self._name = component_name self._comp = component self._root = root self._makeinfo = makeinfo self._output = output def _get_component_dirs(self) -> dict: """ List regular directories from cpp_info and format them to be used in the makefile :return: A dictionary with regular folder name and its formatted path """ dirs = {} for var, flag in _common_cppinfo_dirs().items(): cppinfo_value = getattr(self._comp, var) formatted_dirs = _get_formatted_dirs(cppinfo_value, self._root, _makefy(self._name)) if formatted_dirs: self._makeinfo.dirs_append(var) var = var.replace("dirs", "_dirs") formatted_dirs = self._rootify(self._root, self._dep.ref.name, cppinfo_value) dirs[var] = [_conan_prefix_flag(flag) + it for it in formatted_dirs] return dirs @staticmethod def _rootify(root: str, root_id: str, path_list: list) -> list: """ Replaces component folder path by its root node folder path in case they match :param root: root folder path for component's father :param root_id: component's dependency name :param path_list: folder list available in the component :return: A formatted folder list, solving root folder path as prefix """ root_len = len(root) root_with_sep = root + os.sep root_var_ref = f"$(CONAN_ROOT_{_makefy(root_id)})" return [root_var_ref + path[root_len:].replace("\\", "/") if path.startswith(root_with_sep) else path for path in path_list] def _get_component_flags(self) -> dict: """ List common variables from cpp_info and format them to be used in the makefile :return: A dictionary with regular flag/variable name and its formatted value with prefix """ flags = {} for var, prefix_var in _common_cppinfo_variables().items(): cppinfo_value = getattr(self._comp, var) if not cppinfo_value: continue if "flags" in var: cppinfo_value = [var.replace('"', '\\"') for var in cppinfo_value] if cppinfo_value: flags[var] = [_conan_prefix_flag(prefix_var) + it for it in cppinfo_value] self._makeinfo.flags_append(var) return flags def generate(self) -> str: """ Process component cpp_info variables and generate its Makefile content :return: Component Makefile content """ dirs = self._get_component_dirs() flags = self._get_component_flags() comp_content_gen = DepComponentContentGenerator(self._dep, self._name, dirs, flags, self._output) comp_content = comp_content_gen.content() return comp_content class DepGenerator: """ Process a dependency cpp_info variables and generate its Makefile content """ def __init__(self, dependency, require, output): self._dep = dependency self._req = require self._info = MakeInfo(self._dep.ref.name, [], []) self._output = output @property def makeinfo(self) -> MakeInfo: """ :return: Dependency folder and flags """ return self._info def _get_dependency_dirs(self, root: str, dependency) -> dict: """ List regular directories from cpp_info and format them to be used in the makefile :param root: Package root folder :param dependency: Dependency object :return: A dictionary with regular folder name and its formatted path """ dirs = {} for var, prefix in _common_cppinfo_dirs().items(): cppinfo_value = getattr(dependency.cpp_info, var) if not cppinfo_value: # The root value is not defined, there might be components cppinfo_value = [f"$(CONAN_{var.replace('dirs', '_dirs').upper()}_{_makefy(dependency.ref.name)}_{_makefy(name)})" for name, obj in dependency.cpp_info.components.items() if getattr(obj, var.lower())] prefix = "" formatted_dirs = _get_formatted_dirs(cppinfo_value, root, _makefy(dependency.ref.name)) if formatted_dirs: self._info.dirs_append(var) var = var.replace("dirs", "_dirs") dirs[var] = [_conan_prefix_flag(prefix) + it for it in formatted_dirs] return dirs def _get_dependency_flags(self, dependency) -> dict: """ List common variables from cpp_info and format them to be used in the makefile :param dependency: Dependency object """ flags = {} for var, prefix_var in _common_cppinfo_variables().items(): cppinfo_value = getattr(dependency.cpp_info, var) # Use component cpp_info info when does not provide any value if not cppinfo_value: cppinfo_value = [f"$(CONAN_{var.upper()}_{_makefy(dependency.ref.name)}_{_makefy(name)})" for name, obj in dependency.cpp_info.components.items() if getattr(obj, var.lower())] # avoid repeating same prefix twice prefix_var = "" if "flags" in var: cppinfo_value = [var.replace('"', '\\"') for var in cppinfo_value] if cppinfo_value: self._info.flags_append(var) flags[var] = [_conan_prefix_flag(prefix_var) + it for it in cppinfo_value] return flags def _get_sysroot(self, root: str) -> list: """ Get the sysroot of the dependency. Sysroot is a list of directories, or a single directory """ sysroot = self._dep.cpp_info.sysroot if isinstance(self._dep.cpp_info.sysroot, list) else [self._dep.cpp_info.sysroot] # sysroot may return [''] if not sysroot or not sysroot[0]: return [] return _get_formatted_dirs(sysroot, root, _makefy(self._dep.ref.name)) if sysroot and sysroot[0] else None def _get_root_folder(self): """ Get the root folder of the dependency """ root = self._dep.recipe_folder if self._dep.package_folder is None else self._dep.package_folder return root.replace("\\", "/") def generate(self) -> str: """ Process dependency folders and flags to generate its Makefile content. Plus, execute same steps for each component """ root = self._get_root_folder() sysroot = self._get_sysroot(root) dirs = self._get_dependency_dirs(root, self._dep) flags = self._get_dependency_flags(self._dep) dep_content_gen = DepContentGenerator(self._dep, self._req, root, sysroot, dirs, flags, self._output) content = dep_content_gen.content() for comp_name, comp in self._dep.cpp_info.get_sorted_components().items(): component_gen = DepComponentGenerator(self._dep, self._info, comp_name, comp, root, self._output) content += component_gen.generate() return content class MakeDeps: """ Generates a Makefile with the variables needed to build a project with the specified. """ _title = "# This Makefile has been generated by Conan. DO NOT EDIT!\n" def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile def generate(self) -> None: """ Collects all dependencies and components, then, generating a Makefile """ check_duplicated_generator(self, self._conanfile) host_req = self._conanfile.dependencies.host test_req = self._conanfile.dependencies.test content_buffer = f"{self._title}\n" deps_buffer = "" # Filter the build_requires not activated for any requirement dependencies = list(host_req.items()) + list(test_req.items()) make_infos = [] for require, dep in dependencies: output = ConanOutput(scope=f"{self._conanfile} MakeDeps: {dep}:") dep_gen = DepGenerator(dep, require, output) make_infos.append(dep_gen.makeinfo) deps_buffer += dep_gen.generate() glob_gen = GlobalGenerator(self._conanfile, make_infos) content_buffer += glob_gen.deps_generate() + deps_buffer + glob_gen.generate() save(self._conanfile, CONAN_MAKEFILE_FILENAME, content_buffer) self._conanfile.output.info(f"Generated {CONAN_MAKEFILE_FILENAME}") ================================================ FILE: conan/tools/gnu/pkgconfig.py ================================================ import textwrap from io import StringIO from conan.tools.build import cmd_args_to_string from conan.tools.env import Environment from conan.errors import ConanException class PkgConfig: def __init__(self, conanfile, library, pkg_config_path=None): """ :param conanfile: The current recipe object. Always use ``self``. :param library: The library which ``.pc`` file is to be parsed. It must exist in the pkg_config path. :param pkg_config_path: If defined it will be prepended to ``PKG_CONFIG_PATH`` environment variable, so the execution finds the required files. """ self._conanfile = conanfile self._library = library self._info = {} self._pkg_config_path = pkg_config_path self._variables = None def _parse_output(self, option): executable = self._conanfile.conf.get("tools.gnu:pkg_config", default="pkg-config") command = cmd_args_to_string([executable, '--' + option, self._library, '--print-errors']) env = Environment() if self._pkg_config_path: env.prepend_path("PKG_CONFIG_PATH", self._pkg_config_path) with env.vars(self._conanfile).apply(): # This way we get the environment from ConanFile, from profile (default buildenv) output, err = StringIO(), StringIO() ret = self._conanfile.run(command, stdout=output, stderr=err, quiet=True, ignore_errors=True) if ret != 0: raise ConanException(f"PkgConfig failed. Command: {command}\n" f" stdout:\n{textwrap.indent(output.getvalue(), ' ')}\n" f" stderr:\n{textwrap.indent(err.getvalue(), ' ')}\n") value = output.getvalue().strip() return value def _get_option(self, option): if option not in self._info: self._info[option] = self._parse_output(option) return self._info[option] @property def includedirs(self): return [include[2:] for include in self._get_option('cflags-only-I').split()] @property def cflags(self): return [flag for flag in self._get_option('cflags-only-other').split() if not flag.startswith("-D")] @property def defines(self): return [flag[2:] for flag in self._get_option('cflags-only-other').split() if flag.startswith("-D")] @property def libdirs(self): return [lib[2:] for lib in self._get_option('libs-only-L').split()] @property def libs(self): return [lib[2:] for lib in self._get_option('libs-only-l').split()] @property def linkflags(self): return self._get_option('libs-only-other').split() @property def provides(self): return self._get_option('print-provides') @property def version(self): return self._get_option('modversion') @property def variables(self): if self._variables is None: variable_names = self._parse_output('print-variables').split() self._variables = {} for name in variable_names: self._variables[name] = self._parse_output('variable=%s' % name) return self._variables def fill_cpp_info(self, cpp_info, is_system=True, system_libs=None): """ Method to fill a cpp_info object from the PkgConfig configuration :param cpp_info: Can be the global one (self.cpp_info) or a component one (self.components["foo"].cpp_info). :param is_system: If ``True``, all detected libraries will be assigned to ``cpp_info.system_libs``, and none to ``cpp_info.libs``. :param system_libs: If ``True``, all detected libraries will be assigned to ``cpp_info.system_libs``, and none to ``cpp_info.libs``. """ if not self.provides: raise ConanException("PkgConfig error, '{}' files not available".format(self._library)) self._conanfile.output.verbose(f"PkgConfig fill cpp_info for {self._library}") if is_system: cpp_info.system_libs = self.libs else: system_libs = system_libs or [] cpp_info.libs = [lib for lib in self.libs if lib not in system_libs] cpp_info.system_libs = [lib for lib in self.libs if lib in system_libs] cpp_info.libdirs = self.libdirs cpp_info.sharedlinkflags = self.linkflags cpp_info.exelinkflags = self.linkflags cpp_info.defines = self.defines cpp_info.includedirs = self.includedirs cpp_info.cflags = self.cflags cpp_info.cxxflags = self.cflags ================================================ FILE: conan/tools/gnu/pkgconfigdeps.py ================================================ import os import re import textwrap from jinja2 import Template, StrictUndefined from conan.errors import ConanException from conan.internal import check_duplicated_generator from conan.internal.model.dependencies import get_transitive_requires from conan.internal.util.files import save class _PCFilesDeps: template = textwrap.dedent("""\ {% for k, v in pc_variables.items() %} {{ "{}={}".format(k, v) }} {% endfor %} Name: {{ name }} Description: {{ description }} Version: {{ version }} {% if libflags %} Libs: {{ libflags }} {% endif %} {% if cflags %} Cflags: {{ cflags }} {% endif %} {% if requires|length %} Requires: {{ requires|join(' ') }} {% endif %} """) alias_template = textwrap.dedent("""\ Name: {{name}} Description: Alias {{name}} for {{aliased}} Version: {{version}} Requires: {{aliased}} """) def __init__(self, pkgconfigdeps, dep, suffix=""): self._conanfile = pkgconfigdeps._conanfile # noqa self._properties = pkgconfigdeps._properties # noqa self._transitive_reqs = get_transitive_requires(self._conanfile, dep) self._dep = dep self._suffix = suffix def _get_aliases(self, dep, pkg_name=None, comp_ref_name=None): def _get_dep_aliases(): pkg_aliases = self._get_property("pkg_config_aliases", dep, check_type=list) return pkg_aliases or [] # TODO: LET'S DEPRECATE ALL THE ALIASES MECHANISM!! if pkg_name is None and comp_ref_name is None: return _get_dep_aliases() if comp_ref_name not in dep.cpp_info.components: # Either foo::foo might be referencing the root cpp_info if (dep.ref.name == comp_ref_name or # Or a "replace_require" is used and cpp_info.requires is the root one, e.g., # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"] (dep.ref.name != pkg_name and pkg_name == comp_ref_name)): return _get_dep_aliases() raise ConanException("Component '{name}::{cname}' not found in '{name}' " "package requirement".format(name=dep.ref.name, cname=comp_ref_name)) comp_aliases = self._get_property("pkg_config_aliases", dep, comp_ref_name, check_type=list) return comp_aliases or [] def _get_name(self, dep, pkg_name=None, comp_ref_name=None): def _get_dep_name(): dep_name = self._get_property("pkg_config_name", dep) or dep.ref.name return f"{dep_name}{self._suffix}" if pkg_name is None and comp_ref_name is None: return _get_dep_name() if comp_ref_name not in dep.cpp_info.components: # Either foo::foo might be referencing the root cpp_info if (dep.ref.name == comp_ref_name or # Or a "replace_require" is used and cpp_info.requires is the root one, e.g., # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"] (dep.ref.name != pkg_name and pkg_name == comp_ref_name)): return _get_dep_name() raise ConanException("Component '{name}::{cname}' not found in '{name}' " "package requirement".format(name=dep.ref.name, cname=comp_ref_name)) comp_name = self._get_property("pkg_config_name", dep, comp_ref_name) if comp_name: return f"{comp_name}{self._suffix}" else: dep_name = _get_dep_name() # Creating a component name with namespace, e.g., dep-comp1 return f"{dep_name}-{comp_ref_name}" def _get_property(self, prop, dep, comp_name=None, check_type=None): dep_name = dep.ref.name dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" try: value = self._properties[f"{dep_comp}{self._suffix}"][prop] if check_type is not None and not isinstance(value, check_type): raise ConanException( f'The expected type for {prop} is "{check_type.__name__}", but "{type(value).__name__}" was found') return value except KeyError: return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \ else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type) def _get_pc_variables(self, dep, cpp_info, custom_content=None): """ Get all the freeform variables defined by Conan and users (through ``pkg_config_custom_content``). This last ones will override the Conan defined variables. """ def apply_custom_content(): if isinstance(custom_content, dict): pc_variables.update(custom_content) elif custom_content: # Legacy: custom content is string pc_variable_pattern = re.compile("^(.*)=(.*)") for line in custom_content.splitlines(): match = pc_variable_pattern.match(line) if match: key, value = match.group(1).strip(), match.group(2).strip() pc_variables[key] = value # If editable, package_folder can be None prefix_path = (dep.recipe_folder if dep.package_folder is None else dep.package_folder).replace("\\", "/") pc_variables = {"prefix": prefix_path} # Already formatted directories pc_variables.update(self._get_formatted_dirs("libdir", cpp_info.libdirs, prefix_path)) pc_variables.update(self._get_formatted_dirs("includedir", cpp_info.includedirs, prefix_path)) pc_variables.update(self._get_formatted_dirs("bindir", cpp_info.bindirs, prefix_path)) # Get the custom content introduced by user and sanitize it apply_custom_content() return pc_variables @staticmethod def _get_formatted_dirs(folder_name, folders, prefix_path_): ret = {} for i, directory in enumerate(folders): directory = os.path.normpath(directory).replace("\\", "/") if directory.startswith(prefix_path_): prefix = "${prefix}/" directory = os.path.relpath(directory, prefix_path_).replace("\\", "/") else: prefix = "" if os.path.isabs(directory) else "${prefix}/" suffix = str(i) if i else "" var_name = f"{folder_name}{suffix}" ret[var_name] = f"{prefix}{directory}" return ret def _get_framework_flags(self, cpp_info): # FIXME: GnuDepsFlags used only here. Let's adapt the code and remove this dependency. # self._conanfile is also used only here. from conan.tools.gnu.gnudeps_flags import GnuDepsFlags gnudeps_flags = GnuDepsFlags(self._conanfile, cpp_info) return gnudeps_flags.frameworks + gnudeps_flags.framework_paths def _get_lib_flags(self, libdirvars, cpp_info): framework_flags = self._get_framework_flags(cpp_info) libdirsflags = ['-L"${%s}"' % d for d in libdirvars] system_libs = ["-l%s" % li for li in (cpp_info.libs + cpp_info.system_libs)] shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags return " ".join(libdirsflags + system_libs + shared_flags + framework_flags) def _get_cflags(self, includedirvars, cpp_info): includedirsflags = ['-I"${%s}"' % d for d in includedirvars] cxxflags = [var.replace('"', '\\"') for var in cpp_info.cxxflags] cflags = [var.replace('"', '\\"') for var in cpp_info.cflags] defines = ["-D%s" % var.replace('"', '\\"') for var in cpp_info.defines] return " ".join(includedirsflags + cxxflags + cflags + defines) def _get_component_requirement_names(self, cpp_info): """ Get all the pkg-config valid names from the requirements ones given a CppInfo object. For instance, those requirements could be coming from: ```python from conan import ConanFile class PkgConfigConan(ConanFile): requires = "other/1.0" def package_info(self): self.cpp_info.requires = ["other::cmp1"] # Or: def package_info(self): self.cpp_info.components["cmp"].requires = ["other::cmp1"] ``` """ dep_ref_name = self._dep.ref.name ret = [] for req in cpp_info.requires: pkg_ref_name, comp_ref_name = req.split("::") if "::" in req else (dep_ref_name, req) # For instance, dep == "hello/1.0" and req == "other::cmp1" -> hello != other if dep_ref_name != pkg_ref_name: try: req_conanfile = self._transitive_reqs[pkg_ref_name] except KeyError: continue # If the dependency is not in the transitive, might be skipped else: # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello req_conanfile = self._dep comp_name = self._get_name(req_conanfile, pkg_ref_name, comp_ref_name) if comp_name not in ret: ret.append(comp_name) return ret def items(self): """ Get all the PC files and contents for any dependency: * If the given dependency does not have components: The PC file will be the dependency one. * If the given dependency has components: The PC files will be saved in this order: 1- Package components. 2- Root component. Note: If the root-package PC name matches with any other of the components one, the first one is not going to be created. Components have more priority than root package. * Apart from those PC files, if there are any aliases declared, they will be created too. """ pc_files = {} pc_alias_files = {} pkg_name = self._get_name(self._dep) # First, let's load all the components PC files # Loop through all the package's components for comp_ref_name, comp_cpp_info in self._dep.cpp_info.get_sorted_components().items(): # At first, let's check if we have defined some components requires, e.g., "dep::cmp1" comp_requires = self._get_component_requirement_names(comp_cpp_info) comp_name = self._get_name(self._dep, pkg_name, comp_ref_name) version = (self._get_property("component_version", self._dep, comp_ref_name) or self._get_property("system_package_version", self._dep, comp_ref_name) or self._dep.ref.version) custom_content = self._get_property("pkg_config_custom_content", self._dep, comp_ref_name) pc_variables = self._get_pc_variables(self._dep, comp_cpp_info, custom_content) pc_context = { "name": comp_name, "description": f"Conan component: {comp_name}", "version": version, "requires": comp_requires, "pc_variables": pc_variables, "cflags": self._get_cflags([d for d in pc_variables if d.startswith("includedir")], comp_cpp_info), "libflags": self._get_lib_flags([d for d in pc_variables if d.startswith("libdir")], comp_cpp_info) } pc_files[comp_name] = self._get_pc_content(pc_context) # Aliases for alias in self._get_aliases(self._dep, pkg_name, comp_ref_name): pc_alias_files[alias] = self._get_alias_pc_content({ "name": alias, "version": version, "aliased": comp_name }) # Second, let's load the root package's PC file ONLY # if it does not already exist in components one # Issue related: https://github.com/conan-io/conan/issues/10341 should_skip_main = self._get_property("pkg_config_name", self._dep) == "none" if pkg_name not in pc_files and not should_skip_main: cpp_info = self._dep.cpp_info # At first, let's check if we have defined some global requires, e.g., "other::cmp1" # Note: If DEP has components, they'll be the requirements == pc_files.keys() requires = list(pc_files.keys()) or self._get_component_requirement_names(cpp_info) # If we have found some component requirements it would be enough if not requires: # If no requires were found, let's try to get all the direct visible dependencies, # e.g., requires = "other_pkg/1.0" requires = [self._get_name(req) for req in self._transitive_reqs.values()] version = (self._get_property("system_package_version", self._dep) or self._dep.ref.version) custom_content = self._get_property("pkg_config_custom_content", self._dep) pc_variables = self._get_pc_variables(self._dep, cpp_info, custom_content) pc_context = { "name": pkg_name, "description": f"Conan package: {pkg_name}", "version": version, "requires": requires, "pc_variables": pc_variables, "cflags": self._get_cflags([d for d in pc_variables if d.startswith("includedir")], cpp_info), "libflags": self._get_lib_flags([d for d in pc_variables if d.startswith("libdir")], cpp_info) } pc_files[pkg_name] = self._get_pc_content(pc_context) # Aliases for alias in self._get_aliases(self._dep): pc_alias_files[alias] = self._get_alias_pc_content({ "name": alias, "version": version, "aliased": pkg_name }) # Adding the aliases pc_files.update(pc_alias_files) return pc_files.items() def _get_pc_content(self, context): template = Template(self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) return template.render(context) def _get_alias_pc_content(self, context): template = Template(self.alias_template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined, keep_trailing_newline=True) return template.render(context) class PkgConfigDeps: def __init__(self, conanfile): self._conanfile = conanfile # Activate the build *.pc files for the specified libraries self.build_context_activated = [] # If specified, the files/requires/names for the build context will be renamed appending # a suffix. It is necessary in case of same require and build_require and will cause an error # DEPRECATED: consumers should use build_context_folder instead # FIXME: Conan 3.x: Remove build_context_suffix attribute self.build_context_suffix = {} # By default, the "[generators_folder]/build" folder will save all the *.pc files activated # in the build_context_activated list. # Notice that if the `build_context_suffix` attr is defined, the `build_context_folder` one # will have no effect. # Issue: https://github.com/conan-io/conan/issues/12342 # Issue: https://github.com/conan-io/conan/issues/14935 # FIXME: Conan 3.x: build_context_folder should be "build" by default self.build_context_folder = None # Keeping backward-compatibility self._properties = {} def _get_dependencies(self): # Get all the dependencies host_req = self._conanfile.dependencies.host build_req = self._conanfile.dependencies.build # tool_requires test_req = self._conanfile.dependencies.test # If self.build_context_suffix is not defined, the build requires will be saved # in the self.build_context_folder # FIXME: Conan 3.x: Remove build_context_suffix attribute and the validation function if self.build_context_folder is None: # Legacy flow if self.build_context_suffix: # deprecation warning self._conanfile.output.warning( "PkgConfigDeps.build_context_suffix attribute has been " "deprecated. Use PkgConfigDeps.build_context_folder instead." ) # Check if it exists both as require and as build require without a suffix activated_br = {r.ref.name for r in build_req.values() if r.ref.name in self.build_context_activated} common_names = {r.ref.name for r in host_req.values()}.intersection(activated_br) without_suffixes = [common_name for common_name in common_names if not self.build_context_suffix.get(common_name)] if without_suffixes: raise ConanException( f"The packages {without_suffixes} exist both as 'require' and as" f" 'build require'. You need to specify a suffix using the " f"'build_context_suffix' attribute at the PkgConfigDeps generator.") elif self.build_context_folder is not None and self.build_context_suffix: raise ConanException( "It's not allowed to define both PkgConfigDeps.build_context_folder " "and PkgConfigDeps.build_context_suffix (deprecated).") for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): # Filter the build_requires not activated with PkgConfigDeps.build_context_activated if require.build and dep.ref.name not in self.build_context_activated: continue yield require, dep def generate(self): """ Save all the `*.pc` files """ def _pc_file_name(name_, is_build_context=False, has_suffix=False): # If no suffix is defined, we can save the *.pc file in the build_context_folder build = is_build_context and self.build_context_folder and not has_suffix # Issue: https://github.com/conan-io/conan/issues/12342 # Issue: https://github.com/conan-io/conan/issues/14935 return f"{self.build_context_folder}/{name_}.pc" if build else f"{name_}.pc" check_duplicated_generator(self, self._conanfile) for require, dep in self._get_dependencies(): suffix = self.build_context_suffix.get(require.ref.name, "") if require.build else "" # Save all the *.pc files and their contents for name, content in _PCFilesDeps(self, dep, suffix=suffix).items(): pc_name = _pc_file_name(name, is_build_context=require.build, has_suffix=bool(suffix)) save(pc_name, content) def set_property(self, dep, prop, value): """ Using this method you can overwrite the :ref:`property` values set by the Conan recipes from the consumer. This can be done for `pkg_config_name`, `pkg_config_aliases` and `pkg_config_custom_content` properties. :param dep: Name of the dependency to set the :ref:`property`. For components use the syntax: ``dep_name::component_name``. :param prop: Name of the :ref:`property`. :param value: Value of the property. Use ``None`` to invalidate any value set by the upstream recipe. """ self._properties.setdefault(dep, {}).update({prop: value}) ================================================ FILE: conan/tools/google/__init__.py ================================================ from conan.tools.google.toolchain import BazelToolchain from conan.tools.google.bazeldeps import BazelDeps from conan.tools.google.bazel import Bazel from conan.tools.google.layout import bazel_layout ================================================ FILE: conan/tools/google/bazel.py ================================================ import os import platform from conan.tools.google import BazelToolchain class Bazel: def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile # Use BazelToolchain generated file if exists self._conan_bazelrc = os.path.join(self._conanfile.generators_folder, BazelToolchain.bazelrc_name) self._use_conan_config = os.path.exists(self._conan_bazelrc) self._startup_opts = self._get_startup_command_options() def _safe_run_command(self, command): """ Windows is having problems stopping bazel processes, so it ends up locking some files if something goes wrong. Better to shut down the Bazel server after running each command. """ try: self._conanfile.run(command) finally: if platform.system() == "Windows": self._conanfile.run("bazel" + self._startup_opts + " shutdown") def _get_startup_command_options(self): bazelrc_paths = [] if self._use_conan_config: bazelrc_paths.append(self._conan_bazelrc) # User bazelrc paths have more prio than Conan one # See more info in https://bazel.build/run/bazelrc bazelrc_paths.extend(self._conanfile.conf.get("tools.google.bazel:bazelrc_path", default=[], check_type=list)) opts = " ".join(["--bazelrc=" + rc.replace("\\", "/") for rc in bazelrc_paths]) return f" {opts}" if opts else "" def build(self, args=None, target="//...", clean=True): """ Runs "bazel build " command where: * ``rcpaths``: adds ``--bazelrc=xxxx`` per rc-file path. It listens to ``BazelToolchain`` (``--bazelrc=conan_bzl.rc``), and ``tools.google.bazel:bazelrc_path`` conf. * ``configs``: adds ``--config=xxxx`` per bazel-build configuration. It listens to ``BazelToolchain`` (``--config=conan-config``), and ``tools.google.bazel:configs`` conf. * ``args``: they are any extra arguments to add to the ``bazel build`` execution. * ``targets``: all the target labels. :param target: It is the target label. By default, it's "//..." which runs all the targets. :param args: list of extra arguments to pass to the CLI. :param clean: boolean that indicates to run a "bazel clean" before running the "bazel build". Notice that this is important to ensure a fresh bazel cache every """ # Note: In case of error like this: ... https://bcr.bazel.build/: PKIX path building failed # Check this comment: https://github.com/bazelbuild/bazel/issues/3915#issuecomment-1120894057 bazelrc_build_configs = [] if self._use_conan_config: bazelrc_build_configs.append(BazelToolchain.bazelrc_config) command = "bazel" + self._startup_opts + " build" bazelrc_build_configs.extend(self._conanfile.conf.get("tools.google.bazel:configs", default=[], check_type=list)) for config in bazelrc_build_configs: command += f" --config={config}" if args: command += " ".join(f" {arg}" for arg in args) command += f" {target}" if clean: self._safe_run_command("bazel" + self._startup_opts + " clean") self._safe_run_command(command) def test(self, target=None): """ Runs "bazel test " command. """ if self._conanfile.conf.get("tools.build:skip_test", check_type=bool) or target is None: return self._safe_run_command("bazel" + self._startup_opts + f" test {target}") ================================================ FILE: conan/tools/google/bazeldeps.py ================================================ import os import re import textwrap from jinja2 import Template, StrictUndefined from conan.internal import check_duplicated_generator from conan.internal.model.dependencies import get_transitive_requires from conan.internal.model.pkg_type import PackageType from conan.internal.util.files import save def _relativize_path(path, start_folder): """ Returns a relative path with regard to the given folder. :param path: absolute or relative path. :param start_folder: folder to start relative to. :return: Unix-like path relative if matches to the given pattern. Otherwise, it returns the original path. """ if not path or not start_folder: return path path_ = path.replace("\\", "/").replace("/./", "/") pattern_ = start_folder.replace("\\", "/").replace("/./", "/") match = re.match(pattern_, path_) if match: matching = match[0] if path_.startswith(matching): path_ = path_.replace(matching, "").strip("/") return path_.strip("./") or "./" return path class _BazelDepBuildGenerator: """ This class creates the BUILD.bazel for each dependency where it's declared all the necessary information to load the libraries """ # If both files exist, BUILD.bazel takes precedence over BUILD # https://bazel.build/concepts/build-files dep_build_filename = "BUILD.bazel" dep_build_template = textwrap.dedent("""\ {% macro cc_import_macro(libs) %} {% for lib_info in libs %} cc_import( name = "{{ lib_info['name'] }}_precompiled", {% if lib_info['is_shared'] %} shared_library = "{{ lib_info['lib_path'] }}", {% else %} static_library = "{{ lib_info['lib_path'] }}", {% endif %} {% if lib_info['import_lib_path'] %} interface_library = "{{ lib_info['import_lib_path'] }}", {% endif %} {% if lib_info["linkopts"] %} linkopts = [ {% for linkopt in lib_info["linkopts"] %} {{ linkopt }}, {% endfor %} ], {% endif %} ) {% endfor %} {% endmacro %} {% macro cc_library_macro(obj) %} cc_library( name = "{{ obj["name"] }}", {% if obj["headers"] %} hdrs = glob([ {% for header in obj["headers"] %} {{ header }}, {% endfor %} ], allow_empty = True ), {% endif %} {% if obj["includes"] %} includes = [ {% for include in obj["includes"] %} {{ include }}, {% endfor %} ], {% endif %} {% if obj["defines"] %} defines = [ {% for define in obj["defines"] %} {{ define }}, {% endfor %} ], {% endif %} {% if obj["linkopts"] %} linkopts = [ {% for linkopt in obj["linkopts"] %} {{ linkopt }}, {% endfor %} ], {% endif %} {% if obj["copts"] %} copts = [ {% for copt in obj["copts"] %} {{ copt }}, {% endfor %} ], {% endif %} visibility = ["//visibility:public"], {% if obj["libs"] or obj["dependencies"] or obj.get("component_names", []) %} deps = [ # do not sort {% for lib in obj["libs"] %} ":{{ lib.name }}_precompiled", {% endfor %} {% for name in obj.get("component_names", []) %} ":{{ name }}", {% endfor %} {% for dep in obj["dependencies"] %} "{{ dep }}", {% endfor %} ], {% endif %} ) {% endmacro %} {% macro filegroup_bindirs_macro(obj) %} {% if obj["bindirs"] %} filegroup( name = "{{ obj["name"] }}_binaries", srcs = glob([ {% for bindir in obj["bindirs"] %} "{{ bindir }}/**", {% endfor %} ], allow_empty = True ), visibility = ["//visibility:public"], ) {% endif %} {% endmacro %} # Components precompiled libs {% for component in components %} {{ cc_import_macro(component["libs"]) }} {% endfor %} # Root package precompiled libs {{ cc_import_macro(root["libs"]) }} # Components libraries declaration {% for component in components %} {{ cc_library_macro(component) }} {% endfor %} # Package library declaration {{ cc_library_macro(root) }} # Filegroup library declaration {{ filegroup_bindirs_macro(root) }} """) def __init__(self, conanfile, dep, require): self._conanfile = conanfile self._dep = dep self._is_build_require = require.build self._transitive_reqs = get_transitive_requires(self._conanfile, dep) @property def _build_file_path(self): """ Returns the absolute path to the BUILD file created by Conan """ folder = os.path.join(self._get_repository_name(self._dep), self.dep_build_filename) return folder.replace("\\", "/") @property def _absolute_build_file_path(self): """ Returns the absolute path to the BUILD file created by Conan """ folder = os.path.join(self._conanfile.generators_folder, self._build_file_path) return folder.replace("\\", "/") @property def _package_folder(self): """ Returns the package folder path """ # If editable, package_folder can be None root_folder = self._dep.recipe_folder if self._dep.package_folder is None \ else self._dep.package_folder return root_folder.replace("\\", "/") def _get_repository_name(self, dep): pkg_name = dep.cpp_info.get_property("bazel_repository_name") or dep.ref.name return f"build-{pkg_name}" if self._is_build_require else pkg_name def _get_target_name(self, dep): pkg_name = dep.cpp_info.get_property("bazel_target_name") or dep.ref.name return pkg_name def _get_component_name(self, dep, comp_ref_name): pkg_name = self._get_target_name(dep) if comp_ref_name not in dep.cpp_info.components: # foo::foo might be referencing the root cppinfo if dep.ref.name == comp_ref_name: return pkg_name return f"{pkg_name}-{comp_ref_name}" comp_name = dep.cpp_info.components[comp_ref_name].get_property("bazel_target_name") # If user did not set bazel_target_name, let's create a component name # with a namespace, e.g., dep-comp1 return comp_name or f"{pkg_name}-{comp_ref_name}" def _get_headers(self, cpp_info): return ['"{}/**"'.format(_relativize_path(path, self._package_folder)) for path in cpp_info.includedirs] def _get_bindirs(self, cpp_info): return [_relativize_path(bindir, self._package_folder) for bindir in cpp_info.bindirs] def _get_includes(self, cpp_info): return ['"{}"'.format(_relativize_path(path, self._package_folder)) for path in cpp_info.includedirs] def _get_defines(self, cpp_info): return ['"{}"'.format(define.replace('"', '\\' * 3 + '"')) for define in cpp_info.defines] def _get_linkopts(self, cpp_info): os_build = self._dep.settings_build.get_safe("os") link_opt = '/DEFAULTLIB:{}' if os_build == "Windows" else '-l{}' system_libs = [link_opt.format(lib) for lib in cpp_info.system_libs] shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags frameworkdirs_flags = [] framework_flags = [] for frw in cpp_info.frameworks: framework_flags.extend(["-framework", frw]) for frw_dir in cpp_info.frameworkdirs: frameworkdirs_flags.extend(["-F", frw_dir.replace("\\", "/")]) return [f'"{flag}"' for flag in (system_libs + shared_flags + framework_flags + frameworkdirs_flags)] def _get_copts(self, cpp_info): # FIXME: long discussions between copts (-Iflag) vs includes in Bazel. Not sure yet # includedirsflags = ['"-I{}"'.format(_relativize_path(d, package_folder_path)) # for d in cpp_info.includedirs] cxxflags = [var.replace('"', '\\"') for var in cpp_info.cxxflags] cflags = [var.replace('"', '\\"') for var in cpp_info.cflags] return [f'"{flag}"' for flag in (cxxflags + cflags)] def _get_component_requirement_names(self, cpp_info): """ Get all the valid names from the requirements ones given a CppInfo object. For instance, those requirements could be coming from: ```python from conan import ConanFile class PkgConan(ConanFile): requires = "other/1.0" def package_info(self): self.cpp_info.requires = ["other::cmp1"] # Or: def package_info(self): self.cpp_info.components["cmp"].requires = ["other::cmp1"] ``` """ dep_ref_name = self._dep.ref.name ret = [] for req in cpp_info.requires: pkg_ref_name, comp_ref_name = req.split("::") if "::" in req else (dep_ref_name, req) prefix = ":" # Requirements declared in the same BUILD file # For instance, dep == "hello/1.0" and req == "other::cmp1" -> hello != other if dep_ref_name != pkg_ref_name: try: req_conanfile = self._transitive_reqs[pkg_ref_name] # Requirements declared in another dependency BUILD file prefix = f"@{self._get_repository_name(req_conanfile)}//:" except KeyError: continue # If the dependency is not in the transitive, might be skipped else: # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello req_conanfile = self._dep comp_name = self._get_component_name(req_conanfile, comp_ref_name) dep_name = f"{prefix}{comp_name}" if dep_name not in ret: ret.append(dep_name) return ret def _get_lib_info(self, cpp_info, deduced_cpp_info, component_name=None): def _lib_info(lib_name, virtual_cpp_info): info = { "name": lib_name, "is_shared": virtual_cpp_info.type == PackageType.SHARED, "lib_path": _relativize_path(virtual_cpp_info.location, self._package_folder), "import_lib_path": _relativize_path(virtual_cpp_info.link_location, self._package_folder), "linkopts": [] } if info['is_shared'] and info["lib_path"] and not info["lib_path"].endswith(".dll"): # Issue: https://github.com/conan-io/conan/issues/19190 # Issue: https://github.com/conan-io/conan/issues/19135 # (UNIX) Adding the rpath flag as any application could link through the library # which points out a symlink, but that name does not appear in the library location info["linkopts"] = [f'"-Wl,-rpath,{libdir}"' for libdir in cpp_info.libdirs] return info libs = cpp_info.libs libs_info = [] if libs: if len(libs) > 1: for lib_name in libs: name = f'_{component_name}_{lib_name}' if component_name else f'_{lib_name}' virtual_cpp_info = deduced_cpp_info.components[name] libs_info.append(_lib_info(lib_name, virtual_cpp_info)) else: lib_name = libs[0] virtual_cpp_info = deduced_cpp_info.components[component_name] if component_name else deduced_cpp_info libs_info.append(_lib_info(lib_name, virtual_cpp_info)) return libs_info def _get_build_file_context(self): """ Get the whole package information :return: `_BazelTargetInfo` object with the package information """ build_content = {"components": [], "root": {}} component_names = [] deduced_cpp_info = self._dep.cpp_info.deduce_full_cpp_info(self._dep) if self._dep.cpp_info.has_components: # Loop through all the package's components for comp_ref_name, cmp_cpp_info in self._dep.cpp_info.get_sorted_components().items(): # At first, let's check if we have defined some components requires, e.g., "dep::cmp1" comp_requires_names = self._get_component_requirement_names(cmp_cpp_info) comp_name = self._get_component_name(self._dep, comp_ref_name) component_names.append(comp_name) build_content["components"].append({ "name": comp_name, "libs": self._get_lib_info(cmp_cpp_info, deduced_cpp_info, component_name=comp_ref_name), "bindirs": self._get_bindirs(cmp_cpp_info), "headers": self._get_headers(cmp_cpp_info), "includes": self._get_includes(cmp_cpp_info), "defines": self._get_defines(cmp_cpp_info), "linkopts": self._get_linkopts(cmp_cpp_info), "copts": self._get_copts(cmp_cpp_info), "dependencies": comp_requires_names, }) pkg_name = self._get_target_name(self._dep) # At first, let's check if we have defined some global requires, e.g., "other::cmp1" requires = self._get_component_requirement_names(self._dep.cpp_info) # If we have found some component requires it would be enough if not requires: # If no requires were found, let's try to get all the direct dependencies, # e.g., requires = "other_pkg/1.0" requires = [ f"@{self._get_repository_name(req)}//:{self._get_target_name(req)}" for req in self._transitive_reqs.values() ] cpp_info = self._dep.cpp_info build_content["root"] = { "name": pkg_name, "libs": self._get_lib_info(cpp_info, deduced_cpp_info), "bindirs": self._get_bindirs(cpp_info), "headers": self._get_headers(cpp_info), "includes": self._get_includes(cpp_info), "defines": self._get_defines(cpp_info), "linkopts": self._get_linkopts(cpp_info), "copts": self._get_copts(cpp_info), "dependencies": requires, "component_names": component_names } return build_content @property def dep_context(self): """ Return the dependency context to fill later the conan_deps_module_extension.bzl and so on. """ return { 'repository_name': self._get_repository_name(self._dep), 'package_folder': self._package_folder, 'package_build_file_path': self._absolute_build_file_path, } def items(self): template = Template(self.dep_build_template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) content = template.render(self._get_build_file_context()) return {self._build_file_path: content}.items() class _BazelPathsGenerator: """ Bazel 6.0 needs to know all the dependencies for its current project. So, the only way to do that is to tell the WORKSPACE file how to load all the Conan ones. This is the goal of the function created by this class, the ``load_conan_dependencies`` one. More information: * https://bazel.build/reference/be/workspace#new_local_repository Bazel >= 7.1 needs to know all the dependencies as well, but provided via the MODULE.bazel file. Therefor we provide a static repository rule to load the dependencies. This rule is used by a module extension, passing the package path and the BUILD file path to the repository rule. """ repository_filename = "dependencies.bzl" modules_filename = "conan_deps_module_extension.bzl" repository_rules_filename = "conan_deps_repo_rules.bzl" repository_template = textwrap.dedent("""\ # This Bazel module should be loaded by your WORKSPACE file. # Add these lines to your WORKSPACE one (assuming that you're using the "bazel_layout"): # load("@//conan:dependencies.bzl", "load_conan_dependencies") # load_conan_dependencies() def load_conan_dependencies(): {% for dep_info in dependencies %} native.new_local_repository( name="{{dep_info['repository_name']}}", path="{{dep_info['package_folder']}}", build_file="{{dep_info['package_build_file_path']}}", ) {% endfor %} """) module_template = textwrap.dedent("""\ # This module provides a repo for each requires-dependency in your conanfile. # It's generated by the BazelDeps, and should be used in your Module.bazel file. load(":conan_deps_repo_rules.bzl", "conan_dependency_repo") def _load_dependencies_impl(mctx): {% for dep_info in dependencies %} conan_dependency_repo( name = "{{dep_info['repository_name']}}", package_path = "{{dep_info['package_folder']}}", build_file_path = "{{dep_info['package_build_file_path']}}", ) {% endfor %} return mctx.extension_metadata( # It will only warn you if any direct # dependency is not imported by the 'use_repo' or even it is imported # but not created. Notice that root_module_direct_dev_deps can not be None as we # are giving 'all' value to root_module_direct_deps. # Fix the 'use_repo' calls by running 'bazel mod tidy' root_module_direct_deps = 'all', root_module_direct_dev_deps = [], # Prevent writing function content to lockfiles: # - https://bazel.build/rules/lib/builtins/module_ctx#extension_metadata # Important for remote build. Actually it's not reproducible, as local paths will # be different on different machines. But we assume that conan works correctly here. # IMPORTANT: Not compatible with bazel < 7.1 reproducible = True, ) conan_extension = module_extension( implementation = _load_dependencies_impl, os_dependent = True, arch_dependent = True, ) """) repository_rules_content = textwrap.dedent("""\ # This bazel repository rule is used to load Conan dependencies into the Bazel workspace. # It's used by a generated module file that provides information about the conan packages. # Each conan package is loaded into a bazel repository rule, with having the name of the # package. The whole method is based on symlinks to not copy the whole package into the # Bazel workspace, which is expensive. def _conan_dependency_repo(rctx): package_path = rctx.workspace_root.get_child(rctx.attr.package_path) child_packages = package_path.readdir() for child in child_packages: rctx.symlink(child, child.basename) rctx.symlink(rctx.attr.build_file_path, "BUILD.bazel") conan_dependency_repo = repository_rule( implementation = _conan_dependency_repo, attrs = { "package_path": attr.string( mandatory = True, doc = "The path to the Conan package in conan cache.", ), "build_file_path": attr.string( mandatory = True, doc = "The path to the BUILD file.", ), }, ) """) @classmethod def items(cls, dependencies_context): if not dependencies_context: return {} # Bazel 6.x, but it'll likely be dropped soon repository_template = Template(cls.repository_template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) content_6x = repository_template.render(dependencies=dependencies_context) # Bazel 7.x files module_template = Template(cls.module_template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) content = module_template.render(dependencies=dependencies_context) return { cls.repository_filename: content_6x, # bazel 6.x compatible cls.modules_filename: content, cls.repository_rules_filename: cls.repository_rules_content, "BUILD.bazel": "# This is an empty BUILD file." # Bazel needs this file in each subfolder }.items() class BazelDeps: def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile #: Activates the build context for the specified Conan package names. self.build_context_activated = [] def _get_requirements(self, build_context_activated): """ Simply save the activated requirements (host + build + test), and the deactivated ones """ # All the requirements host_req = self._conanfile.dependencies.host build_req = self._conanfile.dependencies.direct_build # tool_requires test_req = self._conanfile.dependencies.test for require, dep in list(host_req.items()) + list(build_req.items()) + list( test_req.items()): # Require is not used at the moment, but its information could be used, # and will be used in Conan 2.0 # Filter the build_requires not activated with self.build_context_activated if require.build and dep.ref.name not in build_context_activated: continue yield require, dep def generate(self): """ Generates all the targets /BUILD.bazel files, a dependencies.bzl (for bazel<7), a conan_deps_repo_rules.bzl and a conan_deps_module_extension.bzl file (for bazel>=7.1) one in the build folder. In case of bazel < 7, it's important to highlight that the ``dependencies.bzl`` file should be loaded by your WORKSPACE Bazel file: .. code-block:: python load("@//[BUILD_FOLDER]:dependencies.bzl", "load_conan_dependencies") load_conan_dependencies() In case of bazel >= 7.1, the ``conan_deps_module_extension.bzl`` file should be loaded by your Module.bazel file, e.g. like this: .. code-block:: python load_conan_dependencies = use_extension( "//build:conan_deps_module_extension.bzl", "conan_extension" ) use_repo(load_conan_dependencies, "dep-1", "dep-2", ...) """ check_duplicated_generator(self, self._conanfile) dependencies_context = [] for require, dep in self._get_requirements(self.build_context_activated): # Bazel info generator dep_build_generator = _BazelDepBuildGenerator(self._conanfile, dep, require) dependencies_context.append(dep_build_generator.dep_context) for name, content in dep_build_generator.items(): save(name, content) for name, content in _BazelPathsGenerator.items(dependencies_context): save(name, content) ================================================ FILE: conan/tools/google/layout.py ================================================ import os def bazel_layout(conanfile, src_folder=".", build_folder=".", target_folder=None): """Bazel layout is so limited. It does not allow to create its special symlinks in other folder. See more information in https://bazel.build/remote/output-directories""" subproject = conanfile.folders.subproject conanfile.folders.source = src_folder if not subproject else os.path.join(subproject, src_folder) # Bazel always builds the whole project in the root folder, but consumer can put another one conanfile.folders.build = build_folder if not subproject else os.path.join(subproject, build_folder) generators_folder = conanfile.folders.generators or "conan" conanfile.folders.generators = os.path.join(conanfile.folders.build, generators_folder) bindirs = os.path.join(conanfile.folders.build, "bazel-bin") libdirs = os.path.join(conanfile.folders.build, "bazel-bin") # Target folder is useful for working on editable mode if target_folder: bindirs = os.path.join(bindirs, target_folder) libdirs = os.path.join(libdirs, target_folder) conanfile.cpp.build.bindirs = [bindirs] conanfile.cpp.build.libdirs = [libdirs] ================================================ FILE: conan/tools/google/toolchain.py ================================================ """ Creates a simple conan_bzl.rc file which defines a conan-config configuration with all the attributes defined by the consumer. Bear in mind that this is not a complete toolchain, it only fills some common CLI attributes and save them in a ``*.rc`` file. Important: Maybe, this toolchain should create a new Conan platform with the user constraints, but it's not the goal for now as Bazel has tons of platforms and toolchains already available in its bazel_tools repo. For now, it only admits a list of platforms defined by the user. More information related: * Toolchains: https://bazel.build/extending/toolchains (deprecated) * Platforms: https://bazel.build/concepts/platforms (new default since Bazel 7.x) * Migrating to platforms: https://bazel.build/concepts/platforms * Issue related: https://github.com/bazelbuild/bazel/issues/6516 Others: * CROOSTOOL: https://github.com/bazelbuild/bazel/blob/cb0fb033bad2a73e0457f206afb87e195be93df2/tools/cpp/CROSSTOOL * Cross-compiling with Bazel: https://ltekieli.com/cross-compiling-with-bazel/ * bazelrc files: https://bazel.build/run/bazelrc * CLI options: https://bazel.build/reference/command-line-reference * User manual: https://bazel.build/docs/user-manual """ import textwrap from jinja2 import Template from conan.internal import check_duplicated_generator from conan.internal.internal_tools import raise_on_universal_arch from conan.tools.apple import to_apple_arch, is_apple_os from conan.tools.build.cross_building import cross_building from conan.tools.build.flags import cppstd_flag from conan.tools.files import save def _get_cpu_name(conanfile): host_os = conanfile.settings.get_safe('os').lower() host_arch = conanfile.settings.get_safe('arch') if is_apple_os(conanfile): host_os = "darwin" if host_os == "macos" else host_os host_arch = to_apple_arch(conanfile) # FIXME: Probably it's going to fail, but let's try it because it normally follows this syntax return f"{host_os}_{host_arch}" # FIXME: In the future, it could be BazelPlatform instead? Check https://bazel.build/concepts/platforms class BazelToolchain: bazelrc_name = "conan_bzl.rc" bazelrc_config = "conan-config" bazelrc_template = textwrap.dedent("""\ # Automatic bazelrc file created by Conan {% if copt %}build:conan-config {{copt}}{% endif %} {% if conlyopt %}build:conan-config {{conlyopt}}{% endif %} {% if cxxopt %}build:conan-config {{cxxopt}}{% endif %} {% if linkopt %}build:conan-config {{linkopt}}{% endif %} {% if force_pic %}build:conan-config --force_pic={{force_pic}}{% endif %} {% if dynamic_mode %}build:conan-config --dynamic_mode={{dynamic_mode}}{% endif %} {% if compilation_mode %}build:conan-config --compilation_mode={{compilation_mode}}{% endif %} {% if compiler %}build:conan-config --compiler={{compiler}}{% endif %} {% if cpu %}build:conan-config --cpu={{cpu}}{% endif %} {% if crosstool_top %}build:conan-config --crosstool_top={{crosstool_top}}{% endif %}""") def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ raise_on_universal_arch(conanfile) self._conanfile = conanfile # Bazel build parameters shared = self._conanfile.options.get_safe("shared") fpic = self._conanfile.options.get_safe("fPIC") #: Boolean used to add --force_pic=True. Depends on self.options.shared and #: self.options.fPIC values self.force_pic = fpic if (not shared and fpic is not None) else None # FIXME: Keeping this option but it's not working as expected. It's not creating the shared # libraries at all. #: String used to add --dynamic_mode=["fully"|"off"]. Depends on self.options.shared value. self.dynamic_mode = "fully" if shared else "off" #: String used to add --cppstd=[FLAG]. Depends on your settings. self.cppstd = cppstd_flag(self._conanfile) #: List of flags used to add --copt=flag1 ... --copt=flagN self.copt = [] #: List of flags used to add --conlyopt=flag1 ... --conlyopt=flagN self.conlyopt = [] #: List of flags used to add --cxxopt=flag1 ... --cxxopt=flagN self.cxxopt = [] #: List of flags used to add --linkopt=flag1 ... --linkopt=flagN self.linkopt = [] #: String used to add --compilation_mode=["opt"|"dbg"]. Depends on self.settings.build_type self.compilation_mode = {'Release': 'opt', 'Debug': 'dbg'}.get( self._conanfile.settings.get_safe("build_type") ) # Be aware that this parameter does not admit a compiler absolute path # If you want to add it, you will have to use a specific Bazel toolchain #: String used to add --compiler=xxxx. self.compiler = None # cpu is the target architecture, and it's a bit tricky. If it's not a cross-compilation, # let Bazel guess it. #: String used to add --cpu=xxxxx. At the moment, it's only added if cross-building. self.cpu = None # TODO: cross-compilation process is so powerless. Needs to use the new platforms. if cross_building(self._conanfile): # Bazel is using those toolchains/platforms by default. # It's better to let it configure the project in that case self.cpu = _get_cpu_name(conanfile) # This is itself a toolchain but just in case #: String used to add --crosstool_top. self.crosstool_top = None # TODO: Have a look at https://bazel.build/reference/be/make-variables # FIXME: Missing host_xxxx options. When are they needed? Cross-compilation? @staticmethod def _filter_list_empty_fields(v): return list(filter(bool, v)) @property def cxxflags(self): ret = [self.cppstd] conf_flags = self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) ret = ret + self.cxxopt + conf_flags return self._filter_list_empty_fields(ret) @property def cflags(self): conf_flags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list) ret = self.conlyopt + conf_flags return self._filter_list_empty_fields(ret) @property def ldflags(self): conf_flags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) conf_flags.extend(self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list)) linker_scripts = self._conanfile.conf.get("tools.build:linker_scripts", default=[], check_type=list) conf_flags.extend(["-T'" + linker_script + "'" for linker_script in linker_scripts]) ret = self.linkopt + conf_flags return self._filter_list_empty_fields(ret) def _context(self): return { "copt": " ".join(f"--copt={flag}" for flag in self.copt), "conlyopt": " ".join(f"--conlyopt={flag}" for flag in self.cflags), "cxxopt": " ".join(f"--cxxopt={flag}" for flag in self.cxxflags), "linkopt": " ".join(f"--linkopt={flag}" for flag in self.ldflags), "force_pic": self.force_pic, "dynamic_mode": self.dynamic_mode, "compilation_mode": self.compilation_mode, "compiler": self.compiler, "cpu": self.cpu, "crosstool_top": self.crosstool_top, } @property def _content(self): context = self._context() content = Template(self.bazelrc_template).render(context) return content def generate(self): """ Creates a ``conan_bzl.rc`` file with some bazel-build configuration. This last mentioned is put as ``conan-config``. """ check_duplicated_generator(self, self._conanfile) save(self._conanfile, BazelToolchain.bazelrc_name, self._content) ================================================ FILE: conan/tools/intel/__init__.py ================================================ from conan.tools.intel.intel_cc import IntelCC ================================================ FILE: conan/tools/intel/intel_cc.py ================================================ """ Intel generator module for oneAPI Toolkits. For simplicity and clarity, Intel informally refers to some of the terms in this document, as listed below: - ICX - Intel oneAPI DPC++/C++ Compiler - ICC Classic - Intel C++ Compiler Classic - DPCPP - Intel oneAPI DPC++/C++ Compiler DPCPP is built upon ICX as the underlying C++ Compiler, therefore most of this information also applies to DPCPP. Intel oneAPI Toolkit (DPC++/C++ Compiler) - Versioning: https://software.intel.com/content/www/us/en/develop/articles/oneapi-toolkit-version-to-compiler-version-mapping.html - Compiler: https://software.intel.com/content/www/us/en/develop/documentation/oneapi-dpcpp-cpp-compiler-dev-guide-and-reference/top.html """ import os import platform import textwrap from conan.internal import check_duplicated_generator from conan.errors import ConanException def _is_using_intel_oneapi(compiler_version): """Check if the Intel compiler to be used belongs to Intel oneAPI Note: Intel oneAPI Toolkit first version is 2021.1 """ return int(compiler_version.split(".")[0]) >= 2021 class IntelCC: """Class that manages Intel oneAPI DPC++/C++/Classic Compilers vars generation""" filename = "conanintelsetvars" def __init__(self, conanfile): # Let's check the compatibility compiler_version = conanfile.settings.get_safe("compiler.version") mode = conanfile.settings.get_safe("compiler.mode") if _is_using_intel_oneapi(compiler_version): if mode != "classic" and conanfile.settings.get_safe("os") == "Darwin": raise ConanException( 'macOS* is not supported for the icx/icpx or dpcpp compilers. ' 'Use the "classic" mode (icc compiler) instead.') else: # Do not support legacy versions raise ConanException("You have to use 'intel' compiler which is meant for legacy " "versions like Intel Parallel Studio XE.") # Private properties self._conanfile = conanfile self._settings = conanfile.settings self._compiler_version = compiler_version self._mode = mode self._out = conanfile.output # Public properties #: arch setting self.arch = conanfile.settings.get_safe("arch") @property def ms_toolset(self): """Get Microsoft Visual Studio Toolset depending on the mode selected""" if self._mode == "classic": # TODO: Get automatically the classic compiler version return "Intel C++ Compiler 19.2" elif self._mode == "icx": return "Intel C++ Compiler %s" % (self._compiler_version.split('.')[0]) else: # DPC++ compiler return "Intel(R) oneAPI DPC++ Compiler" def generate(self, scope="build"): """Generate the Conan Intel file to be loaded in build environment by default""" check_duplicated_generator(self, self._conanfile) intel_cc_path = self._conanfile.conf.get("tools.intel:installation_path") if intel_cc_path == "": return if platform.system() == "Windows" and not self._conanfile.win_bash: content = textwrap.dedent("""\ @echo off {} """.format(self.command)) filename = self.filename + '.bat' else: filename = self.filename + '.sh' content = self.command from conan.tools.env.environment import create_env_script create_env_script(self._conanfile, content, filename, scope) @property def installation_path(self): """Get the Intel oneAPI installation root path""" installation_path = self._conanfile.conf.get("tools.intel:installation_path") if not installation_path: raise ConanException("To use Intel oneAPI, specify a [conf] entry " "'tools.intel:installation_path' containing the path to the " "installation folder.") self._out.info("Got Intel oneAPI installation folder: %s" % installation_path) return installation_path @property def command(self): """ The Intel oneAPI DPC++/C++ Compiler includes environment configuration scripts to configure your build and development environment variables: - On Linux, the file is a shell script called setvars.sh. - On Windows, the file is a batch file called setvars.bat. - Linux -> ``>> . //setvars.sh `` The compiler environment script file accepts an optional target architecture argument : - intel64: Generate code and use libraries for Intel 64 architecture-based targets. - ia32: Generate code and use libraries for IA-32 architecture-based targets. - Windows -> ``>> call \\setvars.bat [] []`` Where is optional and can be one of the following: - intel64: Generate code and use libraries for Intel 64 architecture (host and target). - ia32: Generate code and use libraries for IA-32 architecture (host and target). With the dpcpp compiler, is intel64 by default. The is optional. If specified, it is one of the following: - vs2019: Microsoft Visual Studio* 2019 - vs2017: Microsoft Visual Studio 2017 :return: `str` setvars.sh|bat command to be run """ # Let's check if user wants to use some custom arguments to run the setvars script command_args = self._conanfile.conf.get("tools.intel:setvars_args", default="") system = platform.system() svars = "setvars.bat" if system == "Windows" else "setvars.sh" command = '"%s"' % os.path.join(self.installation_path, svars) if system == "Windows": command = "call " + command else: command = ". " + command # dot is more portable than source # If user has passed custom arguments if command_args: command += " %s" % command_args return command # Add architecture argument if self.arch == "x86_64": command += " intel64" elif self.arch == "x86": command += " ia32" else: raise ConanException("don't know how to call %s for %s" % (svars, self.arch)) return command ================================================ FILE: conan/tools/layout/__init__.py ================================================ import os def basic_layout(conanfile, src_folder=".", build_folder=None): subproject = conanfile.folders.subproject conanfile.folders.source = src_folder if not subproject else os.path.join(subproject, src_folder) if build_folder: conanfile.folders.build = build_folder if not subproject else os.path.join(subproject, build_folder) else: conanfile.folders.build = "build" if not subproject else os.path.join(subproject, "build") if conanfile.settings.get_safe("build_type"): conanfile.folders.build += "-{}".format(str(conanfile.settings.build_type).lower()) conanfile.folders.generators = os.path.join(conanfile.folders.build, "conan") conanfile.cpp.build.bindirs = ["."] conanfile.cpp.build.libdirs = ["."] if not conanfile.cpp.source.includedirs: conanfile.cpp.source.includedirs = ["include"] ================================================ FILE: conan/tools/meson/__init__.py ================================================ from conan.tools.meson.meson import Meson from conan.tools.meson.toolchain import MesonToolchain ================================================ FILE: conan/tools/meson/helpers.py ================================================ from conan.api.output import ConanOutput from conan.tools.build.flags import cppstd_msvc_flag, disable_flag from conan.internal.model.options import _PackageOption # https://mesonbuild.com/Reference-tables.html#operating-system-names _meson_system_map = { 'Android': 'android', 'Macos': 'darwin', 'iOS': 'darwin', 'watchOS': 'darwin', 'tvOS': 'darwin', 'visionOS': 'darwin', 'FreeBSD': 'freebsd', 'Emscripten': 'emscripten', 'Linux': 'linux', 'SunOS': 'sunos', 'Windows': 'windows', 'WindowsCE': 'windows', 'WindowsStore': 'windows', } # https://mesonbuild.com/Reference-tables.html#cpu-families _meson_cpu_family_map = { 'armv4': ('arm', 'armv4', 'little'), 'armv4i': ('arm', 'armv4i', 'little'), 'armv5el': ('arm', 'armv5el', 'little'), 'armv5hf': ('arm', 'armv5hf', 'little'), 'armv6': ('arm', 'armv6', 'little'), 'armv7': ('arm', 'armv7', 'little'), 'armv7hf': ('arm', 'armv7hf', 'little'), 'armv7s': ('arm', 'armv7s', 'little'), 'armv7k': ('arm', 'armv7k', 'little'), 'armv8': ('aarch64', 'armv8', 'little'), 'armv8_32': ('aarch64', 'armv8_32', 'little'), 'armv8.3': ('aarch64', 'armv8.3', 'little'), 'arm64ec': ('aarch64', 'arm64ec', 'little'), 'avr': ('avr', 'avr', 'little'), 'mips': ('mips', 'mips', 'big'), 'mips64': ('mips64', 'mips64', 'big'), 'ppc32be': ('ppc', 'ppc', 'big'), 'ppc32': ('ppc', 'ppc', 'little'), 'ppc64le': ('ppc64', 'ppc64', 'little'), 'ppc64': ('ppc64', 'ppc64', 'big'), 's390': ('s390', 's390', 'big'), 's390x': ('s390x', 's390x', 'big'), 'sh4le': ('sh4', 'sh4', 'little'), 'sparc': ('sparc', 'sparc', 'big'), 'sparcv9': ('sparc64', 'sparc64', 'big'), 'wasm': ('wasm32', 'wasm32', 'little'), 'wasm64': ('wasm64', 'wasm64', 'little'), 'x86': ('x86', 'x86', 'little'), 'x86_64': ('x86_64', 'x86_64', 'little'), 'riscv32': ('riscv32', 'riscv32', 'little'), 'riscv64': ('riscv64', 'riscv64', 'little') } def get_apple_subsystem(apple_sdk): return { "iphoneos": "ios", "iphonesimulator": "ios-simulator", "appletvos": "tvos", "appletvsimulator": "tvos-simulator", "watchos": "watchos", "watchsimulator": "watchos-simulator", }.get(apple_sdk, "macos") def to_meson_machine(machine_os, machine_arch): """Gets the OS system info as the Meson machine context. :param machine_os: ``str`` OS name. :param machine_arch: ``str`` OS arch. :return: ``dict`` Meson machine context. """ system = _meson_system_map.get(machine_os, machine_os.lower()) default_cpu_tuple = (machine_arch.lower(), machine_arch.lower(), 'little') cpu_tuple = _meson_cpu_family_map.get(machine_arch, default_cpu_tuple) cpu_family, cpu, endian = cpu_tuple[0], cpu_tuple[1], cpu_tuple[2] context = { 'system': system, 'cpu_family': cpu_family, 'cpu': cpu, 'endian': endian, } return context def to_meson_value(value): """Puts any value with a valid str-like Meson format. :param value: ``str``, ``bool``, or ``list``, otherwise, it will do nothing. :return: formatted value as a ``str``. """ # https://mesonbuild.com/Machine-files.html#data-types # we don't need to transform the integer values if isinstance(value, str): return f"'{value}'" elif isinstance(value, bool): return 'true' if value else 'false' elif isinstance(value, list): return '[{}]'.format(', '.join([str(to_meson_value(val)) for val in value])) elif isinstance(value, _PackageOption): ConanOutput().warning(f"Please, do not use a Conan option value directly. " f"Convert 'options.{value.name}' into a valid Python" f"data type, e.g, bool(self.options.shared)", warn_tag="deprecated") return value def to_cppstd_flag(conanfile, compiler, compiler_version, cppstd): """Gets a valid cppstd flag. :param conanfile: ``ConanFile`` instance. :param compiler: ``str`` compiler name. :param compiler_version: ``str`` compiler version. :param cppstd: ``str`` cppstd version. :return: ``str`` cppstd flag. """ if cppstd is None: return None if disable_flag(conanfile, "cppstd"): return None if compiler == "msvc": # Meson's logic with 'vc++X' vs 'c++X' is possibly a little outdated. # Presumably the intent is 'vc++X' is permissive and 'c++X' is not, # but '/permissive-' is the default since 16.8. flag = cppstd_msvc_flag(compiler_version, cppstd) return 'v%s' % flag if flag else None else: return f"gnu++{cppstd[3:]}" if cppstd.startswith("gnu") else f"c++{cppstd}" def to_cstd_flag(conanfile, cstd): """Gets a valid cstd flag. """ if cstd is None: return None if disable_flag(conanfile, "cppstd"): return None return cstd if cstd.startswith("gnu") else f"c{cstd}" ================================================ FILE: conan/tools/meson/meson.py ================================================ import os from conan.tools.build import build_jobs from conan.tools.meson.toolchain import MesonToolchain class Meson: """ This class calls Meson commands when a package is being built. Notice that this one should be used together with the ``MesonToolchain`` generator. """ def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile def configure(self, reconfigure=False): """ Runs ``meson setup [FILE] "BUILD_FOLDER" "SOURCE_FOLDER" [-Dprefix=/]`` command, where ``FILE`` could be ``--native-file conan_meson_native.ini`` (if native builds) or ``--cross-file conan_meson_cross.ini`` (if cross builds). :param reconfigure: ``bool`` value that adds ``--reconfigure`` param to the final command. """ if reconfigure: self._conanfile.output.warning("reconfigure param has been deprecated." " Removing in Conan 2.x.", warn_tag="deprecated") source_folder = self._conanfile.source_folder build_folder = self._conanfile.build_folder generators_folder = self._conanfile.generators_folder cross = os.path.join(generators_folder, MesonToolchain.cross_filename) native = os.path.join(generators_folder, MesonToolchain.native_filename) is_cross_build = os.path.exists(cross) machine_files = self._conanfile.conf.get("tools.meson.mesontoolchain:extra_machine_files", default=[], check_type=list) cmd = "meson setup " if is_cross_build: machine_files.insert(0, cross) cmd += " ".join([f'--cross-file "{file}"' for file in machine_files]) if os.path.exists(native): if not is_cross_build: # machine files are only appended to the cross or the native one machine_files.insert(0, native) cmd += " ".join([f'--native-file "{file}"' for file in machine_files]) else: # extra native file for cross-building scenarios cmd += f' --native-file "{native}"' cmd += ' "{}" "{}"'.format(build_folder, source_folder) cmd += f" --prefix={self._prefix}" self._conanfile.output.info("Meson configure cmd: {}".format(cmd)) self._conanfile.run(cmd) def build(self, target=None): """ Runs ``meson compile -C . -j[N_JOBS] [TARGET]`` in the build folder. You can specify ``N_JOBS`` through the configuration line ``tools.build:jobs=N_JOBS`` in your profile ``[conf]`` section. :param target: ``str`` Specifies the target to be executed. """ meson_build_folder = self._conanfile.build_folder cmd = 'meson compile -C "{}"'.format(meson_build_folder) njobs = build_jobs(self._conanfile) if njobs: cmd += " -j{}".format(njobs) if target: cmd += " {}".format(target) verbosity = self._build_verbosity if verbosity: cmd += " " + verbosity self._conanfile.output.info("Meson build cmd: {}".format(cmd)) self._conanfile.run(cmd) def install(self, cli_args=None): """ Runs ``meson install -C "." --destdir ..`` in the build folder. :param cli_args: List of arguments to be added to the command: ``meson install -C "." --destdir ... arg1 arg2`` """ meson_build_folder = self._conanfile.build_folder.replace("\\", "/") meson_package_folder = self._conanfile.package_folder.replace("\\", "/") # Assuming meson >= 0.57.0 cmd = f'meson install -C "{meson_build_folder}" --destdir "{meson_package_folder}"' verbosity = self._install_verbosity if verbosity: cmd += " " + verbosity if self._conanfile.conf.get("tools.build:install_strip", check_type=bool): cmd += " --strip" if cli_args: cmd += " " + " ".join(cli_args) self._conanfile.run(cmd) def test(self): """ Runs ``meson test -v -C "."`` in the build folder. """ if self._conanfile.conf.get("tools.build:skip_test", check_type=bool): return meson_build_folder = self._conanfile.build_folder cmd = 'meson test -v -C "{}"'.format(meson_build_folder) # TODO: Do we need vcvars for test? # TODO: This should use conanrunenv, but what if meson itself is a build-require? self._conanfile.run(cmd) @property def _build_verbosity(self): # verbosity of build tools. This passes -v to ninja, for example. # See https://github.com/mesonbuild/meson/blob/master/mesonbuild/mcompile.py#L156 verbosity = self._conanfile.conf.get("tools.compilation:verbosity", choices=("quiet", "verbose")) return "--verbose" if verbosity == "verbose" else "" @property def _install_verbosity(self): # https://github.com/mesonbuild/meson/blob/master/mesonbuild/minstall.py#L81 # Errors are always logged, and status about installed files is controlled by this flag, # so it's a bit backwards verbosity = self._conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) return "--quiet" if verbosity else "" @property def _prefix(self): """Generate a valid ``--prefix`` argument value for meson. For conan, the prefix must be similar to the Unix root directory ``/``. The result of this function should be passed to ``meson setup --prefix={self._prefix} ...`` Python 3.13 changed the semantics of ``/`` on the Windows ntpath module, it is now special-cased as a relative directory. Thus, ``os.path.isabs("/")`` is true on Linux but false on Windows. So for Windows, an equivalent path is ``C:\\``. However, this can be parsed wrongly in meson in specific circumstances due to the trailing backslash. Hence, we also use forward slashes for Windows, leaving us with ``C:/`` or similar paths. See also -------- * The meson issue discussing the need to set ``--prefix`` to ``/``: `mesonbuild/meson#12880 `_ * The cpython PR introducing the ``/`` behavior change: `python/cpython#113829 `_ * The issue detailing the erroneous parsing of ``\\``: `conan-io/conan#14213 `_ """ return os.path.abspath("/").replace("\\", "/") ================================================ FILE: conan/tools/meson/toolchain.py ================================================ import os import textwrap from jinja2 import Template, StrictUndefined from conan.errors import ConanException from conan.internal import check_duplicated_generator from conan.internal.internal_tools import raise_on_universal_arch from conan.tools.apple.apple import is_apple_os, apple_min_version_flag, \ resolve_apple_flags, apple_extra_flags from conan.tools.build.cross_building import cross_building, can_run from conan.tools.build.flags import (architecture_link_flag, libcxx_flags, architecture_flag, threads_flags) from conan.tools.env import VirtualBuildEnv from conan.tools.meson.helpers import get_apple_subsystem, to_cppstd_flag, to_cstd_flag, \ to_meson_machine, to_meson_value from conan.tools.microsoft import VCVars, msvc_runtime_flag from conan.internal.util.files import save class MesonToolchain: """ MesonToolchain generator """ native_filename = "conan_meson_native.ini" cross_filename = "conan_meson_cross.ini" _meson_file_template = textwrap.dedent("""\ [properties] {% for it, value in properties.items() -%} {{it}} = {{value}} {% endfor %} [constants] preprocessor_definitions = [{% for it, value in preprocessor_definitions.items() -%} '-D{{ it }}="{{ value}}"'{%- if not loop.last %}, {% endif %}{% endfor %}] [project options] {% for it, value in project_options.items() -%} {{it}} = {{value}} {% endfor %} {% for subproject, listkeypair in subproject_options -%} [{{subproject}}:project options] {% for keypair in listkeypair -%} {% for it, value in keypair.items() -%} {{it}} = {{value}} {% endfor %} {% endfor %} {% endfor %} [binaries] {% if c %} c = {{c}} {% endif %} {% if cpp %} cpp = {{cpp}} {% endif %} {% if ld %} ld = {{ld}} {% endif %} {% if is_apple_system %} {% if objc %} objc = '{{objc}}' {% endif %} {% if objcpp %} objcpp = '{{objcpp}}' {% endif %} {% endif %} {% if c_ld %} c_ld = '{{c_ld}}' {% endif %} {% if cpp_ld %} cpp_ld = '{{cpp_ld}}' {% endif %} {% if ar %} ar = '{{ar}}' {% endif %} {% if strip %} strip = '{{strip}}' {% endif %} {% if as %} as = '{{as}}' {% endif %} {% if windres %} windres = '{{windres}}' {% endif %} {% if pkgconfig %} pkgconfig = '{{pkgconfig}}' {% endif %} {% if pkgconfig %} pkg-config = '{{pkgconfig}}' {% endif %} [built-in options] {% if buildtype %} buildtype = '{{buildtype}}' {% endif %} {% if default_library %} default_library = '{{default_library}}' {% endif %} {% if b_vscrt %} b_vscrt = '{{b_vscrt}}' {% endif %} {% if b_ndebug %} b_ndebug = {{b_ndebug}} {% endif %} {% if b_staticpic %} b_staticpic = {{b_staticpic}} {% endif %} {% if cpp_std %} cpp_std = '{{cpp_std}}' {% endif %} {% if c_std %} c_std = '{{c_std}}' {% endif %} {% if backend %} backend = '{{backend}}' {% endif %} {% if pkg_config_path %} pkg_config_path = '{{pkg_config_path}}' {% endif %} {% if build_pkg_config_path %} build.pkg_config_path = '{{build_pkg_config_path}}' {% endif %} # C/C++ arguments c_args = {{c_args}} + preprocessor_definitions c_link_args = {{c_link_args}} cpp_args = {{cpp_args}} + preprocessor_definitions cpp_link_args = {{cpp_link_args}} {% if is_apple_system %} # Objective-C/C++ arguments objc_args = {{objc_args}} + preprocessor_definitions objc_link_args = {{objc_link_args}} objcpp_args = {{objcpp_args}} + preprocessor_definitions objcpp_link_args = {{objcpp_link_args}} {% endif %} {% for context, values in cross_build.items() %} [{{context}}_machine] system = '{{values["system"]}}' {% if values.get("subsystem") %} subsystem = '{{values["subsystem"]}}' {% endif %} cpu_family = '{{values["cpu_family"]}}' cpu = '{{values["cpu"]}}' endian = '{{values["endian"]}}' {% endfor %} """) def __init__(self, conanfile, backend=None, native=False): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. :param backend: (**DEPRECATED**, use ``self.backend`` instead) ``str`` ``backend`` Meson variable value. By default, ``ninja``. :param native: ``bool`` Indicates whether you want Conan to create the ``conan_meson_native.ini`` in a cross-building context. Notice that it only makes sense if your project's ``meson.build`` uses the ``native=true`` (see also https://mesonbuild.com/Cross-compilation.html#mixing-host-and-build-targets). """ raise_on_universal_arch(conanfile) self._conanfile = conanfile self._native = native self._is_apple_system = is_apple_os(self._conanfile) is_cross_building = cross_building(conanfile) # x86_64->x86 is considered cross-building if not is_cross_building and native: raise ConanException("You can only pass native=True if you're cross-building, " "otherwise, it could cause unexpected results.") self._conanfile_conf = self._conanfile.conf_build if native else self._conanfile.conf # Values are kept as Python built-ins so users can modify them more easily, and they are # only converted to Meson file syntax for rendering # priority: first user conf, then recipe, last one is default "ninja" #: Backend to use. Defined by the conf ``tools.meson.mesontoolchain:backend``. By default, ``ninja``. self.backend = backend or 'ninja' build_type = self._conanfile.settings.get_safe("build_type") #: Build type to use. self.buildtype = {"Debug": "debug", # Note, it is not "'debug'" "Release": "release", "MinSizeRel": "minsize", "RelWithDebInfo": "debugoptimized"}.get(build_type, build_type) #: Disable asserts. self.b_ndebug = "true" if self.buildtype != "debug" else "false" # https://mesonbuild.com/Builtin-options.html#base-options fpic = self._conanfile.options.get_safe("fPIC") shared = self._conanfile.options.get_safe("shared") #: Build static libraries as position independent. By default, ``self.options.get_safe("fPIC")`` self.b_staticpic = fpic if (shared is False and fpic is not None) else None # https://mesonbuild.com/Builtin-options.html#core-options # Do not adjust "debug" if already adjusted "buildtype" #: Default library type, e.g., "shared. self.default_library = ("shared" if shared else "static") if shared is not None else None compiler = self._conanfile.settings.get_safe("compiler") if compiler is None: raise ConanException("MesonToolchain needs 'settings.compiler', but it is not defined") compiler_version = self._conanfile.settings.get_safe("compiler.version") if compiler_version is None: raise ConanException("MesonToolchain needs 'settings.compiler.version', but it is not defined") cppstd = self._conanfile.settings.get_safe("compiler.cppstd") cstd = self._conanfile.settings.get_safe("compiler.cstd") #: C++ language standard to use. Defined by ``to_cppstd_flag()`` by default. self.cpp_std = to_cppstd_flag(self._conanfile, compiler, compiler_version, cppstd) #: C language standard to use. Defined by ``to_cstd_flag()`` by default. self.c_std = to_cstd_flag(self._conanfile, cstd) #: VS runtime library to use. Defined by ``msvc_runtime_flag()`` by default. self.b_vscrt = None if compiler in ("msvc", "clang"): vscrt = msvc_runtime_flag(self._conanfile) if vscrt: self.b_vscrt = str(vscrt).lower() # Extra flags #: List of extra ``CXX`` flags. Added to ``cpp_args`` self.extra_cxxflags = [] #: List of extra ``C`` flags. Added to ``c_args`` self.extra_cflags = [] #: List of extra linker flags. Added to ``c_link_args`` and ``cpp_link_args`` self.extra_ldflags = [] #: List of extra preprocessor definitions. Added to ``c_args`` and ``cpp_args`` with the #: format ``-D[FLAG_N]``. self.extra_defines = [] #: Architecture flag deduced by Conan and added to ``c_args``, ``cpp_args``, ``c_link_args`` and ``cpp_link_args`` self.arch_flag = architecture_flag(self._conanfile) # https://github.com/conan-io/conan/issues/17624 #: Architecture link flag deduced by Conan and added to ``c_link_args`` and ``cpp_link_args`` self.arch_link_flag = architecture_link_flag(self._conanfile) #: Threads flags deduced by Conan and added to ``c_args``, ``cpp_args``, ``c_link_args`` and ``cpp_link_args`` self.threads_flags = threads_flags(self._conanfile) #: Dict-like object that defines Meson ``properties`` with ``key=value`` format self.properties = {} #: Dict-like object that defines Meson ``project options`` with ``key=value`` format self.project_options = { "wrap_mode": "nofallback" # https://github.com/conan-io/conan/issues/10671 } #: Dict-like object that defines Meson ``preprocessor definitions`` self.preprocessor_definitions = {} # Add all the default dirs self.project_options.update(self._get_default_dirs()) #: Dict-like object that defines Meson ``subproject options``. self.subproject_options = {} #: Defines the Meson ``pkg_config_path`` variable self.pkg_config_path = self._conanfile.generators_folder #: Defines the Meson ``build.pkg_config_path`` variable (build context) # Issue: https://github.com/conan-io/conan/issues/12342 # Issue: https://github.com/conan-io/conan/issues/14935 self.build_pkg_config_path = None self.libcxx, self.gcc_cxx11_abi = libcxx_flags(self._conanfile) #: Dict-like object with the build, host, and target as the Meson machine context self.cross_build = {} default_comp = "" default_comp_cpp = "" if native is False and is_cross_building: os_host = conanfile.settings.get_safe("os") arch_host = conanfile.settings.get_safe("arch") os_build = conanfile.settings_build.get_safe('os') arch_build = conanfile.settings_build.get_safe('arch') self.cross_build["build"] = to_meson_machine(os_build, arch_build) self.cross_build["host"] = to_meson_machine(os_host, arch_host) # Check subsystem if it's Apple cross-building only. It requires Meson >= 1.2.0, # but it does not break lower versions. # See https://mesonbuild.com/Reference-tables.html#subsystem-names-since-120 # Issue: https://github.com/conan-io/conan/issues/17873 if self._is_apple_system and is_apple_os(conanfile, build_context=True): sdk_build = conanfile.settings_build.get_safe("os.sdk") sdk_host = conanfile.settings.get_safe("os.sdk") self.cross_build["host"]["subsystem"] = get_apple_subsystem(sdk_host) self.cross_build["build"]["subsystem"] = get_apple_subsystem(sdk_build) # Issue: https://github.com/conan-io/conan/issues/19217 self.properties["needs_exe_wrapper"] = not can_run(self._conanfile) if hasattr(conanfile, 'settings_target') and conanfile.settings_target: settings_target = conanfile.settings_target os_target = settings_target.get_safe("os") arch_target = settings_target.get_safe("arch") self.cross_build["target"] = to_meson_machine(os_target, arch_target) if is_apple_os(self._conanfile): # default cross-compiler in Apple is common default_comp = "clang" default_comp_cpp = "clang++" else: if "clang" in compiler: default_comp = "clang" default_comp_cpp = "clang++" elif compiler == "gcc": default_comp = "gcc" default_comp_cpp = "g++" if "Visual" in compiler or compiler == "msvc": default_comp = "cl" default_comp_cpp = "cl" # Read configuration for sys_root property (honoring existing conf) self._sys_root = self._conanfile_conf.get("tools.build:sysroot", check_type=str) # Read configuration for compilers compilers_by_conf = self._conanfile_conf.get("tools.build:compiler_executables", default={}, check_type=dict) # Read the VirtualBuildEnv to update the variables build_env = self._conanfile.buildenv_build.vars(self._conanfile) if native else ( VirtualBuildEnv(self._conanfile, auto_generate=True).vars()) #: Sets the Meson ``c`` variable, defaulting to the ``CC`` build environment value. #: If provided as a blank-separated string, it will be transformed into a list. #: Otherwise, it remains a single string. self.c = compilers_by_conf.get("c") or self._sanitize_env_format(build_env.get("CC")) or default_comp #: Sets the Meson ``cpp`` variable, defaulting to the ``CXX`` build environment value. #: If provided as a blank-separated string, it will be transformed into a list. #: Otherwise, it remains a single string. self.cpp = compilers_by_conf.get("cpp") or self._sanitize_env_format(build_env.get("CXX")) or default_comp_cpp #: Sets the Meson ``ld`` variable, defaulting to the ``LD`` build environment value. #: If provided as a blank-separated string, it will be transformed into a list. #: Otherwise, it remains a single string. self.ld = self._sanitize_env_format(build_env.get("LD")) # FIXME: Should we use the new tools.build:compiler_executables and avoid buildenv? # Issue related: https://github.com/mesonbuild/meson/issues/6442 # PR related: https://github.com/mesonbuild/meson/pull/6457 #: Defines the Meson ``c_ld`` variable. Defaulted to ``CC_LD`` #: environment value self.c_ld = build_env.get("CC_LD") #: Defines the Meson ``cpp_ld`` variable. Defaulted to ``CXX_LD`` #: environment value self.cpp_ld = build_env.get("CXX_LD") #: Defines the Meson ``ar`` variable. Defaulted to ``AR`` build environment value self.ar = build_env.get("AR") #: Defines the Meson ``strip`` variable. Defaulted to ``STRIP`` build environment value self.strip = build_env.get("STRIP") #: Defines the Meson ``as`` variable. Defaulted to ``AS`` build environment value self.as_ = build_env.get("AS") #: Defines the Meson ``windres`` variable. Defaulted to ``WINDRES`` build environment value self.windres = build_env.get("WINDRES") #: Defines the Meson ``pkgconfig`` variable. Defaulted to ``PKG_CONFIG`` #: build environment value self.pkgconfig = (self._conanfile_conf.get("tools.gnu:pkg_config", check_type=str) or build_env.get("PKG_CONFIG")) #: Defines the Meson ``c_args`` variable. Defaulted to ``CFLAGS`` build environment value self.c_args = self._get_env_list(build_env.get("CFLAGS", [])) #: Defines the Meson ``c_link_args`` variable. Defaulted to ``LDFLAGS`` build #: environment value self.c_link_args = self._get_env_list(build_env.get("LDFLAGS", [])) #: Defines the Meson ``cpp_args`` variable. Defaulted to ``CXXFLAGS`` build environment value self.cpp_args = self._get_env_list(build_env.get("CXXFLAGS", [])) #: Defines the Meson ``cpp_link_args`` variable. Defaulted to ``LDFLAGS`` build #: environment value self.cpp_link_args = self._get_env_list(build_env.get("LDFLAGS", [])) # Apple flags and variables #: Apple arch flag as a list, e.g., ``["-arch", "i386"]`` self.apple_arch_flag = [] #: Apple sysroot flag as a list, e.g., ``["-isysroot", "./Platforms/MacOSX.platform"]`` self.apple_isysroot_flag = [] #: Apple minimum binary version flag as a list, e.g., ``["-mios-version-min", "10.8"]`` self.apple_min_version_flag = [] #: Apple bitcode, visibility and arc flags self.apple_extra_flags = apple_extra_flags(self._conanfile) #: Defines the Meson ``objc`` variable. Defaulted to ``None``, if if any Apple OS ``clang`` self.objc = None #: Defines the Meson ``objcpp`` variable. Defaulted to ``None``, if if any Apple OS ``clang++`` self.objcpp = None #: Defines the Meson ``objc_args`` variable. Defaulted to ``OBJCFLAGS`` build environment value self.objc_args = [] #: Defines the Meson ``objc_link_args`` variable. Defaulted to ``LDFLAGS`` build environment value self.objc_link_args = [] #: Defines the Meson ``objcpp_args`` variable. Defaulted to ``OBJCXXFLAGS`` build environment value self.objcpp_args = [] #: Defines the Meson ``objcpp_link_args`` variable. Defaulted to ``LDFLAGS`` build environment value self.objcpp_link_args = [] self._resolve_apple_flags_and_variables(build_env, compilers_by_conf) if native is False: self._resolve_android_cross_compilation() def _get_default_dirs(self): """ Get all the default directories from cpp.package. Issues related: - https://github.com/conan-io/conan/issues/9713 - https://github.com/conan-io/conan/issues/11596 """ def _get_cpp_info_value(name): elements = getattr(self._conanfile.cpp.package, name) return elements[0] if elements else None ret = {} bindir = _get_cpp_info_value("bindirs") datadir = _get_cpp_info_value("resdirs") libdir = _get_cpp_info_value("libdirs") includedir = _get_cpp_info_value("includedirs") if bindir: ret.update({ 'bindir': bindir, 'sbindir': bindir, 'libexecdir': bindir }) if datadir: ret.update({ 'datadir': datadir, 'localedir': datadir, 'mandir': datadir, 'infodir': datadir }) if includedir: ret["includedir"] = includedir if libdir: ret["libdir"] = libdir return ret def _resolve_apple_flags_and_variables(self, build_env, compilers_by_conf): if not self._is_apple_system: return # Calculating the main Apple flags min_flag, arch_flag, isysroot_flag = ( resolve_apple_flags(self._conanfile, is_cross_building=self.cross_build)) self.apple_arch_flag = arch_flag.split() if arch_flag else [] self.apple_isysroot_flag = isysroot_flag.split() if isysroot_flag else [] self.apple_min_version_flag = [apple_min_version_flag(self._conanfile)] # Objective C/C++ ones self.objc = compilers_by_conf.get("objc", "clang") self.objcpp = compilers_by_conf.get("objcpp", "clang++") enable_arc = self._conanfile.conf.get("tools.apple:enable_arc", check_type=bool) fobj_arc = "" if enable_arc: fobj_arc = "-fobjc-arc" if enable_arc is False: fobj_arc = "-fno-objc-arc" self.objc_args = self._get_env_list(build_env.get('OBJCFLAGS', [])) + [fobj_arc] self.objc_link_args = self._get_env_list(build_env.get('LDFLAGS', [])) self.objcpp_args = self._get_env_list(build_env.get('OBJCXXFLAGS', [])) + [fobj_arc] self.objcpp_link_args = self._get_env_list(build_env.get('LDFLAGS', [])) def _resolve_android_cross_compilation(self): if not self.cross_build or not self.cross_build["host"]["system"] == "android": return ndk_path = self._conanfile_conf.get("tools.android:ndk_path") if not ndk_path: raise ConanException("You must provide a NDK path. Use 'tools.android:ndk_path' " "configuration field.") arch = self._conanfile.settings.get_safe("arch") os_build = self.cross_build["build"]["system"] ndk_bin = os.path.join(ndk_path, "toolchains", "llvm", "prebuilt", "{}-x86_64".format(os_build), "bin") android_api_level = self._conanfile.settings.get_safe("os.api_level") android_target = {'armv7': 'armv7a-linux-androideabi', 'armv8': 'aarch64-linux-android', 'x86': 'i686-linux-android', 'x86_64': 'x86_64-linux-android'}.get(arch) os_build = self._conanfile.settings_build.get_safe('os') compile_ext = ".cmd" if os_build == "Windows" else "" # User has more prio than Conan self.c = os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang{compile_ext}") self.cpp = os.path.join(ndk_bin, f"{android_target}{android_api_level}-clang++{compile_ext}") self.ar = os.path.join(ndk_bin, "llvm-ar") @property def _rpath_link_flag(self): add_rpath_link = self._conanfile.conf.get("tools.build:add_rpath_link", check_type=bool) if not add_rpath_link: return [] runtime_dirs = [] host_req = self._conanfile.dependencies.filter({"build": False}).values() for req in host_req: cppinfo = req.cpp_info.aggregated_components() runtime_dirs.extend(cppinfo.libdirs) return ["-Wl,-rpath-link=" + ":".join(runtime_dirs)] if runtime_dirs else [] def _get_extra_flags(self): # Now, it's time to get all the flags defined by the user cxxflags = self._conanfile_conf.get("tools.build:cxxflags", default=[], check_type=list) cflags = self._conanfile_conf.get("tools.build:cflags", default=[], check_type=list) sharedlinkflags = self._conanfile_conf.get("tools.build:sharedlinkflags", default=[], check_type=list) exelinkflags = self._conanfile_conf.get("tools.build:exelinkflags", default=[], check_type=list) linker_scripts = self._conanfile_conf.get("tools.build:linker_scripts", default=[], check_type=list) linker_script_flags = ['-T' + linker_script for linker_script in linker_scripts] defines = self._conanfile_conf.get("tools.build:defines", default=[], check_type=list) sys_root = [f"--sysroot={self._sys_root}"] if self._sys_root else [""] ld = (sharedlinkflags + exelinkflags + linker_script_flags + sys_root + self.extra_ldflags + self.threads_flags) # Apple extra flags from confs (visibilty, bitcode, arc) cxxflags += self.apple_extra_flags cflags += self.apple_extra_flags ld += self.apple_extra_flags return { "cxxflags": [self.arch_flag] + cxxflags + sys_root + self.extra_cxxflags + self.threads_flags, "cflags": [self.arch_flag] + cflags + sys_root + self.extra_cflags + self.threads_flags, "ldflags": [self.arch_flag] + [self.arch_link_flag] + ld + self._rpath_link_flag, "defines": [f"-D{d}" for d in (defines + self.extra_defines)] } @staticmethod def _get_env_list(v): # FIXME: Should Environment have the "check_type=None" keyword as Conf? return v.strip().split() if not isinstance(v, list) else v @staticmethod def _filter_list_empty_fields(v): return list(filter(bool, v)) @staticmethod def _sanitize_env_format(value): if value is None or isinstance(value, list): return value if not isinstance(value, str): raise ConanException(f"MesonToolchain: Value '{value}' should be a string") ret = [x.strip() for x in value.split() if x] return ret[0] if len(ret) == 1 else ret @property def _context(self): apple_flags = self.apple_isysroot_flag + self.apple_arch_flag + self.apple_min_version_flag extra_flags = self._get_extra_flags() self.c_args.extend(apple_flags + extra_flags["cflags"] + extra_flags["defines"]) self.cpp_args.extend(apple_flags + extra_flags["cxxflags"] + extra_flags["defines"]) self.c_link_args.extend(apple_flags + extra_flags["ldflags"]) self.cpp_link_args.extend(apple_flags + extra_flags["ldflags"]) # Objective C/C++ self.objc_args.extend(self.c_args) self.objcpp_args.extend(self.cpp_args) # These link_args have already the LDFLAGS env value so let's add only the new possible ones self.objc_link_args.extend(self.c_link_args) self.objcpp_link_args.extend(self.cpp_link_args) if self.preprocessor_definitions: self._conanfile.output.warning( "Use 'extra_defines' attribute for compiler preprocessor definitions instead " + "of 'preprocessor_definitions'", warn_tag="deprecated") if self.libcxx: self.cpp_args.append(self.libcxx) self.cpp_link_args.append(self.libcxx) if self.gcc_cxx11_abi: self.cpp_args.append("-D{}".format(self.gcc_cxx11_abi)) subproject_options = {} for subproject, listkeypair in self.subproject_options.items(): if listkeypair: if not isinstance(listkeypair, list): raise ConanException("MesonToolchain.subproject_options must be a list of dicts") subproject_options[subproject] = [{k: to_meson_value(v) for k, v in keypair.items()} for keypair in listkeypair] return { # https://mesonbuild.com/Machine-files.html#properties "properties": {k: to_meson_value(v) for k, v in self.properties.items()}, # https://mesonbuild.com/Machine-files.html#project-specific-options "project_options": {k: to_meson_value(v) for k, v in self.project_options.items()}, # https://mesonbuild.com/Subprojects.html#build-options-in-subproject "subproject_options": subproject_options.items(), # https://mesonbuild.com/Builtin-options.html#directories # https://mesonbuild.com/Machine-files.html#binaries # https://mesonbuild.com/Reference-tables.html#compiler-and-linker-selection-variables "c": to_meson_value(self.c), "cpp": to_meson_value(self.cpp), "ld": to_meson_value(self.ld), "objc": self.objc, "objcpp": self.objcpp, "c_ld": self.c_ld, "cpp_ld": self.cpp_ld, "ar": self.ar, "strip": self.strip, "as": self.as_, "windres": self.windres, "pkgconfig": self.pkgconfig, # https://mesonbuild.com/Builtin-options.html#core-options "buildtype": self.buildtype, "default_library": self.default_library, "backend": self._conanfile_conf.get("tools.meson.mesontoolchain:backend", default=self.backend), # https://mesonbuild.com/Builtin-options.html#base-options "b_vscrt": self.b_vscrt, "b_staticpic": to_meson_value(self.b_staticpic), # boolean "b_ndebug": to_meson_value(self.b_ndebug), # boolean as string # https://mesonbuild.com/Builtin-options.html#compiler-options "cpp_std": self.cpp_std, "c_std": self.c_std, "c_args": to_meson_value(self._filter_list_empty_fields(self.c_args)), "c_link_args": to_meson_value(self._filter_list_empty_fields(self.c_link_args)), "cpp_args": to_meson_value(self._filter_list_empty_fields(self.cpp_args)), "cpp_link_args": to_meson_value(self._filter_list_empty_fields(self.cpp_link_args)), "objc_args": to_meson_value(self._filter_list_empty_fields(self.objc_args)), "objc_link_args": to_meson_value(self._filter_list_empty_fields(self.objc_link_args)), "objcpp_args": to_meson_value(self._filter_list_empty_fields(self.objcpp_args)), "objcpp_link_args": to_meson_value(self._filter_list_empty_fields(self.objcpp_link_args)), "pkg_config_path": self.pkg_config_path, "build_pkg_config_path": self.build_pkg_config_path, #: Deprecated: Dict-like object that defines Meson ``preprocessor definitions``. Use the extra_defines attribute instead. "preprocessor_definitions": self.preprocessor_definitions, "cross_build": self.cross_build, "is_apple_system": self._is_apple_system } @property def _filename(self): if self.cross_build and self._native: return self.native_filename elif self.cross_build: return self.cross_filename else: return self.native_filename @property def _content(self): """ Gets content of the file to be used by Meson as its context. :return: ``str`` whole Meson context content. """ context = self._context content = Template(self._meson_file_template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined).render(context) return content def generate(self): """ Creates a ``conan_meson_native.ini`` (if native builds) or a ``conan_meson_cross.ini`` (if cross builds) with the proper content. If Windows OS, it will be created a ``conanvcvars.bat`` as well. """ check_duplicated_generator(self, self._conanfile) self._conanfile.output.info(f"MesonToolchain generated: {self._filename}") save(self._filename, self._content) # FIXME: Should we check the OS and compiler to call VCVars? VCVars(self._conanfile).generate() ================================================ FILE: conan/tools/microsoft/__init__.py ================================================ from conan.tools.microsoft.layout import vs_layout from conan.tools.microsoft.msbuild import MSBuild from conan.tools.microsoft.msbuilddeps import MSBuildDeps from conan.tools.microsoft.subsystems import unix_path, unix_path_package_info_legacy from conan.tools.microsoft.toolchain import MSBuildToolchain from conan.tools.microsoft.nmaketoolchain import NMakeToolchain from conan.tools.microsoft.nmakedeps import NMakeDeps from conan.tools.microsoft.visual import msvc_runtime_flag, VCVars, is_msvc, \ is_msvc_static_runtime, check_min_vs, msvs_toolset ================================================ FILE: conan/tools/microsoft/layout.py ================================================ import os from conan.errors import ConanException from conan.tools.microsoft.visual import msvc_platform_from_arch def vs_layout(conanfile): """ Initialize a layout for a typical Visual Studio project. :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ subproject = conanfile.folders.subproject conanfile.folders.source = subproject or "." conanfile.folders.generators = os.path.join(subproject, "conan") if subproject else "conan" conanfile.folders.build = subproject or "." conanfile.cpp.source.includedirs = ["include"] try: build_type = str(conanfile.settings.build_type) except ConanException: raise ConanException("The 'vs_layout' requires the 'build_type' setting") try: arch = str(conanfile.settings.arch) except ConanException: raise ConanException("The 'vs_layout' requires the 'arch' setting") if arch != "None" and arch != "x86": msvc_arch = msvc_platform_from_arch(arch) if not msvc_arch: raise ConanException(f"The 'vs_layout' doesn't work with the arch '{arch}'. " "Accepted architectures: 'x86', 'x86_64', 'armv7', 'armv8'") bindirs = os.path.join(msvc_arch, build_type) else: bindirs = build_type conanfile.cpp.build.libdirs = [bindirs] conanfile.cpp.build.bindirs = [bindirs] ================================================ FILE: conan/tools/microsoft/msbuild.py ================================================ from conan.errors import ConanException from conan.tools.microsoft.visual import msvc_platform_from_arch def msbuild_verbosity_cmd_line_arg(conanfile): """ Controls msbuild verbosity. See https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference :return: """ verbosity = conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) if verbosity is not None: verbosity = { "quiet": "Quiet", "verbose": "Detailed", }.get(verbosity) return f'-verbosity:{verbosity}' return "" class MSBuild: """ MSBuild build helper class """ def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile #: Defines the build type. By default, ``settings.build_type``. self.build_type = conanfile.settings.get_safe("build_type") # if platforms: # msvc_arch.update(platforms) arch = conanfile.settings.get_safe("arch") # MSVC default platform for VS projects is "x86", not "Win32" (but CMake default is "Win32") msvc_platform = msvc_platform_from_arch(arch) if arch != "x86" else "x86" if conanfile.settings.get_safe("os") == "WindowsCE": msvc_platform = conanfile.settings.get_safe("os.platform") #: Defines the platform name, e.g., ``ARM`` if ``settings.arch == "armv7"``. self.platform = msvc_platform def command(self, sln, targets=None): """ Gets the ``msbuild`` command line. For instance, :command:`msbuild.exe "MyProject.sln" -p:Configuration= -p:Platform=`. :param sln: ``str`` name of Visual Studio ``*.sln`` file :param targets: ``targets`` is an optional argument, defaults to ``None``, and otherwise it is a list of targets to build :return: ``str`` msbuild command line. """ # TODO: Enable output_binary_log via config cmd = ('msbuild.exe "%s" -p:Configuration="%s" -p:Platform="%s"' % (sln, self.build_type, self.platform)) verbosity = msbuild_verbosity_cmd_line_arg(self._conanfile) if verbosity: cmd += " {}".format(verbosity) maxcpucount = self._conanfile.conf.get("tools.microsoft.msbuild:max_cpu_count", check_type=int) if maxcpucount is not None: cmd += f' -m:"{maxcpucount}"' if maxcpucount > 0 else " -m" if targets: if not isinstance(targets, list): raise ConanException("targets argument should be a list") cmd += ' -target:"{}"'.format(";".join(targets)) return cmd def build(self, sln, targets=None): """ Runs the ``msbuild`` command line obtained from ``self.command(sln)``. :param sln: ``str`` name of Visual Studio ``*.sln`` file :param targets: ``targets`` is an optional argument, defaults to ``None``, and otherwise it is a list of targets to build """ cmd = self.command(sln, targets=targets) self._conanfile.run(cmd) @staticmethod def get_version(_): return NotImplementedError("get_version() method is not supported in MSBuild " "toolchain helper") ================================================ FILE: conan/tools/microsoft/msbuilddeps.py ================================================ import fnmatch import os import re import textwrap from xml.dom import minidom from jinja2 import Template from conan.internal import check_duplicated_generator from conan.errors import ConanException from conan.internal.api.install.generators import relativize_path from conan.internal.model.dependencies import get_transitive_requires from conan.tools.microsoft.visual import msvc_platform_from_arch from conan.internal.util.files import load, save VALID_LIB_EXTENSIONS = (".so", ".lib", ".a", ".dylib", ".bc") class MSBuildDeps: """ MSBuildDeps class generator conandeps.props: unconditional import of all *direct* dependencies only """ _vars_props = textwrap.dedent("""\ {{root_folder}} {{bin_dirs}} {% if host_context %} {{compiler_flags}} {{linker_flags}} {{definitions}} {{include_dirs}} {{res_dirs}} {{lib_dirs}} {{libs}} {{system_libs}} {% endif %} """) _conf_props = textwrap.dedent("""\ {% for dep in deps %} {% endfor %} {% if host_context %} $(Conan{{name}}BinaryDirectories);$(ConanDebugPath) PATH=$(ConanDebugPath);%PATH% WindowsLocalDebugger {% if ca_exclude %} $(Conan{{name}}IncludeDirectories);$(CAExcludePath) {% endif %} $(Conan{{name}}IncludeDirectories)%(AdditionalIncludeDirectories) $(Conan{{name}}PreprocessorDefinitions)%(PreprocessorDefinitions) $(Conan{{name}}CompilerFlags) %(AdditionalOptions) $(Conan{{name}}LibraryDirectories)%(AdditionalLibraryDirectories) $(Conan{{name}}Libraries)%(AdditionalDependencies) $(Conan{{name}}SystemLibs)%(AdditionalDependencies) $(Conan{{name}}LinkerFlags) %(AdditionalOptions) $(Conan{{name}}IncludeDirectories)%(AdditionalIncludeDirectories) $(Conan{{name}}IncludeDirectories)%(AdditionalIncludeDirectories) $(Conan{{name}}PreprocessorDefinitions)%(PreprocessorDefinitions) {% else %} $(Conan{{name}}BinaryDirectories)$(ExecutablePath) {% endif %} """) def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile #: Defines the build type. By default, the value of ``settings.build_type``. self.configuration = conanfile.settings.build_type #: Defines the configuration key used to conditionally select which property sheet to #: import (defaults to ``"Configuration"``). self.configuration_key = "Configuration" # TODO: This platform is not exactly the same as ``msbuild_arch``, because it differs # in x86=>Win32 #: Platform name, e.g., ``Win32`` if ``settings.arch == "x86"``. self.platform = msvc_platform_from_arch(str(conanfile.settings.arch)) #: Defines the platform key used to conditionally select which property sheet to #: import (defaults to ``"Platform"``). self.platform_key = "Platform" ca_exclude = "tools.microsoft.msbuilddeps:exclude_code_analysis" #: List of packages names patterns to add Visual Studio ``CAExcludePath`` property #: to each match as part of its ``conan_[DEP]_[CONFIG].props``. By default, value given by #: ``tools.microsoft.msbuilddeps:exclude_code_analysis`` configuration. self.exclude_code_analysis = self._conanfile.conf.get(ca_exclude, check_type=list) def generate(self): """ Generates ``conan___vars.props``, ``conan__.props``, and ``conan_.props`` files into the ``conanfile.generators_folder``. """ check_duplicated_generator(self, self._conanfile) if self.configuration is None: raise ConanException("MSBuildDeps.configuration is None, it should have a value") if self.platform is None: raise ConanException("MSBuildDeps.platform is None, it should have a value") generator_files = self._content() for generator_file, content in generator_files.items(): save(generator_file, content) def _config_filename(self): props = [self.configuration, self.platform] name = "".join("_%s" % v for v in props) return name.lower() def _condition(self): props = [(self.configuration_key, self.configuration), (self.platform_key, self.platform)] condition = " And ".join("'$(%s)' == '%s'" % (k, v) for k, v in props) return condition @staticmethod def _dep_name(dep, build): dep_name = dep.ref.name if build: # dep.context == CONTEXT_BUILD: dep_name += "_build" return MSBuildDeps._get_valid_xml_format(dep_name) @staticmethod def _get_valid_xml_format(name): return re.compile(r"[.+]").sub("_", name) def _vars_props_file(self, require, dep, name, cpp_info, build): """ content for conan_vars_poco_x86_release.props, containing the variables for 1 config This will be for 1 package or for one component of a package :return: varfile content """ def add_valid_ext(libname, libdirs=None): ext = os.path.splitext(libname)[1] if ext in VALID_LIB_EXTENSIONS: return f"{libname};" lib_name = f"{libname}.lib" if libdirs and not any(lib_name in os.listdir(d) for d in libdirs if os.path.isdir(d)): meson_name = f"lib{libname}.a" if any(meson_name in os.listdir(d) for d in libdirs if os.path.isdir(d)): lib_name = meson_name return f"{lib_name};" pkg_placeholder = "$(Conan{}RootFolder)".format(name) def escape_path(path): # https://docs.microsoft.com/en-us/visualstudio/msbuild/ # how-to-escape-special-characters-in-msbuild # https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-special-characters return path.lstrip("/") def join_paths(paths): # TODO: ALmost copied from CMakeDeps TargetDataContext ret = [] for p in paths: assert os.path.isabs(p), "{} is not absolute".format(p) full_path = escape_path(p) if full_path.startswith(root_folder): rel = full_path[len(root_folder)+1:] full_path = ("%s/%s" % (pkg_placeholder, rel)) ret.append(full_path) return "".join("{};".format(e) for e in ret) root_folder = dep.recipe_folder if dep.package_folder is None else dep.package_folder root_folder = escape_path(root_folder) # Make the root_folder relative to the generated conan_vars_xxx.props file relative_root_folder = relativize_path(root_folder, self._conanfile, "$(MSBuildThisFileDirectory)", normalize=False) bin_dirs = join_paths(cpp_info.bindirs) res_dirs = join_paths(cpp_info.resdirs) include_dirs = join_paths(cpp_info.includedirs) lib_dirs = join_paths(cpp_info.libdirs) libs = "".join([add_valid_ext(lib, cpp_info.libdirs) for lib in cpp_info.libs]) # TODO: Missing objects system_libs = "".join([add_valid_ext(sys_dep) for sys_dep in cpp_info.system_libs]) definitions = "".join("%s;" % d for d in cpp_info.defines) compiler_flags = " ".join(cpp_info.cxxflags + cpp_info.cflags) linker_flags = " ".join(cpp_info.sharedlinkflags + cpp_info.exelinkflags) # traits logic if require and not require.headers: include_dirs = "" if require and not require.libs: lib_dirs = "" libs = "" if require and not require.libs and not require.headers: definitions = "" compiler_flags = "" linker_flags = "" if require and not require.run: bin_dirs = "" fields = { 'name': name, 'root_folder': relative_root_folder, 'bin_dirs': bin_dirs, 'res_dirs': res_dirs, 'include_dirs': include_dirs, 'lib_dirs': lib_dirs, 'libs': libs, # TODO: Missing objects 'system_libs': system_libs, 'definitions': definitions, 'compiler_flags': compiler_flags, 'linker_flags': linker_flags, 'host_context': not build } formatted_template = Template(self._vars_props, trim_blocks=True, lstrip_blocks=True).render(**fields) return formatted_template def _activate_props_file(self, dep_name, vars_filename, deps, build): """ Actual activation of the VS variables, per configuration - conan_pkgname_x86_release.props / conan_pkgname_compname_x86_release.props :param dep_name: pkgname / pkgname_compname :param deps: the name of other things to be included: [dep1, dep2:compA, ...] :param build: if it is a build require or not """ # TODO: This must include somehow the user/channel, most likely pattern to exclude/include # Probably also the negation pattern, exclude all not @mycompany/* ca_exclude = any(fnmatch.fnmatch(dep_name, p) for p in self.exclude_code_analysis or ()) template = Template(self._conf_props, trim_blocks=True, lstrip_blocks=True) content_multi = template.render(host_context=not build, name=dep_name, ca_exclude=ca_exclude, vars_filename=vars_filename, deps=deps) return content_multi @staticmethod def _dep_props_file(dep_name, filename, aggregated_filename, condition, content=None): """ The file aggregating all configurations for a given pkg / component - conan_pkgname.props """ # Current directory is the generators_folder if content: content_multi = content # Useful for aggregating multiple components in one pass elif os.path.isfile(filename): content_multi = load(filename) else: content_multi = textwrap.dedent("""\ True """) content_multi = Template(content_multi).render({"name": dep_name}) # parse the multi_file and add new import statement if needed dom = minidom.parseString(content_multi) import_vars = dom.getElementsByTagName('ImportGroup')[0] # Current vars children = import_vars.getElementsByTagName("Import") for node in children: if aggregated_filename == node.getAttribute("Project") \ and condition == node.getAttribute("Condition"): break else: # create a new import statement import_node = dom.createElement('Import') import_node.setAttribute('Condition', condition) import_node.setAttribute('Project', aggregated_filename) import_vars.appendChild(import_node) content_multi = dom.toprettyxml() content_multi = "\n".join(line for line in content_multi.splitlines() if line.strip()) return content_multi def _conandeps(self): """ this is a .props file including direct declared dependencies """ # Current directory is the generators_folder conandeps_filename = "conandeps.props" direct_deps = self._conanfile.dependencies.filter({"direct": True}) pkg_aggregated_content = textwrap.dedent("""\ """) for req, dep in direct_deps.items(): dep_name = self._dep_name(dep, req.build) filename = "conan_%s.props" % dep_name comp_condition = "'$(conan_%s_props_imported)' != 'True'" % dep_name pkg_aggregated_content = self._dep_props_file("", conandeps_filename, filename, condition=comp_condition, content=pkg_aggregated_content) return {conandeps_filename: pkg_aggregated_content} def _package_props_files(self, require, dep, build=False): """ all the files for a given package: - conan_pkgname_vars_config.props: definition of variables, one per config - conan_pkgname_config.props: The one using those variables. This is very different for Host and build, build only activate - conan_pkgname.props: Conditional aggregate xxx_config.props based on active config """ conf_name = self._config_filename() condition = self._condition() dep_name = self._dep_name(dep, build) result = {} pkg_deps = get_transitive_requires(self._conanfile, dep) # only non-skipped dependencies if dep.cpp_info.has_components: pkg_aggregated_content = None for comp_name, comp_info in dep.cpp_info.components.items(): full_comp_name = "{}_{}".format(dep_name, self._get_valid_xml_format(comp_name)) vars_filename = "conan_%s_vars%s.props" % (full_comp_name, conf_name) activate_filename = "conan_%s%s.props" % (full_comp_name, conf_name) comp_filename = "conan_%s.props" % full_comp_name pkg_filename = "conan_%s.props" % dep_name public_deps = [] # To store the xml dependencies/file names for required_pkg, required_comp in comp_info.parsed_requires(): if required_pkg is not None: # Points to a component of a different package try: required = pkg_deps[required_pkg] except KeyError: # The transitive dep might have been skipped required = None if required: # The transitive dep might have been skipped required_name = self._dep_name(required, build) public_deps.append(required_name if required_pkg == required_comp else "{}_{}".format(required_name, required_comp)) else: # Points to a component of same package public_deps.append("{}_{}".format(dep_name, required_comp)) public_deps = [self._get_valid_xml_format(d) for d in public_deps] result[vars_filename] = self._vars_props_file(require, dep, full_comp_name, comp_info, build=build) result[activate_filename] = self._activate_props_file(full_comp_name, vars_filename, public_deps, build=build) result[comp_filename] = self._dep_props_file(full_comp_name, comp_filename, activate_filename, condition) comp_condition = "'$(conan_%s_props_imported)' != 'True'" % full_comp_name pkg_aggregated_content = self._dep_props_file(dep_name, pkg_filename, comp_filename, condition=comp_condition, content=pkg_aggregated_content) result[pkg_filename] = pkg_aggregated_content else: cpp_info = dep.cpp_info vars_filename = "conan_%s_vars%s.props" % (dep_name, conf_name) activate_filename = "conan_%s%s.props" % (dep_name, conf_name) pkg_filename = "conan_%s.props" % dep_name public_deps = [self._dep_name(d, build) for d in pkg_deps.values()] result[vars_filename] = self._vars_props_file(require, dep, dep_name, cpp_info, build=build) result[activate_filename] = self._activate_props_file(dep_name, vars_filename, public_deps, build=build) result[pkg_filename] = self._dep_props_file(dep_name, pkg_filename, activate_filename, condition=condition) return result def _content(self): if not self._conanfile.settings.get_safe("build_type"): raise ConanException("The 'msbuild' generator requires a 'build_type' setting value") result = {} for req, dep in self._conanfile.dependencies.host.items(): result.update(self._package_props_files(req, dep, build=False)) for req, dep in self._conanfile.dependencies.test.items(): result.update(self._package_props_files(req, dep, build=False)) for req, dep in self._conanfile.dependencies.build.items(): result.update(self._package_props_files(req, dep, build=True)) # Include all direct build_requires for host context. This might change result.update(self._conandeps()) return result ================================================ FILE: conan/tools/microsoft/nmakedeps.py ================================================ import os from conan.internal import check_duplicated_generator from conan.tools import CppInfo from conan.tools.env import Environment class NMakeDeps: def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile self._environment = None # TODO: This is similar from AutotoolsDeps: Refactor and make common def _get_cpp_info(self): ret = CppInfo(self._conanfile) deps = self._conanfile.dependencies.host.topological_sort deps = [dep for dep in reversed(deps.values())] for dep in deps: dep_cppinfo = dep.cpp_info.aggregated_components() # In case we have components, aggregate them, we do not support isolated # "targets" with autotools ret.merge(dep_cppinfo) return ret @property def environment(self): # TODO: Seems we want to make this uniform, equal to other generators if self._environment is None: cpp_info = self._get_cpp_info() lib_paths = ";".join(cpp_info.libdirs or []) def format_lib(lib): ext = os.path.splitext(lib)[1] return lib if ext in (".so", ".lib", ".a", ".dylib", ".bc") else '%s.lib' % lib ret = [] ret.extend(cpp_info.exelinkflags or []) ret.extend(cpp_info.sharedlinkflags or []) ret.extend([format_lib(lib) for lib in cpp_info.libs or []]) ret.extend([format_lib(lib) for lib in cpp_info.system_libs or []]) link_args = " ".join(ret) def format_define(define): if "=" in define: # CL env-var can't accept '=' sign in /D option, it can be replaced by '#' sign: # https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables macro, value = define.split("=", 1) if value and not value.isnumeric(): value = f'\"{value}\"' define = f"{macro}#{value}" return f"/D\"{define}\"" cl_flags = [f'-I"{p}"' for p in cpp_info.includedirs or []] cl_flags.extend(cpp_info.cflags or []) cl_flags.extend(cpp_info.cxxflags or []) cl_flags.extend([format_define(define) for define in cpp_info.defines or []]) env = Environment() env.append("CL", " ".join(cl_flags)) env.append_path("LIB", lib_paths) env.append("_LINK_", link_args) self._environment = env return self._environment def vars(self, scope="build"): return self.environment.vars(self._conanfile, scope=scope) def generate(self, scope="build"): check_duplicated_generator(self, self._conanfile) self.vars(scope).save_script("conannmakedeps") ================================================ FILE: conan/tools/microsoft/nmaketoolchain.py ================================================ from conan.internal import check_duplicated_generator from conan.tools.build.flags import build_type_flags, cppstd_flag, build_type_link_flags from conan.tools.env import Environment from conan.tools.microsoft.visual import msvc_runtime_flag, VCVars class NMakeToolchain: """ https://learn.microsoft.com/en-us/cpp/build/reference/running-nmake?view=msvc-170#toolsini-and-nmake We have also explored the usage of Tools.ini: https://learn.microsoft.com/en-us/cpp/build/reference/running-nmake?view=msvc-170 but not possible, because it cannot include other files, it will also potentially collide with a user Tool.ini, without easy resolution. At least the environment is additive. """ def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile # Flags self.extra_cflags = [] self.extra_cxxflags = [] self.extra_ldflags = [] self.extra_defines = [] @staticmethod def _format_options(options): return [f"{opt[0].replace('-', '/')}{opt[1:]}" for opt in options if len(opt) > 1] @staticmethod def _format_defines(defines): formated_defines = [] for define in defines: if "=" in define: # CL env-var can't accept '=' sign in /D option, it can be replaced by '#' sign: # https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables macro, value = define.split("=", 1) if value and not value.isnumeric(): value = f'\\"{value}\\"' define = f"{macro}#{value}" formated_defines.append(f"/D\"{define}\"") return formated_defines @property def _cl(self): bt_flags = build_type_flags(self._conanfile) bt_flags = bt_flags if bt_flags else [] rt_flags = msvc_runtime_flag(self._conanfile) rt_flags = [f"/{rt_flags}"] if rt_flags else [] cflags = [] cflags.extend(self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list)) cflags.extend(self.extra_cflags) cxxflags = [] cppstd = cppstd_flag(self._conanfile) if cppstd: cxxflags.append(cppstd) cxxflags.extend(self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list)) cxxflags.extend(self.extra_cxxflags) defines = [] build_type = self._conanfile.settings.get_safe("build_type") if build_type in ["Release", "RelWithDebInfo", "MinSizeRel"]: defines.append("NDEBUG") defines.extend(self._conanfile.conf.get("tools.build:defines", default=[], check_type=list)) defines.extend(self.extra_defines) return (["/nologo"] + self._format_options(bt_flags + rt_flags + cflags + cxxflags) + self._format_defines(defines)) @property def _link(self): bt_ldflags = build_type_link_flags(self._conanfile.settings) bt_ldflags = bt_ldflags if bt_ldflags else [] ldflags = [] ldflags.extend(bt_ldflags) ldflags.extend(self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list)) ldflags.extend(self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list)) ldflags.extend(self.extra_ldflags) return ["/nologo"] + self._format_options(ldflags) @property def _rcflags(self): rcflags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) return self._format_options(rcflags) if rcflags else [] def environment(self): env = Environment() # Injection of compile flags in CL env-var: # https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables env.append("CL", self._cl) # Injection of link flags in _LINK_ env-var: # https://learn.microsoft.com/en-us/cpp/build/reference/linking env.append("_LINK_", self._link) if self._rcflags: env.append("RCFLAGS", self._rcflags) # Also define some special env-vars which can override special NMake macros: # https://learn.microsoft.com/en-us/cpp/build/reference/special-nmake-macros conf_compilers = self._conanfile.conf.get("tools.build:compiler_executables", default={}, check_type=dict) if conf_compilers: compilers_mapping = { "AS": "asm", "CC": "c", "CPP": "cpp", "CXX": "cpp", "RC": "rc", } for env_var, comp in compilers_mapping.items(): if comp in conf_compilers: env.define(env_var, conf_compilers[comp]) return env def vars(self): return self.environment().vars(self._conanfile, scope="build") def generate(self, env=None, scope="build"): check_duplicated_generator(self, self._conanfile) env = env or self.environment() env.vars(self._conanfile, scope=scope).save_script("conannmaketoolchain") VCVars(self._conanfile).generate(scope=scope) ================================================ FILE: conan/tools/microsoft/subsystems.py ================================================ from conan.internal.subsystems import deduce_subsystem, subsystem_path def unix_path(conanfile, path, scope="build"): subsystem = deduce_subsystem(conanfile, scope=scope) return subsystem_path(subsystem, path) def unix_path_package_info_legacy(conanfile, path, path_flavor=None): message = "The use of 'unix_path_legacy_compat' is deprecated in Conan 2.0 and does not " \ "perform path conversions. This is retained for compatibility with Conan 1.x " \ "and will be removed in a future version." conanfile.output.warning(message, warn_tag="deprecated") return path ================================================ FILE: conan/tools/microsoft/toolchain.py ================================================ import os import textwrap from xml.dom import minidom from jinja2 import Template from conan.internal import check_duplicated_generator from conan.internal.api.detect.detect_vs import vs_installation_path from conan.tools.build import build_jobs from conan.tools.intel.intel_cc import IntelCC from conan.tools.microsoft.visual import VCVars, msvs_toolset, msvc_runtime_flag, \ msvc_platform_from_arch, vs_ide_version from conan.errors import ConanException from conan.internal.util.files import save, load class MSBuildToolchain: """ MSBuildToolchain class generator """ filename = "conantoolchain.props" _config_toolchain_props = textwrap.dedent("""\ {% if toolset_version_full_path %} {% endif %} {{ defines }}%(PreprocessorDefinitions) {{ compiler_flags }} %(AdditionalOptions) {{ runtime_library }} {% if cstd %}{{ cstd }}{% endif %} {{ cppstd }}{{ parallel }}{{ compile_options }} {{ linker_flags }} %(AdditionalOptions) {{ defines }}%(PreprocessorDefinitions) {% if rc_flags %}{{ rc_flags }} %(AdditionalOptions){% endif %} {% if winsdk_version %} {{ winsdk_version}} {% endif %} {{ toolset }} {% for k, v in properties.items() %} <{{k}}>{{ v }} {% endfor %} """) def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile #: Dict-like that defines the preprocessor definitions self.preprocessor_definitions = {} #: Dict with compile options that will be added as value in the ClCompile section self.compile_options = {} #: List of all the CXX flags self.cxxflags = [] #: List of all the C flags self.cflags = [] #: List of all the LD linker flags self.ldflags = [] #: List of all the RC (resource compiler) flags self.rcflags = [] #: The build type. By default, the ``conanfile.settings.build_type`` value self.configuration = conanfile.settings.build_type #: The runtime flag. By default, it'll be based on the `compiler.runtime` setting. self.runtime_library = self._runtime_library() #: cppstd value. By default, ``compiler.cppstd`` one. self.cppstd = conanfile.settings.get_safe("compiler.cppstd") self.cstd = conanfile.settings.get_safe("compiler.cstd") #: VS IDE Toolset, e.g., ``"v140"``. If ``compiler=msvc``, you can use ``compiler.toolset`` #: setting, else, it'll be based on ``msvc`` version. self.toolset = msvs_toolset(conanfile) self.properties = {} self.toolset_version_full_path = _get_toolset_props(conanfile) def _name_condition(self, settings): platform = msvc_platform_from_arch(settings.get_safe("arch")) props = [("Configuration", self.configuration), ("Platform", platform)] name = "".join("_%s" % v for _, v in props if v is not None) condition = " And ".join("'$(%s)' == '%s'" % (k, v) for k, v in props if v is not None) return name.lower(), condition def generate(self): """ Generates a ``conantoolchain.props``, a ``conantoolchain_.props``, and, if ``compiler=msvc``, a ``conanvcvars.bat`` files. In the first two cases, they'll have the valid XML format with all the good settings like any other VS project ``*.props`` file. The last one emulates the ``vcvarsall.bat`` env script. See also :class:`VCVars`. """ check_duplicated_generator(self, self._conanfile) name, condition = self._name_condition(self._conanfile.settings) config_filename = "conantoolchain{}.props".format(name) # Writing the props files self._write_config_toolchain(config_filename) self._write_main_toolchain(config_filename, condition) if self._conanfile.settings.get_safe("compiler") == "intel-cc": IntelCC(self._conanfile).generate() else: VCVars(self._conanfile).generate() def _runtime_library(self): return { "MT": "MultiThreaded", "MTd": "MultiThreadedDebug", "MD": "MultiThreadedDLL", "MDd": "MultiThreadedDebugDLL", }.get(msvc_runtime_flag(self._conanfile), "") @property def context_config_toolchain(self): def format_macro(key, value): return '%s=%s' % (key, value) if value is not None else key cxxflags, cflags, defines, sharedlinkflags, exelinkflags, rcflags = self._get_extra_flags() preprocessor_definitions = "".join(["%s;" % format_macro(k, v) for k, v in self.preprocessor_definitions.items()]) defines = preprocessor_definitions + "".join("%s;" % d for d in defines) self.cxxflags.extend(cxxflags) self.cflags.extend(cflags) self.ldflags.extend(sharedlinkflags + exelinkflags) self.rcflags.extend(rcflags) cppstd = "stdcpp%s" % self.cppstd if self.cppstd else "" cstd = f"stdc{self.cstd}" if self.cstd else "" runtime_library = self.runtime_library toolset = self.toolset or "" conf_options = self._conanfile.conf.get("tools.microsoft.msbuildtoolchain:compile_options", default={}, check_type=dict) self.compile_options.update(conf_options) parallel = "" njobs = build_jobs(self._conanfile) if njobs: parallel = "".join( ["\n True", "\n {}".format(njobs)]) compile_options = "".join("\n <{k}>{v}".format(k=k, v=v) for k, v in self.compile_options.items()) winsdk_version = self._conanfile.conf.get("tools.microsoft:winsdk_version", check_type=str) winsdk_version = winsdk_version or self._conanfile.settings.get_safe("os.version") return { 'defines': defines, 'compiler_flags': " ".join(self.cxxflags + self.cflags), 'linker_flags': " ".join(self.ldflags), 'rc_flags': " ".join(self.rcflags), "cppstd": cppstd, "cstd": cstd, "runtime_library": runtime_library, "toolset": toolset, "compile_options": compile_options, "parallel": parallel, "properties": self.properties, "winsdk_version": winsdk_version, "toolset_version_full_path": self.toolset_version_full_path } def _write_config_toolchain(self, config_filename): config_filepath = os.path.join(self._conanfile.generators_folder, config_filename) config_props = Template(self._config_toolchain_props, trim_blocks=True, lstrip_blocks=True).render(**self.context_config_toolchain) self._conanfile.output.info("MSBuildToolchain created %s" % config_filename) save(config_filepath, config_props) def _write_main_toolchain(self, config_filename, condition): main_toolchain_path = os.path.join(self._conanfile.generators_folder, self.filename) if os.path.isfile(main_toolchain_path): content = load(main_toolchain_path) else: content = textwrap.dedent("""\ {} {} """) conan_package_name = self._conanfile.name if self._conanfile.name else "" conan_package_version = self._conanfile.version if self._conanfile.version else "" content = content.format(conan_package_name, conan_package_version) dom = minidom.parseString(content) try: import_group = dom.getElementsByTagName('ImportGroup')[0] except Exception: raise ConanException("Broken {}. Remove the file and try again".format(self.filename)) children = import_group.getElementsByTagName("Import") for node in children: if (config_filename == node.getAttribute("Project") and condition == node.getAttribute("Condition")): break # the import statement already exists else: # create a new import statement import_node = dom.createElement('Import') import_node.setAttribute('Condition', condition) import_node.setAttribute('Project', config_filename) import_group.appendChild(import_node) conan_toolchain = dom.toprettyxml() conan_toolchain = "\n".join(line for line in conan_toolchain.splitlines() if line.strip()) self._conanfile.output.info("MSBuildToolchain writing {}".format(self.filename)) save(main_toolchain_path, conan_toolchain) def _get_extra_flags(self): # Now, it's time to get all the flags defined by the user cxxflags = self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) cflags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list) sharedlinkflags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) exelinkflags = self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list) rcflags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) defines = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list) return cxxflags, cflags, defines, sharedlinkflags, exelinkflags, rcflags def _get_toolset_props(conanfile): msvc_update = conanfile.conf.get("tools.microsoft:msvc_update") compiler_update = msvc_update or conanfile.settings.get_safe("compiler.update") if compiler_update is None: return vs_version = vs_ide_version(conanfile) if int(vs_version) <= 14: return vs_install_path = conanfile.conf.get("tools.microsoft.msbuild:installation_path") vs_path = vs_install_path or vs_installation_path(vs_version) if not vs_path or not os.path.isdir(vs_path): return basebuild = os.path.normpath(os.path.join(vs_path, "VC/Auxiliary/Build")) # The equivalent of compiler 19.26 is toolset 14.26 compiler_version = str(conanfile.settings.compiler.version) vcvars_ver = "14.{}{}".format(compiler_version[-1], compiler_update) for folder in os.listdir(basebuild): if not os.path.isdir(os.path.join(basebuild, folder)): continue if folder.startswith(vcvars_ver): result = folder return os.path.join(basebuild, result, f"Microsoft.VCToolsVersion.{result}.props") ================================================ FILE: conan/tools/microsoft/visual.py ================================================ import os import textwrap from conan.api.output import ConanOutput from conan.internal import check_duplicated_generator from conan.internal.api.detect.detect_vs import vs_installation_path from conan.errors import ConanException, ConanInvalidConfiguration from conan.tools.scm import Version from conan.tools.intel.intel_cc import IntelCC from conan.internal.util.files import save CONAN_VCVARS = "conanvcvars" def msvc_platform_from_arch(arch): return {"x86": "Win32", "x86_64": "x64", "armv7": "ARM", "armv8": "ARM64", "arm64ec": "ARM64EC"}.get(arch) def check_min_vs(conanfile, version, raise_invalid=True): """ This is a helper method to allow the migration of 1.X -> 2.0 and VisualStudio -> msvc settings without breaking recipes. The legacy "Visual Studio" with different toolset is not managed, not worth the complexity. :param raise_invalid: ``bool`` Whether to raise or return False if the version check fails :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. :param version: ``str`` Visual Studio or msvc version number. """ compiler = conanfile.settings.get_safe("compiler") compiler_version = None if compiler == "Visual Studio": compiler_version = conanfile.settings.get_safe("compiler.version") compiler_version = {"17": "193", "16": "192", "15": "191", "14": "190", "12": "180", "11": "170"}.get(compiler_version) elif compiler == "msvc": compiler_version = conanfile.settings.get_safe("compiler.version") msvc_update = conanfile.conf.get("tools.microsoft:msvc_update") compiler_update = msvc_update or conanfile.settings.get_safe("compiler.update") if compiler_version and compiler_update is not None: compiler_version += ".{}".format(compiler_update) if compiler_version and Version(compiler_version) < version: if raise_invalid: msg = f"This package doesn't work with VS compiler version '{compiler_version}'" \ f", it requires at least '{version}'" raise ConanInvalidConfiguration(msg) else: return False return True def msvc_version_to_vs_ide_version(version): """ Gets the Visual Studio IDE version given the ``msvc`` compiler one. :param version: ``str`` or ``int`` msvc version :return: VS IDE version """ _visuals = {'170': '11', '180': '12', '190': '14', '191': '15', '192': '16', '193': '17', '194': '17', # Note both 193 and 194 belong to VS 17 2022 '195': '18'} return _visuals[str(version)] def msvc_version_to_toolset_version(version): """ Gets the Visual Studio IDE toolset version given the ``msvc`` compiler one. :param version: ``str`` or ``int`` msvc version :return: VS IDE toolset version """ toolsets = {'170': 'v110', '180': 'v120', '190': 'v140', '191': 'v141', '192': 'v142', "193": 'v143', "194": 'v143', "195": 'v145'} return toolsets.get(str(version)) class VCVars: """ VCVars class generator to generate a ``conanvcvars.bat`` script that activates the correct Visual Studio prompt. This generator will be automatically called by other generators such as ``CMakeToolchain`` when considered necessary, for example if building with Visual Studio compiler using the CMake ``Ninja`` generator, which needs an active Visual Studio prompt. Then, it is not necessary to explicitly instantiate this generator in most cases. """ def __init__(self, conanfile): """ :param conanfile: ``ConanFile object`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile def generate(self, scope="build"): """ Creates a ``conanvcvars.bat`` file that calls Visual ``vcvars`` with the necessary args to activate the correct Visual Studio prompt matching the Conan settings. :param scope: ``str`` activation scope, by default "build". It means it will add a call to this ``conanvcvars.bat`` from the aggregating general ``conanbuild.bat``, which is the script that will be called by default in ``self.run()`` calls and build helpers such as ``cmake.configure()`` and ``cmake.build()``. """ check_duplicated_generator(self, self._conanfile) conanfile = self._conanfile os_ = conanfile.settings.get_safe("os") build_os_ = conanfile.settings_build.get_safe("os") if (os_ != "Windows" and os_ != "WindowsStore") or build_os_ != "Windows": return compiler = conanfile.settings.get_safe("compiler") if compiler not in ("msvc", "clang"): return vs_install_path = conanfile.conf.get("tools.microsoft.msbuild:installation_path") if vs_install_path == "": # Empty string means "disable" return vs_version, vcvars_ver = _vcvars_versions(conanfile) if vs_version is None: return vcvarsarch = _vcvars_arch(conanfile) winsdk_version = conanfile.conf.get("tools.microsoft:winsdk_version", check_type=str) winsdk_version = winsdk_version or conanfile.settings.get_safe("os.version") # The vs_install_path is like # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community # C:\Program Files (x86)\Microsoft Visual Studio\2017\Community # C:\Program Files (x86)\Microsoft Visual Studio 14.0 vcvars = vcvars_command(vs_version, architecture=vcvarsarch, platform_type=None, winsdk_version=winsdk_version, vcvars_ver=vcvars_ver, vs_install_path=vs_install_path) content = textwrap.dedent(f"""\ @echo off set __VSCMD_ARG_NO_LOGO=1 set VSCMD_SKIP_SENDTELEMETRY=1 echo conanvcvars.bat: Activating environment Visual Studio {vs_version} - {vcvarsarch} - winsdk_version={winsdk_version} - vcvars_ver={vcvars_ver} {vcvars} """) from conan.tools.env.environment import create_env_script conan_vcvars_bat = f"{CONAN_VCVARS}.bat" create_env_script(conanfile, content, conan_vcvars_bat, scope) _create_deactivate_vcvars_file(conanfile, conan_vcvars_bat) try: is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=bool) if is_ps1 is not None: ConanOutput().warning( "Boolean values for 'tools.env.virtualenv:powershell' are deprecated. " "Please specify 'powershell.exe' or 'pwsh' instead, appending arguments " "if needed (for example: 'powershell.exe -argument'). " "To unset this configuration, use `tools.env.virtualenv:powershell=!`, " "which matches the previous 'False' behavior.", warn_tag="deprecated" ) except ConanException: is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=str) if is_ps1: content_ps1 = textwrap.dedent(rf""" if (-not $env:VSCMD_ARG_VCVARS_VER){{ Push-Location "$PSScriptRoot" cmd /c "conanvcvars.bat&set" | foreach {{ if ($_ -match "=") {{ $v = $_.split("=", 2); set-item -force -path "ENV:\$($v[0])" -value "$($v[1])" }} }} Pop-Location write-host conanvcvars.ps1: Activated environment}} """).strip() conan_vcvars_ps1 = f"{CONAN_VCVARS}.ps1" create_env_script(conanfile, content_ps1, conan_vcvars_ps1, scope) _create_deactivate_vcvars_file(conanfile, conan_vcvars_ps1) def _create_deactivate_vcvars_file(conanfile, filename): if conanfile.conf.get("tools.env:deactivation_mode") == "function": return deactivate_filename = f"deactivate_{filename}" message = f"[{deactivate_filename}]: *** vcvars env cannot be deactivated ***\n" is_ps1 = filename.endswith(".ps1") if is_ps1: content = f"Write-Host {message}" else: content = f"echo {message}" path = os.path.join(conanfile.generators_folder, deactivate_filename) save(path, content) def vs_ide_version(conanfile): """ Gets the VS IDE version as string. It'll use the ``compiler.version`` (if exists) and/or the ``tools.microsoft.msbuild:vs_version`` if ``compiler`` is ``msvc``. :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. :return: ``str`` Visual IDE version number. """ compiler = conanfile.settings.get_safe("compiler") compiler_version = conanfile.settings.get_safe("compiler.version") if compiler == "msvc": toolset_override = conanfile.conf.get("tools.microsoft.msbuild:vs_version", check_type=str) if toolset_override: visual_version = toolset_override else: visual_version = msvc_version_to_vs_ide_version(compiler_version) else: visual_version = compiler_version return visual_version def msvc_runtime_flag(conanfile): """ Gets the MSVC runtime flag given the ``compiler.runtime`` value from the settings. :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. :return: ``str`` runtime flag. """ settings = conanfile.settings runtime = settings.get_safe("compiler.runtime") if runtime is not None: if runtime == "static": runtime = "MT" elif runtime == "dynamic": runtime = "MD" else: raise ConanException("compiler.runtime should be 'static' or 'dynamic'") runtime_type = settings.get_safe("compiler.runtime_type") if runtime_type == "Debug": runtime = "{}d".format(runtime) return runtime return "" def vcvars_command(version, architecture=None, platform_type=None, winsdk_version=None, vcvars_ver=None, start_dir_cd=True, vs_install_path=None): """ Conan-agnostic construction of vcvars command https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line :param version: ``str`` Visual Studio version. :param architecture: ``str`` Specifies the host and target architecture to use. :param platform_type: ``str`` Allows you to specify ``store`` or ``uwp`` as the platform type. :param winsdk_version: ``str`` Specifies the version of the Windows SDK to use. :param vcvars_ver: ``str`` Specifies the Visual Studio compiler toolset to use. :param start_dir_cd: ``bool`` If ``True``, the command will execute ``set "VSCMD_START_DIR=%CD%`` at first. :param vs_install_path: ``str`` Visual Studio installation path. :return: ``str`` complete _vcvarsall_ command. """ cmd = [] if start_dir_cd: cmd.append('set "VSCMD_START_DIR=%CD%" &&') # The "call" is useful in case it is called from another .bat script cmd.append('call "%s" ' % _vcvars_path(version, vs_install_path)) if architecture: cmd.append(architecture) if platform_type: cmd.append(platform_type) if winsdk_version: cmd.append(winsdk_version) if vcvars_ver: cmd.append("-vcvars_ver=%s" % vcvars_ver) return " ".join(cmd) def _vcvars_path(version, vs_install_path): # TODO: This comes from conans/client/tools/win.py vcvars_command() vs_path = vs_install_path or vs_installation_path(version) if not vs_path or not os.path.isdir(vs_path): raise ConanException(f"VS non-existing installation: Visual Studio {version}. " "If using a non-default toolset from a VS IDE version consider " "specifying it with the 'tools.microsoft.msbuild:vs_version' conf") if int(version) > 14: vcpath = os.path.join(vs_path, "VC/Auxiliary/Build/vcvarsall.bat") else: vcpath = os.path.join(vs_path, "VC/vcvarsall.bat") vcpath = os.path.normpath(vcpath) return vcpath def _vcvars_versions(conanfile): compiler = conanfile.settings.get_safe("compiler") msvc_update = conanfile.conf.get("tools.microsoft:msvc_update") if compiler == "clang": # The vcvars only needed for LLVM/Clang and VS ClangCL, who define runtime if not conanfile.settings.get_safe("compiler.runtime"): # NMake Makefiles will need vcvars activated, for VS target, defined with runtime return None, None toolset_version = conanfile.settings.get_safe("compiler.runtime_version") vs_version = {"v140": "14", "v141": "15", "v142": "16", "v143": "17", "v144": "17", "v145": "18"}.get(toolset_version) if vs_version is None: raise ConanException("Visual Studio Runtime version (v140-v145) not defined. Please, " "add the compiler.runtime_version=[v140-v145] setting to your " "profile.") vcvars_ver = {"v140": "14.0", "v141": "14.1", "v142": "14.2", "v143": "14.3", "v144": "14.4", "v145": "14.5"}.get(toolset_version) if vcvars_ver and msvc_update is not None: vcvars_ver += f"{msvc_update}" else: vs_version = vs_ide_version(conanfile) if int(vs_version) <= 14: vcvars_ver = None else: compiler_version = str(conanfile.settings.compiler.version) compiler_update = msvc_update or conanfile.settings.get_safe("compiler.update", "") # The equivalent of compiler 19.26 is toolset 14.26 vcvars_ver = "14.{}{}".format(compiler_version[-1], compiler_update) return vs_version, vcvars_ver def _vcvars_arch(conanfile): """ Computes the vcvars command line architecture based on conanfile settings (host) and settings_build. """ settings_host = conanfile.settings settings_build = conanfile.settings_build arch_host = str(settings_host.arch) arch_build = str(settings_build.arch) arch = None if arch_build == 'x86_64': arch = {'x86': "amd64_x86", 'x86_64': 'amd64', 'armv7': 'amd64_arm', 'armv8': 'amd64_arm64', 'arm64ec': 'amd64_arm64'}.get(arch_host) elif arch_build == 'x86': arch = {'x86': 'x86', 'x86_64': 'x86_amd64', 'armv7': 'x86_arm', 'armv8': 'x86_arm64'}.get(arch_host) elif arch_build == 'armv8': arch = {'x86': 'arm64_x86', 'x86_64': 'arm64_x64', 'armv7': 'arm64_arm', 'armv8': 'arm64'}.get(arch_host) if not arch: raise ConanException('vcvars unsupported architectures %s-%s' % (arch_build, arch_host)) return arch def is_msvc(conanfile, build_context=False): """ Validates if the current compiler is ``msvc``. :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. :param build_context: If True, will use the settings from the build context, not host ones :return: ``bool`` True, if the host compiler is ``msvc``, otherwise, False. """ if not build_context: settings = conanfile.settings else: settings = conanfile.settings_build return settings.get_safe("compiler") == "msvc" def is_msvc_static_runtime(conanfile): """ Validates when building with Visual Studio or msvc and MT on runtime. :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. :return: ``bool`` True, if ``msvc + runtime MT``. Otherwise, False. """ return is_msvc(conanfile) and "MT" in msvc_runtime_flag(conanfile) def msvs_toolset(conanfile): """ Returns the corresponding platform toolset based on the compiler setting. In case no toolset is configured in the profile, it will return a toolset based on the compiler version, otherwise, it will return the toolset from the profile. When there is no compiler version neither toolset configured, it will return None It supports msvc, intel-cc and clang compilers. For clang, is assumes the ClangCl toolset, as provided by the Visual Studio installer. :param conanfile: Conanfile instance to access settings.compiler :return: A toolset when compiler.version is valid or compiler.toolset is configured. Otherwise, None. """ settings = conanfile.settings compiler = settings.get_safe("compiler") compiler_version = settings.get_safe("compiler.version") if compiler == "msvc": subs_toolset = settings.get_safe("compiler.toolset") if subs_toolset: return subs_toolset return msvc_version_to_toolset_version(compiler_version) if compiler == "clang": return "ClangCl" if compiler == "intel-cc": return IntelCC(conanfile).ms_toolset ================================================ FILE: conan/tools/premake/__init__.py ================================================ from conan.tools.premake.premake import Premake from conan.tools.premake.premakedeps import PremakeDeps from conan.tools.premake.toolchain import PremakeToolchain ================================================ FILE: conan/tools/premake/constants.py ================================================ # Source: https://premake.github.io/docs/architecture/ CONAN_TO_PREMAKE_ARCH = { "x86": "x86", "x86_64": "x86_64", "armv4": "arm", "armv4i": "arm", "armv5el": "arm", "armv5hf": "arm", "armv6": "arm", "armv7": "arm", "armv7hf": "arm", "armv7s": "arm", "armv7k": "arm", "armv8": "arm64", "armv8_32": "arm64", "armv8.3": "arm64", "arm64ec": "arm64", "e2k-v2": "e2k", "e2k-v3": "e2k", "e2k-v4": "e2k", "e2k-v5": "e2k", "e2k-v6": "e2k", "e2k-v7": "e2k", "riscv64": "riscv64", "wasm": "wasm32", "wasm64": "wasm64", "asm.js": "wasm32", } ================================================ FILE: conan/tools/premake/premake.py ================================================ import textwrap from pathlib import Path from conan.tools.build.cpu import build_jobs from jinja2 import Template from conan.errors import ConanException from conan.tools.files import save from conan.tools.microsoft.msbuild import MSBuild from conan.tools.premake.toolchain import PremakeToolchain from conan.tools.premake.constants import CONAN_TO_PREMAKE_ARCH # Source: https://learn.microsoft.com/en-us/cpp/overview/compiler-versions?view=msvc-170 PREMAKE_VS_VERSION = { '190': '2015', '191': '2017', '192': '2019', '193': '2022', '194': '2022', # still 2022 '195': '2026' } class Premake: """ This class calls Premake commands when a package is being built. Notice that this one should be used together with the ``PremakeToolchain`` generator. This premake generator is only compatible with ``premake5``. """ filename = "conanfile.premake5.lua" # Conan premake file which will preconfigure toolchain and then will call the user's premake file _premake_file_template = textwrap.dedent( """\ #!lua include("{{luafile}}") include("{{premake_conan_toolchain}}") """ ) def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile #: Path to the root premake5 lua file (default is ``premake5.lua``) self.luafile = (Path(self._conanfile.source_folder) / "premake5.lua").as_posix() #: Key value pairs. Will translate to "--{key}={value}" self.arguments = {} # https://premake.github.io/docs/Command-Line-Arguments/ if "msvc" in self._conanfile.settings.compiler: msvc_version = PREMAKE_VS_VERSION.get(str(self._conanfile.settings.compiler.version)) self.action = f'vs{msvc_version}' else: self.action = "gmake" # New generator (old gmakelegacy is deprecated) self._premake_conan_toolchain = Path(self._conanfile.generators_folder) / PremakeToolchain.filename @staticmethod def _expand_args(args): return ' '.join([f'--{key}={value}' for key, value in args.items()]) def configure(self): """ Runs ``premake5 [FILE]`` which will generate respective build scripts depending on the ``action``. """ if self._premake_conan_toolchain.exists(): content = Template(self._premake_file_template).render( premake_conan_toolchain=self._premake_conan_toolchain.as_posix(), luafile=self.luafile ) conan_luafile = Path(self._conanfile.build_folder) / self.filename save(self._conanfile, conan_luafile, content) arch = str(self._conanfile.settings.arch) if arch not in CONAN_TO_PREMAKE_ARCH: raise ConanException(f"Premake does not support {arch} architecture.") self.arguments["arch"] = CONAN_TO_PREMAKE_ARCH[arch] else: # Old behavior, for backward compatibility conan_luafile = self.luafile premake_options = dict() premake_options["file"] = f'"{conan_luafile}"' premake_command = ( f"premake5 {self._expand_args(premake_options)} {self.action} " f"{self._expand_args(self.arguments)}{self._premake_verbosity}" ) self._conanfile.run(premake_command) @property def _premake_verbosity(self): verbosity = self._conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) return " --verbose" if verbosity == "verbose" else "" @property def _compilation_verbosity(self): verbosity = self._conanfile.conf.get("tools.compilation:verbosity", choices=("quiet", "verbose")) # --verbose does not print compilation commands but internal Makefile progress logic return " verbose=1" if verbosity == "verbose" else "" def build(self, workspace, targets=None, configuration=None, msbuild_platform=None): """ Depending on the action, this method will run either ``msbuild`` or ``make`` with ``N_JOBS``. You can specify ``N_JOBS`` through the configuration line ``tools.build:jobs=N_JOBS`` in your profile ``[conf]`` section. :param workspace: ``str`` Specifies the solution to be compiled (only used by ``MSBuild``). :param targets: ``List[str]`` Declare the projects to be built (None to build all projects). :param configuration: ``str`` Specify the configuration build type, default to build_type ("Release" or "Debug"), but this allow setting custom configuration type. :param msbuild_platform: ``str`` Specify the platform for the internal MSBuild generator (only used by ``MSBuild``). """ if not self._premake_conan_toolchain.exists(): raise ConanException("Premake.build() method requires PremakeToolchain to work properly") build_type = configuration or str(self._conanfile.settings.build_type) if self.action.startswith("vs"): msbuild = MSBuild(self._conanfile) if msbuild_platform: msbuild.platform = msbuild_platform msbuild.build_type = build_type msbuild.build(sln=f"{workspace}.sln", targets=targets) else: targets = "all" if targets is None else " ".join(targets) njobs = build_jobs(self._conanfile) self._conanfile.run(f"make config={build_type.lower()} {targets} -j{njobs}{self._compilation_verbosity}") ================================================ FILE: conan/tools/premake/premakedeps.py ================================================ import itertools import glob import re from conan.internal import check_duplicated_generator from conan.internal.util.files import save from conan.tools.premake.constants import CONAN_TO_PREMAKE_ARCH # Filename format strings PREMAKE_VAR_FILE = "conan_{pkgname}_vars_{config}.premake5.lua" PREMAKE_PKG_FILE = "conan_{pkgname}.premake5.lua" PREMAKE_ROOT_FILE = "conandeps.premake5.lua" PREMAKE_CONFIG_FILE = "conanconfig_{config}.premake5.lua" PREMAKE_CONFIG_ROOT_FILE = "conanconfig.premake5.lua" # File template format strings PREMAKE_TEMPLATE_CONFIG = """ include "conanutils.premake5.lua" t_conan_deps_order = {{}} t_conan_deps_order["{config}"] = {{{order}}} if conan_deps_order == nil then conan_deps_order = {{}} end conan_premake_tmerge(conan_deps_order, t_conan_deps_order) """ PREMAKE_TEMPLATE_UTILS = """ function conan_premake_tmerge(dst, src) for k, v in pairs(src) do if type(v) == "table" then if type(dst[k] or 0) == "table" then conan_premake_tmerge(dst[k] or {}, src[k] or {}) else dst[k] = v end else dst[k] = v end end return dst end """ PREMAKE_TEMPLATE_VAR = """ include "conanutils.premake5.lua" t_conandeps = {{}} t_conandeps["{config}"] = {{}} t_conandeps["{config}"]["{pkgname}"] = {{}} t_conandeps["{config}"]["{pkgname}"]["includedirs"] = {{{deps.includedirs}}} t_conandeps["{config}"]["{pkgname}"]["libdirs"] = {{{deps.libdirs}}} t_conandeps["{config}"]["{pkgname}"]["bindirs"] = {{{deps.bindirs}}} t_conandeps["{config}"]["{pkgname}"]["libs"] = {{{deps.libs}}} t_conandeps["{config}"]["{pkgname}"]["system_libs"] = {{{deps.system_libs}}} t_conandeps["{config}"]["{pkgname}"]["defines"] = {{{deps.defines}}} t_conandeps["{config}"]["{pkgname}"]["cxxflags"] = {{{deps.cxxflags}}} t_conandeps["{config}"]["{pkgname}"]["cflags"] = {{{deps.cflags}}} t_conandeps["{config}"]["{pkgname}"]["sharedlinkflags"] = {{{deps.sharedlinkflags}}} t_conandeps["{config}"]["{pkgname}"]["exelinkflags"] = {{{deps.exelinkflags}}} t_conandeps["{config}"]["{pkgname}"]["frameworks"] = {{{deps.frameworks}}} if conandeps == nil then conandeps = {{}} end conan_premake_tmerge(conandeps, t_conandeps) """ PREMAKE_TEMPLATE_ROOT_BUILD = """ includedirs(conandeps[conf][pkg]["includedirs"]) bindirs(conandeps[conf][pkg]["bindirs"]) defines(conandeps[conf][pkg]["defines"]) """ PREMAKE_TEMPLATE_ROOT_LINK = """ libdirs(conandeps[conf][pkg]["libdirs"]) links(conandeps[conf][pkg]["libs"]) links(conandeps[conf][pkg]["system_libs"]) links(conandeps[conf][pkg]["frameworks"]) """ PREMAKE_TEMPLATE_ROOT_FUNCTION = """ function {function_name}(conf, pkg) if conf == nil then {filter_call} elseif pkg == nil then local order = conan_deps_order[conf] for index, lib in ipairs(order) do {function_name}(conf, lib) end else {lua_content} end end """ PREMAKE_TEMPLATE_ROOT_GLOBAL = """ function conan_setup(conf, pkg) conan_setup_build(conf, pkg) conan_setup_link(conf, pkg) end """ # Helper class that expands cpp_info meta information in lua readable string sequences class _PremakeTemplate: def __init__(self, req, dep_cpp_info): def _format_paths(paths): if not paths: return "" return ",\n".join(f'"{p}"'.replace("\\", "/") for p in paths) def _format_flags(flags): if not flags: return "" return ", ".join('"%s"' % p.replace('"', '\\"') for p in flags) # Headers dependant with_headers = req and req.headers self.includedirs = _format_paths(dep_cpp_info.includedirs if with_headers else []) self.defines = _format_flags(dep_cpp_info.defines if with_headers else []) self.cxxflags = _format_flags(dep_cpp_info.cxxflags if with_headers else []) self.cflags = _format_flags(dep_cpp_info.cflags if with_headers else []) self.sharedlinkflags = _format_flags(dep_cpp_info.sharedlinkflags if with_headers else []) self.exelinkflags = _format_flags(dep_cpp_info.exelinkflags if with_headers else []) # Libs dependant with_libs = req and req.libs self.libs = _format_flags(dep_cpp_info.libs if with_libs else []) self.libdirs = _format_paths(dep_cpp_info.libdirs if with_libs else []) # Run dependant with_run = req and req.run self.bindirs = _format_paths(dep_cpp_info.bindirs if with_run else []) self.system_libs = _format_flags(dep_cpp_info.system_libs) self.frameworks = ", ".join('"%s.framework"' % p.replace('"', '\\"') for p in dep_cpp_info.frameworks) if dep_cpp_info.frameworks else "" self.sysroot = f"{dep_cpp_info.sysroot}".replace("\\", "/") \ if dep_cpp_info.sysroot else "" class PremakeDeps: """ PremakeDeps class generator conandeps.premake5.lua: unconditional import of all *direct* dependencies only """ def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile # Tab configuration self.tab = " " # Return value buffer self.output_files = {} # Extract configuration and architecture form conanfile self.configuration = conanfile.settings.build_type self.architecture = conanfile.settings.arch def generate(self): """ Generates ``conan__vars_.premake5.lua``, ``conan__.premake5.lua``, and ``conan_.premake5.lua`` files into the ``conanfile.generators_folder``. """ check_duplicated_generator(self, self._conanfile) # Current directory is the generators_folder generator_files = self.content for generator_file, content in generator_files.items(): save(generator_file, content) def _config_suffix(self): return f"{self.configuration}_{CONAN_TO_PREMAKE_ARCH[str(self.architecture)]}".lower() def _output_lua_file(self, filename, content): self.output_files[filename] = "\n".join(["#!lua", *content]) def _indent_string(self, string, indent=1): return "\n".join([ f"{self.tab * indent}{line}" for line in list(filter(None, string.splitlines())) ]) def _premake_filtered(self, content, configuration, architecture, indent=0): """ - Surrounds the lua line(s) contained within ``content`` with a premake "filter" and returns the result. - A "filter" will affect all premake function calls after it's set. It's used to limit following project setup function call(s) to a certain scope. Here it is used to limit the calls in content to only apply if the premake ``configuration`` and ``architecture`` matches the parameters in this function call. """ lines = list(itertools.chain.from_iterable([cnt.splitlines() for cnt in content])) return [ # Set new filter f'{self.tab * indent}filter {{ "configurations:{configuration}", "architecture:{architecture}" }}', # Emit content *[f"{self.tab * indent}{self.tab}{line.strip()}" for line in list(filter(None, lines))], # Clear active filter f"{self.tab * indent}filter {{}}", ] @property def content(self): check_duplicated_generator(self, self._conanfile) self.output_files = {} conf_name = self._config_suffix() # Global utility file self._output_lua_file("conanutils.premake5.lua", [PREMAKE_TEMPLATE_UTILS]) # Extract all dependencies in topological order: some linkers like ld or gold prunes the # functions which are not being used in the lookup table. If the less dependant libraries are # passed first, the linker will not be able to resolve the symbols in the dependent libraries # as they will have been removed host_req = self._conanfile.dependencies.host.topological_sort test_req = self._conanfile.dependencies.test.topological_sort build_req = self._conanfile.dependencies.direct_build.topological_sort # Merge into one list full_req = list(host_req.items()) + list(test_req.items()) + list(build_req.items()) # Process dependencies and accumulate globally required data pkg_files = [] dep_names = [] config_sets = [] for require, dep in full_req: dep_name = require.ref.name dep_names.append(dep_name) # Convert and aggregate dependency's dep_aggregate = dep.cpp_info.aggregated_components() # Generate config dependent package variable and setup premake file var_filename = PREMAKE_VAR_FILE.format(pkgname=dep_name, config=conf_name) self._output_lua_file(var_filename, [ PREMAKE_TEMPLATE_VAR.format(pkgname=dep_name, config=conf_name, deps=_PremakeTemplate(require, dep_aggregate)) ]) # Create list of all available profiles by searching on disk file_pattern = PREMAKE_VAR_FILE.format(pkgname=dep_name, config="*") file_regex = PREMAKE_VAR_FILE.format(pkgname=re.escape(dep_name), config="(([^_]*)_(.*))") available_config_files = glob.glob(file_pattern) # Add filename of current generations var file if not already present if var_filename not in available_config_files: available_config_files.append(var_filename) profiles = [ (regex_res[0], regex_res.group(1), regex_res.group(2), regex_res.group(3)) for regex_res in [ re.search(file_regex, file_name) for file_name in available_config_files ] ] config_sets = [profile[1] for profile in profiles] # Emit package premake file pkg_filename = PREMAKE_PKG_FILE.format(pkgname=dep_name) pkg_files.append(pkg_filename) self._output_lua_file(pkg_filename, [ # Includes *['include "{}"'.format(profile[0]) for profile in profiles], ]) # Output global premake file self._output_lua_file(PREMAKE_ROOT_FILE, [ # Includes *[f'include "{pkg_file}"' for pkg_file in pkg_files], # Global order for each configuration 'include "conanconfig.premake5.lua"', # Functions PREMAKE_TEMPLATE_ROOT_FUNCTION.format( function_name="conan_setup_build", lua_content=PREMAKE_TEMPLATE_ROOT_BUILD, filter_call="\n".join( ["\n".join(self._premake_filtered( [f'conan_setup_build("{config}")'], config.split("_", 1)[0], config.split("_", 1)[1], 2) ) for config in config_sets] ) ), PREMAKE_TEMPLATE_ROOT_FUNCTION.format( function_name="conan_setup_link", lua_content=PREMAKE_TEMPLATE_ROOT_LINK, filter_call="\n".join( ["\n".join(self._premake_filtered( [f'conan_setup_link("{config}")'], config.split("_", 1)[0], config.split("_", 1)[1], 2) ) for config in config_sets] ) ), PREMAKE_TEMPLATE_ROOT_GLOBAL ]) # Output configuration file for the current build configuration self._output_lua_file(PREMAKE_CONFIG_FILE.format(config=conf_name), [ PREMAKE_TEMPLATE_CONFIG.format( config=conf_name, order=", ".join(f'"{name}"' for name in reversed(dep_names)) ) ]) # Output root configuration file available_config_files = glob.glob(PREMAKE_CONFIG_FILE.format(config="*")) available_configs = [file_name.split("_", 1)[1].split(".")[0] for file_name in available_config_files] available_configs.append(conf_name) self._output_lua_file(PREMAKE_CONFIG_ROOT_FILE, [ *['include "{}"'.format(PREMAKE_CONFIG_FILE.format(config=config)) for config in available_configs], ]) return self.output_files ================================================ FILE: conan/tools/premake/toolchain.py ================================================ from conan.tools.build.flags import architecture_flag, architecture_link_flag, libcxx_flags, threads_flags import os import textwrap from pathlib import Path from conan.tools.env.virtualbuildenv import VirtualBuildEnv from jinja2 import Template from conan.tools.build.cross_building import cross_building from conan.tools.files import save from conan.tools.microsoft.visual import VCVars from conan.tools.premake.premakedeps import PREMAKE_ROOT_FILE def _generate_flags(self, conanfile): template = textwrap.dedent( """\ {% if extra_cflags %} -- C flags retrieved from CFLAGS environment, conan.conf(tools.build:cflags), extra_cflags and compiler settings filter { files { "**.c" } } buildoptions { {{ extra_cflags }} } filter {} {% endif %} {% if extra_cxxflags %} -- CXX flags retrieved from CXXFLAGS environment, conan.conf(tools.build:cxxflags), extra_cxxflags and compiler settings filter { files { "**.cpp", "**.cxx", "**.cc" } } buildoptions { {{ extra_cxxflags }} } filter {} {% endif %} {% if extra_ldflags %} -- Link flags retrieved from LDFLAGS environment, conan.conf(tools.build:sharedlinkflags), conan.conf(tools.build:exelinkflags), extra_cxxflags and compiler settings linkoptions { {{ extra_ldflags }} } {% endif %} {% if extra_rcflags %} -- RC flags retrieved from conan.conf(tools.build:rcflags) filter { files { "**.rc" } } buildoptions { {{ extra_rcflags }} } filter {} {% endif %} {% if extra_defines %} -- Defines retrieved from DEFINES environment, conan.conf(tools.build:defines) and extra_defines defines { {{ extra_defines }} } {% endif %} """ ) def format_list(items): return ", ".join(f'"{item}"' for item in items) if items else None def to_list(value): return value if isinstance(value, list) else [value] if value else [] arch_flags = to_list(architecture_flag(self._conanfile)) cxx_flags, libcxx_compile_definitions = libcxx_flags(self._conanfile) arch_link_flags = to_list(architecture_link_flag(self._conanfile)) thread_flags_list = threads_flags(self._conanfile) extra_defines = format_list( conanfile.conf.get("tools.build:defines", default=[], check_type=list) + self.extra_defines + to_list(libcxx_compile_definitions) ) extra_c_flags = format_list( conanfile.conf.get("tools.build:cflags", default=[], check_type=list) + self.extra_cflags + arch_flags + thread_flags_list ) extra_cxx_flags = format_list( conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) + to_list(cxx_flags) + self.extra_cxxflags + arch_flags + thread_flags_list ) extra_ld_flags = format_list( conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) + conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list) + self.extra_ldflags + arch_flags + arch_link_flags + thread_flags_list ) extra_rc_flags = format_list(conanfile.conf.get("tools.build:rcflags", default=[], check_type=list)) return ( Template(template, trim_blocks=True, lstrip_blocks=True) .render( extra_defines=extra_defines, extra_cflags=extra_c_flags, extra_cxxflags=extra_cxx_flags, extra_ldflags=extra_ld_flags, extra_rcflags=extra_rc_flags, ) .strip() ) class _PremakeProject: _premake_project_template = textwrap.dedent( """\ project "{{ name }}" {% if kind %} kind "{{ kind }}" {% endif %} {% if flags %} {{ flags | indent(indent_level, first=True) }} {% endif %} """ ) def __init__(self, name, conanfile) -> None: self.name = name self.kind = None self.extra_cxxflags = [] self.extra_cflags = [] self.extra_ldflags = [] self.extra_defines = [] self.disable = False self._conanfile = conanfile def _generate(self): """Generates project block""" flags_content = _generate_flags(self, self._conanfile) # Generate flags specific to this project return Template(self._premake_project_template, trim_blocks=True, lstrip_blocks=True).render( name=self.name, kind="None" if self.disable else self.kind, flags=flags_content, indent_level=4, ) class PremakeToolchain: """ PremakeToolchain generator """ filename = "conantoolchain.premake5.lua" # Keep template indented correctly for Lua output _premake_file_template = textwrap.dedent( """\ #!lua -- Conan auto-generated toolchain file {% if has_conan_deps %} -- Include conandeps.premake5.lua with Conan dependency setup include("conandeps.premake5.lua") {% endif %} -- Base build directory local locationDir = path.normalize("{{ build_folder }}") -- Generate workspace configurations for wks in premake.global.eachWorkspace() do workspace(wks.name) -- Set base location for all workspaces location(locationDir) targetdir(path.join(locationDir, "bin")) objdir(path.join(locationDir, "obj")) {% if cppstd %} cppdialect "{{ cppstd }}" {% endif %} {% if cstd %} cdialect "{{ cstd }}" {% endif %} {% if shared != None %} -- IMPORTANT: this global setting will only apply `project`s which do not have `kind` set. -- IMPORTANT: This will not override existing `kind` set in `project` block. -- To let conan take control over `kind` of the libraries, DO NOT SET `kind` (StaticLib or -- SharedLib) in `project` block. kind "{{ "SharedLib" if shared else "StaticLib" }}" {% endif %} {% if fpic != None %} -- Enable position independent code pic "{{ "On" if fpic else "Off" }}" {% endif %} filter { "architecture: not wasm64" } -- TODO: There is an issue with premake and "wasm64" when system is declared "emscripten" system "{{ target_build_os }}" filter {} {% if macho_to_amd64 %} -- TODO: this should be fixed by premake: https://github.com/premake/premake-core/issues/2136 buildoptions "-arch x86_64" linkoptions "-arch x86_64" {% endif %} {% if target_build_os == "emscripten" %} filter { "system:emscripten", "kind:ConsoleApp or WindowedApp" } -- Replace built in .wasm extension to .js to generate also a JavaScript files targetextension ".js" filter {} {% endif %} {% if flags %} {{ flags | indent(indent_level, first=True) }} {% endif %} filter { "system:macosx" } -- SHARED LIBS -- In the future we could add an opt in configuration to run -- fix_apple_shared_install_name on executables to have a similar behavior as CMake -- generator. Premake does not allow adding absolute RCPATHS -- Due to this limitation, if a consumer depends on a premake shared recipe, it will -- require to run conanrun script to setup proper DYLD_LIBRARY_PATH -- Reference: https://github.com/premake/premake-core/issues/2262#issuecomment-2378250385 linkoptions { "-Wl,-rpath,@loader_path" } filter {} conan_setup() end {% for project in projects.values() %} {{ project._generate() }} {% endfor %} """ ) def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile self._projects = {} # Extra flags #: List of extra ``CXX`` flags. Added to ``buildoptions``. self.extra_cxxflags = [] #: List of extra ``C`` flags. Added to ``buildoptions``. self.extra_cflags = [] #: List of extra linker flags. Added to ``linkoptions``. self.extra_ldflags = [] #: List of extra preprocessor definitions. Added to ``defines``. self.extra_defines = [] def project(self, project_name): """ The returned object will also have the same properties as the workspace but will only affect the project with the name. :param project_name: The name of the project inside the workspace to be updated. :return: ```` object which allow to set project specific flags. """ if project_name not in self._projects: self._projects[project_name] = _PremakeProject(project_name, self._conanfile) return self._projects[project_name] def generate(self): """ Creates a ``conantoolchain.premake5.lua`` file which will properly configure build paths, binary paths, configuration settings and compiler/linker flags based on toolchain configuration. """ premake_conan_deps = Path(self._conanfile.generators_folder) / PREMAKE_ROOT_FILE cppstd = self._conanfile.settings.get_safe("compiler.cppstd") if cppstd: # See premake possible cppstd values: https://premake.github.io/docs/cppdialect/ if cppstd.startswith("gnu"): cppstd = f"gnu++{cppstd[3:]}" elif cppstd[0].isnumeric(): cppstd = f"c++{cppstd}" compilers_build_mapping = self._conanfile.conf.get( "tools.build:compiler_executables", default={}, check_type=dict ) if compilers_build_mapping: build_env = VirtualBuildEnv(self._conanfile, auto_generate=False) env = build_env.environment() if "c" in compilers_build_mapping: env.define("CC", compilers_build_mapping["c"]) if "cpp" in compilers_build_mapping: env.define("CXX", compilers_build_mapping["cpp"]) build_env.generate() macho_to_amd64 = ( self._conanfile.settings.arch if cross_building(self._conanfile) and self._conanfile.settings.os == "Macos" else None ) content = Template(self._premake_file_template, trim_blocks=True, lstrip_blocks=True).render( # Pass posix path for better cross-platform compatibility in Lua build_folder=Path(self._conanfile.build_folder).as_posix(), has_conan_deps=premake_conan_deps.exists(), cppstd=cppstd, cstd=self._conanfile.settings.get_safe("compiler.cstd"), shared=self._conanfile.options.get_safe("shared"), fpic=self._conanfile.options.get_safe("fPIC"), target_build_os=self._target_build_os(), macho_to_amd64=macho_to_amd64, projects=self._projects, flags=_generate_flags(self, self._conanfile), indent_level=8, ) save( self, os.path.join(self._conanfile.generators_folder, self.filename), content, ) # Generate VCVars if using MSVC if "msvc" in self._conanfile.settings.compiler: VCVars(self._conanfile).generate() def _target_build_os(self): conan_os = str(self._conanfile.settings.os) if conan_os == "Macos": return "macosx" return conan_os.lower() ================================================ FILE: conan/tools/qbs/__init__.py ================================================ from conan.tools.qbs.qbs import Qbs from conan.tools.qbs.qbsdeps import QbsDeps from conan.tools.qbs.qbsprofile import QbsProfile ================================================ FILE: conan/tools/qbs/common.py ================================================ architecture_map = { 'x86': 'x86', 'x86_64': 'x86_64', 'ppc32be': 'ppc', 'ppc32': 'ppc', 'ppc64le': 'ppc64', 'ppc64': 'ppc64', 'armv4': 'arm', 'armv4i': 'arm', 'armv5el': 'arm', 'armv5hf': 'arm', 'armv6': 'arm', 'armv7': 'arm', 'armv7hf': 'arm', 'armv7s': 'arm', 'armv7k': 'arm', 'armv8': 'arm64', 'armv8_32': 'arm64', 'armv8.3': 'arm64', 'sparc': 'sparc', 'sparcv9': 'sparc64', 'mips': 'mips', 'mips64': 'mips64', 'avr': 'avr', 's390': 's390x', 's390x': 's390x', 'asm.js': None, 'wasm': None, 'wasm64': None, 'sh4le': 'sh' } build_variant_map = { 'Debug': 'debug', 'Release': 'release', 'RelWithDebInfo': 'profiling', 'MinSizeRel': 'release' } optimization_map = { 'MinSizeRel': 'small' } cxx_language_version_map = { '98': 'c++98', 'gnu98': 'c++98', '11': 'c++11', 'gnu11': 'c++11', '14': 'c++14', 'gnu14': 'c++14', '17': 'c++17', 'gnu17': 'c++17', '20': 'c++20', 'gnu20': 'c++20' } target_platform_map = { 'Windows': 'windows', 'WindowsStore': 'windows', 'WindowsCE': 'windows', 'Linux': 'linux', 'Macos': 'macos', 'Android': 'android', 'iOS': 'ios', 'watchOS': 'watchos', 'tvOS': 'tvos', 'visionOS': 'xros', 'FreeBSD': 'freebsd', 'SunOS': 'solaris', 'AIX': 'aix', 'Emscripten': None, 'Arduino': 'none', 'Neutrino': 'qnx', } runtime_library_map = { 'static': 'static', 'dynamic': 'dynamic', 'MD': 'dynamic', 'MT': 'static', 'MDd': 'dynamic', 'MTd': 'static', } ================================================ FILE: conan/tools/qbs/qbs.py ================================================ import os import shutil from conan.tools.build import build_jobs, cmd_args_to_string from conan.errors import ConanException def _configuration_dict_to_commandlist(name, config_dict): command_list = ['config:%s' % name] for key, value in config_dict.items(): if type(value) is bool: if value: b = 'true' else: b = 'false' command_list.append('%s:%s' % (key, b)) else: command_list.append('%s:%s' % (key, value)) return command_list class Qbs: """ Qbs helper to use together with the QbsDeps feature. This class provides helper methods that wraps calls to the Qbs tool. """ def __init__(self, conanfile, project_file=None): """ :param conanfile: The current recipe object. Always use ``self``. :param project_file: The name to the main project file. If not set, Qbs will try to autodetect the project file. """ self.profile = None self._conanfile = conanfile self._set_project_file(project_file) self.jobs = build_jobs(conanfile) self._configuration = dict() def _set_project_file(self, project_file): if not project_file: self._project_file = self._conanfile.source_folder else: self._project_file = project_file if not os.path.exists(self._project_file): raise ConanException('Qbs: could not find project file %s' % self._project_file) def add_configuration(self, name, values): """ Adds a build configuration for the multi-configuration build. This Qbs feature is rarely needed since each conan package can contain only one configuration, however might be useful when creating multiple versions of the same product that should be put in the same Conan package. :param name: the name of the configuration. This corresponds to the ``config`` parameter of ``qbs resolve``, ``qbs build`` and ``qbs install`` commands. :param values: the dict containing Qbs properties and their values for this configuration. """ self._configuration[name] = values def _qbs_settings_paths(self): generators_folder = self._conanfile.generators_folder qbs_settings_path = os.path.join(generators_folder, 'qbs_settings.txt') if not os.path.exists(qbs_settings_path): return None, None return os.path.join(generators_folder, 'conan-qbs-settings-dir'), qbs_settings_path def _get_common_arguments(self): args = [] settings_dir, _ = self._qbs_settings_paths() if settings_dir is not None: args.extend(['--settings-dir', settings_dir]) args.extend([ '--build-directory', self._conanfile.build_folder, '--file', self._project_file, ]) return args def resolve(self, parallel=True): """ Wraps the ``qbs resolve`` call. If QbsDeps generator is used, this will also set the necessary properites of the Qbs "conan" module provider automatically adding dependencies to the project. :param parallel: Whether to use multi-threaded resolving. Defaults to ``True``. """ generators_folder = self._conanfile.generators_folder settings_dir, settings_path = self._qbs_settings_paths() if settings_dir is not None: shutil.rmtree(settings_dir, ignore_errors=True) import_args = ['--settings-dir', settings_dir, '--import', settings_path] import_cmd = 'qbs config %s' % cmd_args_to_string(import_args) self._conanfile.run(import_cmd) args = self._get_common_arguments() if parallel: args.extend(['--jobs', '%s' % self.jobs]) else: args.extend(['--jobs', '1']) if self.profile: args.append('profile:%s' % self.profile) if os.path.exists(os.path.join(generators_folder, 'conan-qbs-deps')): args.append('moduleProviders.conan.installDirectory:' + generators_folder) for name in self._configuration: config = self._configuration[name] args.extend(_configuration_dict_to_commandlist(name, config)) cmd = 'qbs resolve %s' % cmd_args_to_string(args) self._conanfile.run(cmd) def _build(self, products, all_products): args = self._get_common_arguments() args.append('--no-install') if all_products: args.append('--all-products') elif products: args.extend(['--products', ','.join(products or [])]) args.extend(['--jobs', '%s' % self.jobs]) for name in self._configuration: args.append('config:%s' % name) cmd = 'qbs build %s' % cmd_args_to_string(args) self._conanfile.run(cmd) def build(self, products=None): """ Wraps the ``qbs build`` call. :param products: The list of product names to build. If not set, builds all products that have builtByDefault set to true. This parameter corresponds to the ``--products`` option of the ``qbs build`` command. The resolve() method should be called before calling this method. """ return self._build(products=products, all_products=False) def build_all(self): """ Wraps the ``qbs build --all-products`` call. This method builds all products, even if their builtByDefault property is false. The resolve() method should be called before calling this method. """ return self._build(products=None, all_products=True) def install(self): """ Wraps the ``qbs install`` call. Perfoms the installation of files marked as installable in the Qbs project. The build() or build_all() methods should be called before calling this method. """ args = self._get_common_arguments() args.extend(['--no-build', '--install-root', self._conanfile.package_folder]) for name in self._configuration: args.append('config:%s' % name) cmd = 'qbs install %s' % cmd_args_to_string(args) self._conanfile.run(cmd) ================================================ FILE: conan/tools/qbs/qbsdeps.py ================================================ from conan.tools.files import save from conan.errors import ConanException from conan.internal.model.dependencies import get_transitive_requires import json import os class _QbsDepsModuleFile: def __init__(self, qbsdeps, dep, component, deps, module_name): self._qbsdeps = qbsdeps self._dep = dep self._component = component self._deps = deps self._module_name = module_name self._build_bindirs = qbsdeps._build_bindirs self._version = (component.get_property("component_version") or component.get_property("system_package_version") or dep.ref.version) @property def filename(self): return self._module_name + '.json' @property def version(self): return self._version def get_content(self): cpp_info_attrs = [ 'includedirs', 'srcdirs', 'libdirs', 'resdirs', 'bindirs', 'builddirs', 'frameworkdirs', 'system_libs', 'frameworks', 'libs', 'defines', 'cflags', 'cxxflags', 'sharedlinkflags', 'exelinkflags', 'objects', 'sysroot' ] return { 'package_name': self._dep.ref.name, 'package_dir': self._get_package_dir(), 'version': str(self._version), 'cpp_info': {k : getattr(self._component, k) for k in cpp_info_attrs}, 'build_bindirs': self._build_bindirs, 'dependencies': [{'name': n, "version": str(v)} for n, v in self._deps], 'settings': {k: v for k, v in self._dep.settings.items()}, 'options': {k: v for k, v in self._dep.options.items()} } def _get_package_dir(self): # If editable, package_folder can be None root_folder = self._dep.recipe_folder if self._dep.package_folder is None \ else self._dep.package_folder return root_folder.replace("\\", "/") def render(self): return json.dumps(self.get_content(), indent=4) class _QbsDepGenerator: """ Handles a single package, can create multiple modules in case of several components """ def __init__(self, conanfile, dep, build_bindirs): self._conanfile = conanfile self._dep = dep self._build_bindirs = build_bindirs @property def content(self): qbs_files = {} transitive_reqs = get_transitive_requires(self._conanfile, self._dep) def _get_package_name(dep): # TODO: pkgconfig uses suffix, do we need it? see: # https://github.com/conan-io/conan/blob/develop2/conan/tools/gnu/pkgconfigdeps.py#L319 return dep.cpp_info.get_property("pkg_config_name") or dep.ref.name def _get_component_name(dep, comp_name): if comp_name not in dep.cpp_info.components: if dep.ref.name == comp_name: return _get_package_name(dep) raise ConanException("Component '{name}::{cname}' not found in '{name}' " "package requirement".format(name=dep.ref.name, cname=comp_name)) # TODO: pkgconfig uses suffix, do we need it? # We re-use pkg_config_name for compatiblitity with the Qbs pkg-config provider: # in that case, Qbs/its users do not need to do additional mapping on their side pkg_config_name = dep.cpp_info.components[comp_name].get_property("pkg_config_name") return pkg_config_name or comp_name def _get_name_with_namespace(namespace, name): """ Build a name with a namespace, e.g., openssl-crypto """ return f"{namespace}-{name}" def get_components(dep): ret = {} for comp_ref_name, info in dep.cpp_info.get_sorted_components().items(): comp_name = _get_component_name(dep, comp_ref_name) ret[comp_name] = info return ret # copy & paste from pkgconfig deps def get_cpp_info_requires_names(dep, cpp_info): ret = [] dep_ref_name = dep.ref.name for req in cpp_info.requires: pkg_ref_name, comp_ref_name = ( req.split("::") if "::" in req else (dep_ref_name, req) ) if dep_ref_name != pkg_ref_name: try: req_conanfile = transitive_reqs[pkg_ref_name] except KeyError: # If the dependency is not in the transitive, might be skipped continue # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello else: req_conanfile = dep comp_name = _get_component_name(req_conanfile, comp_ref_name) if not comp_name: pkg_name = _get_package_name(req_conanfile) # Creating a component name with namespace, e.g., dep-comp1 comp_name = _get_name_with_namespace(pkg_name, comp_ref_name) ret.append((comp_name, req_conanfile.ref.version)) return ret if not self._dep.cpp_info.has_components: module_name = _get_package_name(self._dep) requires = get_cpp_info_requires_names(self._dep, self._dep.cpp_info) if not requires: # If no requires were found, let's try to get all the direct visible # dependencies, e.g., requires = "other_pkg/1.0" for deprequire, _ in self._dep.dependencies.direct_host.items(): requires.append((deprequire.ref.name, deprequire.ref.version)) file = _QbsDepsModuleFile( self, self._dep, self._dep.cpp_info, requires, module_name ) qbs_files[file.filename] = file else: full_requires = [] for module_name, component in get_components(self._dep).items(): requires = get_cpp_info_requires_names(self._dep, component) file = _QbsDepsModuleFile(self, self._dep, component, requires, module_name) qbs_files[file.filename] = file full_requires.append((module_name, file.version)) module_name = _get_package_name(self._dep) file = _QbsDepsModuleFile( self, self._dep, self._dep.cpp_info, full_requires, module_name) # We create the root package's module file ONLY # if it does not already exist in components # An example is a grpc package where they have a "grpc" component if file.filename not in qbs_files: qbs_files[file.filename] = file return qbs_files class QbsDeps: """ This class will generate a JSON file for each dependency inside the "conan-qbs-deps" folder. Each JSON file contains information necesary for Qbs ``"conan" module provider`` to be able to generate Qbs module files. """ def __init__(self, conanfile): """ :param conanfile: The current recipe object. Always use ``self``. """ self._conanfile = conanfile @property def content(self): """ Returns all dependency information as a Python dict object where key is the dependency name and value is a dict with dependency properties. """ qbs_files = {} build_bindirs = { dep.ref.name: dep.cpp_info.bindirs for _, dep in self._conanfile.dependencies.build.items()} for require, dep in self._conanfile.dependencies.items(): # skip build deps for now if require.build: continue dep_build_bindirs = build_bindirs.get(dep.ref.name, []) qbs_files.update(_QbsDepGenerator(self._conanfile, dep, dep_build_bindirs).content) return qbs_files def generate(self): """ This method will save the generated files to the "conan-qbs-deps" directory inside the ``conanfile.generators_folder`` directory. Generates a single JSON file per dependency or component. """ for file_name, qbs_deps_file in self.content.items(): save(self._conanfile, os.path.join('conan-qbs-deps', file_name), qbs_deps_file.render()) ================================================ FILE: conan/tools/qbs/qbsprofile.py ================================================ import os import shutil import platform import textwrap from jinja2 import Template from conan.internal import check_duplicated_generator from conan.errors import ConanException from conan.tools.env import VirtualBuildEnv from conan.tools.microsoft import msvs_toolset from conan.tools.microsoft.visual import vs_installation_path, _vcvars_path, _vcvars_versions from conan.tools.qbs import common from conan.internal.util.files import save def _find_msvc(conanfile): vs_install_path = conanfile.conf.get("tools.microsoft.msbuild:installation_path") vs_version, vcvars_ver = _vcvars_versions(conanfile) vs_path = vs_install_path or vs_installation_path(vs_version) if vs_path is None or vs_version is None: return None vc_install_dir = os.path.join(vs_path, 'VC', 'Tools', 'MSVC') if not os.path.exists(vc_install_dir): return None compiler_versions = [v for v in os.listdir(vc_install_dir) if v.startswith(vcvars_ver)] compiler_versions.sort(reverse=True) build_arch_map = { 'x86': 'Hostx86', 'x86_64': 'Hostx64', 'armv6': 'arm', 'armv7': 'arm', 'armv8': 'arm64', } host_arch_map = { 'x86': 'x86', 'x86_64': 'x64', 'armv6': 'arm', 'armv7': 'arm', 'armv8': 'arm64', } build_arch = build_arch_map.get(str(conanfile.settings_build.arch)) host_arch = host_arch_map.get(str(conanfile.settings.arch)) if not host_arch or not build_arch: return None def cl_path(version): return os.path.join(vc_install_dir, version, 'bin', build_arch, host_arch, 'cl.exe') compiler_paths = [cl_path(version) for version in compiler_versions] compiler_paths = [p for p in compiler_paths if os.path.exists(p)] if len(compiler_paths) == 0: return None return compiler_paths[0] def _find_clangcl(conanfile): vs_install_path = conanfile.conf.get("tools.microsoft.msbuild:installation_path") vs_version, _ = _vcvars_versions(conanfile) vs_path = vs_install_path or vs_installation_path(vs_version) compiler_path = os.path.join(vs_path, 'VC', 'Tools', 'Llvm', 'bin', 'clang-cl.exe') vcvars_path = _vcvars_path(vs_version, vs_install_path) if not os.path.exists(compiler_path): return None return compiler_path, vcvars_path class _LinkerFlagsParser: def __init__(self, ld_flags): self.driver_linker_flags = [] self.linker_flags = [] for item in ld_flags: if item.startswith('-Wl'): self.linker_flags.extend(item.split(',')[1:]) else: self.driver_linker_flags.append(item) class QbsProfile: """ Qbs profiles generator. This class generates file with the toolchain information that can be imported by Qbs. """ def __init__(self, conanfile, profile='conan', default_profile='conan'): """ :param conanfile: The current recipe object. Always use ``self``. :param profile: The name of the profile in settings. Defaults to ``"conan"``. :param default_profile: The name of the default profile. Defaults to ``"conan"``. """ self._conanfile = conanfile self._profile = profile self._default_profile = default_profile self.extra_cflags = [] self.extra_cxxflags = [] self.extra_defines = [] self.extra_sharedlinkflags = [] self.extra_exelinkflags = [] self._build_env = VirtualBuildEnv(self._conanfile, auto_generate=True).vars() @property def filename(self): """ The name of the generated file. Returns ``qbs_settings.txt``. """ return 'qbs_settings.txt' @property def content(self): """ Returns the content of the settings file as dict of Qbs properties. """ result = self._toolchain_properties() result.update(self._properties_from_settings()) result.update(self._properties_from_conf()) result.update(self._properties_from_options()) # result.update(self._properties_from_env(self._build_env)) return result def render(self): """ Returns the content of the settings file as string. """ template = textwrap.dedent('''\ {%- for key, value in profile_values.items() %} profiles.{{profile}}.{{ key }}:{{ value }} {%- endfor %} defaultProfile: {{default_profile}} ''') t = Template(template) context = { 'profile_values': self.content, 'profile': self._profile, 'default_profile': self._default_profile, } result = t.render(**context) return result def generate(self): """ This method will save the generated files to the conanfile.generators_folder. Generates the "qbs_settings.txt" file. This file contains Qbs settings such as toolchain properties and can be imported using ``qbs config --import``. """ check_duplicated_generator(self, self._conanfile) self._check_for_compiler() save(self.filename, self.render()) def _check_for_compiler(self): compiler = self._conanfile.settings.get_safe('compiler') if not compiler: raise ConanException('Qbs: need compiler to be set in settings') if compiler not in ['msvc', 'gcc', 'clang', 'apple-clang']: raise ConanException(f'Qbs: compiler {compiler} not supported') def _get_qbs_toolchain(self): compiler = self._conanfile.settings.get_safe('compiler') the_os = self._conanfile.settings.get_safe('os') if the_os == 'Windows': if compiler == 'msvc': if msvs_toolset(self._conanfile) == 'ClangCL': return 'clang-cl' return 'msvc' if compiler == 'gcc': return 'mingw' if compiler == 'clang': return 'clang-cl' raise ConanException('unknown windows compiler') if compiler == 'apple-clang': return 'xcode' # todo: other compilers? return compiler def _default_compiler_names(self, toolchain): if toolchain == 'msvc': return 'cl', 'cl' if toolchain == 'clang-cl': return 'clang-cl', 'clang-cl' if toolchain in ('gcc', 'mingw'): return 'gcc', 'g++' if toolchain in ('clang', 'xcode'): return 'clang', 'clang++' # what about other toolchains? IAR, Cosmic have a bunch of compilers based on arch return toolchain, toolchain def _find_exe(self, exe): if platform.system() == 'Windows': exe = exe + '.exe' if os.path.isabs(exe): return exe paths = self._build_env.get("PATH", "") for p in paths.split(os.pathsep): path = os.path.join(p, exe) if os.path.exists(path): return path return shutil.which(exe) def _toolchain_properties(self): toolchain = self._get_qbs_toolchain() the_os = self._conanfile.settings.get_safe('os') vcvars_path = None if the_os == 'Windows' and toolchain == 'msvc': compiler = _find_msvc(self._conanfile) elif the_os == 'Windows' and toolchain == 'clang-cl': compiler, vcvars_path = _find_clangcl(self._conanfile) else: # TODO: use CC also for msvc? c_compiler_default, cxx_compiler_default = self._default_compiler_names(toolchain) compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={}, check_type=dict) c_compiler = ( compilers_by_conf.get("c") or self._build_env.get("CC") or c_compiler_default) c_compiler = self._find_exe(c_compiler) cxx_compiler = ( compilers_by_conf.get("cpp") or self._build_env.get("CXX") or cxx_compiler_default) cxx_compiler = self._find_exe(cxx_compiler) compiler = cxx_compiler or c_compiler if compiler is None: raise ConanException('cannot find compiler') result = { 'qbs.toolchainType': toolchain, 'cpp.compilerName': os.path.basename(compiler), 'cpp.toolchainInstallPath': os.path.dirname(compiler).replace('\\', '/') } # GCC and friends also have separate props for compilers gcc_toolchains = ['clang', 'gcc', 'llvm', 'mingw', 'qcc', 'xcode'] if toolchain in gcc_toolchains: result['cpp.cCompilerName'] = os.path.basename(c_compiler) result['cpp.cxxCompilerName'] = os.path.basename(cxx_compiler) if vcvars_path: result["cpp.vcvarsallPath"] = vcvars_path return result def _properties_from_settings(self): result = {} def map_qbs_property(key, qbs_property, value_map, fallback=None): value = value_map.get(self._conanfile.settings.get_safe(key)) or fallback if value is not None: result[qbs_property] = value map_qbs_property('arch', 'qbs.architecture', common.architecture_map) map_qbs_property('os', 'qbs.targetPlatform', common.target_platform_map, "undefined") map_qbs_property('build_type', 'qbs.buildVariant', common.build_variant_map) map_qbs_property( 'compiler.cppstd', 'cpp.cxxLanguageVersion', common.cxx_language_version_map) map_qbs_property('compiler.runtime', 'cpp.runtimeLibrary', common.runtime_library_map) return result def _properties_from_options(self): result = {} def maybe_bool_str(b): return None if b is None else str(b).lower() fpic = maybe_bool_str(self._conanfile.options.get_safe('fPIC')) if fpic: result["cpp.positionIndependentCode"] = fpic return result def _properties_from_conf(self): result = {} def map_list_property(key, qbs_property, extra): value = self._conanfile.conf.get(key, default=[], check_type=list) value.extend(extra) if len(value) > 0: result[qbs_property] = value map_list_property("tools.build:cflags", "cpp.cFlags", self.extra_cflags) map_list_property("tools.build:cxxflags", "cpp.cxxFlags", self.extra_cxxflags) map_list_property("tools.build:rcflags", "cpp.rcFlags", []) map_list_property("tools.build:defines", "cpp.defines", self.extra_defines) def ldflags(): conf = self._conanfile.conf result = conf.get("tools.build:sharedlinkflags", default=[], check_type=list) result.extend(self.extra_sharedlinkflags) result.extend(conf.get("tools.build:exelinkflags", default=[], check_type=list)) result.extend(self.extra_exelinkflags) linker_scripts = conf.get("tools.build:linker_scripts", default=[], check_type=list) result.extend(["-T'" + linker_script + "'" for linker_script in linker_scripts]) return result ld_flags = ldflags() if len(ld_flags) > 0: parser = _LinkerFlagsParser(ld_flags) result['cpp.linkerFlags'] = parser.linker_flags result['cpp.driverLinkerFlags'] = parser.driver_linker_flags sysroot = self._conanfile.conf.get("tools.build:sysroot") if sysroot is not None: sysroot = sysroot.replace("\\", "/") result['qbs.sysroot'] = sysroot return result ================================================ FILE: conan/tools/ros/__init__.py ================================================ from conan.tools.ros.rosenv import ROSEnv ================================================ FILE: conan/tools/ros/rosenv.py ================================================ import os from conan.api.output import Color from conan.tools.env import Environment from conan.tools.env.environment import create_env_script class ROSEnv: """ Generator to serve as integration for Robot Operating System 2 development workspaces. IMPORTANT: This generator should be used together with CMakeDeps and CMakeToolchain generators. """ def __init__(self, conanfile): """ :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile self.variables = {} self._build_script_sh_file = "conanrosenv-build.sh" self._build_script_bat_file = "conanrosenv-build.bat" self._wrapper_script_sh_file = "conanrosenv.sh" self._wrapper_script_bat_file = "conanrosenv.bat" def generate(self): """ Creates a ``conanrosenv.sh`` with the environment variables that are needed to build and execute ROS packages with Conan dependencies. """ cmake_toolchain_path = os.path.join(self._conanfile.generators_folder, "conan_toolchain.cmake") self.variables["CMAKE_TOOLCHAIN_FILE"] = cmake_toolchain_path build_type = self._conanfile.settings.get_safe("build_type") if build_type: self.variables["CMAKE_BUILD_TYPE"] = build_type # Add ROS required variables to VirtualBuildEnv rosbuildenv = Environment() for k, v in self.variables.items(): rosbuildenv.define(k, v) for build_script_file in [self._build_script_sh_file, self._build_script_bat_file]: rosbuildenv.vars(self._conanfile, "build").save_script(build_script_file) # TODO: Add powrshell support generating .ps1 files self._generate_sh_files() self._generate_bat_files() def _generate_sh_files(self): # Generate conanrosenv.sh script wrapper that calls conanbuild.sh and conanrun.sh conanbuild_sh_path = os.path.join(self._conanfile.generators_folder, "conanbuild.sh") conanrun_sh_path = os.path.join(self._conanfile.generators_folder, "conanrun.sh") rosenv_sh_wrapper_content = [f". \"{conanbuild_sh_path}\"", f". \"{conanrun_sh_path}\""] create_env_script(self._conanfile, "\n".join(rosenv_sh_wrapper_content), self._wrapper_script_sh_file, None) conanrosenv_path = os.path.join(self._conanfile.generators_folder, self._wrapper_script_sh_file) msg = f"Generated ROSEnv Conan file: {self._wrapper_script_sh_file}\n" + \ f"Use 'source {conanrosenv_path}' to set the ROSEnv Conan before 'colcon build'" self._conanfile.output.info(msg, fg=Color.CYAN) def _generate_bat_files(self): # Generate conanrosenv.bat script wrapper that calls conanbuild.bat and conanrun.bat conanbuild_bat_path = os.path.join(self._conanfile.generators_folder, "conanbuild.bat") conanrun_bat_path = os.path.join(self._conanfile.generators_folder, "conanrun.bat") rosenv_bat_wrapper_content = [ "@echo off", f"CALL \"{conanbuild_bat_path}\"", f"CALL \"{conanrun_bat_path}\"" ] create_env_script(self._conanfile, "\n".join(rosenv_bat_wrapper_content), self._wrapper_script_bat_file, None) conanrosenv_path = os.path.join(self._conanfile.generators_folder, self._wrapper_script_bat_file) msg = f"Generated ROSEnv Conan file: {self._wrapper_script_bat_file}\n" + \ f"Use 'call {conanrosenv_path}' to set the ROSEnv Conan before 'colcon build'" self._conanfile.output.info(msg, fg=Color.CYAN) ================================================ FILE: conan/tools/sbom/__init__.py ================================================ from conan.tools.sbom.cyclonedx import cyclonedx_1_4, cyclonedx_1_6 ================================================ FILE: conan/tools/sbom/cyclonedx.py ================================================ from conan import conan_version def cyclonedx_1_4(conanfile, name=None, add_build=False, add_tests=False, **kwargs): """ (Experimental) Generate cyclone 1.4 SBOM with JSON format Creates a CycloneDX 1.4 Software Bill of Materials (SBOM) from a given dependency graph. Parameters: conanfile: The conanfile instance. name (str, optional): Custom name for the metadata field. add_build (bool, optional, default=False): Include build dependencies. add_tests (bool, optional, default=False): Include test dependencies. Returns: The generated CycloneDX 1.4 document as a string. Example usage: ``` cyclonedx_1_4(conanfile, name="custom_name", add_build=True, add_test=True, **kwargs) ``` """ import uuid import time from datetime import datetime, timezone graph = conanfile.subgraph has_special_root_node = not (getattr(graph.root.ref, "name", False) and getattr(graph.root.ref, "version", False) and getattr(graph.root.ref, "revision", False)) special_id = str(uuid.uuid4()) name_default = getattr(graph.root.ref, "name", False) or "conan-sbom" name_default += f"/{graph.root.ref.version}" if getattr(graph.root.ref, "version", False) else "" nodes = [node for node in graph.nodes if should_add_node(node, add_build, add_tests)] if has_special_root_node: nodes = nodes[1:] dependencies = [] if has_special_root_node: deps = {"ref": special_id, "dependsOn": [_calculate_bomref(d.dst) for d in graph.root.edges if should_add_node(d.dst, add_build, add_tests)]} dependencies.append(deps) for c in nodes: deps = {"ref": _calculate_bomref(c)} dep = [d for d in c.edges if should_add_node(d.dst, add_build, add_tests)] depends_on = [_calculate_bomref(d.dst) for d in dep if should_add_node(d.dst, add_build, add_tests)] if depends_on: deps["dependsOn"] = depends_on dependencies.append(deps) sbom_cyclonedx_1_4 = { **({"components": [{ "author": node.conanfile.author or "Unknown", "bom-ref": _calculate_bomref(node), "description": node.conanfile.description, **({"externalReferences": [{ "type": "website", "url": node.conanfile.homepage }]} if node.conanfile.homepage else {}), **({"licenses": _calculate_licenses(node)} if node.conanfile.license else {}), "name": node.name, "purl": f"pkg:conan/{node.name}@{node.ref.version}", "type": "application" if node.conanfile.package_type == "application" else "library", "version": str(node.ref.version), } for node in nodes]} if nodes else {}), **({"dependencies": dependencies} if dependencies else {}), "metadata": { "component": { "author": conanfile.author or "Unknown", "bom-ref": special_id if has_special_root_node else _calculate_bomref(conanfile), "name": name if name else name_default, "type": "application" if conanfile.package_type == "application" else "library", }, "timestamp": f"{datetime.fromtimestamp(time.time(), tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}", "tools": [{ "externalReferences": [{ "type": "website", "url": "https://github.com/conan-io/conan" }], "name": "Conan-io" }], }, "serialNumber": f"urn:uuid:{uuid.uuid4()}", "bomFormat": "CycloneDX", "specVersion": "1.4", "version": 1, } return sbom_cyclonedx_1_4 def cyclonedx_1_6(conanfile, name=None, add_build=False, add_tests=False, **kwargs): """ (Experimental) Generate cyclone 1.6 SBOM with JSON format Creates a CycloneDX 1.6 Software Bill of Materials (SBOM) from a given dependency graph. Parameters: conanfile: The conanfile instance. name (str, optional): Custom name for the metadata field. add_build (bool, optional, default=False): Include build dependencies. add_tests (bool, optional, default=False): Include test dependencies. Returns: The generated CycloneDX 1.6 document as a string. Example usage: ``` cyclonedx_1_6(conanfile, name="custom_name", add_build=True, add_test=True, **kwargs) ``` """ import uuid import time from datetime import datetime, timezone graph = conanfile.subgraph has_special_root_node = not (getattr(graph.root.ref, "name", False) and getattr(graph.root.ref, "version", False) and getattr(graph.root.ref, "revision", False)) special_id = str(uuid.uuid4()) name_default = getattr(graph.root.ref, "name", False) or "conan-sbom" name_default += f"/{graph.root.ref.version}" if getattr(graph.root.ref, "version", False) else "" nodes = [node for node in graph.nodes if should_add_node(node, add_build, add_tests)] if has_special_root_node: nodes = nodes[1:] dependencies = [] if has_special_root_node: deps = {"ref": special_id, "dependsOn": [_calculate_bomref(d.dst) for d in graph.root.edges if should_add_node(d.dst, add_build, add_tests)]} dependencies.append(deps) for c in nodes: deps = {"ref": _calculate_bomref(c)} dep = [d for d in c.edges if should_add_node(d.dst, add_build, add_tests)] depends_on = [_calculate_bomref(d.dst) for d in dep if should_add_node(d.dst, add_build, add_tests)] if depends_on: deps["dependsOn"] = depends_on dependencies.append(deps) sbom_cyclonedx_1_6 = { **({"components": [{ **({"authors": [{"name": node.conanfile.author}]} if node.conanfile.author else {}), "bom-ref": _calculate_bomref(node), "description": node.conanfile.description, **({"externalReferences": [{ "type": "website", "url": node.conanfile.homepage }]} if node.conanfile.homepage else {}), **({"licenses": _calculate_licenses(node)} if node.conanfile.license else {}), "name": node.name, "purl": f"pkg:conan/{node.name}@{node.ref.version}", "type": "application" if node.conanfile.package_type == "application" else "library", "version": str(node.ref.version), } for node in nodes]} if nodes else {}), **({"dependencies": dependencies} if dependencies else {}), "metadata": { "component": { **({"authors": [{"name": conanfile.author}]} if conanfile.author else {}), "bom-ref": special_id if has_special_root_node else _calculate_bomref(conanfile), "name": name if name else name_default, "type": "application" if conanfile.package_type == "application" else "library" }, "timestamp": f"{datetime.fromtimestamp(time.time(), tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}", "tools": { "components": [{ "type": "application", "name": "Conan-io", "version": str(conan_version), }] }, }, "serialNumber": f"urn:uuid:{uuid.uuid4()}", "bomFormat": "CycloneDX", "specVersion": "1.6", "version": 1, } return sbom_cyclonedx_1_6 def _calculate_licenses(component): from conan.tools.sbom.spdx_licenses import NORMALIZED_VALID_SPDX_LICENSES licenses = component.conanfile.license if isinstance(licenses, str): # Just one license field = "id" if licenses.lower() in NORMALIZED_VALID_SPDX_LICENSES else "name" return [{"license": {field: licenses}}] return [ # More than one license {"license": { "id" if lic.lower() in NORMALIZED_VALID_SPDX_LICENSES else "name": lic }} for lic in licenses ] def _calculate_bomref(component): user = f"&user={component.ref.user}" if component.ref.user else "" channel = f"&channel={component.ref.channel}" if component.ref.channel else "" return f"pkg:conan/{component.name}@{component.ref.version}?rref={component.ref.revision}{user}{channel}" def should_add_node(node, add_build, add_tests): return (node.context == "host" or add_build) and (not node.test or add_tests) ================================================ FILE: conan/tools/sbom/spdx_licenses.py ================================================ # This list comes from: https://github.com/spdx/license-list-data/blob/v3.26.0/json/licenses.json VALID_SPDX_LICENSES = { "0BSD", "3D-Slicer-1.0", "AAL", "Abstyles", "AdaCore-doc", "Adobe-2006", "Adobe-Display-PostScript", "Adobe-Glyph", "Adobe-Utopia", "ADSL", "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "Afmparse", "AGPL-1.0", "AGPL-1.0-only", "AGPL-1.0-or-later", "AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later", "Aladdin", "AMD-newlib", "AMDPLPA", "AML", "AML-glslang", "AMPAS", "ANTLR-PD", "ANTLR-PD-fallback", "any-OSI", "any-OSI-perl-modules", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APAFML", "APL-1.0", "App-s2p", "APSL-1.0", "APSL-1.1", "APSL-1.2", "APSL-2.0", "Arphic-1999", "Artistic-1.0", "Artistic-1.0-cl8", "Artistic-1.0-Perl", "Artistic-2.0", "ASWF-Digital-Assets-1.0", "ASWF-Digital-Assets-1.1", "Baekmuk", "Bahyph", "Barr", "bcrypt-Solar-Designer", "Beerware", "Bitstream-Charter", "Bitstream-Vera", "BitTorrent-1.0", "BitTorrent-1.1", "blessing", "BlueOak-1.0.0", "Boehm-GC", "Boehm-GC-without-fee", "Borceux", "Brian-Gladman-2-Clause", "Brian-Gladman-3-Clause", "BSD-1-Clause", "BSD-2-Clause", "BSD-2-Clause-Darwin", "BSD-2-Clause-first-lines", "BSD-2-Clause-FreeBSD", "BSD-2-Clause-NetBSD", "BSD-2-Clause-Patent", "BSD-2-Clause-Views", "BSD-3-Clause", "BSD-3-Clause-acpica", "BSD-3-Clause-Attribution", "BSD-3-Clause-Clear", "BSD-3-Clause-flex", "BSD-3-Clause-HP", "BSD-3-Clause-LBNL", "BSD-3-Clause-Modification", "BSD-3-Clause-No-Military-License", "BSD-3-Clause-No-Nuclear-License", "BSD-3-Clause-No-Nuclear-License-2014", "BSD-3-Clause-No-Nuclear-Warranty", "BSD-3-Clause-Open-MPI", "BSD-3-Clause-Sun", "BSD-4-Clause", "BSD-4-Clause-Shortened", "BSD-4-Clause-UC", "BSD-4.3RENO", "BSD-4.3TAHOE", "BSD-Advertising-Acknowledgement", "BSD-Attribution-HPND-disclaimer", "BSD-Inferno-Nettverk", "BSD-Protection", "BSD-Source-beginning-file", "BSD-Source-Code", "BSD-Systemics", "BSD-Systemics-W3Works", "BSL-1.0", "BUSL-1.1", "bzip2-1.0.5", "bzip2-1.0.6", "C-UDA-1.0", "CAL-1.0", "CAL-1.0-Combined-Work-Exception", "Caldera", "Caldera-no-preamble", "Catharon", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5", "CC-BY-2.5-AU", "CC-BY-3.0", "CC-BY-3.0-AT", "CC-BY-3.0-AU", "CC-BY-3.0-DE", "CC-BY-3.0-IGO", "CC-BY-3.0-NL", "CC-BY-3.0-US", "CC-BY-4.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", "CC-BY-NC-3.0", "CC-BY-NC-3.0-DE", "CC-BY-NC-4.0", "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0", "CC-BY-NC-ND-3.0-DE", "CC-BY-NC-ND-3.0-IGO", "CC-BY-NC-ND-4.0", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.0-DE", "CC-BY-NC-SA-2.0-FR", "CC-BY-NC-SA-2.0-UK", "CC-BY-NC-SA-2.5", "CC-BY-NC-SA-3.0", "CC-BY-NC-SA-3.0-DE", "CC-BY-NC-SA-3.0-IGO", "CC-BY-NC-SA-4.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", "CC-BY-ND-3.0", "CC-BY-ND-3.0-DE", "CC-BY-ND-4.0", "CC-BY-SA-1.0", "CC-BY-SA-2.0", "CC-BY-SA-2.0-UK", "CC-BY-SA-2.1-JP", "CC-BY-SA-2.5", "CC-BY-SA-3.0", "CC-BY-SA-3.0-AT", "CC-BY-SA-3.0-DE", "CC-BY-SA-3.0-IGO", "CC-BY-SA-4.0", "CC-PDDC", "CC-PDM-1.0", "CC-SA-1.0", "CC0-1.0", "CDDL-1.0", "CDDL-1.1", "CDL-1.0", "CDLA-Permissive-1.0", "CDLA-Permissive-2.0", "CDLA-Sharing-1.0", "CECILL-1.0", "CECILL-1.1", "CECILL-2.0", "CECILL-2.1", "CECILL-B", "CECILL-C", "CERN-OHL-1.1", "CERN-OHL-1.2", "CERN-OHL-P-2.0", "CERN-OHL-S-2.0", "CERN-OHL-W-2.0", "CFITSIO", "check-cvs", "checkmk", "ClArtistic", "Clips", "CMU-Mach", "CMU-Mach-nodoc", "CNRI-Jython", "CNRI-Python", "CNRI-Python-GPL-Compatible", "COIL-1.0", "Community-Spec-1.0", "Condor-1.1", "copyleft-next-0.3.0", "copyleft-next-0.3.1", "Cornell-Lossless-JPEG", "CPAL-1.0", "CPL-1.0", "CPOL-1.02", "Cronyx", "Crossword", "CrystalStacker", "CUA-OPL-1.0", "Cube", "curl", "cve-tou", "D-FSL-1.0", "DEC-3-Clause", "diffmark", "DL-DE-BY-2.0", "DL-DE-ZERO-2.0", "DOC", "DocBook-Schema", "DocBook-Stylesheet", "DocBook-XML", "Dotseqn", "DRL-1.0", "DRL-1.1", "DSDP", "dtoa", "dvipdfm", "ECL-1.0", "ECL-2.0", "eCos-2.0", "EFL-1.0", "EFL-2.0", "eGenix", "Elastic-2.0", "Entessa", "EPICS", "EPL-1.0", "EPL-2.0", "ErlPL-1.1", "etalab-2.0", "EUDatagrid", "EUPL-1.0", "EUPL-1.1", "EUPL-1.2", "Eurosym", "Fair", "FBM", "FDK-AAC", "Ferguson-Twofish", "Frameworx-1.0", "FreeBSD-DOC", "FreeImage", "FSFAP", "FSFAP-no-warranty-disclaimer", "FSFUL", "FSFULLR", "FSFULLRWD", "FTL", "Furuseth", "fwlw", "GCR-docs", "GD", "generic-xts", "GFDL-1.1", "GFDL-1.1-invariants-only", "GFDL-1.1-invariants-or-later", "GFDL-1.1-no-invariants-only", "GFDL-1.1-no-invariants-or-later", "GFDL-1.1-only", "GFDL-1.1-or-later", "GFDL-1.2", "GFDL-1.2-invariants-only", "GFDL-1.2-invariants-or-later", "GFDL-1.2-no-invariants-only", "GFDL-1.2-no-invariants-or-later", "GFDL-1.2-only", "GFDL-1.2-or-later", "GFDL-1.3", "GFDL-1.3-invariants-only", "GFDL-1.3-invariants-or-later", "GFDL-1.3-no-invariants-only", "GFDL-1.3-no-invariants-or-later", "GFDL-1.3-only", "GFDL-1.3-or-later", "Giftware", "GL2PS", "Glide", "Glulxe", "GLWTPL", "gnuplot", "GPL-1.0", "GPL-1.0+", "GPL-1.0-only", "GPL-1.0-or-later", "GPL-2.0", "GPL-2.0+", "GPL-2.0-only", "GPL-2.0-or-later", "GPL-2.0-with-autoconf-exception", "GPL-2.0-with-bison-exception", "GPL-2.0-with-classpath-exception", "GPL-2.0-with-font-exception", "GPL-2.0-with-GCC-exception", "GPL-3.0", "GPL-3.0+", "GPL-3.0-only", "GPL-3.0-or-later", "GPL-3.0-with-autoconf-exception", "GPL-3.0-with-GCC-exception", "Graphics-Gems", "gSOAP-1.3b", "gtkbook", "Gutmann", "HaskellReport", "hdparm", "HIDAPI", "Hippocratic-2.1", "HP-1986", "HP-1989", "HPND", "HPND-DEC", "HPND-doc", "HPND-doc-sell", "HPND-export-US", "HPND-export-US-acknowledgement", "HPND-export-US-modify", "HPND-export2-US", "HPND-Fenneberg-Livingston", "HPND-INRIA-IMAG", "HPND-Intel", "HPND-Kevlin-Henney", "HPND-Markus-Kuhn", "HPND-merchantability-variant", "HPND-MIT-disclaimer", "HPND-Netrek", "HPND-Pbmplus", "HPND-sell-MIT-disclaimer-xserver", "HPND-sell-regexpr", "HPND-sell-variant", "HPND-sell-variant-MIT-disclaimer", "HPND-sell-variant-MIT-disclaimer-rev", "HPND-UC", "HPND-UC-export-US", "HTMLTIDY", "IBM-pibs", "ICU", "IEC-Code-Components-EULA", "IJG", "IJG-short", "ImageMagick", "iMatix", "Imlib2", "Info-ZIP", "Inner-Net-2.0", "InnoSetup", "Intel", "Intel-ACPI", "Interbase-1.0", "IPA", "IPL-1.0", "ISC", "ISC-Veillard", "Jam", "JasPer-2.0", "JPL-image", "JPNIC", "JSON", "Kastrup", "Kazlib", "Knuth-CTAN", "LAL-1.2", "LAL-1.3", "Latex2e", "Latex2e-translated-notice", "Leptonica", "LGPL-2.0", "LGPL-2.0+", "LGPL-2.0-only", "LGPL-2.0-or-later", "LGPL-2.1", "LGPL-2.1+", "LGPL-2.1-only", "LGPL-2.1-or-later", "LGPL-3.0", "LGPL-3.0+", "LGPL-3.0-only", "LGPL-3.0-or-later", "LGPLLR", "Libpng", "libpng-2.0", "libselinux-1.0", "libtiff", "libutil-David-Nugent", "LiLiQ-P-1.1", "LiLiQ-R-1.1", "LiLiQ-Rplus-1.1", "Linux-man-pages-1-para", "Linux-man-pages-copyleft", "Linux-man-pages-copyleft-2-para", "Linux-man-pages-copyleft-var", "Linux-OpenIB", "LOOP", "LPD-document", "LPL-1.0", "LPL-1.02", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", "LPPL-1.3a", "LPPL-1.3c", "lsof", "Lucida-Bitmap-Fonts", "LZMA-SDK-9.11-to-9.20", "LZMA-SDK-9.22", "Mackerras-3-Clause", "Mackerras-3-Clause-acknowledgment", "magaz", "mailprio", "MakeIndex", "Martin-Birgmeier", "McPhee-slideshow", "metamail", "Minpack", "MIPS", "MirOS", "MIT", "MIT-0", "MIT-advertising", "MIT-Click", "MIT-CMU", "MIT-enna", "MIT-feh", "MIT-Festival", "MIT-Khronos-old", "MIT-Modern-Variant", "MIT-open-group", "MIT-testregex", "MIT-Wu", "MITNFA", "MMIXware", "Motosoto", "MPEG-SSG", "mpi-permissive", "mpich2", "MPL-1.0", "MPL-1.1", "MPL-2.0", "MPL-2.0-no-copyleft-exception", "mplus", "MS-LPL", "MS-PL", "MS-RL", "MTLL", "MulanPSL-1.0", "MulanPSL-2.0", "Multics", "Mup", "NAIST-2003", "NASA-1.3", "Naumen", "NBPL-1.0", "NCBI-PD", "NCGL-UK-2.0", "NCL", "NCSA", "Net-SNMP", "NetCDF", "Newsletr", "NGPL", "NICTA-1.0", "NIST-PD", "NIST-PD-fallback", "NIST-Software", "NLOD-1.0", "NLOD-2.0", "NLPL", "Nokia", "NOSL", "Noweb", "NPL-1.0", "NPL-1.1", "NPOSL-3.0", "NRL", "NTP", "NTP-0", "Nunit", "O-UDA-1.0", "OAR", "OCCT-PL", "OCLC-2.0", "ODbL-1.0", "ODC-By-1.0", "OFFIS", "OFL-1.0", "OFL-1.0-no-RFN", "OFL-1.0-RFN", "OFL-1.1", "OFL-1.1-no-RFN", "OFL-1.1-RFN", "OGC-1.0", "OGDL-Taiwan-1.0", "OGL-Canada-2.0", "OGL-UK-1.0", "OGL-UK-2.0", "OGL-UK-3.0", "OGTSL", "OLDAP-1.1", "OLDAP-1.2", "OLDAP-1.3", "OLDAP-1.4", "OLDAP-2.0", "OLDAP-2.0.1", "OLDAP-2.1", "OLDAP-2.2", "OLDAP-2.2.1", "OLDAP-2.2.2", "OLDAP-2.3", "OLDAP-2.4", "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OLDAP-2.8", "OLFL-1.3", "OML", "OpenPBS-2.3", "OpenSSL", "OpenSSL-standalone", "OpenVision", "OPL-1.0", "OPL-UK-3.0", "OPUBL-1.0", "OSET-PL-2.1", "OSL-1.0", "OSL-1.1", "OSL-2.0", "OSL-2.1", "OSL-3.0", "PADL", "Parity-6.0.0", "Parity-7.0.0", "PDDL-1.0", "PHP-3.0", "PHP-3.01", "Pixar", "pkgconf", "Plexus", "pnmstitch", "PolyForm-Noncommercial-1.0.0", "PolyForm-Small-Business-1.0.0", "PostgreSQL", "PPL", "PSF-2.0", "psfrag", "psutils", "Python-2.0", "Python-2.0.1", "python-ldap", "Qhull", "QPL-1.0", "QPL-1.0-INRIA-2004", "radvd", "Rdisc", "RHeCos-1.1", "RPL-1.1", "RPL-1.5", "RPSL-1.0", "RSA-MD", "RSCPL", "Ruby", "Ruby-pty", "SAX-PD", "SAX-PD-2.0", "Saxpath", "SCEA", "SchemeReport", "Sendmail", "Sendmail-8.23", "Sendmail-Open-Source-1.1", "SGI-B-1.0", "SGI-B-1.1", "SGI-B-2.0", "SGI-OpenGL", "SGP4", "SHL-0.5", "SHL-0.51", "SimPL-2.0", "SISSL", "SISSL-1.2", "SL", "Sleepycat", "SMAIL-GPL", "SMLNJ", "SMPPL", "SNIA", "snprintf", "softSurfer", "Soundex", "Spencer-86", "Spencer-94", "Spencer-99", "SPL-1.0", "ssh-keyscan", "SSH-OpenSSH", "SSH-short", "SSLeay-standalone", "SSPL-1.0", "StandardML-NJ", "SugarCRM-1.1.3", "Sun-PPP", "Sun-PPP-2000", "SunPro", "SWL", "swrule", "Symlinks", "TAPR-OHL-1.0", "TCL", "TCP-wrappers", "TermReadKey", "TGPPL-1.0", "ThirdEye", "threeparttable", "TMate", "TORQUE-1.1", "TOSL", "TPDL", "TPL-1.0", "TrustedQSL", "TTWL", "TTYP0", "TU-Berlin-1.0", "TU-Berlin-2.0", "Ubuntu-font-1.0", "UCAR", "UCL-1.0", "ulem", "UMich-Merit", "Unicode-3.0", "Unicode-DFS-2015", "Unicode-DFS-2016", "Unicode-TOU", "UnixCrypt", "Unlicense", "UPL-1.0", "URT-RLE", "Vim", "VOSTROM", "VSL-1.0", "W3C", "W3C-19980720", "W3C-20150513", "w3m", "Watcom-1.0", "Widget-Workshop", "Wsuipa", "WTFPL", "wwl", "wxWindows", "X11", "X11-distribute-modifications-variant", "X11-swapped", "Xdebug-1.03", "Xerox", "Xfig", "XFree86-1.1", "xinetd", "xkeyboard-config-Zinoviev", "xlock", "Xnet", "xpp", "XSkat", "xzoom", "YPL-1.0", "YPL-1.1", "Zed", "Zeeff", "Zend-2.0", "Zimbra-1.3", "Zimbra-1.4", "Zlib", "zlib-acknowledgement", "ZPL-1.1", "ZPL-2.0", "ZPL-2.1" } NORMALIZED_VALID_SPDX_LICENSES = { "0bsd", "3d-slicer-1.0", "aal", "abstyles", "adacore-doc", "adobe-2006", "adobe-display-postscript", "adobe-glyph", "adobe-utopia", "adsl", "afl-1.1", "afl-1.2", "afl-2.0", "afl-2.1", "afl-3.0", "afmparse", "agpl-1.0", "agpl-1.0-only", "agpl-1.0-or-later", "agpl-3.0", "agpl-3.0-only", "agpl-3.0-or-later", "aladdin", "amd-newlib", "amdplpa", "aml", "aml-glslang", "ampas", "antlr-pd", "antlr-pd-fallback", "any-osi", "any-osi-perl-modules", "apache-1.0", "apache-1.1", "apache-2.0", "apafml", "apl-1.0", "app-s2p", "apsl-1.0", "apsl-1.1", "apsl-1.2", "apsl-2.0", "arphic-1999", "artistic-1.0", "artistic-1.0-cl8", "artistic-1.0-perl", "artistic-2.0", "aswf-digital-assets-1.0", "aswf-digital-assets-1.1", "baekmuk", "bahyph", "barr", "bcrypt-solar-designer", "beerware", "bitstream-charter", "bitstream-vera", "bittorrent-1.0", "bittorrent-1.1", "blessing", "blueoak-1.0.0", "boehm-gc", "boehm-gc-without-fee", "borceux", "brian-gladman-2-clause", "brian-gladman-3-clause", "bsd-1-clause", "bsd-2-clause", "bsd-2-clause-darwin", "bsd-2-clause-first-lines", "bsd-2-clause-freebsd", "bsd-2-clause-netbsd", "bsd-2-clause-patent", "bsd-2-clause-views", "bsd-3-clause", "bsd-3-clause-acpica", "bsd-3-clause-attribution", "bsd-3-clause-clear", "bsd-3-clause-flex", "bsd-3-clause-hp", "bsd-3-clause-lbnl", "bsd-3-clause-modification", "bsd-3-clause-no-military-license", "bsd-3-clause-no-nuclear-license", "bsd-3-clause-no-nuclear-license-2014", "bsd-3-clause-no-nuclear-warranty", "bsd-3-clause-open-mpi", "bsd-3-clause-sun", "bsd-4-clause", "bsd-4-clause-shortened", "bsd-4-clause-uc", "bsd-4.3reno", "bsd-4.3tahoe", "bsd-advertising-acknowledgement", "bsd-attribution-hpnd-disclaimer", "bsd-inferno-nettverk", "bsd-protection", "bsd-source-beginning-file", "bsd-source-code", "bsd-systemics", "bsd-systemics-w3works", "bsl-1.0", "busl-1.1", "bzip2-1.0.5", "bzip2-1.0.6", "c-uda-1.0", "cal-1.0", "cal-1.0-combined-work-exception", "caldera", "caldera-no-preamble", "catharon", "catosl-1.1", "cc-by-1.0", "cc-by-2.0", "cc-by-2.5", "cc-by-2.5-au", "cc-by-3.0", "cc-by-3.0-at", "cc-by-3.0-au", "cc-by-3.0-de", "cc-by-3.0-igo", "cc-by-3.0-nl", "cc-by-3.0-us", "cc-by-4.0", "cc-by-nc-1.0", "cc-by-nc-2.0", "cc-by-nc-2.5", "cc-by-nc-3.0", "cc-by-nc-3.0-de", "cc-by-nc-4.0", "cc-by-nc-nd-1.0", "cc-by-nc-nd-2.0", "cc-by-nc-nd-2.5", "cc-by-nc-nd-3.0", "cc-by-nc-nd-3.0-de", "cc-by-nc-nd-3.0-igo", "cc-by-nc-nd-4.0", "cc-by-nc-sa-1.0", "cc-by-nc-sa-2.0", "cc-by-nc-sa-2.0-de", "cc-by-nc-sa-2.0-fr", "cc-by-nc-sa-2.0-uk", "cc-by-nc-sa-2.5", "cc-by-nc-sa-3.0", "cc-by-nc-sa-3.0-de", "cc-by-nc-sa-3.0-igo", "cc-by-nc-sa-4.0", "cc-by-nd-1.0", "cc-by-nd-2.0", "cc-by-nd-2.5", "cc-by-nd-3.0", "cc-by-nd-3.0-de", "cc-by-nd-4.0", "cc-by-sa-1.0", "cc-by-sa-2.0", "cc-by-sa-2.0-uk", "cc-by-sa-2.1-jp", "cc-by-sa-2.5", "cc-by-sa-3.0", "cc-by-sa-3.0-at", "cc-by-sa-3.0-de", "cc-by-sa-3.0-igo", "cc-by-sa-4.0", "cc-pddc", "cc-pdm-1.0", "cc-sa-1.0", "cc0-1.0", "cddl-1.0", "cddl-1.1", "cdl-1.0", "cdla-permissive-1.0", "cdla-permissive-2.0", "cdla-sharing-1.0", "cecill-1.0", "cecill-1.1", "cecill-2.0", "cecill-2.1", "cecill-b", "cecill-c", "cern-ohl-1.1", "cern-ohl-1.2", "cern-ohl-p-2.0", "cern-ohl-s-2.0", "cern-ohl-w-2.0", "cfitsio", "check-cvs", "checkmk", "clartistic", "clips", "cmu-mach", "cmu-mach-nodoc", "cnri-jython", "cnri-python", "cnri-python-gpl-compatible", "coil-1.0", "community-spec-1.0", "condor-1.1", "copyleft-next-0.3.0", "copyleft-next-0.3.1", "cornell-lossless-jpeg", "cpal-1.0", "cpl-1.0", "cpol-1.02", "cronyx", "crossword", "crystalstacker", "cua-opl-1.0", "cube", "curl", "cve-tou", "d-fsl-1.0", "dec-3-clause", "diffmark", "dl-de-by-2.0", "dl-de-zero-2.0", "doc", "docbook-schema", "docbook-stylesheet", "docbook-xml", "dotseqn", "drl-1.0", "drl-1.1", "dsdp", "dtoa", "dvipdfm", "ecl-1.0", "ecl-2.0", "ecos-2.0", "efl-1.0", "efl-2.0", "egenix", "elastic-2.0", "entessa", "epics", "epl-1.0", "epl-2.0", "erlpl-1.1", "etalab-2.0", "eudatagrid", "eupl-1.0", "eupl-1.1", "eupl-1.2", "eurosym", "fair", "fbm", "fdk-aac", "ferguson-twofish", "frameworx-1.0", "freebsd-doc", "freeimage", "fsfap", "fsfap-no-warranty-disclaimer", "fsful", "fsfullr", "fsfullrwd", "ftl", "furuseth", "fwlw", "gcr-docs", "gd", "generic-xts", "gfdl-1.1", "gfdl-1.1-invariants-only", "gfdl-1.1-invariants-or-later", "gfdl-1.1-no-invariants-only", "gfdl-1.1-no-invariants-or-later", "gfdl-1.1-only", "gfdl-1.1-or-later", "gfdl-1.2", "gfdl-1.2-invariants-only", "gfdl-1.2-invariants-or-later", "gfdl-1.2-no-invariants-only", "gfdl-1.2-no-invariants-or-later", "gfdl-1.2-only", "gfdl-1.2-or-later", "gfdl-1.3", "gfdl-1.3-invariants-only", "gfdl-1.3-invariants-or-later", "gfdl-1.3-no-invariants-only", "gfdl-1.3-no-invariants-or-later", "gfdl-1.3-only", "gfdl-1.3-or-later", "giftware", "gl2ps", "glide", "glulxe", "glwtpl", "gnuplot", "gpl-1.0", "gpl-1.0+", "gpl-1.0-only", "gpl-1.0-or-later", "gpl-2.0", "gpl-2.0+", "gpl-2.0-only", "gpl-2.0-or-later", "gpl-2.0-with-autoconf-exception", "gpl-2.0-with-bison-exception", "gpl-2.0-with-classpath-exception", "gpl-2.0-with-font-exception", "gpl-2.0-with-gcc-exception", "gpl-3.0", "gpl-3.0+", "gpl-3.0-only", "gpl-3.0-or-later", "gpl-3.0-with-autoconf-exception", "gpl-3.0-with-gcc-exception", "graphics-gems", "gsoap-1.3b", "gtkbook", "gutmann", "haskellreport", "hdparm", "hidapi", "hippocratic-2.1", "hp-1986", "hp-1989", "hpnd", "hpnd-dec", "hpnd-doc", "hpnd-doc-sell", "hpnd-export-us", "hpnd-export-us-acknowledgement", "hpnd-export-us-modify", "hpnd-export2-us", "hpnd-fenneberg-livingston", "hpnd-inria-imag", "hpnd-intel", "hpnd-kevlin-henney", "hpnd-markus-kuhn", "hpnd-merchantability-variant", "hpnd-mit-disclaimer", "hpnd-netrek", "hpnd-pbmplus", "hpnd-sell-mit-disclaimer-xserver", "hpnd-sell-regexpr", "hpnd-sell-variant", "hpnd-sell-variant-mit-disclaimer", "hpnd-sell-variant-mit-disclaimer-rev", "hpnd-uc", "hpnd-uc-export-us", "htmltidy", "ibm-pibs", "icu", "iec-code-components-eula", "ijg", "ijg-short", "imagemagick", "imatix", "imlib2", "info-zip", "inner-net-2.0", "innosetup", "intel", "intel-acpi", "interbase-1.0", "ipa", "ipl-1.0", "isc", "isc-veillard", "jam", "jasper-2.0", "jpl-image", "jpnic", "json", "kastrup", "kazlib", "knuth-ctan", "lal-1.2", "lal-1.3", "latex2e", "latex2e-translated-notice", "leptonica", "lgpl-2.0", "lgpl-2.0+", "lgpl-2.0-only", "lgpl-2.0-or-later", "lgpl-2.1", "lgpl-2.1+", "lgpl-2.1-only", "lgpl-2.1-or-later", "lgpl-3.0", "lgpl-3.0+", "lgpl-3.0-only", "lgpl-3.0-or-later", "lgpllr", "libpng", "libpng-2.0", "libselinux-1.0", "libtiff", "libutil-david-nugent", "liliq-p-1.1", "liliq-r-1.1", "liliq-rplus-1.1", "linux-man-pages-1-para", "linux-man-pages-copyleft", "linux-man-pages-copyleft-2-para", "linux-man-pages-copyleft-var", "linux-openib", "loop", "lpd-document", "lpl-1.0", "lpl-1.02", "lppl-1.0", "lppl-1.1", "lppl-1.2", "lppl-1.3a", "lppl-1.3c", "lsof", "lucida-bitmap-fonts", "lzma-sdk-9.11-to-9.20", "lzma-sdk-9.22", "mackerras-3-clause", "mackerras-3-clause-acknowledgment", "magaz", "mailprio", "makeindex", "martin-birgmeier", "mcphee-slideshow", "metamail", "minpack", "mips", "miros", "mit", "mit-0", "mit-advertising", "mit-click", "mit-cmu", "mit-enna", "mit-feh", "mit-festival", "mit-khronos-old", "mit-modern-variant", "mit-open-group", "mit-testregex", "mit-wu", "mitnfa", "mmixware", "motosoto", "mpeg-ssg", "mpi-permissive", "mpich2", "mpl-1.0", "mpl-1.1", "mpl-2.0", "mpl-2.0-no-copyleft-exception", "mplus", "ms-lpl", "ms-pl", "ms-rl", "mtll", "mulanpsl-1.0", "mulanpsl-2.0", "multics", "mup", "naist-2003", "nasa-1.3", "naumen", "nbpl-1.0", "ncbi-pd", "ncgl-uk-2.0", "ncl", "ncsa", "net-snmp", "netcdf", "newsletr", "ngpl", "nicta-1.0", "nist-pd", "nist-pd-fallback", "nist-software", "nlod-1.0", "nlod-2.0", "nlpl", "nokia", "nosl", "noweb", "npl-1.0", "npl-1.1", "nposl-3.0", "nrl", "ntp", "ntp-0", "nunit", "o-uda-1.0", "oar", "occt-pl", "oclc-2.0", "odbl-1.0", "odc-by-1.0", "offis", "ofl-1.0", "ofl-1.0-no-rfn", "ofl-1.0-rfn", "ofl-1.1", "ofl-1.1-no-rfn", "ofl-1.1-rfn", "ogc-1.0", "ogdl-taiwan-1.0", "ogl-canada-2.0", "ogl-uk-1.0", "ogl-uk-2.0", "ogl-uk-3.0", "ogtsl", "oldap-1.1", "oldap-1.2", "oldap-1.3", "oldap-1.4", "oldap-2.0", "oldap-2.0.1", "oldap-2.1", "oldap-2.2", "oldap-2.2.1", "oldap-2.2.2", "oldap-2.3", "oldap-2.4", "oldap-2.5", "oldap-2.6", "oldap-2.7", "oldap-2.8", "olfl-1.3", "oml", "openpbs-2.3", "openssl", "openssl-standalone", "openvision", "opl-1.0", "opl-uk-3.0", "opubl-1.0", "oset-pl-2.1", "osl-1.0", "osl-1.1", "osl-2.0", "osl-2.1", "osl-3.0", "padl", "parity-6.0.0", "parity-7.0.0", "pddl-1.0", "php-3.0", "php-3.01", "pixar", "pkgconf", "plexus", "pnmstitch", "polyform-noncommercial-1.0.0", "polyform-small-business-1.0.0", "postgresql", "ppl", "psf-2.0", "psfrag", "psutils", "python-2.0", "python-2.0.1", "python-ldap", "qhull", "qpl-1.0", "qpl-1.0-inria-2004", "radvd", "rdisc", "rhecos-1.1", "rpl-1.1", "rpl-1.5", "rpsl-1.0", "rsa-md", "rscpl", "ruby", "ruby-pty", "sax-pd", "sax-pd-2.0", "saxpath", "scea", "schemereport", "sendmail", "sendmail-8.23", "sendmail-open-source-1.1", "sgi-b-1.0", "sgi-b-1.1", "sgi-b-2.0", "sgi-opengl", "sgp4", "shl-0.5", "shl-0.51", "simpl-2.0", "sissl", "sissl-1.2", "sl", "sleepycat", "smail-gpl", "smlnj", "smppl", "snia", "snprintf", "softsurfer", "soundex", "spencer-86", "spencer-94", "spencer-99", "spl-1.0", "ssh-keyscan", "ssh-openssh", "ssh-short", "ssleay-standalone", "sspl-1.0", "standardml-nj", "sugarcrm-1.1.3", "sun-ppp", "sun-ppp-2000", "sunpro", "swl", "swrule", "symlinks", "tapr-ohl-1.0", "tcl", "tcp-wrappers", "termreadkey", "tgppl-1.0", "thirdeye", "threeparttable", "tmate", "torque-1.1", "tosl", "tpdl", "tpl-1.0", "trustedqsl", "ttwl", "ttyp0", "tu-berlin-1.0", "tu-berlin-2.0", "ubuntu-font-1.0", "ucar", "ucl-1.0", "ulem", "umich-merit", "unicode-3.0", "unicode-dfs-2015", "unicode-dfs-2016", "unicode-tou", "unixcrypt", "unlicense", "upl-1.0", "urt-rle", "vim", "vostrom", "vsl-1.0", "w3c", "w3c-19980720", "w3c-20150513", "w3m", "watcom-1.0", "widget-workshop", "wsuipa", "wtfpl", "wwl", "wxwindows", "x11", "x11-distribute-modifications-variant", "x11-swapped", "xdebug-1.03", "xerox", "xfig", "xfree86-1.1", "xinetd", "xkeyboard-config-zinoviev", "xlock", "xnet", "xpp", "xskat", "xzoom", "ypl-1.0", "ypl-1.1", "zed", "zeeff", "zend-2.0", "zimbra-1.3", "zimbra-1.4", "zlib", "zlib-acknowledgement", "zpl-1.1", "zpl-2.0", "zpl-2.1" } ================================================ FILE: conan/tools/scm/__init__.py ================================================ from conan.tools.scm.git import Git from conan.internal.model.version import Version ================================================ FILE: conan/tools/scm/git.py ================================================ import fnmatch import os from conan.api.output import Color from conan.tools.files import chdir, update_conandata from conan.errors import ConanException from conan.internal.model.conf import ConfDefinition from conan.internal.util.files import mkdir from conan.internal.util.runners import check_output_runner class Git: """ Git is a wrapper for several common patterns used with *git* tool. """ def __init__(self, conanfile, folder=".", excluded=None): """ :param conanfile: Conanfile instance. :param folder: Current directory, by default ``.``, the current working directory. :param excluded: Files to be excluded from the "dirty" checks. It will compose with the configuration ``core.scm:excluded`` (the configuration has higher priority). It is a list of patterns to ``fnmatch``. """ self._conanfile = conanfile self.folder = folder self._excluded = excluded global_conf = conanfile._conan_helpers.global_conf # noqa _conan_helpers conf_excluded = global_conf.get("core.scm:excluded", check_type=list) if conf_excluded: if excluded: c = ConfDefinition() c.loads(f"core.scm:excluded={excluded}") c.update_conf_definition(global_conf) self._excluded = c.get("core.scm:excluded", check_type=list) else: self._excluded = conf_excluded self._local_url = global_conf.get("core.scm:local_url", choices=["allow", "block"]) def run(self, cmd, hidden_output=None): """ Executes ``git `` :return: The console output of the command. """ print_cmd = cmd if hidden_output is None else cmd.replace(hidden_output, "") self._conanfile.output.info(f"RUN: git {print_cmd}", fg=Color.BRIGHT_BLUE) with chdir(self._conanfile, self.folder): # We tried to use self.conanfile.run(), but it didn't work: # - when using win_bash, crashing because access to .settings (forbidden in source()) # - the ``conan source`` command, not passing profiles, buildenv not injected return check_output_runner("git {}".format(cmd)).strip() def get_commit(self, repository=False): """ :param repository: By default gets the commit of the defined folder, use repo=True to get the commit of the repository instead. :return: The current commit, with ``git rev-list HEAD -n 1 -- ``. The latest commit is returned, irrespective of local not committed changes. """ try: # commit = self.run("rev-parse HEAD") For the whole repo # This rev-list knows to capture the last commit for the folder # --full-history is needed to not avoid wrong commits: # https://github.com/conan-io/conan/issues/10971 # https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt-Defaultmode path = '' if repository else '-- "."' commit = self.run(f'rev-list HEAD -n 1 --full-history {path}') return commit except Exception as e: raise ConanException("Unable to get git commit in '%s': %s" % (self.folder, str(e))) def get_remote_url(self, remote="origin"): """ Obtains the URL of the remote git remote repository, with ``git remote -v`` **Warning!** Be aware that This method will get the output from ``git remote -v``. If you added tokens or credentials to the remote in the URL, they will be exposed. Credentials shouldn’t be added to git remotes definitions, but using a credentials manager or similar mechanism. If you still want to use this approach, it is your responsibility to strip the credentials from the result. :param remote: Name of the remote git repository ('origin' by default). :return: URL of the remote git remote repository. """ remotes = self.run("remote -v") for r in remotes.splitlines(): name, url = r.split(maxsplit=1) if name == remote: url, _ = url.rsplit(None, 1) # if the url still has (fetch) or (push) at the end if url.endswith("(fetch)") or url.endswith("(push)"): url, _ = url.rsplit(None, 1) if os.path.exists(url): # Windows local directory url = url.replace("\\", "/") return url def commit_in_remote(self, commit, remote="origin"): """ Checks that the given commit exists in the remote, with ``branch -r --contains `` and checking an occurrence of a branch in that remote exists. :param commit: Commit to check. :param remote: Name of the remote git repository ('origin' by default). :return: True if the given commit exists in the remote, False otherwise. """ if not remote: return False # Potentially do two checks here. If the clone is a shallow clone, then we won't be # able to find the commit. try: branches = self.run("branch -r --contains {}".format(commit)) if "{}/".format(remote) in branches: return True except Exception as e: raise ConanException("Unable to check remote commit in '%s': %s" % (self.folder, str(e))) try: # This will raise if commit not present. self.run(f"fetch {remote} --refetch --dry-run {commit}") return True except (Exception,): # For example, if using an older git<2.36, see # https://github.com/conan-io/conan/issues/18470, then lets try the old approach try: # This will raise if commit not present. self.run("fetch {} --dry-run --depth=1 {}".format(remote, commit)) return True except (Exception,): # Don't raise an error because it could fail for many more reasons. return False def is_dirty(self, repository=False): """ Returns if the current folder is dirty, running ``git status -s`` The ``Git(..., excluded=[])`` argument and the ``core.scm:excluded`` configuration will define file patterns to be skipped from this check. :param repository: By default checks if the current folder is dirty. If repository=True it will check the root repository folder instead, not the current one. :return: True, if the current folder is dirty. Otherwise, False. """ path = '' if repository else '.' status = self.run(f"status {path} --short --no-branch --untracked-files").strip() self._conanfile.output.debug(f"Git status:\n{status}") if not self._excluded: return bool(status) # Parse the status output, line by line, and match it with "_excluded" lines = [line.strip() for line in status.splitlines()] # line is of the form STATUS PATH, get the path by splitting # (Taking into account that STATUS is one word, PATH might be many) lines = [line.split(maxsplit=1)[1].strip('"') for line in lines if line] lines = [line for line in lines if not any(fnmatch.fnmatch(line, p) for p in self._excluded)] self._conanfile.output.debug(f"Filtered git status: {lines}") return bool(lines) def get_url_and_commit(self, remote="origin", repository=False): """ This is an advanced method, that returns both the current commit, and the remote repository url. This method is intended to capture the current remote coordinates for a package creation, so that can be used later to build again from sources from the same commit. This is the behavior: * If the repository is dirty, it will raise an exception. Doesn’t make sense to capture coordinates of something dirty, as it will not be reproducible. If there are local changes, and the user wants to test a local conan create, should commit the changes first (locally, not push the changes). * If the repository is not dirty, but the commit doesn’t exist in the given remote, the method will return that commit and the URL of the local user checkout. This way, a package can be conan create created locally, testing everything works, before pushing some changes to the remote. * If the repository is not dirty, and the commit exists in the specified remote, it will return that commit and the url of the remote. **Warning!** Be aware that This method will get the output from ``git remote -v``. If you added tokens or credentials to the remote in the URL, they will be exposed. Credentials shouldn’t be added to git remotes definitions, but using a credentials manager or similar mechanism. If you still want to use this approach, it is your responsibility to strip the credentials from the result. :param remote: Name of the remote git repository ('origin' by default). :param repository: By default gets the commit of the defined folder, use repo=True to get the commit of the repository instead. :return: (url, commit) tuple """ dirty = self.is_dirty(repository=repository) if dirty: raise ConanException("Repo is dirty, cannot capture url and commit: " "{}".format(self.folder)) commit = self.get_commit(repository=repository) url = self.get_remote_url(remote=remote) in_remote = self.commit_in_remote(commit, remote=remote) if in_remote: return url, commit if self._local_url == "block": raise ConanException(f"Current commit {commit} doesn't exist in remote {remote}\n" "Failing according to 'core.scm:local_url=block' conf") if self._local_url != "allow": self._conanfile.output.warning("Current commit {} doesn't exist in remote {}\n" "This revision will not be buildable in other " "computer".format(commit, remote)) return self.get_repo_root(), commit def get_repo_root(self): """ Get the current repository top folder with ``git rev-parse --show-toplevel`` :return: Repository top folder. """ folder = self.run("rev-parse --show-toplevel") return folder.replace("\\", "/") def clone(self, url, target="", args=None, hide_url=True): """ Performs a ``git clone `` operation, where target is the target directory. :param url: URL of remote repository. :param target: Target folder. :param args: Extra arguments to pass to the git clone as a list. :param hide_url: Hides the URL from the log output to prevent accidental credential leaks. Can be disabled by passing ``False``. """ args = args or [] if os.path.exists(url): url = url.replace("\\", "/") # Windows local directory mkdir(self.folder) self._conanfile.output.info("Cloning git repo") target_path = f'"{target}"' if target else "" # quote in case there are spaces in path # Avoid printing the clone command, it can contain tokens self.run('clone "{}" {} {}'.format(url, " ".join(args), target_path), hidden_output=url if hide_url else None) def fetch_commit(self, url, commit, hide_url=True): """ Experimental: does a single commit fetch and checkout, instead of a full clone, should be faster. :param url: URL of remote repository. :param commit: The commit ref to checkout. :param hide_url: Hides the URL from the log output to prevent accidental credential leaks. Can be disabled by passing ``False``. """ if os.path.exists(url): url = url.replace("\\", "/") # Windows local directory mkdir(self.folder) self._conanfile.output.info("Shallow fetch of git repo") self.run('init') self.run(f'remote add origin "{url}"', hidden_output=url if hide_url else None) self.run(f'fetch --depth 1 origin {commit}') self.run('checkout FETCH_HEAD') def checkout(self, commit): """ Checkouts the given commit using ``git checkout ``. :param commit: Commit to checkout. """ self._conanfile.output.info("Checkout: {}".format(commit)) self.run('checkout {}'.format(commit)) def included_files(self): """ Run ``git ls-files --full-name --others --cached --exclude-standard`` to the get the list of files not ignored by ``.gitignore`` :return: List of files. """ files = self.run("ls-files --full-name --others --cached --exclude-standard") files = files.splitlines() return files def coordinates_to_conandata(self, repository=False): """ Capture the "url" and "commit" from the Git repo, calling ``get_url_and_commit()``, and then store those in the ``conandata.yml`` under the "scm" key. This information can be used later to clone and checkout the exact source point that was used to create this package, and can be useful even if the recipe uses ``exports_sources`` as mechanism to embed the sources. :param repository: By default gets the commit of the defined folder, use repository=True to get the commit of the repository instead. """ scm_url, scm_commit = self.get_url_and_commit(repository=repository) update_conandata(self._conanfile, {"scm": {"commit": scm_commit, "url": scm_url}}) def checkout_from_conandata_coordinates(self): """ Reads the "scm" field from the ``conandata.yml``, that must contain at least "url" and "commit" and then do a ``clone(url, target=".")``, ``fetch ``, followed by a ``checkout(commit)``. """ sources = self._conanfile.conan_data["scm"] self.clone(url=sources["url"], target=".", args=["--origin=origin"]) self.run(f"fetch origin {sources['commit']}") self.checkout(commit=sources["commit"]) ================================================ FILE: conan/tools/scons/__init__.py ================================================ from conan.tools.scons.sconsdeps import SConsDeps ================================================ FILE: conan/tools/scons/sconsdeps.py ================================================ from jinja2 import Template from conan.tools import CppInfo from conan.internal.util.files import save class SConsDeps: def __init__(self, conanfile): self._conanfile = conanfile self._ordered_deps = None self._generator_file = 'SConscript_conandeps' @property def ordered_deps(self): if self._ordered_deps is None: deps = self._conanfile.dependencies.host.topological_sort self._ordered_deps = [dep for dep in reversed(deps.values())] return self._ordered_deps def _get_cpp_info(self): ret = CppInfo(self._conanfile) for dep in self.ordered_deps: dep_cppinfo = dep.cpp_info.aggregated_components() ret.merge(dep_cppinfo) return ret def generate(self): save(self._generator_file, self._content) @property def _content(self): template = Template(""" "{{dep_name}}" : { "CPPPATH" : {{info.includedirs or []}}, "LIBPATH" : {{info.libdirs or []}}, "BINPATH" : {{info.bindirs or []}}, "LIBS" : {{(info.libs or []) + (info.system_libs or [])}}, "FRAMEWORKS" : {{info.frameworks or []}}, "FRAMEWORKPATH" : {{info.frameworkdirs or []}}, "CPPDEFINES" : {{info.defines or []}}, "CXXFLAGS" : {{info.cxxflags or []}}, "CCFLAGS" : {{info.cflags or []}}, "SHLINKFLAGS" : {{info.sharedlinkflags or []}}, "LINKFLAGS" : {{info.exelinkflags or []}}, }, {% if dep_version is not none %}"{{dep_name}}_version" : "{{dep_version}}",{% endif %} """) sections = ["conandeps = {\n"] all_flags = template.render(dep_name="conandeps", dep_version=None, info=self._get_cpp_info()) sections.append(all_flags) # TODO: Add here in 2.0 the "skip": False trait host_req = self._conanfile.dependencies.filter({"build": False}).values() for dep in host_req: dep_flags = template.render(dep_name=dep.ref.name, dep_version=dep.ref.version, info=dep.cpp_info) sections.append(dep_flags) sections.append("}\n") sections.append("Return('conandeps')\n") return "\n".join(sections) ================================================ FILE: conan/tools/system/__init__.py ================================================ from conan.tools.system.python_manager import PyEnv, PipEnv ================================================ FILE: conan/tools/system/package_manager.py ================================================ import platform from conan.tools.build import cross_building from conan.internal.graph.graph import CONTEXT_BUILD from conan.errors import ConanException class _SystemPackageManagerTool: mode_check = "check" # Check if installed, fail if not mode_install = "install" mode_report = "report" # Only report what would be installed, no check (can run in any system) mode_report_installed = "report-installed" # report installed and missing packages tool_name = None version_separator = "" install_command = "" update_command = "" check_command = "" check_version_command = "" full_package_name = "{name}{arch_separator}{arch_name}" accepted_install_codes = [0] accepted_update_codes = [0] accepted_check_codes = [0, 1] def __init__(self, conanfile): """ :param conanfile: The current recipe object. Always use ``self``. """ self._conanfile = conanfile self._active_tool = self._conanfile.conf.get("tools.system.package_manager:tool", default=self.get_default_tool()) self._sudo = self._conanfile.conf.get("tools.system.package_manager:sudo", default=False, check_type=bool) self._sudo_askpass = self._conanfile.conf.get("tools.system.package_manager:sudo_askpass", default=False, check_type=bool) self._mode = self._conanfile.conf.get("tools.system.package_manager:mode", default=self.mode_check) self._arch = self._conanfile.settings_build.get_safe('arch') \ if self._conanfile.context == CONTEXT_BUILD else self._conanfile.settings.get_safe('arch') self._arch_names = {} self._arch_separator = "" def get_default_tool(self): os_name = platform.system() if os_name in ["Linux", "FreeBSD"]: import distro os_name = distro.id() or os_name elif os_name == "Windows" and self._conanfile.settings.get_safe("os.subsystem") == "msys2": os_name = "msys2" manager_mapping = {"apt-get": ["Linux", "ubuntu", "debian", "raspbian", "linuxmint", 'astra', 'elbrus', 'altlinux', 'pop'], "apk": ["alpine"], "yum": ["pidora", "scientific", "xenserver", "amazon", "amzn"], "dnf": ["fedora", "rhel", "centos", "mageia", "nobara", "almalinux", "rocky", "oracle"], "brew": ["Darwin"], "pacman": ["arch", "manjaro", "msys2", "endeavouros"], "choco": ["Windows"], "zypper": ["opensuse", "sles"], "pkg": ["freebsd"], "pkgutil": ["Solaris"]} # first check exact match of name for tool, distros in manager_mapping.items(): if os_name in distros: return tool # in case we did not detect any exact match, check # if the name is contained inside the returned distro name # like for opensuse, that can have opensuse-version names for tool, distros in manager_mapping.items(): for d in distros: if d in os_name: return tool # No default package manager was found for the system, # so notify the user self._conanfile.output.info("A default system package manager couldn't be found for {}, " "system packages will not be installed.".format(os_name)) def _split_package_name(self, package, host_package): name, version = (package.split("=")[0], package.split("=")[1]) if "=" in package else (package, "") arch_separator, arch_name = "", "" version_separator = self.version_separator if version else "" if self._arch in self._arch_names and cross_building(self._conanfile) and host_package: arch_separator = self._arch_separator arch_name = self._arch_names.get(self._arch) return name, version, arch_separator, arch_name, version_separator def get_package_name(self, package, host_package=True): # Only if the package is for building, for example a library, # we should add the host arch when cross building. # If the package is a tool that should be installed on the current build # machine we should not add the arch. name, version, arch_separator, arch_name, version_separator = self._split_package_name(package, host_package) return self.full_package_name.format(name=name, arch_separator=arch_separator, arch_name=arch_name, version_separator=version_separator, version=version) @property def sudo_str(self): sudo = "sudo " if self._sudo else "" askpass = "-A " if self._sudo and self._sudo_askpass else "" return "{}{}".format(sudo, askpass) def run(self, method, *args, **kwargs): if self._active_tool == self.__class__.tool_name: return method(*args, **kwargs) def _conanfile_run(self, command, accepted_returns, quiet=True): # When checking multiple packages, this is too noisy ret = self._conanfile.run(command, ignore_errors=True, quiet=quiet) if ret not in accepted_returns: raise ConanException("Command '%s' failed" % command) return ret def install_substitutes(self, *args, **kwargs): """ Will try to call the install() method with several lists of packages passed as a variable number of parameters. This is useful if, for example, the names of the packages are different from one distro or distro version to another. For example, ``libxcb`` for ``Apt`` is named ``libxcb-util-dev`` in Ubuntu >= 15.0 and ``libxcb-util0-dev`` for other versions. You can call to: .. code-block:: python # will install the first list of packages that succeeds in the installation Apt.install_substitutes(["libxcb-util-dev"], ["libxcb-util0-dev"]) :param packages_alternatives: try to install the list of packages passed as a parameter. :param update: try to update the package manager database before checking and installing. :param check: check if the packages are already installed before installing them. :return: the return code of the executed package manager command. """ return self.run(self._install_substitutes, *args, **kwargs) def install(self, *args, **kwargs): """ Will try to install the list of packages passed as a parameter. Its behaviour is affected by the value of ``tools.system.package_manager:mode`` :ref:`configuration`. :param packages: try to install the list of packages passed as a parameter. :param update: try to update the package manager database before checking and installing. :param check: check if the packages are already installed before installing them. :return: the return code of the executed package manager command. """ return self.run(self._install, *args, **kwargs) def update(self, *args, **kwargs): """ Update the system package manager database. Its behaviour is affected by the value of ``tools.system.package_manager:mode`` :ref:`configuration`. :return: the return code of the executed package manager update command. """ return self.run(self._update, *args, **kwargs) def check(self, *args, **kwargs): """ Check if the list of packages passed as parameter are already installed. :param packages: list of packages to check. :return: list of packages from the packages argument that are not installed in the system. """ return self.run(self._check, *args, **kwargs) def _install_substitutes(self, *packages_substitutes, update=False, check=True, **kwargs): errors = [] for packages in packages_substitutes: try: return self.install(packages, update, check, **kwargs) except ConanException as e: errors.append(e) for error in errors: self._conanfile.output.warning(str(error)) raise ConanException("None of the installs for the package substitutes succeeded.") def _install(self, packages, update=False, check=True, host_package=True, **kwargs): orig_packages = packages pkgs = self._conanfile.system_requires.setdefault(self._active_tool, {}) install_pkgs = pkgs.setdefault("install", []) install_pkgs.extend(p for p in packages if p not in install_pkgs) if self._mode == self.mode_report: return if check or self._mode in (self.mode_check, self.mode_report_installed): packages = self.check(packages, host_package=host_package) missing_pkgs = pkgs.setdefault("missing", []) missing_pkgs.extend(p for p in packages if p not in missing_pkgs) if self._mode == self.mode_report_installed: return if self._mode == self.mode_check and packages: raise ConanException("System requirements: '{0}' are missing but can't install " "because tools.system.package_manager:mode is '{1}'." "Please update packages manually or set " "'tools.system.package_manager:mode' " "to '{2}' in the [conf] section of the profile, " "or in the command line using " "'-c tools.system.package_manager:mode={2}'".format(", ".join(packages), self.mode_check, self.mode_install)) elif packages: if update: self.update() packages_arch = [self.get_package_name(package, host_package=host_package) for package in packages] if packages_arch: command = self.install_command.format(sudo=self.sudo_str, tool=self.tool_name, packages=" ".join(packages_arch), **kwargs) return self._conanfile_run(command, self.accepted_install_codes, quiet=False) else: self._conanfile.output.info(f"System requirements: {' '.join(orig_packages)} already installed") def _update(self): # we just update the package manager database in case we are in 'install mode' # in case we are in check mode just ignore if self._mode == self.mode_install: command = self.update_command.format(sudo=self.sudo_str, tool=self.tool_name) return self._conanfile_run(command, self.accepted_update_codes, quiet=False) def _check(self, packages, host_package=True): missing = [pkg for pkg in packages if self.check_package(pkg, host_package) != 0] return missing def check_package(self, package, host_package=True): name, version, arch_separator, arch_name, _ = self._split_package_name(package, host_package) check_arch = self._arch if host_package else self._conanfile.settings_build.get_safe('arch') arch_package = arch_name or self._arch_names.get(check_arch) package = self.full_package_name.format(name=name, arch_separator=arch_separator, arch_name=arch_name, version="", version_separator="") command = self.check_command.format(tool=self.tool_name, package=package, arch_package=arch_package, base_name=name) if version: if self.check_version_command: command = self.check_version_command.format(tool=self.tool_name, package=package, version=version, arch_package=arch_package, base_name=name) else: self._conanfile.output.warning(f"System requirements: \"{self.tool_name}\" doesn't support package versions," f" \"{package}\" will be installed without a specific version.") return self._conanfile_run(command, self.accepted_check_codes) class Apt(_SystemPackageManagerTool): # TODO: apt? apt-get? tool_name = "apt-get" version_separator = "=" full_package_name = "{name}{arch_separator}{arch_name}{version_separator}{version}" install_command = "{sudo}{tool} install -y {recommends}{packages}" update_command = "{sudo}{tool} update" check_command = r"dpkg-query -W -f='${{Architecture}}\n' {base_name} | grep -qEx '({arch_package}|all)'" check_version_command = r"dpkg-query -W -f='${{Architecture}} ${{Version}}\n' {base_name} | grep -qEx '({arch_package}|all) {version}'" def __init__(self, conanfile, arch_names=None): """ :param conanfile: The current recipe object. Always use ``self``. :param arch_names: This argument maps the Conan architecture setting with the package manager tool architecture names. It is ``None`` by default, which means that it will use a default mapping for the most common architectures. For example, if you are using ``x86_64`` Conan architecture setting, it will map this value to ``amd64`` for *Apt* and try to install the ``:amd64`` package. """ super(Apt, self).__init__(conanfile) self._arch_names = {"x86_64": "amd64", "x86": "i386", "ppc32": "powerpc", "ppc64le": "ppc64el", "armv7": "arm", "armv7hf": "armhf", "armv8": "arm64", "s390x": "s390x", "riscv64": "riscv64"} if arch_names is None else arch_names self._arch_separator = ":" def install(self, packages, update=False, check=True, recommends=False, host_package=True): """ Will try to install the list of packages passed as a parameter. Its behaviour is affected by the value of ``tools.system.package_manager:mode`` :ref:`configuration`. :param packages: try to install the list of packages passed as a parameter. :param update: try to update the package manager database before checking and installing. :param check: check if the packages are already installed before installing them. :param host_package: install the packages for the host machine architecture (the machine that will run the software), it has an effect when cross building. :param recommends: if the parameter ``recommends`` is ``False`` it will add the ``'--no-install-recommends'`` argument to the *apt-get* command call. :return: the return code of the executed apt command. """ recommends_str = '' if recommends else '--no-install-recommends ' return super(Apt, self).install(packages, update=update, check=check, host_package=host_package, recommends=recommends_str) class Yum(_SystemPackageManagerTool): tool_name = "yum" version_separator = "-" full_package_name = "{name}{version_separator}{version}{arch_separator}{arch_name}" install_command = "{sudo}{tool} install -y {packages}" update_command = "{sudo}{tool} check-update -y" check_command = "rpm -q {package}" check_version_command = "rpm -q {package}-{version}" accepted_update_codes = [0, 100] def __init__(self, conanfile, arch_names=None): """ :param conanfile: the current recipe object. Always use ``self``. :param arch_names: this argument maps the Conan architecture setting with the package manager tool architecture names. It is ``None`` by default, which means that it will use a default mapping for the most common architectures. For example, if you are using ``x86`` Conan architecture setting, it will map this value to ``i?86`` for *Yum* and try to install the ``.i?86`` package. """ super(Yum, self).__init__(conanfile) self._arch_names = {"x86_64": "x86_64", "x86": "i?86", "ppc32": "powerpc", "ppc64le": "ppc64le", "armv7": "armv7", "armv7hf": "armv7hl", "armv8": "aarch64", "s390x": "s390x", "riscv64": "riscv64"} if arch_names is None else arch_names self._arch_separator = "." class Dnf(Yum): tool_name = "dnf" version_separator = "-" full_package_name = "{name}{version_separator}{version}{arch_separator}{arch_name}" check_version_command = "rpm -q {package}-{version}" class Brew(_SystemPackageManagerTool): tool_name = "brew" version_separator = "@" full_package_name = "{name}{version_separator}{version}" install_command = "{sudo}{tool} install {packages}" update_command = "{sudo}{tool} update" check_command = 'test -n "$({tool} ls --versions {package})"' check_version_command = 'brew list --versions {package} | grep "{version}"' class Pkg(_SystemPackageManagerTool): tool_name = "pkg" version_separator = "-" full_package_name = "{name}{version_separator}{version}" install_command = "{sudo}{tool} install -y {packages}" update_command = "{sudo}{tool} update" check_command = "{tool} info {package}" check_version_command = "{tool} info {package} | grep \"Version: {version}\"" class PkgUtil(_SystemPackageManagerTool): tool_name = "pkgutil" version_separator = "@" full_package_name = "{name}{version_separator}{version}" install_command = "{sudo}{tool} --install --yes {packages}" update_command = "{sudo}{tool} --catalog" check_command = 'test -n "`{tool} --list {package}`"' class Chocolatey(_SystemPackageManagerTool): tool_name = "choco" version_separator = " --version " full_package_name = "{name}{version_separator}{version}" install_command = "{tool} install --yes {packages}" update_command = "{tool} outdated" check_command = '{tool} list --exact {package} | findstr /c:"1 packages installed."' check_version_command = '{tool} list --local-only {package} | findstr /i "{version}"' class PacMan(_SystemPackageManagerTool): tool_name = "pacman" install_command = "{sudo}{tool} -S --noconfirm {packages}" update_command = "{sudo}{tool} -Syyu --noconfirm" check_command = "{tool} -Qi {package}" def __init__(self, conanfile, arch_names=None): """ :param conanfile: the current recipe object. Always use ``self``. :param arch_names: this argument maps the Conan architecture setting with the package manager tool architecture names. It is ``None`` by default, which means that it will use a default mapping for the most common architectures. If you are using ``x86`` Conan architecture setting, it will map this value to ``lib32`` for *PacMan* and try to install the ``-lib32`` package. """ super(PacMan, self).__init__(conanfile) self._arch_names = {"x86": "lib32"} if arch_names is None else arch_names self._arch_separator = "-" class Apk(_SystemPackageManagerTool): tool_name = "apk" version_separator = "=" full_package_name = "{name}{version_separator}{version}" install_command = "{sudo}{tool} add --no-cache {packages}" update_command = "{sudo}{tool} update" check_command = "{tool} info -e {package}" check_version_command = "{tool} info {package} | grep \"{version}\"" def __init__(self, conanfile, _arch_names=None): """ Constructor method. Note that *Apk* does not support architecture names since Alpine Linux does not support multiarch. Therefore, the ``arch_names`` argument is ignored. :param conanfile: the current recipe object. Always use ``self``. """ super(Apk, self).__init__(conanfile) self._arch_names = {} self._arch_separator = "" class Zypper(_SystemPackageManagerTool): tool_name = "zypper" version_separator = "=" # < or > full_package_name = "{name}{arch_separator}{arch_name}{version_separator}{version}" install_command = "{sudo}{tool} --non-interactive in {packages}" update_command = "{sudo}{tool} --non-interactive ref" check_command = "rpm -q {package}" check_version_command = "rpm -q {package}-{version}" ================================================ FILE: conan/tools/system/python_manager.py ================================================ import platform import os import shutil import sys from conan.tools.build import cmd_args_to_string from conan.tools.env.environment import Environment from conan.errors import ConanException from conan.api.output import ( ConanOutput, LEVEL_QUIET, LEVEL_ERROR, LEVEL_WARNING, LEVEL_STATUS, LEVEL_VERBOSE, LEVEL_DEBUG, LEVEL_TRACE, ) from conan.internal.util.files import rmdir def _get_pip_verbosity(): return { LEVEL_QUIET: "-qqq", LEVEL_ERROR: "-qq", LEVEL_WARNING: "-q", LEVEL_VERBOSE: "-v", LEVEL_DEBUG: "-vv", LEVEL_TRACE: "-vvv", }.get(ConanOutput.get_output_level(), "") def _get_uv_verbosity(): return { LEVEL_QUIET: "-qq", LEVEL_ERROR: "-qq", LEVEL_WARNING: "-q", LEVEL_VERBOSE: "--verbose", LEVEL_DEBUG: "--verbose", LEVEL_TRACE: "--verbose", }.get(ConanOutput.get_output_level(), "") class PyEnv: def __init__(self, conanfile, folder=None, name="", py_version=None): """ :param conanfile: The current conanfile ``self`` :param folder: Optional folder, by default the ``build_folder`` :param name: Optional name for the virtualenv, by default ``conan_pyenv`` :param py_version: Optional python version to create the virtualenv using UV """ if sys.version_info.minor < 8 and py_version: raise ConanException("'uv' needs Python >= 3.8. Please upgrade your Python interpreter" " or use 'PyEnv' without defining the 'py_version'.") self._conanfile = conanfile self._default_python = self._conanfile.conf.get("tools.system.pyenv:python_interpreter") # tools.system.pipenv deprecated warning message. if not self._default_python and self._conanfile.conf.get("tools.system.pipenv:python_interpreter"): self._default_python = self._conanfile.conf.get("tools.system.pipenv:python_interpreter") ConanOutput().warning("'tools.system.pipenv:python_interpreter' " "is deprecated, use 'tools.system.pyenv:python_interpreter'", warn_tag="deprecated") if not self._default_python: python = "python" if platform.system() == "Windows" else "python3" default_python = shutil.which(python) self._default_python = os.path.realpath(default_python) if default_python else None if not self._default_python: raise ConanException("Conan could not find a Python executable path. Please, install " "Python system-wide or set the " "'tools.system.pyenv:python_interpreter' " "conf to the full path of a Python executable") self._env_name = f"conan_pyenv{f'_{name}' if name else ''}" if py_version: self._env_name += f'_{py_version.replace(".", "_")}' base_env_dir = os.path.abspath(folder or conanfile.build_folder) self._env_dir = os.path.join(base_env_dir, self._env_name) if not os.path.exists(self._env_dir): if py_version: self._create_uv_venv(base_env_dir, py_version) else: self._create_venv() @property def env_dir(self): """Root directory of the virtual environment.""" return self._env_dir.replace("\\", "/") @property def env_exe(self): """Path to the Python executable inside the virtual environment.""" return self._get_env_python(self._env_dir).replace("\\", "/") @property def bin_path(self): """Path to the bin or Scripts directory inside the virtual environment.""" bins = "Scripts" if platform.system() == "Windows" else "bin" return os.path.join(self._env_dir, bins).replace("\\", "/") @staticmethod def _get_env_python(env_dir): _env_bin_dir = os.path.join(env_dir, "Scripts" if platform.system() == "Windows" else "bin") return os.path.join(_env_bin_dir, "python.exe" if platform.system() == "Windows" else "python") def generate(self): """ Create a conan environment to use the python venv in the next steps of the conanfile. """ env = Environment() env.prepend_path("PATH", self.bin_path) env.vars(self._conanfile).save_script(self._env_name) def run(self, args): return self._conanfile.run(cmd_args_to_string([self.env_exe] + list(args))) def install(self, packages, pip_args=None): """ Will try to install the list of pip packages passed as a parameter. :param packages: try to install the list of pip packages passed as a parameter. :param pip_args: additional argument list to be passed to the 'pip install' command, e.g.: ['--no-cache-dir', '--index-url', 'https://my.pypi.org/simple']. Defaults to ``None``. :return: the return code of the executed pip command. """ args = [self.env_exe, "-m", "pip", "install", "--disable-pip-version-check"] pip_verbosity = _get_pip_verbosity() if pip_verbosity: args.append(pip_verbosity) if pip_args: args.extend(pip_args) args += [f'"{p}"' for p in packages] command = " ".join(args) return self._conanfile.run(command) def _create_venv(self): try: self._conanfile.run(cmd_args_to_string([self._default_python, '-m', 'venv', self._env_dir])) except ConanException as e: raise ConanException(f"PyEnv could not create a Python virtual " f"environment using '{self._default_python}': {e}") def _create_uv_venv(self, base_env_dir, py_version): uv_env_dir = None try: uv_path = shutil.which("uv") if uv_path: uv_cmd = [uv_path] else: uv_env_dir = os.path.join(base_env_dir, f"uv_{self._env_name}") self._conanfile.run(cmd_args_to_string( [self._default_python, '-m', 'venv', uv_env_dir]) ) python_exe = self._get_env_python(uv_env_dir) pip_args = [python_exe, "-m", "pip", "install", "--disable-pip-version-check"] pip_verbosity = _get_pip_verbosity() if pip_verbosity: pip_args.append(pip_verbosity) pip_args.append("uv") self._conanfile.run(cmd_args_to_string(pip_args)) uv_cmd = [python_exe, "-m", "uv"] uv_venv_args = uv_cmd + ['venv', '--seed', '--python', py_version, self._env_dir] uv_verbosity = _get_uv_verbosity() if uv_verbosity: uv_venv_args.append(uv_verbosity) self._conanfile.run(cmd_args_to_string(uv_venv_args)) self._conanfile.output.info(f"Virtual environment for Python " f"{py_version} created successfully using UV.") except Exception as e: raise ConanException(f"PyEnv could not create a Python {py_version} virtual " f"environment using UV and '{self._default_python}': {e}") finally: if uv_env_dir: rmdir(uv_env_dir) class PipEnv(PyEnv): def __init__(self, conanfile, folder=None, name="", py_version=None): super().__init__(conanfile, folder, name, py_version) ConanOutput().warning("'PipEnv()' is deprecated, use 'PyEnv()'", warn_tag="deprecated") ================================================ FILE: conans/__init__.py ================================================ ================================================ FILE: conans/conan.py ================================================ import sys from conan.cli.cli import main def run(): main(sys.argv[1:]) if __name__ == '__main__': import multiprocessing multiprocessing.freeze_support() run() ================================================ FILE: conans/conan_server.py ================================================ import argparse import os from conans.server.launcher import ServerLauncher def run(): parser = argparse.ArgumentParser(description='Launch the server') parser.add_argument('--migrate', default=False, action='store_true', help='Run the pending migrations') parser.add_argument('--server_dir', '-d', default=None, help='Specify where to store server config and data.') args = parser.parse_args() launcher = ServerLauncher(force_migration=args.migrate, server_dir=args.server_dir or os.environ.get("CONAN_SERVER_HOME")) launcher.launch() if __name__ == '__main__': run() ================================================ FILE: conans/migrations.py ================================================ import os from conan import conan_version from conan.api.output import ConanOutput from conan.internal.loader import load_python_file from conan.errors import ConanException, ConanMigrationError from conan.internal.model.version import Version from conan.internal.util.files import load, save CONAN_VERSION = "version.txt" class Migrator: def __init__(self, conf_path, current_version): self.conf_path = conf_path self.current_version = current_version self.file_version_path = os.path.join(self.conf_path, CONAN_VERSION) def migrate(self): try: old_version = self._load_old_version() if old_version is None or old_version < self.current_version: self._apply_migrations(old_version) self._update_version_file() elif self.current_version < old_version: # backwards migrations ConanOutput().warning(f"Downgrading cache from Conan {old_version} to " f"{self.current_version}") self._apply_back_migrations() self._update_version_file() except Exception as e: ConanOutput().error(str(e), error_type="exception") raise ConanMigrationError(e) def _update_version_file(self): try: save(self.file_version_path, str(self.current_version)) except Exception as error: raise ConanException("Can't write version file in '{}': {}" .format(self.file_version_path, str(error))) def _load_old_version(self): try: tmp = load(self.file_version_path) old_version = Version(tmp) except Exception: old_version = None return old_version def _apply_migrations(self, old_version): """ Apply any migration script. :param old_version: ``str`` previous Conan version. """ pass def _apply_back_migrations(self): migrations = os.path.join(self.conf_path, "migrations") if not os.path.exists(migrations): return # Order by versions, and filter only newer than the current version migration_files = [] for f in os.listdir(migrations): if not f.endswith(".py"): continue version, remain = f.split("_", 1) version = Version(version) if version > conan_version: migration_files.append((version, remain)) migration_files = [f"{v}_{r}" for (v, r) in reversed(sorted(migration_files))] for migration in migration_files: ConanOutput().warning(f"Applying downgrade migration {migration}") migration = os.path.join(migrations, migration) try: migrate_module, _ = load_python_file(migration) migrate_method = migrate_module.migrate migrate_method(self.conf_path) except Exception as e: ConanOutput().error(f"There was an error running downgrade migration: {e}. " f"Recommended to remove the cache and start from scratch", error_type="exception") os.remove(migration) ================================================ FILE: conans/requirements.txt ================================================ requests>=2.25, <3.0.0 urllib3>=1.26.6, <3.0 colorama>=0.4.3, <0.5.0 PyYAML>=6.0, <7.0 patch-ng>=1.18.0, <1.19 fasteners>=0.15 distro>=1.4.0, <2.0.0; platform_system == 'Linux' or platform_system == 'FreeBSD' Jinja2>=3.0, <4.0.0 python-dateutil>=2.8.0, <3 ================================================ FILE: conans/requirements_dev.txt ================================================ pytest>=7, <8.0.0 pytest-xdist # To launch in N cores with pytest -n WebTest>=3.0.0, <4 bottle PyJWT pluginbase docker setuptools pytest-cov ================================================ FILE: conans/requirements_runner.txt ================================================ paramiko docker>=7.1.0 ================================================ FILE: conans/requirements_server.txt ================================================ # Server bottle>=0.12.8, < 0.14 pluginbase>=0.5 PyJWT>=2.4.0, <3.0.0 ================================================ FILE: conans/server/__init__.py ================================================ from conan.internal import REVISIONS COMPLEX_SEARCH_CAPABILITY = "complex_search" SERVER_CAPABILITIES = [COMPLEX_SEARCH_CAPABILITY, REVISIONS] # Server is always with revisions ================================================ FILE: conans/server/conf/__init__.py ================================================ """ Server's configuration variables """ import os import random import string from datetime import timedelta from configparser import ConfigParser, NoSectionError from conan.errors import ConanException from conans.server.conf.default_server_conf import default_server_conf from conans.server.store.disk_adapter import ServerDiskAdapter from conans.server.store.server_store import ServerStore from conan.internal.util.files import mkdir, save, load MIN_CLIENT_COMPATIBLE_VERSION = '0.25.0' def get_env(env_key, default=None, environment=None): """Get the env variable associated with env_key""" if environment is None: environment = os.environ env_var = environment.get(env_key, default) if env_var != default: if isinstance(default, str): return env_var elif isinstance(default, bool): return env_var == "1" or env_var == "True" elif isinstance(default, int): return int(env_var) elif isinstance(default, float): return float(env_var) elif isinstance(default, list): if env_var.strip(): return [var.strip() for var in env_var.split(",")] return [] return env_var class ConanServerConfigParser(ConfigParser): """ defines the configuration of the server. It can load values from environment variables or from file. Environment variables have PRECEDENCE over file values """ def __init__(self, base_folder, environment=None, is_custom_path=False): environment = environment or os.environ ConfigParser.__init__(self) environment = environment or os.environ self.optionxform = str # This line keeps the case of the key, important for users case if is_custom_path: self.conan_folder = base_folder else: self.conan_folder = os.path.join(base_folder, '.conan_server') self.config_filename = os.path.join(self.conan_folder, 'server.conf') self._loaded = False self.env_config = {"updown_secret": get_env("CONAN_UPDOWN_SECRET", None, environment), "authorize_timeout": get_env("CONAN_AUTHORIZE_TIMEOUT", None, environment), "disk_storage_path": get_env("CONAN_STORAGE_PATH", None, environment), "jwt_secret": get_env("CONAN_JWT_SECRET", None, environment), "jwt_expire_minutes": get_env("CONAN_JWT_EXPIRE_MINUTES", None, environment), "write_permissions": [], "read_permissions": [], "ssl_enabled": get_env("CONAN_SSL_ENABLED", None, environment), "port": get_env("CONAN_SERVER_PORT", None, environment), "public_port": get_env("CONAN_SERVER_PUBLIC_PORT", None, environment), "host_name": get_env("CONAN_HOST_NAME", None, environment), "custom_authenticator": get_env("CONAN_CUSTOM_AUTHENTICATOR", None, environment), "custom_authorizer": get_env("CONAN_CUSTOM_AUTHORIZER", None, environment), # "user:pass,user2:pass2" "users": get_env("CONAN_SERVER_USERS", None, environment)} def _get_file_conf(self, section, varname=None): """ Gets the section or variable from config file. If the queried element is not found an exception is raised. """ try: if not os.path.exists(self.config_filename): jwt_random_secret = ''.join(random.choice(string.ascii_letters) for _ in range(32)) updown_random_secret = ''.join(random.choice(string.ascii_letters) for _ in range(32)) server_conf = default_server_conf.format(jwt_secret=jwt_random_secret, updown_secret=updown_random_secret) save(self.config_filename, server_conf) if not self._loaded: self._loaded = True # To avoid encoding problems we use our tools.load self.read_string(load(self.config_filename)) if varname: section = dict(self.items(section)) return section[varname] else: return self.items(section) except NoSectionError: raise ConanException("No section '%s' found" % section) except Exception as exc: raise ConanException("Invalid configuration, " "missing %s: %s" % (section, varname)) @property def ssl_enabled(self): try: ssl_enabled = self._get_conf_server_string("ssl_enabled").lower() return ssl_enabled == "true" or ssl_enabled == "1" except ConanException: return None @property def port(self): return int(self._get_conf_server_string("port")) @property def public_port(self): try: return int(self._get_conf_server_string("public_port")) except ConanException: return self.port @property def host_name(self): try: return self._get_conf_server_string("host_name") except ConanException: return None @property def public_url(self): host_name = self.host_name ssl_enabled = self.ssl_enabled protocol_version = "v2" if host_name is None and ssl_enabled is None: # No hostname and ssl config means that the transfer and the # logical endpoint are the same and a relative URL is sufficient return protocol_version elif host_name is None or ssl_enabled is None: raise ConanException("'host_name' and 'ssl_enable' have to be defined together.") else: protocol = "https" if ssl_enabled else "http" port = ":%s" % self.public_port if self.public_port != 80 else "" return "%s://%s%s/%s" % (protocol, host_name, port, protocol_version) @property def disk_storage_path(self): """If adapter is disk, means the directory for storage""" try: disk_path = self._get_conf_server_string("disk_storage_path") if disk_path.startswith("."): disk_path = os.path.join(os.path.dirname(self.config_filename), disk_path) disk_path = os.path.abspath(disk_path) ret = os.path.expanduser(disk_path) except ConanException: # If storage_path is not defined, use the current dir # So tests use test folder instead of user/.conan_server ret = os.path.dirname(self.config_filename) ret = os.path.normpath(ret) # Convert to O.S paths mkdir(ret) return ret @property def read_permissions(self): if self.env_config["read_permissions"]: return self.env_config["read_permissions"] else: return self._get_file_conf("read_permissions") @property def write_permissions(self): if self.env_config["write_permissions"]: return self.env_config["write_permissions"] else: return self._get_file_conf("write_permissions") @property def custom_authenticator(self): try: return self._get_conf_server_string("custom_authenticator") except ConanException: return None @property def custom_authorizer(self): try: return self._get_conf_server_string("custom_authorizer") except ConanException: return None @property def users(self): def validate_pass_encoding(password): try: password.encode('ascii') except (UnicodeDecodeError, UnicodeEncodeError): raise ConanException("Password contains invalid characters. " "Only ASCII encoding is supported") return password if self.env_config["users"]: pairs = self.env_config["users"].split(",") return {pair.split(":")[0]: validate_pass_encoding(pair.split(":")[1]) for pair in pairs} else: tmp = dict(self._get_file_conf("users")) tmp = {key: validate_pass_encoding(value) for key, value in tmp.items()} return tmp @property def jwt_secret(self): try: return self._get_conf_server_string("jwt_secret") except ConanException: raise ConanException("'jwt_secret' setting is needed. Please, write a value " "in server.conf or set CONAN_JWT_SECRET env value.") @property def updown_secret(self): try: return self._get_conf_server_string("updown_secret") except ConanException: raise ConanException("'updown_secret' setting is needed. Please, write a value " "in server.conf or set CONAN_UPDOWN_SECRET env value.") def _get_conf_server_string(self, keyname): """ Gets the value of a server config value either from the environment or the config file. Values from the environment have priority. If the value is not defined or empty an exception is raised. """ if self.env_config[keyname]: return self.env_config[keyname] value = self._get_file_conf("server", keyname) if value == "": raise ConanException("no value for 'server.%s' is defined in the config file" % keyname) return value @property def authorize_timeout(self): return timedelta(seconds=int(self._get_conf_server_string("authorize_timeout"))) @property def jwt_expire_time(self): return timedelta(minutes=float(self._get_conf_server_string("jwt_expire_minutes"))) def get_server_store(disk_storage_path, public_url): disk_controller_url = "%s/%s" % (public_url, "files") adapter = ServerDiskAdapter(disk_controller_url, disk_storage_path) return ServerStore(adapter) ================================================ FILE: conans/server/conf/default_server_conf.py ================================================ default_server_conf = """[server] # WARNING! Change default variable of jwt_secret. You should change it periodically # It only affects to current authentication tokens, you can change safetely anytime # When it changes user are just forced to log in again jwt_secret: {jwt_secret} jwt_expire_minutes: 120 ssl_enabled: False port: 9300 # Public port where files will be served. If empty will be used "port" public_port: host_name: localhost # Authorize timeout are seconds the client has to upload/download files until authorization expires authorize_timeout: 1800 # Just for disk storage adapter # updown_secret is the key used to generate the upload/download authorization token disk_storage_path: ./data disk_authorize_timeout: 1800 updown_secret: {updown_secret} # Check docs.conan.io to implement a different authenticator plugin for conan_server # if custom_authenticator is not specified, [users] section will be used to authenticate # the users. # # custom_authenticator: my_authenticator # Check docs.conan.io to implement a different authorizer plugin for conan_server # if custom_authorizer is not specified, the [read_permissions] and [write_permissions] # sections will be used to grant/reject access. # # custom_authorizer: my_authorizer # name/version@user/channel: user1, user2, user3 # # The rules are applied in order. # If a rule matches your package, then the server wont look further. # Place your more restrictive rules first. # # Example: All versions of opencv package from lasote user in testing channel is only # writeable by default_user and default_user2. Rest of packages are not writtable by anything # except the author. # # opencv/2.3.4@lasote/testing: default_user, default_user2 # [write_permissions] # name/version@user/channel: user1, user2, user3 # The rules are applied in order. If a rule applies to a conan, system wont look further. # # Example: # All versions of opencv package from lasote user in testing channel are only # readable by default_user and default_user2. # All versions of internal package from any user/channel are only readable by # authenticated users. # Rest of packages are world readable. # # opencv/*@lasote/testing: default_user default_user2 # internal/*@*/*: ? # */*@*/*: * # # By default all users can read all blocks # [read_permissions] */*@*/*: * [users] #default_user: defaultpass demo: demo """ ================================================ FILE: conans/server/crypto/__init__.py ================================================ ================================================ FILE: conans/server/crypto/jwt/__init__.py ================================================ ================================================ FILE: conans/server/crypto/jwt/jwt_credentials_manager.py ================================================ from datetime import datetime, timezone from calendar import timegm import jwt class JWTCredentialsManager: """JWT for manage auth credentials""" def __init__(self, secret, expire_time): """expire_time is a timedelta secret is a string with the secret encoding key""" self.secret = secret self.expire_time = expire_time def get_token_for(self, user): """Generates a token with the brl_user and additional data dict if needed""" profile_fields = {"user": user, "exp": timegm((datetime.now(timezone.utc) + self.expire_time).timetuple())} return jwt.encode(profile_fields, self.secret, algorithm="HS256") def get_user(self, token): """Gets the user from credentials object. None if no credentials. Can raise jwt.ExpiredSignature and jwt.DecodeError""" profile = jwt.decode(token, self.secret, algorithms=["HS256"]) return profile.get("user", None) ================================================ FILE: conans/server/launcher.py ================================================ #!/usr/bin/python import os import sys from conan.internal import REVISIONS from conans.server import SERVER_CAPABILITIES from conans.server.conf import get_server_store from conans.server.crypto.jwt.jwt_credentials_manager import JWTCredentialsManager from conans.server.migrate import migrate_and_get_server_config from conans.server.plugin_loader import load_authentication_plugin, load_authorization_plugin from conans.server.rest.server import ConanServer from conans.server.service.authorize import BasicAuthorizer, BasicAuthenticator class ServerLauncher(object): def __init__(self, force_migration=False, server_dir=None): if sys.version_info.major == 2: raise Exception("The conan_server needs Python>=3 for running") self.force_migration = force_migration if server_dir: user_folder = server_folder = server_dir else: user_folder = os.path.expanduser("~") server_folder = os.path.join(user_folder, '.conan_server') server_config = migrate_and_get_server_config( user_folder, self.force_migration, server_dir is not None ) custom_authn = server_config.custom_authenticator if custom_authn: authenticator = load_authentication_plugin(server_folder, custom_authn) else: authenticator = BasicAuthenticator(dict(server_config.users)) custom_authz = server_config.custom_authorizer if custom_authz: authorizer = load_authorization_plugin(server_folder, custom_authz) else: authorizer = BasicAuthorizer(server_config.read_permissions, server_config.write_permissions) credentials_manager = JWTCredentialsManager(server_config.jwt_secret, server_config.jwt_expire_time) server_store = get_server_store(server_config.disk_storage_path, server_config.public_url) server_capabilities = SERVER_CAPABILITIES server_capabilities.append(REVISIONS) self.server = ConanServer(server_config.port, credentials_manager, authorizer, authenticator, server_store, server_capabilities) if not self.force_migration: print("***********************") print("Using config: %s" % server_config.config_filename) print("Storage: %s" % server_config.disk_storage_path) print("Public URL: %s" % server_config.public_url) print("PORT: %s" % server_config.port) print("***********************") def launch(self): if not self.force_migration: self.server.run(host="0.0.0.0") ================================================ FILE: conans/server/migrate.py ================================================ from conan import conan_version from conans.server.conf import ConanServerConfigParser from conans.server.migrations import ServerMigrator def migrate_and_get_server_config(base_folder, force_migration=False, is_custom_path=False): server_config = ConanServerConfigParser(base_folder, is_custom_path=is_custom_path) storage_path = server_config.disk_storage_path migrator = ServerMigrator(server_config.conan_folder, storage_path, conan_version, force_migration) migrator.migrate() # Init again server_config, migrator could change something server_config = ConanServerConfigParser(base_folder, is_custom_path=is_custom_path) return server_config ================================================ FILE: conans/server/migrations.py ================================================ from conans.migrations import Migrator class ServerMigrator(Migrator): def __init__(self, conf_path, store_path, current_version, force_migrations): self.force_migrations = force_migrations self.store_path = store_path super(ServerMigrator, self).__init__(conf_path, current_version) ================================================ FILE: conans/server/plugin_loader.py ================================================ import os def load_plugin(server_folder, plugin_type, plugin_name): try: from pluginbase import PluginBase plugin_base = PluginBase(package="plugins/%s" % plugin_type) plugins_dir = os.path.join(server_folder, "plugins", plugin_type) plugin_source = plugin_base.make_plugin_source( searchpath=[plugins_dir]) plugin = plugin_source.load_plugin(plugin_name).get_class() # it is necessary to keep a reference to the plugin, otherwise it is removed # and some imports fail plugin.plugin_source = plugin_source return plugin except Exception: print("Error loading %s plugin '%s'" % (plugin_type, plugin_name)) raise def load_authentication_plugin(server_folder, plugin_name): return load_plugin(server_folder, "authenticator", plugin_name) def load_authorization_plugin(server_folder, plugin_name): return load_plugin(server_folder, "authorizer", plugin_name) ================================================ FILE: conans/server/rest/__init__.py ================================================ ================================================ FILE: conans/server/rest/api_v2.py ================================================ from bottle import Bottle from conan.internal.errors import EXCEPTION_CODE_MAPPING from conans.server.rest.bottle_plugins.http_basic_authentication import HttpBasicAuthentication from conans.server.rest.bottle_plugins.jwt_authentication import JWTAuthentication from conans.server.rest.bottle_plugins.return_handler import ReturnHandlerPlugin from conans.server.rest.controller.v2.ping import PingController from conans.server.rest.controller.v2.users import UsersController from conans.server.rest.controller.v2.conan import ConanControllerV2 from conans.server.rest.controller.v2.delete import DeleteControllerV2 from conans.server.rest.controller.v2.revisions import RevisionsController from conans.server.rest.controller.v2.search import SearchControllerV2 class ApiV2(Bottle): def __init__(self, credentials_manager, server_capabilities): self.credentials_manager = credentials_manager self.server_capabilities = server_capabilities Bottle.__init__(self) def setup(self): self.install_plugins() # Capabilities in a ping PingController().attach_to(self) SearchControllerV2().attach_to(self) DeleteControllerV2().attach_to(self) ConanControllerV2().attach_to(self) RevisionsController().attach_to(self) # Install users controller UsersController().attach_to(self) def install_plugins(self): # Second, check Http Basic Auth self.install(HttpBasicAuthentication()) # Map exceptions to http return codes self.install(ReturnHandlerPlugin(EXCEPTION_CODE_MAPPING)) # Handle jwt auth self.install(JWTAuthentication(self.credentials_manager)) ================================================ FILE: conans/server/rest/bottle_plugins/__init__.py ================================================ ================================================ FILE: conans/server/rest/bottle_plugins/authorization_header.py ================================================ import inspect from abc import ABCMeta, abstractmethod from bottle import PluginError, request class AuthorizationHeader(object, metaclass=ABCMeta): """ Generic plugin to handle Authorization header. Must be extended and implement some abstract methods in subclasses """ name = 'authorizationheader' api = 2 def __init__(self, keyword): # Required self.keyword = keyword def setup(self, app): """ Make sure that other installed plugins don't affect the same keyword argument. """ for other in app.plugins: if not isinstance(other, self.__class__): continue if other.keyword == self.keyword: raise PluginError("Found another AuthorizationHeaderBottlePlugin plugin with " "conflicting settings (non-unique keyword).") def apply(self, callback, context): """ Test if the original callback accepts a 'self.keyword' keyword. """ args = inspect.getfullargspec(context.callback)[0] # logger.debug("Call: %s" % str(callback)) if self.keyword not in args: return callback def wrapper(*args, **kwargs): """ Check for user credentials in http header """ # Get Authorization header_value = self.get_authorization_header_value() new_kwargs = self.parse_authorization_value(header_value) if not new_kwargs: raise self.get_invalid_header_response() kwargs.update(new_kwargs) return callback(*args, **kwargs) # kwargs has :xxx variables from url # Replace the route callback with the wrapped one. return wrapper def get_authorization_header_value(self): """ Get from the request the header of http basic auth: http://en.wikipedia.org/wiki/Basic_access_authentication """ auth_type = self.get_authorization_type() if request.headers.get("Authorization", None) is not None: auth_line = request.headers.get("Authorization", None) if not auth_line.startswith("%s " % auth_type): raise self.get_invalid_header_response() return auth_line[len(auth_type) + 1:] else: return None @abstractmethod def get_authorization_type(self): """Abstract. Example: Basic (for http basic auth) or Beagle for JWT""" raise NotImplementedError() @abstractmethod def parse_authorization_value(self, header_value): """Abstract. Parse header_value and return kwargs to apply bottle method parameters""" raise NotImplementedError() @abstractmethod def get_invalid_header_response(self): """A response from a malformed header""" raise NotImplementedError() ================================================ FILE: conans/server/rest/bottle_plugins/http_basic_authentication.py ================================================ import base64 from collections import namedtuple from bottle import HTTPResponse from conans.server.rest.bottle_plugins.authorization_header import AuthorizationHeader class UserPasswordPair(namedtuple('UserPasswordPair', ['user', 'password'])): """ Simple tuple for store user and pass """ pass class HttpBasicAuthentication(AuthorizationHeader): """ The HttpBasicAuthenticationBottlePlugin plugin requires Http Basic Authentication """ name = 'basichttpauth' api = 2 def __init__(self, keyword='http_basic_credentials'): self.keyword = keyword super(HttpBasicAuthentication, self).__init__(keyword) def get_authorization_type(self): """String in Authorization header for type""" return "Basic" def parse_authorization_value(self, header_value): """Parse header_value and return kwargs to apply bottle method parameters""" if header_value is None: return None # HTTP protocol is utf-8 username, password = base64.b64decode(header_value).decode().split(":", 1) ret = UserPasswordPair(username, password) return {self.keyword: ret} def get_invalid_header_response(self): """A response from a malformed header. Includes WWW-Authenticate for ask browser to request user and password""" return HTTPResponse("'Http Authentication not implemented'", "401 Unauthorized", {"WWW-Authenticate": 'Basic realm="Login Required"'}) ================================================ FILE: conans/server/rest/bottle_plugins/jwt_authentication.py ================================================ from bottle import HTTPResponse from conans.server.rest.bottle_plugins.authorization_header import AuthorizationHeader class JWTAuthentication(AuthorizationHeader): """ The HttpBasicAuthenticationBottlePlugin plugin requires Http Basic Authentication """ name = 'jwtauthenticationbottleplugin' api = 2 def __init__(self, manager, keyword='auth_user'): """ Manager should be a JWTCredentialsManager """ self.manager = manager self.keyword = keyword super(JWTAuthentication, self).__init__(keyword) def get_authorization_type(self): """String in Authorization header for type""" return "Bearer" def parse_authorization_value(self, header_value): """Parse header_value and return kwargs to apply bottle method parameters""" try: if not header_value: username = None else: # Check if its valid obtaining the password_timestamp username = self.manager.get_user(token=header_value) except Exception: # Check if resp = HTTPResponse("Wrong JWT token!", "401 Unauthorized") resp.set_header('Content-Type', 'text/plain') raise resp return {self.keyword: username} def get_invalid_header_response(self): """A response from a malformed header. Includes WWW-Authenticate for ask browser to request user and password""" return HTTPResponse("'Http Authentication not implemented'", "401 Unauthorized") ================================================ FILE: conans/server/rest/bottle_plugins/return_handler.py ================================================ from bottle import HTTPResponse from conan.errors import ConanException class ReturnHandlerPlugin(object): """ The ReturnHandlerPlugin plugin unify REST return and exception management """ name = 'ReturnHandlerPlugin' api = 2 def __init__(self, exception_mapping): self.exception_mapping = exception_mapping def setup(self, app): """ Make sure that other installed plugins don't affect the same keyword argument. """ for other in app.plugins: if not isinstance(other, ReturnHandlerPlugin): continue def apply(self, callback, _): """ Apply plugin """ def wrapper(*args, **kwargs): """ Capture possible exceptions to manage the return """ try: # The encoding from browsers is utf-8, so we assume it for key, value in kwargs.items(): if isinstance(value, str): kwargs[key] = value return callback(*args, **kwargs) # kwargs has :xxx variables from url except HTTPResponse: raise except ConanException as excep: return get_response_from_exception(excep, self.exception_mapping) except Exception as e: # logger.error(e) # logger.error(traceback.print_exc()) return get_response_from_exception(e, self.exception_mapping) return wrapper def get_response_from_exception(excep, exception_mapping): status = exception_mapping.get(excep.__class__, None) if status is None: status = 500 ret = HTTPResponse(status=status, body=str(excep)) ret.add_header("Content-Type", "text/plain") return ret ================================================ FILE: conans/server/rest/bottle_routes.py ================================================ from conan.internal.rest.rest_routes import RestRoutes class BottleRoutes(RestRoutes): def __getattribute__(self, item): tmp = super(BottleRoutes, self).__getattribute__(item) tmp = tmp.replace("{path}", "").replace("{", "<").replace("}", ">") if not tmp.startswith("/"): return "/{}".format(tmp) return tmp ================================================ FILE: conans/server/rest/controller/__init__.py ================================================ ================================================ FILE: conans/server/rest/controller/v2/__init__.py ================================================ from conan.api.model import PkgReference from conan.api.model import RecipeReference def get_package_ref(name, version, username, channel, package_id, revision, p_revision): ref = RecipeReference(name, version, username, channel, revision) return PkgReference(ref, package_id, p_revision) ================================================ FILE: conans/server/rest/controller/v2/conan.py ================================================ from bottle import request from conan.internal.errors import NotFoundException from conan.api.model import RecipeReference from conans.server.rest.bottle_routes import BottleRoutes from conans.server.rest.controller.v2 import get_package_ref from conans.server.service.v2.service_v2 import ConanServiceV2 class ConanControllerV2(object): @staticmethod def attach_to(app): conan_service = ConanServiceV2(app.authorizer, app.server_store) r = BottleRoutes() @app.route(r.package_revision_files, method=["GET"]) def get_package_file_list(name, version, username, channel, package_id, auth_user, revision, p_revision): pref = get_package_ref(name, version, username, channel, package_id, revision, p_revision) ret = conan_service.get_package_file_list(pref, auth_user) return ret @app.route(r.package_revision_file, method=["GET"]) def get_package_file(name, version, username, channel, package_id, the_path, auth_user, revision, p_revision): pref = get_package_ref(name, version, username, channel, package_id, revision, p_revision) file_generator = conan_service.get_package_file(pref, the_path, auth_user) return file_generator @app.route(r.package_revision_file, method=["PUT"]) def upload_package_file(name, version, username, channel, package_id, the_path, auth_user, revision, p_revision): if "X-Checksum-Deploy" in request.headers: raise NotFoundException("Non checksum storage") pref = get_package_ref(name, version, username, channel, package_id, revision, p_revision) conan_service.upload_package_file(request.body, request.headers, pref, the_path, auth_user) @app.route(r.recipe_revision_files, method=["GET"]) def get_recipe_file_list(name, version, username, channel, auth_user, revision): ref = RecipeReference(name, version, username, channel, revision) ret = conan_service.get_recipe_file_list(ref, auth_user) return ret @app.route(r.recipe_revision_file, method=["GET"]) def get_recipe_file(name, version, username, channel, the_path, auth_user, revision): ref = RecipeReference(name, version, username, channel, revision) file_generator = conan_service.get_recipe_file(ref, the_path, auth_user) return file_generator @app.route(r.recipe_revision_file, method=["PUT"]) def upload_recipe_file(name, version, username, channel, the_path, auth_user, revision): if "X-Checksum-Deploy" in request.headers: raise NotFoundException("Not a checksum storage") ref = RecipeReference(name, version, username, channel, revision) conan_service.upload_recipe_file(request.body, request.headers, ref, the_path, auth_user) ================================================ FILE: conans/server/rest/controller/v2/delete.py ================================================ from conan.api.model import RecipeReference from conans.server.rest.bottle_routes import BottleRoutes from conans.server.rest.controller.v2 import get_package_ref from conans.server.service.v2.service_v2 import ConanServiceV2 class DeleteControllerV2(object): """ Serve requests related with Conan """ @staticmethod def attach_to(app): r = BottleRoutes() @app.route(r.recipe, method="DELETE") @app.route(r.recipe_revision, method="DELETE") def remove_recipe(name, version, username, channel, auth_user, revision=None): """ Remove any existing conanfiles or its packages created. Will remove all revisions, packages and package revisions (parent folder) if no revision is passed """ ref = RecipeReference(name, version, username, channel, revision) conan_service = ConanServiceV2(app.authorizer, app.server_store) conan_service.remove_recipe(ref, auth_user) @app.route(r.package_recipe_revision, method=["DELETE"]) @app.route(r.package_revision, method=["DELETE"]) def remove_package(name, version, username, channel, package_id, auth_user, revision=None, p_revision=None): """ - If both RRev and PRev are specified, it will remove the specific package revision of the specific recipe revision. - If PRev is NOT specified but RRev is specified (package_recipe_revision_url) it will remove all the package revisions """ pref = get_package_ref(name, version, username, channel, package_id, revision, p_revision) conan_service = ConanServiceV2(app.authorizer, app.server_store) conan_service.remove_package(pref, auth_user) @app.route(r.packages_revision, method="DELETE") def remove_all_packages(name, version, username, channel, auth_user, revision=None): """ Remove all packages from a RREV""" ref = RecipeReference(name, version, username, channel, revision) conan_service = ConanServiceV2(app.authorizer, app.server_store) conan_service.remove_all_packages(ref, auth_user) ================================================ FILE: conans/server/rest/controller/v2/ping.py ================================================ from bottle import response from conans.server.rest.bottle_routes import BottleRoutes class PingController(object): @staticmethod def attach_to(app): r = BottleRoutes() @app.route(r.ping, method=["GET"]) def ping(): """ Response OK. Useful to get server capabilities """ response.headers['X-Conan-Server-Capabilities'] = ",".join(app.server_capabilities) return ================================================ FILE: conans/server/rest/controller/v2/revisions.py ================================================ from conan.api.model import RecipeReference from conans.server.rest.bottle_routes import BottleRoutes from conans.server.rest.controller.v2 import get_package_ref from conans.server.service.v2.service_v2 import ConanServiceV2 from conan.internal.util.dates import from_timestamp_to_iso8601 class RevisionsController(object): """ Serve requests related with Conan """ @staticmethod def attach_to(app): r = BottleRoutes() @app.route(r.recipe_revisions, method="GET") def get_recipe_revisions_references(name, version, username, channel, auth_user): """ Gets a JSON with the revisions for the specified recipe """ conan_reference = RecipeReference(name, version, username, channel) conan_service = ConanServiceV2(app.authorizer, app.server_store) revs = conan_service.get_recipe_revisions_references(conan_reference, auth_user) return _format_revs_return(revs) @app.route(r.recipe_latest, method="GET") def get_latest_recipe_reference(name, version, username, channel, auth_user): """ Gets a JSON with the revisions for the specified recipe """ conan_reference = RecipeReference(name, version, username, channel) conan_service = ConanServiceV2(app.authorizer, app.server_store) rev = conan_service.get_latest_revision(conan_reference, auth_user) return _format_rev_return(rev) @app.route(r.package_revisions, method="GET") def get_package_revisions_references(name, version, username, channel, package_id, auth_user, revision): """ Get a JSON with the revisions for a specified RREV """ package_reference = get_package_ref(name, version, username, channel, package_id, revision, p_revision=None) conan_service = ConanServiceV2(app.authorizer, app.server_store) prefs = conan_service.get_package_revisions_references(package_reference, auth_user) return _format_prefs_return(prefs) @app.route(r.package_revision_latest, method="GET") def get_latest_package_reference(name, version, username, channel, package_id, auth_user, revision): """ Gets a JSON with the revisions for the specified recipe """ package_reference = get_package_ref(name, version, username, channel, package_id, revision, p_revision=None) conan_service = ConanServiceV2(app.authorizer, app.server_store) pref = conan_service.get_latest_package_reference(package_reference, auth_user) return _format_pref_return(pref) def _format_rev_return(rev): # FIXME: fix this when RecipeReference return {"revision": rev[0], "time": from_timestamp_to_iso8601(rev[1])} def _format_revs_return(revs): return {"revisions": [_format_rev_return(rev) for rev in revs]} def _format_pref_return(pref): return {"revision": pref.revision, "time": from_timestamp_to_iso8601(pref.timestamp)} def _format_prefs_return(revs): return {"revisions": [_format_pref_return(rev) for rev in revs]} ================================================ FILE: conans/server/rest/controller/v2/search.py ================================================ from bottle import request from conan.api.model import RecipeReference from conans.server.rest.bottle_routes import BottleRoutes from conans.server.service.v2.search import SearchService class SearchControllerV2(object): """ Serve requests related with Conan """ @staticmethod def attach_to(app): r = BottleRoutes() @app.route(r.common_search, method=["GET"]) def search(auth_user): pattern = request.params.get("q", None) ignore_case = request.params.get("ignorecase", True) if isinstance(ignore_case, str): ignore_case = False if 'false' == ignore_case.lower() else True search_service = SearchService(app.authorizer, app.server_store, auth_user) references = [repr(ref) for ref in search_service.search(pattern, ignore_case)] return {"results": references} @app.route(r.common_search_packages, method=["GET"]) @app.route(r.common_search_packages_revision, method=["GET"]) def search_packages(name, version, username, channel, auth_user, revision=None): # query is no longer server side list_only = request.params.get("list_only", False) if isinstance(list_only, str): list_only = False if 'false' == list_only.lower() else True search_service = SearchService(app.authorizer, app.server_store, auth_user) ref = RecipeReference(name, version, username, channel, revision) info = search_service.search_packages(ref, list_only) return info ================================================ FILE: conans/server/rest/controller/v2/users.py ================================================ from bottle import response from conan.internal.errors import AuthenticationException from conans.server.rest.bottle_routes import BottleRoutes from conans.server.service.user_service import UserService class UsersController(object): """ Serve requests related with users """ def attach_to(self, app): r = BottleRoutes() @app.route(r.common_authenticate, method=["GET"]) def authenticate(http_basic_credentials): if not http_basic_credentials: raise AuthenticationException("Wrong user or password") user_service = UserService(app.authenticator, app.credentials_manager) token = user_service.authenticate(http_basic_credentials.user, http_basic_credentials.password) response.content_type = 'text/plain' return token @app.route(r.common_check_credentials, method=["GET"]) def check_credentials(auth_user): """Just check if valid token. It not exception is raised from Bottle plugin""" if not auth_user: raise AuthenticationException("Logged user needed!") response.content_type = 'text/plain' return auth_user ================================================ FILE: conans/server/rest/server.py ================================================ import bottle from bottle import Bottle from conans.server.rest.api_v2 import ApiV2 from conans.server.rest.controller.v2.ping import PingController class ConanServer(object): """ Server class. Instances api_v2 application and run it. Receives the store. """ store = None root_app = None def __init__(self, run_port, credentials_manager, authorizer, authenticator, server_store, server_capabilities): self.run_port = run_port server_capabilities = server_capabilities or [] self.root_app = bottle.Bottle() self.api_v2 = ApiV2(credentials_manager, server_capabilities) self.api_v2.authorizer = authorizer self.api_v2.authenticator = authenticator self.api_v2.server_store = server_store self.api_v2.setup() self.root_app.mount("/v2/", self.api_v2) # FIXME: Artifactory is not returning the capabilities in V2, so the client uses v1 # for that. So we need to implement the ping at V1. class ApiV1Ping(Bottle): def __init__(self, server_capabilities): self.server_capabilities = server_capabilities Bottle.__init__(self) def setup(self): # Capabilities in a ping PingController().attach_to(self) ping_v1 = ApiV1Ping(server_capabilities) ping_v1.setup() self.root_app.mount("/v1/", ping_v1) def run(self, **kwargs): port = kwargs.pop("port", self.run_port) debug_set = kwargs.pop("debug", False) host = kwargs.pop("host", "localhost") bottle.Bottle.run(self.root_app, host=host, port=port, debug=debug_set, reloader=False) ================================================ FILE: conans/server/revision_list.py ================================================ import json from collections import namedtuple from conan.internal.util.dates import revision_timestamp_now _RevisionEntry = namedtuple("RevisionEntry", "revision time") class RevisionList(object): def __init__(self): self._data = [] @staticmethod def loads(contents): ret = RevisionList() ret._data = [_RevisionEntry(e["revision"], e["time"]) for e in json.loads(contents)["revisions"]] return ret def dumps(self): return json.dumps({"revisions": [{"revision": e.revision, "time": e.time} for e in self._data]}) def add_revision(self, revision_id): lt = self.latest_revision() if lt and lt.revision == revision_id: # Each uploaded file calls to update the revision return index = self._find_revision_index(revision_id) if index: self._data.pop(index) self._data.append(_RevisionEntry(revision_id, self._now())) @staticmethod def _now(): return revision_timestamp_now() def latest_revision(self): if not self._data: return None return self._data[-1] def get_time(self, revision): tmp = self._find_revision_index(revision) if tmp is None: return None return self._data[tmp].time def as_list(self): return list(reversed(self._data)) def remove_revision(self, revision_id): index = self._find_revision_index(revision_id) if index is None: return self._data.pop(index) def _find_revision_index(self, revision_id): for i, rev in enumerate(self._data): if rev.revision == revision_id: return i return None def __eq__(self, other): return self.dumps() == other.dumps() ================================================ FILE: conans/server/server_launcher.py ================================================ import os from conans.server.launcher import ServerLauncher launcher = ServerLauncher(server_dir=os.environ.get("CONAN_SERVER_HOME")) app = launcher.server.root_app def main(*args): launcher.launch() if __name__ == "__main__": main() ================================================ FILE: conans/server/service/__init__.py ================================================ ================================================ FILE: conans/server/service/authorize.py ================================================ """ This module handles user authentication and permissions for read or write a conans or package. This only reads from file the users and permissions. Replace this module with other that keeps the interface or super class. """ from abc import ABCMeta, abstractmethod from conan.internal.errors import InternalErrorException, AuthenticationException, ForbiddenException # ############################################ # ############ ABSTRACT CLASSES ############## # ############################################ from conan.api.model import RecipeReference class Authorizer(object, metaclass=ABCMeta): """ Handles the access permissions to conans and packages """ @abstractmethod def check_read_conan(self, username, ref): """ username: User that request to read the conans ref: RecipeReference """ raise NotImplementedError() @abstractmethod def check_write_conan(self, username, ref): """ username: User that request to write the conans ref: RecipeReference """ raise NotImplementedError() @abstractmethod def check_delete_conan(self, username, ref): """ username: User requesting a recipe's deletion ref: ConanFileReference """ raise NotImplementedError() @abstractmethod def check_read_package(self, username, pref): """ username: User that request to read the package pref: PackageReference """ raise NotImplementedError() @abstractmethod def check_write_package(self, username, pref): """ username: User that request to write the package pref: PackageReference """ raise NotImplementedError() @abstractmethod def check_delete_package(self, username, pref): """ username: User requesting a package's deletion pref: PackageReference """ raise NotImplementedError() class Authenticator(object, metaclass=ABCMeta): """ Handles the user authentication """ @abstractmethod def valid_user(self, username, plain_password): """ username: User that request to read the conans plain_password: """ raise NotImplementedError() # ######################################################## # ############ BASIC IMPLEMENTATION CLASSES ############## # ######################################################## class BasicAuthenticator(Authenticator): """ Handles the user authentication from a dict of plain users and passwords. users is {username: plain-text-passwd} """ def __init__(self, users): self.users = users def valid_user(self, username, plain_password): """ username: User that request to read the conans plain_password: return: True if match False if don't """ return username in self.users and self.users[username] == plain_password class BasicAuthorizer(Authorizer): """ Reads permissions from the config file (server.cfg) """ def __init__(self, read_permissions, write_permissions): """List of tuples with refs and users: [(ref, "user, user, user"), (ref, "user3, user, user")] """ self.read_permissions = read_permissions self.write_permissions = write_permissions def check_read_conan(self, username, ref): """ username: User that request to read the conans ref: RecipeReference """ self._check_any_rule_ok(username, self.read_permissions, ref) def check_write_conan(self, username, ref): """ username: User that request to write the conans ref: RecipeReference """ self._check_any_rule_ok(username, self.write_permissions, ref) def check_delete_conan(self, username, ref): """ username: User that request to write the conans ref: RecipeReference """ self.check_write_conan(username, ref) def check_read_package(self, username, pref): """ username: User that request to read the package pref: PackageReference """ self.check_read_conan(username, pref.ref) def check_write_package(self, username, pref): """ username: User that request to write the package pref: PackageReference """ self.check_write_conan(username, pref.ref) def check_delete_package(self, username, pref): """ username: User that request to write the package pref: PackageReference """ self.check_write_package(username, pref) def _check_any_rule_ok(self, username, rules, *args, **kwargs): for rule in rules: # raises if don't ret = self._check_rule_ok(username, rule, *args, **kwargs) if ret: # A rule is applied ok, if not apply keep looking return True if username: raise ForbiddenException("Permission denied") else: raise AuthenticationException() def _check_rule_ok(self, username, rule, ref): """Checks if a rule specified in config file applies to current conans reference and current user""" try: rule_ref = RecipeReference.loads(rule[0]) except Exception: # TODO: Log error raise InternalErrorException("Invalid server configuration. " "Contact the administrator.") authorized_users = [_.strip() for _ in rule[1].split(",")] if len(authorized_users) < 1: raise InternalErrorException("Invalid server configuration. " "Contact the administrator.") # Check if rule apply ref if self._check_ref_apply_for_rule(rule_ref, ref): if authorized_users[0] == "*" or username in authorized_users: return True # Ok, applies and match username else: if username: if authorized_users[0] == "?": return True # Ok, applies and match any authenticated username else: raise ForbiddenException("Permission denied") else: raise AuthenticationException("Authentication error") return False def _check_ref_apply_for_rule(self, rule_ref: RecipeReference, ref: RecipeReference): """Checks if a conans reference specified in config file applies to current conans reference""" return not((rule_ref.name != "*" and rule_ref.name != ref.name) or (rule_ref.version != "*" and rule_ref.version != ref.version) or (rule_ref.user != "*" and rule_ref.user != ref.user) or (rule_ref.channel != "*" and rule_ref.channel != ref.channel)) ================================================ FILE: conans/server/service/mime.py ================================================ def get_mime_type(filepath): if filepath.endswith(".tgz"): mimetype = "x-gzip" elif filepath.endswith(".txz"): mimetype = "x-xz" else: mimetype = "auto" return mimetype ================================================ FILE: conans/server/service/user_service.py ================================================ from conan.internal.errors import AuthenticationException class UserService(object): def __init__(self, authenticator, credentials_manager): self.authenticator = authenticator self.credentials_manager = credentials_manager def authenticate(self, username, password): valid = self.authenticator.valid_user(username, password) # If user is valid returns a token if valid: token = self.credentials_manager.get_token_for(username) return token else: raise AuthenticationException("Wrong user or password") ================================================ FILE: conans/server/service/v2/__init__.py ================================================ ================================================ FILE: conans/server/service/v2/search.py ================================================ import copy import os import re from fnmatch import translate from conan.internal.errors import ForbiddenException, RecipeNotFoundException from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.paths import CONANINFO from conans.server.utils.files import list_folder_subdirs from conan.internal.util.files import load def _get_local_infos_min(server_store, ref, list_only): result = {} new_ref = ref subdirs = list_folder_subdirs(server_store.packages(new_ref), level=1) # Seems that Python3.12 os.walk is retrieving a different order of folders for package_id in sorted(subdirs): if package_id in result: continue if list_only: result[package_id] = {} continue # Read conaninfo pref = PkgReference(new_ref, package_id) revision_entry = server_store.get_last_package_revision(pref) if not revision_entry: continue # server can store empty package-revision list files pref.revision = revision_entry.revision info_path = os.path.join(server_store.package(pref), CONANINFO) if not os.path.exists(info_path): raise Exception(f"No conaninfo.txt file for listed {pref}") content = load(info_path) # From Conan 1.48 the conaninfo.txt is sent raw. result[package_id] = {"content": content} return result def search_packages(server_store, ref, list_only): """ Return a dict like this: {package_ID: {name: "OpenCV", version: "2.14", settings: {os: Windows}}} param ref: RecipeReference object """ if ref.revision is None: # TODO: Verify this sometimes happen latest_rev = server_store.get_last_revision(ref).revision ref.revision = latest_rev ref_norev = copy.copy(ref) ref_norev.revision = None if not os.path.exists(server_store.conan_revisions_root(ref_norev)): raise RecipeNotFoundException(ref) infos = _get_local_infos_min(server_store, ref, list_only) return infos class SearchService(object): def __init__(self, authorizer, server_store, auth_user): self._authorizer = authorizer self._server_store = server_store self._auth_user = auth_user def search_packages(self, reference, list_only=False): """Shared between v1 and v2, v1 will iterate rrevs""" self._authorizer.check_read_conan(self._auth_user, reference) info = search_packages(self._server_store, reference, list_only) return info def _search_recipes(self, pattern=None, ignorecase=True): subdirs = list_folder_subdirs(basedir=self._server_store.store, level=5) def underscore_to_none(field): return field if field != "_" else None ret = set() if not pattern: for folder in subdirs: fields_dir = [underscore_to_none(d) for d in folder.split("/")] r = RecipeReference(*fields_dir) r.revision = None ret.add(r) else: # Conan references in main storage pattern = str(pattern) b_pattern = translate(pattern) b_pattern = re.compile(b_pattern, re.IGNORECASE) if ignorecase else re.compile(b_pattern) for subdir in subdirs: fields_dir = [underscore_to_none(d) for d in subdir.split("/")] new_ref = RecipeReference(*fields_dir) new_ref.revision = None if new_ref.partial_match(b_pattern): ret.add(new_ref) return sorted(ret) def search(self, pattern=None, ignorecase=True): """ Get all the info about any package Attributes: pattern = wildcards like opencv/* """ refs = self._search_recipes(pattern, ignorecase) filtered = [] # Filter out restricted items for ref in refs: try: self._authorizer.check_read_conan(self._auth_user, ref) filtered.append(ref) except ForbiddenException: pass return filtered ================================================ FILE: conans/server/service/v2/service_v2.py ================================================ import copy import os from bottle import FileUpload, static_file from conan.internal.errors import NotFoundException, RecipeNotFoundException, PackageNotFoundException from conan.internal.paths import CONAN_MANIFEST from conan.api.model import PkgReference from conans.server.service.mime import get_mime_type from conans.server.store.server_store import ServerStore from conan.internal.util.files import mkdir class ConanServiceV2: def __init__(self, authorizer, server_store): assert(isinstance(server_store, ServerStore)) self._authorizer = authorizer self._server_store = server_store # RECIPE METHODS def get_recipe_file_list(self, ref, auth_user): self._authorizer.check_read_conan(auth_user, ref) try: file_list = self._server_store.get_recipe_file_list(ref) except NotFoundException: raise RecipeNotFoundException(ref) if not file_list: raise RecipeNotFoundException(ref) # Send speculative metadata (empty) for files (non breaking future changes) return {"files": {key: {} for key in file_list}} def get_recipe_file(self, reference, filename, auth_user): self._authorizer.check_read_conan(auth_user, reference) path = self._server_store.get_recipe_file_path(reference, filename) return static_file(os.path.basename(path), root=os.path.dirname(path), mimetype=get_mime_type(path)) def upload_recipe_file(self, body, headers, reference, filename, auth_user): self._authorizer.check_write_conan(auth_user, reference) # FIXME: Check that reference contains revision (MANDATORY TO UPLOAD) path = self._server_store.get_recipe_file_path(reference, filename) self._upload_to_path(body, headers, path) # If the upload was ok, of the manifest, update the pointer to the latest if filename == CONAN_MANIFEST: self._server_store.update_last_revision(reference) def get_recipe_revisions_references(self, ref, auth_user): self._authorizer.check_read_conan(auth_user, ref) ref_norev = copy.copy(ref) ref_norev.revision = None root = self._server_store.conan_revisions_root(ref_norev) if not self._server_store.path_exists(root): raise RecipeNotFoundException(ref) return self._server_store.get_recipe_revisions_references(ref) def get_package_revisions_references(self, pref, auth_user): self._authorizer.check_read_conan(auth_user, pref.ref) ref_norev = copy.copy(pref.ref) ref_norev.revision = None root = self._server_store.conan_revisions_root(ref_norev) if not self._server_store.path_exists(root): raise RecipeNotFoundException(pref.ref) ret = self._server_store.get_package_revisions_references(pref) return ret def get_latest_revision(self, ref, auth_user): self._authorizer.check_read_conan(auth_user, ref) tmp = self._server_store.get_last_revision(ref) if not tmp: raise RecipeNotFoundException(ref) return tmp def get_latest_package_reference(self, pref, auth_user): self._authorizer.check_read_conan(auth_user, pref.ref) _pref = self._server_store.get_last_package_revision(pref) if not _pref: raise PackageNotFoundException(pref) return _pref # PACKAGE METHODS def get_package_file_list(self, pref, auth_user): self._authorizer.check_read_conan(auth_user, pref.ref) file_list = self._server_store.get_package_file_list(pref) if not file_list: raise PackageNotFoundException(pref) # Send speculative metadata (empty) for files (non breaking future changes) return {"files": {key: {} for key in file_list}} def get_package_file(self, pref, filename, auth_user): self._authorizer.check_read_conan(auth_user, pref.ref) path = self._server_store.get_package_file_path(pref, filename) return static_file(os.path.basename(path), root=os.path.dirname(path), mimetype=get_mime_type(path)) def upload_package_file(self, body, headers, pref, filename, auth_user): self._authorizer.check_write_conan(auth_user, pref.ref) # Check if the recipe exists recipe_path = self._server_store.export(pref.ref) if not os.path.exists(recipe_path): raise RecipeNotFoundException(pref.ref) path = self._server_store.get_package_file_path(pref, filename) self._upload_to_path(body, headers, path) # If the upload was ok, of the manifest, update the pointer to the latest if filename == CONAN_MANIFEST: self._server_store.update_last_package_revision(pref) # Misc @staticmethod def _upload_to_path(body, headers, path): file_saver = FileUpload(body, None, filename=os.path.basename(path), headers=headers) if os.path.exists(path): os.unlink(path) if not os.path.exists(os.path.dirname(path)): mkdir(os.path.dirname(path)) file_saver.save(os.path.dirname(path)) # REMOVE def remove_recipe(self, ref, auth_user): self._authorizer.check_delete_conan(auth_user, ref) self._server_store.remove_recipe(ref) def remove_package(self, pref, auth_user): self._authorizer.check_delete_package(auth_user, pref) for rrev in self._server_store.get_recipe_revisions_references(pref.ref): new_ref = copy.copy(pref.ref) new_ref.revision = rrev.revision # FIXME: Just assign rrev when introduce RecipeReference new_pref = PkgReference(new_ref, pref.package_id, pref.revision) for _pref in self._server_store.get_package_revisions_references(new_pref): self._server_store.remove_package(_pref) def remove_all_packages(self, ref, auth_user): self._authorizer.check_delete_conan(auth_user, ref) for rrev in self._server_store.get_recipe_revisions_references(ref): tmp = copy.copy(ref) tmp.revision = rrev.revision self._server_store.remove_all_packages(tmp) ================================================ FILE: conans/server/store/__init__.py ================================================ ================================================ FILE: conans/server/store/disk_adapter.py ================================================ import os import fasteners from conan.internal.errors import NotFoundException from conan.internal.util.files import rmdir from conans.server.utils.files import path_exists, relative_dirs class ServerDiskAdapter: """Manage access to disk files with common methods required for conan operations""" def __init__(self, base_url, base_storage_path): """ :param base_url Base url for generate urls to download and upload operations""" self.base_url = base_url # URLs are generated removing this base path self._store_folder = base_storage_path def get_file_list(self, absolute_path=""): if not path_exists(absolute_path, self._store_folder): raise NotFoundException("") paths = relative_dirs(absolute_path) abs_paths = [os.path.join(absolute_path, relpath) for relpath in paths] return abs_paths def delete_folder(self, path): """Delete folder from disk. Path already contains base dir""" if not path_exists(path, self._store_folder): raise NotFoundException("") rmdir(path) def path_exists(self, path): return os.path.exists(path) def read_file(self, path, lock_file): with fasteners.InterProcessLock(lock_file): with open(path) as f: return f.read() def write_file(self, path, contents, lock_file): with fasteners.InterProcessLock(lock_file): with open(path, "w") as f: f.write(contents) ================================================ FILE: conans/server/store/server_store.py ================================================ import os from os.path import join, normpath, relpath from conan.internal.errors import RecipeNotFoundException, PackageNotFoundException from conan.internal.paths import CONAN_MANIFEST from conan.api.model import PkgReference from conan.api.model import RecipeReference from conans.server.revision_list import RevisionList REVISIONS_FILE = "revisions.txt" SERVER_EXPORT_FOLDER = "export" SERVER_PACKAGES_FOLDER = "package" def ref_dir_repr(ref): return "/".join([ref.name, str(ref.version), ref.user or "_", ref.channel or "_"]) class ServerStore(object): def __init__(self, storage_adapter): self._storage_adapter = storage_adapter self._store_folder = storage_adapter._store_folder @property def store(self): return self._store_folder def base_folder(self, ref): assert ref.revision is not None, "BUG: server store needs RREV to get recipe reference" tmp = normpath(join(self.store, ref_dir_repr(ref))) return join(tmp, ref.revision) def conan_revisions_root(self, ref): """Parent folder of the conan package, for all the revisions""" assert not ref.revision, "BUG: server store doesn't need RREV to conan_revisions_root" return normpath(join(self.store, ref_dir_repr(ref))) def packages(self, ref): return join(self.base_folder(ref), SERVER_PACKAGES_FOLDER) def package(self, pref): assert pref.revision is not None, "BUG: server store needs PREV for package" tmp = join(self.packages(pref.ref), pref.package_id) return join(tmp, pref.revision) def export(self, ref): return join(self.base_folder(ref), SERVER_EXPORT_FOLDER) def get_recipe_file_path(self, ref, filename): abspath = join(self.export(ref), filename) return abspath def get_package_file_path(self, pref, filename): p_path = self.package(pref) abspath = join(p_path, filename) return abspath def path_exists(self, path): return self._storage_adapter.path_exists(path) # ############ ONLY FILE LIST SNAPSHOTS (APIv2) def get_recipe_file_list(self, ref): """Returns a [filepath] """ assert isinstance(ref, RecipeReference) files = self._get_file_list(self.export(ref)) if CONAN_MANIFEST not in files: raise RecipeNotFoundException(ref) return files def get_package_file_list(self, pref): """Returns a [filepath] """ assert isinstance(pref, PkgReference) files = self._get_file_list(self.package(pref)) if CONAN_MANIFEST not in files: raise PackageNotFoundException(pref) return files def _get_file_list(self, relative_path): file_list = self._storage_adapter.get_file_list(relative_path) file_list = [relpath(old_key, relative_path) for old_key in file_list] return file_list def _delete_empty_dirs(self, ref): lock_files = {REVISIONS_FILE, "%s.lock" % REVISIONS_FILE} ref_path = normpath(join(self.store, ref_dir_repr(ref))) if ref.revision: ref_path = join(ref_path, ref.revision) for _ in range(4 if not ref.revision else 5): if os.path.exists(ref_path): if set(os.listdir(ref_path)) == lock_files: for lock_file in lock_files: os.unlink(os.path.join(ref_path, lock_file)) try: # Take advantage that os.rmdir does not delete non-empty dirs os.rmdir(ref_path) except OSError: break # not empty ref_path = os.path.dirname(ref_path) # ######### DELETE (APIv1 and APIv2) def remove_recipe(self, ref): assert isinstance(ref, RecipeReference) if not ref.revision: self._storage_adapter.delete_folder(self.conan_revisions_root(ref)) else: self._storage_adapter.delete_folder(self.base_folder(ref)) self._remove_revision_from_index(ref) self._delete_empty_dirs(ref) def remove_package(self, pref): assert isinstance(pref, PkgReference) assert pref.revision is not None, "BUG: server store needs PREV remove_package" assert pref.ref.revision is not None, "BUG: server store needs RREV remove_package" package_folder = self.package(pref) self._storage_adapter.delete_folder(package_folder) self._remove_package_revision_from_index(pref) def remove_all_packages(self, ref): assert ref.revision is not None, "BUG: server store needs RREV remove_all_packages" assert isinstance(ref, RecipeReference) packages_folder = self.packages(ref) self._storage_adapter.delete_folder(packages_folder) # Methods to manage revisions def get_last_revision(self, ref): assert(isinstance(ref, RecipeReference)) rev_file_path = self._recipe_revisions_file(ref) return self._get_latest_revision(rev_file_path) def get_recipe_revisions_references(self, ref): """Returns a RevisionList""" if ref.revision: tmp = RevisionList() tmp.add_revision(ref.revision) return tmp.as_list() rev_file_path = self._recipe_revisions_file(ref) revs = self._get_revisions_list(rev_file_path).as_list() if not revs: raise RecipeNotFoundException(ref) return revs def get_last_package_revision(self, pref): assert(isinstance(pref, PkgReference)) rev_file_path = self._package_revisions_file(pref) rev = self._get_latest_revision(rev_file_path) if rev: return PkgReference(pref.ref, pref.package_id, rev.revision, rev.time) return None def update_last_revision(self, ref): assert(isinstance(ref, RecipeReference)) rev_file_path = self._recipe_revisions_file(ref) self._update_last_revision(rev_file_path, ref) def update_last_package_revision(self, pref): assert(isinstance(pref, PkgReference)) rev_file_path = self._package_revisions_file(pref) self._update_last_revision(rev_file_path, pref) def _update_last_revision(self, rev_file_path, ref): if self._storage_adapter.path_exists(rev_file_path): rev_file = self._storage_adapter.read_file(rev_file_path, lock_file=rev_file_path + ".lock") rev_list = RevisionList.loads(rev_file) else: rev_list = RevisionList() assert ref.revision is not None, "Invalid revision for: %s" % repr(ref) rev_list.add_revision(ref.revision) self._storage_adapter.write_file(rev_file_path, rev_list.dumps(), lock_file=rev_file_path + ".lock") def get_package_revisions_references(self, pref): """Returns a RevisionList""" assert pref.ref.revision is not None, \ "BUG: server store needs PREV get_package_revisions_references" if pref.revision: tmp = RevisionList() tmp.add_revision(pref.revision) return [PkgReference(pref.ref, pref.package_id, rev.revision, rev.time) for rev in tmp.as_list()] tmp = self._package_revisions_file(pref) ret = self._get_revisions_list(tmp).as_list() if not ret: raise PackageNotFoundException(pref) return [PkgReference(pref.ref, pref.package_id, rev.revision, rev.time) for rev in ret] def _get_revisions_list(self, rev_file_path): if self._storage_adapter.path_exists(rev_file_path): rev_file = self._storage_adapter.read_file(rev_file_path, lock_file=rev_file_path + ".lock") rev_list = RevisionList.loads(rev_file) return rev_list else: return RevisionList() def _get_latest_revision(self, rev_file_path): rev_list = self._get_revisions_list(rev_file_path) if not rev_list: return None return rev_list.latest_revision() def _recipe_revisions_file(self, ref): recipe_folder = normpath(join(self._store_folder, ref_dir_repr(ref))) return join(recipe_folder, REVISIONS_FILE) def _package_revisions_file(self, pref): tmp = normpath(join(self._store_folder, ref_dir_repr(pref.ref))) revision = {None: ""}.get(pref.ref.revision, pref.ref.revision) p_folder = join(tmp, revision, SERVER_PACKAGES_FOLDER, pref.package_id) return join(p_folder, REVISIONS_FILE) def get_package_revision_time(self, pref): try: rev_list = self._load_package_revision_list(pref) except (IOError, OSError): return None return rev_list.get_time(pref.revision) def _remove_revision_from_index(self, ref): rev_list = self._load_revision_list(ref) rev_list.remove_revision(ref.revision) self._save_revision_list(rev_list, ref) def _remove_package_revision_from_index(self, pref): rev_list = self._load_package_revision_list(pref) rev_list.remove_revision(pref.revision) self._save_package_revision_list(rev_list, pref) def _load_revision_list(self, ref): path = self._recipe_revisions_file(ref) rev_file = self._storage_adapter.read_file(path, lock_file=path + ".lock") return RevisionList.loads(rev_file) def _save_revision_list(self, rev_list, ref): path = self._recipe_revisions_file(ref) self._storage_adapter.write_file(path, rev_list.dumps(), lock_file=path + ".lock") def _save_package_revision_list(self, rev_list, pref): path = self._package_revisions_file(pref) self._storage_adapter.write_file(path, rev_list.dumps(), lock_file=path + ".lock") def _load_package_revision_list(self, pref): path = self._package_revisions_file(pref) rev_file = self._storage_adapter.read_file(path, lock_file=path + ".lock") return RevisionList.loads(rev_file) ================================================ FILE: conans/server/utils/__init__.py ================================================ ================================================ FILE: conans/server/utils/files.py ================================================ import os import sys def list_folder_subdirs(basedir, level): ret = [] for root, dirs, _ in os.walk(basedir): rel_path = os.path.relpath(root, basedir) if rel_path == ".": continue dir_split = rel_path.split(os.sep) if len(dir_split) == level: ret.append("/".join(dir_split)) dirs[:] = [] # Stop iterate subdirs return ret def path_exists(path, basedir): """Case sensitive, for windows, optional basedir for skip caps check for tmp folders in testing for example (returned always in lowercase for some strange reason)""" exists = os.path.exists(path) if not exists or sys.platform == "linux2": return exists path = os.path.normpath(path) path = os.path.relpath(path, basedir) chunks = path.split(os.sep) tmp = basedir for chunk in chunks: if chunk and chunk not in os.listdir(tmp): return False tmp = os.path.normpath(tmp + os.sep + chunk) return True def relative_dirs(path): """ Walks a dir and return a list with the relative paths """ ret = [] for dirpath, _, fnames in os.walk(path): for filename in fnames: tmp = os.path.join(dirpath, filename) tmp = tmp[len(path) + 1:] ret.append(tmp) return ret ================================================ FILE: contributors.txt ================================================ Contributors ------------- This is a very partial list of contributors to this project source code, in alphabetical order, those who explicitly wanted to add details here. For the whole list (+220), refer to https://github.com/conan-io/conan/graphs/contributors Many thanks to all of them! - Bocanegra Algarra, Raul (raul.bocanegra.algarra@gmail.com) - Cicholewski, Adrian (et666@t-online.de) - Dauphin, Loïc (astralien3000@yahoo.fr, @astralien3000) - Díaz Más, Luis (piponazo@gmail, @pipotux) - Dragly, Svenn-Arne (dragly.org) - Ford, Andrew (andrewford55139@gmail.com) - Hieta, Tobias (tobias@plex.tv, @tobiashieta) - Hochstedler, Reid - Ivek, Tomislav (tomislav.ivek@gmail.com, @tomivek) - Koch, Marco (marco-koch@t-online.de, @MarcoKoch) - Kourkoulis, Dimitri (@dimi309) - Lee, Jeongseok (jslee02@gmail.com, @jslee02) - Lord, Matthew( @luckielordie ) - Márki, Róbert (gsmiko@gmail.com, @robertmrk) - Ray, Chris (chris@xaltotun.com) - Ries, Uilian (uilianries@gmail.com, @uilianries) - Sechet, Olivier (osechet@gmail.com) - Sturm, Fabian (f@rtfs.org, @sturmf) - Williams, Jordan (jordan@jwillikers.com) ================================================ FILE: pyinstaller.py ================================================ """ This file is able to create a self contained Conan executable that contains all it needs, including the Python interpreter, so it wouldnt be necessary to have Python installed in the system It is important to install the dependencies and the project first with "pip install -e ." which configures the project as "editable", that is, to run from the current source folder After creating the executable, it can be pip uninstalled $ pip install -e . $ python pyinstaller.py This has to run in the same platform that will be using the executable, pyinstaller does not cross-build The resulting executable can be put in the system PATH of the running machine """ import argparse import os import platform import shutil import sys from conan import __version__ def _run_bin(pyinstaller_path): # run the binary to test if working conan_bin = os.path.join(pyinstaller_path, 'dist', 'conan', 'conan') if platform.system() == 'Windows': conan_bin = '"' + conan_bin + '.exe' + '"' retcode = os.system(conan_bin) if retcode != 0: raise Exception("Binary not working") def _windows_version_file(version): template = """# UTF-8 # # For more details about fixed file info 'ffi' see: # http://msdn.microsoft.com/en-us/library/ms646997.aspx VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. filevers={version_tuple}, prodvers={version_tuple}, # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. flags=0x0, # The operating system for which this file was designed. # 0x4 - NT and there is no need to change it. OS=0x4, # The general type of file. # 0x1 - the file is an application. fileType=0x1, # The function of the file. # 0x0 - the function is not defined for this fileType subtype=0x0, # Creation date and time stamp. date=(0, 0) ), kids=[ StringFileInfo( [ StringTable( u'000004b0', [StringStruct(u'Comments', u'This executable was created with pyinstaller'), StringStruct(u'CompanyName', u'JFrog'), StringStruct(u'FileDescription', u'Conan C, C++ Open Source Package Manager'), StringStruct(u'FileVersion', u'{version}'), StringStruct(u'LegalCopyright', u'Copyright 2020 JFrog'), StringStruct(u'ProductName', u'Conan'), StringStruct(u'ProductVersion', u'{version}')]) ]), VarFileInfo([VarStruct(u'Translation', [0, 1200])]) ] )""" if "-" in version: version, _ = version.split("-") version_tuple = tuple([int(v) for v in version.split(".")] + [0]) return template.format(version=version, version_tuple=version_tuple) def pyinstall(source_folder, onefile=False): try: import PyInstaller.__main__ # noqa Pyinstaller is not in the default requirements.txt except ImportError: print(f"ERROR: PyInstaller not found. Please install it with " f"'python -m pip install pyinstaller'") sys.exit(-1) pyinstaller_path = os.path.join(os.getcwd(), 'pyinstaller') if not os.path.exists(pyinstaller_path): os.mkdir(pyinstaller_path) try: shutil.rmtree(os.path.join(pyinstaller_path)) except Exception as e: print("Unable to remove old folder", e) conan_path = os.path.join(source_folder, 'conans', 'conan.py') hidden_imports = [] if not os.path.exists(pyinstaller_path): os.mkdir(pyinstaller_path) if platform.system() != "Windows": hidden_imports.append("setuptools.msvc") win_ver = "" else: win_ver_file = os.path.join(pyinstaller_path, 'windows-version-file') content = _windows_version_file(__version__) with open(win_ver_file, 'w') as file: file.write(content) win_ver = ["--version-file", win_ver_file] if onefile: distpath = os.path.join(pyinstaller_path, "dist", "conan") else: distpath = os.path.join(pyinstaller_path, "dist") command_args = [conan_path, "--noconfirm", f"--paths={source_folder}", "--console", f"--distpath={distpath}"] # Python standard library modules to ensure in the bundle hidden_imports.append("glob") hidden_imports.append("pathlib") # inclusion of full conan package (with exclusion of irrelevant modules) command_args.append("--collect-submodules=conan") command_args.append("--exclude-module=conan.test.*") command_args.extend(f"--hidden-import={name}" for name in hidden_imports) if win_ver: command_args.extend(win_ver) if onefile: command_args.append("--onefile") PyInstaller.__main__.run(command_args) _run_bin(pyinstaller_path) return os.path.abspath(os.path.join(pyinstaller_path, 'dist', 'conan')) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--onefile', action='store_true') parser.set_defaults(onefile=False) args = parser.parse_args() src_folder = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) output_folder = pyinstall(src_folder, args.onefile) print("\n**************Conan binaries created!******************\n" "\nAppend this folder to your system PATH: '%s'\n" "Feel free to move the whole folder to another location." % output_folder) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" ================================================ FILE: pytest.ini ================================================ [pytest] norecursedirs = '.*', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv', 'assets' testpaths = 'test' markers = docker_runner: Mark tests that require Docker to run. artifactory_ready: These tests can be run against a full Artifactory slow: These tests can executed more occasionally, slow, but unlikely to fail in normal PRs filterwarnings = ignore:'cgi' is deprecated:DeprecationWarning ================================================ FILE: setup.cfg ================================================ [pep8] count = False max-line-length = 100 statistics = False ; Coverage files are generated across different OS and machines, thus, coverage ; reports will contain different absolute paths to reference to the same ; project relative paths. ; This option makes coverage to use relative paths to the project root, ; removing the need for setting arbitrary runners paths described in here: ; https://coverage.readthedocs.io/en/coverage-4.0.3/config.html#paths [coverage:run] relative_files = True ================================================ FILE: setup.py ================================================ """A setuptools based setup module. See: https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ # Always prefer setuptools over distutils from setuptools import find_packages, setup import os import re from os import path # The tests utils are used by conan-package-tools here = path.abspath(path.dirname(__file__)) excluded_test_packages = ["test*"] def get_requires(filename): requirements = [] with open(filename, "rt") as req_file: for line in req_file.read().splitlines(): if not line.strip().startswith("#"): requirements.append(line) return requirements def load_version(): """ Loads a file content """ filename = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "conan", "__init__.py")) with open(filename, "rt") as version_file: conan_init = version_file.read() version = re.search(r"__version__ = '([0-9a-z.-]+)'", conan_init).group(1) return version def generate_long_description_file(): this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, 'README.md')) as f: long_description = f.read() return long_description project_requirements = get_requires("conans/requirements.txt") dev_requirements = get_requires("conans/requirements_dev.txt") runners_requirements = get_requires("conans/requirements_runner.txt") excluded_server_packages = ["conans.server*"] exclude = excluded_test_packages + excluded_server_packages setup( name='conan', python_requires='>=3.7', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html version=load_version(), # + ".rc1", description='Conan C/C++ package manager', long_description=generate_long_description_file(), long_description_content_type='text/markdown', # The project's main homepage. url='https://conan.io', project_urls={ 'Documentation': 'https://docs.conan.io', 'Source': 'https://github.com/conan-io/conan', 'Tracker': 'https://github.com/conan-io/conan/issues', }, # Author details author='JFrog LTD', author_email='luism@jfrog.com', # Choose your license license='MIT', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13' ], # What does your project relate to? keywords=['C/C++', 'package', 'libraries', 'developer', 'manager', 'dependency', 'tool', 'c', 'c++', 'cpp'], # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). packages=find_packages(exclude=exclude), # Alternatively, if you want to distribute just a my_module.py, uncomment # this: # py_modules=["my_module"], # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=project_requirements, # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, # for example: # $ pip install -e .[dev,test,runners] extras_require={ 'dev': dev_requirements, 'test': dev_requirements, 'runners': runners_requirements }, # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. package_data={ 'conans': ['*.txt'], }, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. See: # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa # In this case, 'data_file' will be installed into '/my_data' # data_files=[('my_data', ['data/data_file'])], # To provide executable scripts, use entry points in preference to the # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. entry_points={ 'console_scripts': [ 'conan=conans.conan:run', ], }, ) ================================================ FILE: setup_server.py ================================================ """A setuptools based setup module. See: https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ # To use a consistent encoding from os import path # Always prefer setuptools over distutils from setuptools import find_packages, setup import os import re # The tests utils are used by conan-package-tools here = path.abspath(path.dirname(__file__)) excluded_test_packages = ["test"] def get_requires(filename): requirements = [] with open(filename, "rt") as req_file: for line in req_file.read().splitlines(): if not line.strip().startswith("#"): requirements.append(line) return requirements def load_version(): """ Loads a file content """ filename = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "conan", "__init__.py")) with open(filename, "rt") as version_file: conan_init = version_file.read() version = re.search(r"__version__ = '([0-9a-z.-]+)'", conan_init).group(1) return version def generate_long_description_file(): this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: long_description = f.read() return long_description project_requirements = get_requires("conans/requirements.txt") project_requirements.extend(get_requires("conans/requirements_server.txt")) dev_requirements = get_requires("conans/requirements_dev.txt") setup( name='conan_server', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html version=load_version(), # + ".rc1", description='Conan Server of Conan C/C++ package manager', long_description=generate_long_description_file(), long_description_content_type='text/markdown', # The project's main homepage. url='https://conan.io', # Author details author='JFrog LTD', author_email='luism@jfrog.com', # Choose your license license='MIT', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13' ], # What does your project relate to? keywords=['C/C++', 'package', 'libraries', 'developer', 'manager', 'dependency', 'tool', 'c', 'c++', 'cpp'], # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). packages=find_packages(exclude=excluded_test_packages + ["./setup.py"]), # Alternatively, if you want to distribute just a my_module.py, uncomment # this: # py_modules=["my_module"], # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=project_requirements, # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, # for example: # $ pip install -e .[dev,test] extras_require={ 'dev': dev_requirements, 'test': dev_requirements, }, # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. package_data={ 'conans': ['*.txt'], }, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. See: # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa # In this case, 'data_file' will be installed into '/my_data' # data_files=[('my_data', ['data/data_file'])], # To provide executable scripts, use entry points in preference to the # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. entry_points={ 'console_scripts': [ 'conan_server=conans.conan_server:run' ], }, ) ================================================ FILE: test/.gitignore ================================================ conftest_user.py ================================================ FILE: test/README.md ================================================ # Conan Testing Conan tests fall into three categories: - **Unit tests** in `test/unittests` folder. These tests should test small pieces of code like functions, methods, or properties. As long as it's possible they should not rely on anything external like the file system or system configuration, and in case they need to do it should be mocked. - **Integration tests** in `test/integration` folder. We consider integration tests the ones that only will need pure python to execute, but that may test the interaction between different Conan modules. They could test the result of the execution of one or several Conan commands, but shouldn't depend on any external tools like compilers, build systems, or version-control system tools. - **Functional tests** in `test/functional` folder. Under this category, we add tests that are testing the complete Conan functionality. They may call external tools (please read the section below to check the tools installed on the CI). These tests should be avoided as long as it's possible as they may take considerable CI time. ## Writing tests We use [Pytest](https://docs.pytest.org/en/stable/) as the testing framework. There are some important things to have in mind regarding test discovery and style. ### Naming files and methods Pytest follows this [convention](https://docs.pytest.org/en/stable/goodpractices.html) for test discovery: - Name your Python test files starting in `test_`. ``` test ├── README.md ├── conftest.py ├── unittests │   ├── __init__.py │   ├── test_mytest.py │ ... ... ``` - Tests inside those Python files should follow this name convention: - `test` prefixed test functions or methods outside of class. - `test` prefixed test functions or methods inside `Test` prefixed test classes (without an `__init__` method). ```python class TestSomeFunctionality: def test_plus_name(self): client = TestClient() conanfile = textwrap.dedent(""" ... ``` ### Marking tests Please mark your tests if they need to. Besides the [built-in Pytest markers](https://docs.pytest.org/en/stable/mark.html#mark) we interpret some markers related to external tools: `cmake`, `gcc`, `clang`, `visual_studio`, `mingw`, `autotools`, `pkg_config`, `premake`, `meson`, `file`, `git`, `svn`, `compiler` and `conan`. For example: ```python @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows for vcvars") @pytest.mark.tool("visual_studio") def test_vcvars_priority(self): client = TestClient() ... ``` If the test needs any of those tools to run it should be marked as using that tool and moved to the `test/functional` folder. Note that only tests in ``test/functional`` might need the ``@pytest.mark.tool`` annotation. Tests in ``integration`` or ``unittest`` should never require an extra tool. ### Parametrizing tests Please, if you need to run several combinations of the same testing code use parameterization. You can use the builtin `pytest.mark.parametrize` decorator to enable parametrization of arguments for a test function: ```python @pytest.mark.parametrize("use_components", [False, True]) def test_build_modules_alias_target(self, use_components): ... ``` ## Running tests locally If you want to run the Coman test suite locally, please check the [README on the front page](https://github.com/conan-io/conan#running-the-tests). Recall it is not expected for contributors to run the full test suite locally, only: - Run the ``unittest`` and ``integration`` tests. These shouldn't require any external tools - If doing modifications to some specific build-system integration, locate the relevant folder under ``functional/toolchains`` and run those tests only. The reason is that the ``functional`` test suite uses too many different external tools, and installing all of them can be tedious. The Conan CI system will run those tests. ## Installation of tools Work in progress! Note the ``test/conftest.py`` file contains the upstream configuration of tools. This file should not be changed, but users can create a ``test/conftest_user.py`` file containing their local definitions of tools, that will override the ``conftest.py`` definitions. ### Windows msys2, mingw64 and mingw32 Download msys2 (64 bit) from msys2.org To install mingw64 and mingw32 open a msys2 terminal and type: ``` $ pacman -Syuu $ pacman -S mingw-w64-x86_64-toolchain $ pacman -S mingw-w64-i686-toolchain $ pacman -S base-devel gcc $ pacman -S autoconf-wrapper $ pacman -S automake ``` ================================================ FILE: test/__init__.py ================================================ ================================================ FILE: test/conftest.py ================================================ import os import pathlib import platform import uuid from shutil import which import pytest from conan.internal.api.detect.detect_vs import vswhere """ To override these locations with your own in your dev machine: 1. Create a conftest_user.py just besides this conftest.py file 2. This file is .gitignored, it will not be committed 3. Override the tools_locations, you can completely disabled some tools, tests will be skipped 4. Empty dicts, without specifying the path, means the tool is already in the system path tools_locations = { 'svn': {"disabled": True}, 'cmake': { "default": "3.19", "3.15": {}, "3.16": {"disabled": True}, "3.17": {"disabled": True}, "3.19": {"path": {"Windows": "C:/ws/cmake/cmake-3.19.7-windows-x86_64/bin"}}, # To explicitly skip one tool for one version, define the path as 'skip-tests' # if you don't define the path for one platform it will run the test with the # tool in the path. For example here it will skip the test with CMake in Darwin but # in Linux it will run with the version found in the path if it's not specified "3.23": {"path": {"Windows": "C:/ws/cmake/cmake-3.19.7-windows-x86_64/bin", "Darwin": "skip-tests"}}, }, 'ninja': { "1.10.2": {} }, 'meson': {"disabled": True}, 'bazel': { "system": {"path": {'Windows': 'C:/ws/bazel/4.2.0'}}, } } """ MacOS_arm = all([platform.system() == "Darwin", platform.machine() == "arm64"]) homebrew_root = "/opt/homebrew" if MacOS_arm else "/usr/local" windows_choco_root = "C:/ProgramData/chocolatey/lib/" msys2_path = os.getenv("MSYS2_PATH", "C:/msys64") tools_locations = { "clang": { "exe": "clang", "default": "20", "20": { "path": {'Windows': 'C:/Program Files/LLVM/bin'} # by choco } }, 'visual_studio': {"default": "15", "15": {}, "16": {"disabled": True}, "17": {}}, 'pkg_config': { "exe": "pkg-config", "default": "0.28", "0.28": { "path": { # Using chocolatey in Windows -> choco install pkgconfiglite --version 0.28 'Windows': f"{windows_choco_root}/pkgconfiglite/tools/pkg-config-lite-0.28-1/bin", 'Darwin': f"{homebrew_root}/bin", 'Linux': "/usr/bin" } }}, 'autotools': {"exe": "autoconf"}, 'cmake': { "default": "3.15", "3.15": { "path": {'Windows': 'C:/tools/cmake/3.15.7/cmake-3.15.7-win64-x64/bin', 'Darwin': '/Users/runner/Applications/CMake/3.15.7/bin', 'Linux': '/usr/share/cmake-3.15.7/bin'} }, "3.19": { "path": {'Windows': 'C:/tools/cmake/3.19.7/cmake-3.19.7-win64-x64/bin', 'Darwin': '/Users/runner/Applications/CMake/3.19.7/bin', 'Linux': '/usr/share/cmake-3.19.7/bin'} }, "3.23": { "path": {'Windows': 'C:/tools/cmake/3.23.5/cmake-3.23.5-windows-x86_64/bin', 'Darwin': '/Users/runner/Applications/CMake/3.23.5/bin', 'Linux': "/usr/share/cmake-3.23.5/bin"} }, "3.27": { "path": {'Windows': 'C:/tools/cmake/3.27.9/cmake-3.27.9-windows-x86_64/bin', 'Darwin': '/Users/runner/Applications/CMake/3.27.9/bin', 'Linux': "/usr/share/cmake-3.27.9/bin"} }, "4.2": { "path": {'Windows': 'C:/tools/cmake/4.2.1/cmake-4.2.1-windows-x86_64/bin', 'Darwin': '/Users/runner/Applications/CMake/4.2.1/bin', 'Linux': "/usr/share/cmake-4.2.1/bin"} } }, 'ninja': { "default": "1.10.2", "1.10.2": { "path": {'Windows': f'{windows_choco_root}/ninja/tools'} } }, # This is the non-msys2 mingw, which is 32 bits x86 arch 'mingw': { "disabled": True, "platform": "Windows", "default": "system", "exe": "mingw32-make", "system": {"path": {'Windows': "C:/ProgramData/mingw64/mingw64/bin"}}, }, 'mingw32': { "platform": "Windows", "default": "system", "exe": "mingw32-make", "system": {"path": {'Windows': f"{msys2_path}/mingw32/bin"}}, }, 'ucrt64': { "platform": "Windows", "default": "system", "exe": "mingw32-make", "system": {"path": {'Windows': f"{msys2_path}/ucrt64/bin"}}, }, 'mingw64': { "platform": "Windows", "default": "system", "exe": "mingw32-make", "system": {"path": {'Windows': f"{msys2_path}/mingw64/bin"}}, }, 'msys2': { "platform": "Windows", "default": "system", "exe": "make", "system": {"path": {'Windows': f"{msys2_path}/usr/bin"}}, }, 'msys2_clang64': { "platform": "Windows", "default": "system", "exe": "clang", "system": {"path": {'Windows': f"{msys2_path}/clang64/bin"}}, }, 'msys2_mingw64_clang64': { "disabled": True, "platform": "Windows", "default": "system", "exe": "clang", "system": {"path": {'Windows': f"{msys2_path}/mingw64/bin"}}, }, 'cygwin': { "platform": "Windows", "default": "system", "exe": "make", "system": {"path": {'Windows': "C:/tools/cygwin/bin"}}, }, 'bazel': { "default": "7.x", "6.x": {"path": {'Linux': '/usr/share/bazel-6.5.0/bin', 'Windows': 'C:/tools/bazel/6.5.0', 'Darwin': '/Users/runner/Applications/bazel/6.5.0'}}, "7.x": {"path": {'Linux': '/usr/share/bazel-7.6.2/bin', 'Windows': 'C:/tools/bazel/7.6.2', 'Darwin': '/Users/runner/Applications/bazel/7.6.2'}}, "8.x": {"path": {'Linux': '/usr/share/bazel-8.4.2/bin', 'Windows': 'C:/tools/bazel/8.4.2', 'Darwin': '/Users/runner/Applications/bazel/8.4.2'}}, }, 'premake': { "exe": "premake5", "default": "5.0.0", "5.0.0": { "path": {'Linux': '/usr/share/premake', 'Windows': 'skip-tests', 'Darwin': 'skip-tests'} } }, 'xcodegen': {"platform": "Darwin"}, 'apt_get': {"exe": "apt-get"}, 'brew': {}, 'android_ndk': { "platform": "Darwin", "exe": "ndk-build", "default": "system", "system": { "path": {'Darwin': os.getenv("ANDROID_NDK")} # 'Windows': os.getenv("ANDROID_NDK_HOME"), } }, "qbs": { "exe": "qbs", "default": "2.6.0", "2.6.0": { "path": {'Linux': '/usr/share/qbs/bin'} } }, "emcc": {}, "node": {}, # TODO: Intel oneAPI is not installed in CI yet. Uncomment this line whenever it's done. # "intel_oneapi": { # "default": "2021.3", # "exe": "dpcpp", # "2021.3": {"path": {"Linux": "/opt/intel/oneapi/compiler/2021.3.0/linux/bin"}} # } } # TODO: Make this match the default tools (compilers) above automatically try: from test.conftest_user import tools_locations as user_tool_locations def update(d, u): for k, v in u.items(): if isinstance(v, dict): d[k] = update(d.get(k, {}), v) else: d[k] = v return d update(tools_locations, user_tool_locations) except ImportError as e: user_tool_locations = None tools_environments = { 'mingw32': {'Windows': {'MSYSTEM': 'MINGW32'}}, 'mingw64': {'Windows': {'MSYSTEM': 'MINGW64'}}, 'ucrt64': {'Windows': {'MSYSTEM': 'UCRT64'}}, 'msys2_clang64': {"Windows": {"MSYSTEM": "CLANG64"}} } _cached_tools = {} def _get_tool(name, version): # None: not cached yet # False = tool not available, legally skipped # True = tool not available, test error # (path, env) = tool available cached = _cached_tools.setdefault(name, {}).get(version) if cached is not None: return cached result = _get_individual_tool(name, version) _cached_tools[name][version] = result return result def _get_individual_tool(name, version): tool = tools_locations.get(name, {}) if tool.get("disabled"): return False tool_platform = platform.system() if tool.get("platform", tool_platform) != tool_platform: return None, None version = version or tool.get("default") tool_version = tool.get(version) if tool_version is not None: assert isinstance(tool_version, dict) if tool_version.get("disabled"): return False if name == "visual_studio": if vswhere(): # TODO: Missing version detection return None, None tool_path = tool_version.get("path", {}).get(tool_platform) tool_path = tool_path.replace("/", "\\") if tool_platform == "Windows" and tool_path is not None else tool_path # To allow to skip for a platform, we can put the path to None # "cmake": { "3.23": { # "path": {'Windows': 'C:/cmake/cmake-3.23.1-windows-x86_64/bin', # 'Darwin': '/Users/jenkins/cmake/cmake-3.23.1/bin', # 'Linux': None}} # } if tool_path == "skip-tests": return False elif tool_path is not None and not os.path.isdir(tool_path): return True else: if version is not None: # if the version is specified, it should be in the conf return True tool_path = None try: tool_env = tools_environments[name][tool_platform] except KeyError: tool_env = None cached = tool_path, tool_env # Check this particular tool is installed old_environ = None if tool_path is not None: old_environ = dict(os.environ) os.environ["PATH"] = tool_path + os.pathsep + os.environ["PATH"] exe = tool.get("exe", name) exe_found = which(exe) # TODO: This which doesn't detect version either exe_path = str(pathlib.Path(exe_found).parent) if exe_found else None if not exe_found: cached = True if tool_path is None: # will fail the test, not exe found and path None cached = True elif tool_path is not None and tool_path not in exe_found: # finds the exe in a path that is not the one set in the conf -> fail cached = True elif tool_path is None: cached = exe_path, tool_env if old_environ is not None: os.environ.clear() os.environ.update(old_environ) return cached def pytest_configure(config): # register an additional marker config.addinivalue_line( "markers", "tool(name, version): mark test to require a tool by name" ) def pytest_runtest_teardown(item): if hasattr(item, "old_environ"): os.environ.clear() os.environ.update(item.old_environ) def pytest_runtest_setup(item): tools_paths = [] tools_env_vars = dict() for mark in item.iter_markers(): if mark.name.startswith("tool_"): raise Exception("Invalid decorator @pytest.mark.{}".format(mark.name)) kwargs = [mark.kwargs for mark in item.iter_markers(name="tool")] if any(kwargs): raise Exception("Invalid decorator @pytest.mark Do not use kwargs: {}".format(kwargs)) tools_params = [mark.args for mark in item.iter_markers(name="tool")] for tool_params in tools_params: if len(tool_params) == 1: tool_name = tool_params[0] tool_version = None elif len(tool_params) == 2: tool_name, tool_version = tool_params else: raise Exception("Invalid arguments for mark.tool: {}".format(tool_params)) result = _get_tool(tool_name, tool_version) if result is True: version_msg = "Any" if tool_version is None else tool_version pytest.fail("Required '{}' tool version '{}' is not available".format(tool_name, version_msg)) if result is False: version_msg = "Any" if tool_version is None else tool_version pytest.skip("Required '{}' tool version '{}' is not available".format(tool_name, version_msg)) tool_path, tool_env = result if tool_path: tools_paths.append(tool_path) if tool_env: tools_env_vars.update(tool_env) # Fix random failures CI because of this: https://issues.jenkins.io/browse/JENKINS-9104 if tool_name == "visual_studio": tools_env_vars['_MSPDBSRV_ENDPOINT_'] = str(uuid.uuid4()) if tools_paths or tools_env_vars: item.old_environ = dict(os.environ) tools_env_vars['PATH'] = os.pathsep.join(tools_paths + [os.environ["PATH"]]) os.environ.update(tools_env_vars) ================================================ FILE: test/functional/__init__.py ================================================ ================================================ FILE: test/functional/command/__init__.py ================================================ ================================================ FILE: test/functional/command/dockerfiles/Dockerfile ================================================ FROM ubuntu:22.04 RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ cmake \ python3 \ python3-pip \ python3-venv \ && rm -rf /var/lib/apt/lists/* COPY . /root/conan-io RUN cd /root/conan-io && pip install -e . ================================================ FILE: test/functional/command/dockerfiles/Dockerfile_args ================================================ ARG BASE_IMAGE FROM $BASE_IMAGE RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ cmake \ python3 \ python3-pip \ python3-venv \ && rm -rf /var/lib/apt/lists/* COPY . /root/conan-io RUN cd /root/conan-io && pip install -e . ================================================ FILE: test/functional/command/dockerfiles/Dockerfile_ninja ================================================ FROM ubuntu:22.04 RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ cmake \ ninja-build \ python3 \ python3-pip \ python3-venv \ && rm -rf /var/lib/apt/lists/* COPY . /root/conan-io RUN cd /root/conan-io && pip install -e . ================================================ FILE: test/functional/command/dockerfiles/Dockerfile_profile_detect ================================================ FROM ubuntu:22.04 RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ cmake \ python3 \ python3-pip \ python3-venv \ && rm -rf /var/lib/apt/lists/* COPY . /root/conan-io RUN cd /root/conan-io && pip install -e . RUN conan profile detect ================================================ FILE: test/functional/command/dockerfiles/Dockerfile_test ================================================ FROM ubuntu:22.04 RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ cmake \ python3 \ python3-pip \ python3-venv \ && rm -rf /var/lib/apt/lists/* COPY . /root/conan-io RUN cd /root/conan-io && pip install -e . ================================================ FILE: test/functional/command/export_test.py ================================================ import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.scm import git_add_changes_commit from conan.test.utils.tools import TestClient from conan.internal.util.files import save @pytest.mark.tool("git") class TestRevisionModeSCM: def test_revision_mode_scm(self): t = TestClient() conanfile = str(GenConanfile().with_class_attribute('revision_mode = "scm"')) commit = t.init_git_repo({'conanfile.py': conanfile}) t.run(f"export . --name=pkg --version=0.1") assert t.exported_recipe_revision() == commit # Now it will fail if dirty t.save({"conanfile.py": conanfile + "\n#comment"}) t.run(f"export . --name=pkg --version=0.1", assert_error=True) assert "Can't have a dirty repository using revision_mode='scm' and doing" in t.out # Commit to fix commit2 = git_add_changes_commit(t.current_folder, msg="fix") t.run(f"export . --name=pkg --version=0.1") assert t.exported_recipe_revision() == commit2 def test_revision_mode_scm_subfolder(self): """ emulates a mono-repo with 2 subprojects, when a change is done in a subproject it gets a different folder commit """ t = TestClient() conanfile = str(GenConanfile().with_class_attribute('revision_mode = "scm_folder"')) commit = t.init_git_repo({'pkga/conanfile.py': conanfile, 'pkgb/conanfile.py': conanfile}) t.save({"pkgb/conanfile.py": conanfile + "\n#comment"}) commit_b = git_add_changes_commit(os.path.join(t.current_folder, "pkgb"), msg="fix") # pkga still gets the initial commit, as it didn't change its contents t.run(f"export pkga --name=pkga --version=0.1") assert t.exported_recipe_revision() == commit # but pkgb will get the commit of the new changed folder t.run(f"export pkgb --name=pkgb --version=0.1") assert t.exported_recipe_revision() == commit_b # if pkgb is dirty, we should still be able to correctly create pkga in 'scm_folder' mode t.save({"pkgb/conanfile.py": conanfile + "\n#new comment"}) t.run(f"export pkga --name=pkga --version=0.1") assert t.exported_recipe_revision() == commit def test_auto_revision_without_commits(self): """If we have a repo but without commits, it has to fail when the revision_mode=scm""" t = TestClient() t.run_command('git init .') t.save({"conanfile.py": GenConanfile("lib", "0.1").with_revision_mode("scm")}) t.run("export .", assert_error=True) # It errors, because no commits yet assert "Cannot detect revision using 'scm' mode from repository" in t.out @pytest.mark.parametrize("conf_excluded, recipe_excluded", [("", ["*.cpp", "*.txt", "src/*"]), (["*.cpp", "*.txt", "src/*"], ""), ('+["*.cpp", "*.txt"]', ["src/*"]), ('+["*.cpp"]', ["*.txt", "src/*"])]) def test_revision_mode_scm_excluded_files(self, conf_excluded, recipe_excluded): t = TestClient() recipe_excluded = f'revision_mode_excluded = {recipe_excluded}' if recipe_excluded else "" conf_excluded = f'core.scm:excluded={conf_excluded}' if conf_excluded else "" save(t.paths.global_conf_path, conf_excluded) conanfile = GenConanfile("pkg", "0.1").with_class_attribute('revision_mode = "scm"') \ .with_class_attribute(recipe_excluded) commit = t.init_git_repo({'conanfile.py': str(conanfile), "test.cpp": "mytest"}) t.run(f"export .") assert t.exported_recipe_revision() == commit t.save({"test.cpp": "mytest2", "new.txt": "new", "src/potato": "hello"}) t.run(f"export . -vvv") assert t.exported_recipe_revision() == commit t.save({"test.py": ""}) t.run(f"export .", assert_error=True) assert "ERROR: Can't have a dirty repository using revision_mode='scm'" in t.out ================================================ FILE: test/functional/command/profile_test.py ================================================ import os import platform import textwrap import pytest from conan.internal.api.profile.detect import detect_defaults_settings from conan.test.utils.mocks import RedirectedTestOutput from conan.test.utils.tools import TestClient, redirect_output from conan.test.utils.env import environment_update from conan.internal.util.files import save from conan.internal.util.runners import detect_runner from conan.tools.microsoft.visual import vcvars_command class TestProfile: def test_list_empty(self): client = TestClient() client.run("profile list") assert "Profiles found in the cache:" in client.out def test_list(self): client = TestClient() profiles = ["default", "profile1", "profile2", "profile3", "nested" + os.path.sep + "profile4", "nested" + os.path.sep + "two" + os.path.sep + "profile5", "nested" + os.path.sep + "profile6"] if platform.system() != "Windows": profiles.append("symlink_me" + os.path.sep + "profile7") for profile in profiles: save(os.path.join(client.paths.profiles_path, profile), "") if platform.system() != "Windows": os.symlink(os.path.join(client.paths.profiles_path, 'symlink_me'), os.path.join(client.paths.profiles_path, 'link')) # profile7 will be shown twice because it is symlinked. profiles.append("link" + os.path.sep + "profile7") # Make sure local folder doesn't interact with profiles os.mkdir(os.path.join(client.current_folder, "profile3")) client.run("profile list") for p in profiles: assert p in client.out # Test profile list json file # FIXME: Json cannot be captured from tests? # client.run("profile list --format=json > profiles_list.json") # json_content = client.load("profiles_list.json") # json_obj = json.loads(json_content) # self.assertEqual(list, type(json_obj)) # self.assertEqual(profiles, json_obj) def test_show(self): client = TestClient() profile1 = textwrap.dedent(""" [settings] os=Windows [options] MyOption=32 [conf] tools.build:jobs=20 """) save(os.path.join(client.paths.profiles_path, "profile1"), profile1) client.run("profile show -pr=profile1") assert "[settings]\nos=Windows" in client.out assert "MyOption=32" in client.out # assert "CC=/path/tomy/gcc_build", client.out) # assert "CXX=/path/tomy/g++_build", client.out) # assert "package:VAR=value", client.out) assert "tools.build:jobs=20" in client.out def test_missing_subarguments(self): client = TestClient() client.run("profile", assert_error=True) assert "ERROR: Exiting with code: 2" in client.out class TestDetectCompilers: def test_detect_default_compilers(self): platform_default_compilers = { "Linux": "gcc", "Darwin": "apple-clang", "Windows": "msvc" } result = detect_defaults_settings() # result is a list of tuples (name, value) so converting it to dict result = dict(result) platform_compiler = platform_default_compilers.get(platform.system(), None) if platform_compiler is not None: assert result.get("compiler", None) == platform_compiler @pytest.mark.tool("gcc") @pytest.mark.skipif(platform.system() != "Darwin", reason="only OSX test") def test_detect_default_in_mac_os_using_gcc_as_default(self): """ Test if gcc in Mac OS X is using apple-clang as frontend """ # See: https://github.com/conan-io/conan/issues/2231 _, output = detect_runner("gcc --version") assert "clang" in output, "Apple gcc doesn't point to clang with gcc frontend anymore!" # Not test scenario gcc should display clang in output # see: https://stackoverflow.com/questions/19535422/os-x-10-9-gcc-links-to-clang output = RedirectedTestOutput() # Initialize each command with redirect_output(output): with environment_update({"CC": "gcc"}): result = detect_defaults_settings() # result is a list of tuples (name, value) so converting it to dict result = dict(result) # No compiler should be detected assert result.get("compiler", None) is None assert "gcc detected as a frontend using apple-clang" in output def test_profile_new(self): c = TestClient() c.run("profile detect --name=./MyProfile2") profile = c.load("MyProfile2") assert "os=" in profile assert "compiler.runtime_type" not in profile # Even in Windows c.run("profile detect --name=./MyProfile2", assert_error=True) assert "MyProfile2' already exists" in c.out c.run("profile detect --name=./MyProfile2 --force") # will not raise error assert "build_type=Release" in c.load("MyProfile2") c.save({"MyProfile2": "potato"}) c.run("profile detect --name=./MyProfile2 --exist-ok") # wont raise, won't overwrite assert "Profile './MyProfile2' already exists, skipping detection" in c.out assert c.load("MyProfile2") == "potato" @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows and msvc") def test_profile_new_msvc_vcvars(self): c = TestClient() # extract location of cl.exe from vcvars vcvars = vcvars_command(version="15", architecture="x64") ret = c.run_command(f"{vcvars} && where cl.exe") assert ret == 0 cl_executable = c.out.splitlines()[-1] assert os.path.isfile(cl_executable) cl_location = os.path.dirname(cl_executable) # Try different variations, including full path and full path with quotes for var in ["cl", "cl.exe", cl_executable, f'"{cl_executable}"']: output = RedirectedTestOutput() with redirect_output(output): with environment_update({"CC": var, "PATH": cl_location}): c.run("profile detect --name=./cl-profile --force") profile = c.load("cl-profile") assert "compiler=msvc" in profile assert "compiler.version=191" in profile ================================================ FILE: test/functional/command/report_test.py ================================================ import json import os import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.fixture def fixture_client(): tc = TestClient(light=True, default_server_user=True) patch_file = textwrap.dedent(""" diff --git a/foo.txt b/foo.txt new file mode 100644 index 0000000..e311fbb --- /dev/null +++ b/foo.txt @@ -0,0 +1 @@ +{version} """) conandata_yml = textwrap.dedent(""" patches: "{version}": - patch_file: "patches/patch.patch" """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import apply_conandata_patches, export_conandata_patches, save class TestConan(ConanFile): name = "pkg" exports = "{version}.txt" def export_sources(self): export_conandata_patches(self) def source(self): save(self, "myfile.txt", "{version}") if self.version == "2.0": save(self, "new-file-for-v2.txt", "a new file for the new version") apply_conandata_patches(self) """) tc.save({ "v1/conanfile.py": conanfile.format(version="v1"), "v1/conandata.yml": conandata_yml.format(version="1.0"), "v1/v1.txt": "1.0", "v1/patches/patch.patch": patch_file.format(version="1.0"), "v2/conanfile.py": conanfile.format(version="v2"), "v2/conandata.yml": conandata_yml.format(version="2.0"), "v2/v2.txt": "2.0", "v2/patches/patch.patch": patch_file.format(version="2.0"), }) tc.run("create v1 --version=1.0") v1_layout = tc.exported_layout() tc.test_v1_path = v1_layout.export() tc.run("create v2 --version=2.0") tc.test_v2_revision = tc.exported_layout().reference.revision tc.run("upload * -r=default -c") tc.run("remove * -c") return tc @pytest.mark.parametrize("old_args", [ "-op=v1 -or=pkg/1.0", "-or=pkg/1.0" ]) @pytest.mark.parametrize("new_args", [ "-np=v2 -nr=pkg/2.0", "-nr=pkg/2.0#{revision}", ]) @pytest.mark.parametrize("formatter", [ "-f=json --out-file=output.json", "-f=html --out-file=output.html" ]) @pytest.mark.tool("git") def test_compare_paths(fixture_client, old_args, new_args, formatter): tc = fixture_client v2_revision = fixture_client.test_v2_revision v1_path = tc.test_v1_path.replace("\\", "/") new_args = new_args.format(revision=v2_revision) tc.run(f'report diff {old_args} {new_args} {formatter}') if "json" in formatter: output_json = json.loads(tc.load("output.json")) assert any(os.path.join(v1_path, "conanfile.py").replace("\\", "/") in k for k in output_json.keys()) assert any(os.path.join(v1_path, "conanmanifest.txt").replace("\\", "/") in k for k in output_json.keys()) # We have patch information assert any(os.path.abspath(os.path.join(v1_path, "..", "es", "patches", "patch.patch")).replace("\\", "/") in k for k in output_json.keys()) # We have exports information assert any(os.path.abspath(os.path.join(v1_path, "..", "e", "v1.txt")).replace("\\", "/") in k for k in output_json.keys()) # New files assert any("new-file-for-v2.txt" in k for k in output_json.keys()) elif "html" in formatter: output_html = tc.load("output.html") # We have patch information assert "(old)/es/patches/patch.patch" in output_html assert "(new)/s/new-file-for-v2.txt" in output_html # We have exports information assert "(old)/e/v1.txt" in output_html ================================================ FILE: test/functional/command/runner_test.py ================================================ import textwrap import os import pytest import docker from conan.test.utils.tools import TestClient from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_h, gen_function_cpp def docker_from_env(): try: return docker.from_env() except (Exception,): rancher = f'unix://{os.path.expanduser("~")}/.rd/docker.sock' return docker.DockerClient(base_url=rancher, version='auto') # Rancher def docker_skip(test_image='ubuntu:22.04'): try: docker_client = docker_from_env() if test_image: docker_client.images.pull(test_image) except docker.errors.DockerException: return True return False def conan_base_path(): import conans return os.path.dirname(os.path.dirname(conans.__file__)) def dockerfile_path(name=None): path = os.path.join(os.path.dirname(__file__), "dockerfiles") if name: path = os.path.join(path, name) return path @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_cache_shared(): """ Tests the ``conan create . `` """ client = TestClient() profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path()} build_context={conan_base_path()} image=conan-runner-default-test cache=shared remove=True """) client.save({"host": profile_host, "build": profile_build}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h host -pr:b build") assert "[100%] Built target example" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_cache_shared_profile_from_cache(): """ Tests the ``conan create . `` """ client = TestClient() profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path()} build_context={conan_base_path()} image=conan-runner-default-test cache=shared remove=True """) client.save({"default_host": profile_host, "default_build": profile_build}, path=client.paths.profiles_path) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h default_host -pr:b default_build") assert "[100%] Built target example" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_cache_shared_profile_folder(): """ Tests the ``conan create . `` """ client = TestClient() profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path()} build_context={conan_base_path()} image=conan-runner-default-test cache=shared remove=True """) client.save({"build": profile_build}) client.save_home({"profiles/docker_default": profile_host}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h docker_default -pr:b build") assert "[100%] Built target example" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_dockerfile_folder_path(): """ Tests the ``conan create . `` """ client = TestClient() profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host_copy = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path()} build_context={conan_base_path()} image=conan-runner-default-test cache=copy remove=True """) profile_host_clean = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path()} build_context={conan_base_path()} image=conan-runner-default-test cache=clean remove=True """) client.save({"host_copy": profile_host_copy, "host_clean": profile_host_clean, "build": profile_build}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h host_copy -pr:b build") assert "Restore: pkg/0.2" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out assert "Removing container" in client.out client.run("create . -pr:h host_clean -pr:b build") assert "Restore: pkg/0.2" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_profile_default_folder(): """ Tests the ``conan create . `` """ client = TestClient() profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path("Dockerfile_test")} build_context={conan_base_path()} image=conan-runner-default-test cache=copy remove=True """) client.save_home({"profiles/host_from_profile": profile_host, "profiles/build_from_profile": profile_build}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h host_from_profile -pr:b build_from_profile") assert "Container conan-runner-docker running" in client.out assert "Restore: pkg/0.2" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_dockerfile_file_path(): """ Tests the ``conan create . `` """ client = TestClient() profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path("Dockerfile_test")} build_context={conan_base_path()} image=conan-runner-default-test cache=copy remove=True """) client.save({"host": profile_host, "build": profile_build}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h host -pr:b build") assert "Container conan-runner-docker running" in client.out assert "Restore: pkg/0.2" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") @pytest.mark.parametrize("build_type,shared", [("Release", False), ("Debug", True)]) def test_create_docker_runner_with_ninja(build_type, shared): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class Library(ConanFile): name = "hello" version = "1.0" settings = 'os', 'arch', 'compiler', 'build_type' exports_sources = 'hello.h', '*.cpp', 'CMakeLists.txt' options = {'shared': [True, False]} default_options = {'shared': False} def generate(self): tc = CMakeToolchain(self, generator="Ninja") tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.sep.join([".", "myapp"])) def package(self): cmake = CMake(self) cmake.install() """) client = TestClient(path_with_spaces=False) client.save({'conanfile.py': conanfile, "CMakeLists.txt": gen_cmakelists(libsources=["hello.cpp"], appsources=["main.cpp"], install=True), "hello.h": gen_function_h(name="hello"), "hello.cpp": gen_function_cpp(name="hello", includes=["hello"]), "main.cpp": gen_function_cpp(name="main", includes=["hello"], calls=["hello"])}) profile = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker image=conan-runner-ninja-test dockerfile={dockerfile_path("Dockerfile_ninja")} build_context={conan_base_path()} cache=copy remove=True """) client.save({"profile": profile}) settings = "-s os=Linux -s build_type={} -o hello/*:shared={}".format(build_type, shared) # create should also work client.run("create . --name=hello --version=1.0 {} -pr:h=profile -pr:b=profile".format(settings)) assert 'cmake -G "Ninja"' in client.out assert "main: {}!".format(build_type) in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_from_configfile(): """ Tests the ``conan create . `` """ client = TestClient() configfile = textwrap.dedent(f""" image: conan-runner-default-test build: dockerfile: {dockerfile_path("Dockerfile_test")} build_context: {conan_base_path()} run: name: my-custom-conan-runner-container """) client.save({"configfile.yaml": configfile}) profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker configfile={os.path.join(client.current_folder, 'configfile.yaml')} cache=copy remove=True """) client.save({"host": profile_host, "build": profile_build}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h 'host' -pr:b 'build'") assert "Container my-custom-conan-runner-container running" in client.out assert "Restore: pkg/0.2" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_from_configfile_with_args(): """ Tests the ``conan create . `` """ client = TestClient() # Ensure the network exists docker_client = docker_from_env() docker_client.networks.create("my-network") configfile = textwrap.dedent(f""" image: conan-runner-default-test-with-args build: dockerfile: {dockerfile_path("Dockerfile_args")} build_context: {conan_base_path()} build_args: BASE_IMAGE: ubuntu:22.04 run: name: my-conan-runner-container-with-args network: my-network """) client.save({"configfile.yaml": configfile}) profile_build = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker configfile={os.path.join(client.current_folder, 'configfile.yaml')} cache=copy remove=True """) client.save({"host": profile_host, "build": profile_build}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h 'host' -pr:b 'build'") assert "command/dockerfiles/Dockerfile_args" in client.out assert "Restore: pkg/0.2" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out assert "Removing container" in client.out docker_client.networks.get("my-network").remove() @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_default_build_profile(): """ Tests the ``conan create . `` """ client = TestClient() profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path()} build_context={conan_base_path()} image=conan-runner-default-test cache=clean remove=True """) client.save({"host_clean": profile_host}) client.run("new cmake_lib -d name=pkg -d version=0.2") client.run("create . -pr:h host_clean -vverbose") assert "Copying default profile" in client.out assert "Restore: pkg/0.2" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out assert "Removing container" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip('ubuntu:22.04'), reason="Only docker running") def test_create_docker_runner_profile_composition(): """ Tests the ``conan create . `` with profile composition """ client = TestClient() profile = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker image=conan-runner-ninja-test """) profile_extension = textwrap.dedent(f"""\ [runner] type=docker dockerfile={dockerfile_path("Dockerfile_ninja")} build_context={conan_base_path()} cache=copy remove=True """) client.save({"profile": profile, "profile_extension": profile_extension}) client.run("new cmake_lib -d name=pkg -d version=2.0") client.run("create . -pr:h profile -pr:h profile_extension") assert "[100%] Built target example" in client.out assert "Restore: pkg/2.0 in pkgc6abef0178849" in client.out assert "Restore: pkg/2.0:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out assert "Restore: pkg/2.0:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out @pytest.mark.docker_runner @pytest.mark.skipif(docker_skip(), reason="Only docker running") def test_create_docker_runner_in_subfolder(): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load, copy from conan.tools.cmake import CMake class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def layout(self): self.folders.root = ".." self.folders.source = "." self.folders.build = "build" def export_sources(self): folder = os.path.join(self.recipe_folder, "..") copy(self, "*.txt", folder, self.export_sources_folder) copy(self, "src/*.cpp", folder, self.export_sources_folder) copy(self, "include/*.h", folder, self.export_sources_folder) def source(self): cmake_file = load(self, "CMakeLists.txt") def build(self): path = os.path.join(self.source_folder, "CMakeLists.txt") cmake_file = load(self, path) cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() """) header = textwrap.dedent(""" #pragma once void hello(); """) source = textwrap.dedent(""" #include void hello() { std::cout << "Hello!" << std::endl; } """) cmakelist = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(pkg CXX) add_library(pkg src/hello.cpp) target_include_directories(pkg PUBLIC include) set_target_properties(pkg PROPERTIES PUBLIC_HEADER "include/hello.h") install(TARGETS pkg) """) profile_host = textwrap.dedent(f"""\ [settings] arch={{{{ detect_api.detect_arch() }}}} build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [runner] type=docker dockerfile={dockerfile_path()} build_context={conan_base_path()} image=conan-runner-default-test cache=clean remove=True """) client.save({"conan/conanfile.py": conanfile, "conan/host": profile_host, "include/hello.h": header, "src/hello.cpp": source, "CMakeLists.txt": cmakelist}) with client.chdir("conan"): client.run("create . -pr:h host -vverbose") assert "Restore: pkg/1.0" in client.out assert "Removing container" in client.out ================================================ FILE: test/functional/command/test_build.py ================================================ import json import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_build_different_folders(): conanfile = textwrap.dedent(""" import os from conan import ConanFile class AConan(ConanFile): def build(self): self.output.warning("Build folder=>%s" % self.build_folder) self.output.warning("Src folder=>%s" % self.source_folder) assert(os.path.exists(self.build_folder)) assert(os.path.exists(self.source_folder)) """) client = TestClient() client.save({"conanfile.py": conanfile}) with client.chdir("build1"): client.run("install ..") # Try relative to cwd client.run("build . --output-folder build2") assert "Build folder=>%s" % os.path.join(client.current_folder, "build2") in client.out assert "Src folder=>%s" % client.current_folder in client.out def test_build_dots_names(): client = TestClient() conanfile_dep = textwrap.dedent(""" from conan import ConanFile class AConan(ConanFile): pass """) client.save({"conanfile.py": conanfile_dep}) client.run("create . --name=hello.pkg --version=0.1 --user=lasote --channel=testing --format=json", redirect_stdout="hello.pkg.json") hellopkg_result = json.loads(client.load("hello.pkg.json")) client.run("create . --name=hello-tools --version=0.1 --user=lasote --channel=testing --format=json", redirect_stdout="hello-tools.json") hellotools_result = json.loads(client.load("hello-tools.json")) conanfile_scope_env = textwrap.dedent(""" from conan import ConanFile class AConan(ConanFile): requires = "hello.pkg/0.1@lasote/testing", "hello-tools/0.1@lasote/testing" def generate(self): self.output.info("HELLO ROOT PATH: %s" % self.dependencies["hello.pkg"].package_folder) self.output.info("HELLO ROOT PATH: %s" % self.dependencies["hello-tools"].package_folder) def build(self): pass """) client.save({"conanfile.py": conanfile_scope_env}, clean_first=True) client.run("build conanfile.py --build=missing") assert hellopkg_result["graph"]["nodes"]["1"]["package_folder"] in client.out assert hellotools_result["graph"]["nodes"]["1"]["package_folder"] in client.out def test_build_with_deps_env_info(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class AConan(ConanFile): name = "lib" version = "1.0" def package_info(self): self.buildenv_info.define("MYVAR", "23") """) client.save({"conanfile.py": conanfile}) client.run("export . --user=lasote --channel=stable") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.env import VirtualBuildEnv import os class AConan(ConanFile): build_requires = "lib/1.0@lasote/stable" def build(self): build_env = VirtualBuildEnv(self).vars() assert build_env.get("MYVAR") == "23" with build_env.apply(): assert(os.environ["MYVAR"] == "23") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("build . --build missing") def test_build_single_full_reference(): client = TestClient() client.save({"conanfile.py": GenConanfile("foo", "1.0")}) client.run("create . --build='*'") assert "foo/1.0: Forced build from source" in client.out def test_build_multiple_full_reference(): client = TestClient() client.save({"conanfile.py": GenConanfile("foo", "1.0")}) client.run("create .") client.save({"conanfile.py": GenConanfile("bar", "1.0").with_requires("foo/1.0")}) client.run("create --build foo/1.0@ --build bar/1.0@ .") assert "foo/1.0: Forced build from source" in client.out assert "bar/1.0: Forced build from source" in client.out def test_debug_build_release_deps(): # https://github.com/conan-io/conan/issues/2899 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): name = "{name}" {requires} settings = "build_type" def build(self): self.output.info("BUILD: %s BuildType=%s!" % (self.name, self.settings.build_type)) def package_info(self): self.output.info("PACKAGE_INFO: %s BuildType=%s!" % (self.name, self.settings.build_type)) """) client.save({"conanfile.py": conanfile.format(name="dep", requires="")}) client.run("create . --name=dep --version=0.1 --user=user --channel=testing -s build_type=Release") client.save({"conanfile.py": conanfile.format(name="mypkg", requires="requires = 'dep/0.1@user/testing'")}) client.run("build . -s mypkg/*:build_type=Debug -s build_type=Release") assert "dep/0.1@user/testing: PACKAGE_INFO: dep BuildType=Release!" in client.out assert "conanfile.py (mypkg/None): BUILD: mypkg BuildType=Debug!" in client.out ================================================ FILE: test/functional/command/test_config_install.py ================================================ import os import shutil import stat import textwrap import pytest from unittest.mock import patch from conan.api.model import Remote from conan.internal.api.config.config_installer import _hide_password from conan.internal.rest.file_downloader import FileDownloader from conan.internal.paths import DEFAULT_CONAN_HOME from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import scan_folder, temp_folder, tgz_with_contents from conan.test.utils.tools import TestClient, zipdir from conan.internal.util.files import load, mkdir, save, save_files def make_file_read_only(file_path): mode = os.stat(file_path).st_mode os.chmod(file_path, mode & ~ stat.S_IWRITE) remotes = """{ "remotes": [ { "name": "myrepo1", "url": "https://myrepourl.net", "verify_ssl": false }, { "name": "my-repo-2", "url": "https://myrepo2.com", "verify_ssl": true } ] } """ settings_yml = """os: Windows: Linux: arch: [x86, x86_64] """ class TestConfigInstall: @staticmethod def _create_profile_folder(folder=None): folder = folder or temp_folder(path_with_spaces=False) save_files(folder, {"settings.yml": settings_yml, "remotes.json": remotes, "profiles/linux": "#linuxprofile", "profiles/windows": "#winprofile", "hooks/dummy": "#hook dummy", "hooks/foo.py": "#hook foo", "hooks/custom/custom.py": "#hook custom", ".git/hooks/foo": "foo", "hooks/.git/hooks/before_push": "before_push", "pylintrc": "#Custom pylint", "python/myfuncs.py": "does not matter", "python/__init__.py": "" }) return folder def test_config_fails_no_storage(self): folder = temp_folder(path_with_spaces=False) save_files(folder, {"remotes.json": remotes}) client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=1.0") client.run('config install "%s"' % folder) client.run("remote list") assert "myrepo1: https://myrepourl.net [Verify SSL: False, Enabled: True]" in client.out assert "my-repo-2: https://myrepo2.com [Verify SSL: True, Enabled: True]" in client.out def _create_zip(self, zippath=None): folder = self._create_profile_folder() zippath = zippath or os.path.join(folder, "myconfig.zip") zipdir(folder, zippath) return zippath @staticmethod def _get_files(folder): relpaths = scan_folder(folder) files = {} for path in relpaths: with open(os.path.join(folder, path), "r") as file_handle: files[path] = file_handle.read() return files def _create_tgz(self, tgz_path=None): folder = self._create_profile_folder() tgz_path = tgz_path or os.path.join(folder, "myconfig.tar.gz") files = self._get_files(folder) return tgz_with_contents(files, tgz_path) @staticmethod def _check(c): settings_path = c.paths.settings_path assert load(settings_path).splitlines() == settings_yml.splitlines() api = c.api cache_remotes = api.remotes.list() assert list(cache_remotes) == [ Remote("myrepo1", "https://myrepourl.net", False, False), Remote("my-repo-2", "https://myrepo2.com", True, False), ] assert sorted(os.listdir(c.paths.profiles_path)) == sorted(["default", "linux", "windows"]) assert c.load_home("profiles/linux") == "#linuxprofile" assert c.load_home("profiles/windows") == "#winprofile" assert "#Custom pylint" == c.load_home("pylintrc") assert "" == c.load_home("python/__init__.py") assert "#hook dummy" == c.load_home("hooks/dummy") assert "#hook foo" == c.load_home("hooks/foo.py") assert "#hook custom" == c.load_home("hooks/custom/custom.py") assert not os.path.exists(os.path.join(c.cache_folder, "hooks", ".git")) assert not os.path.exists(os.path.join(c.cache_folder, ".git")) def test_install_file(self): """ should install from a file in current dir """ zippath = self._create_zip() c = TestClient(light=True) for filetype in ["", "--type=file"]: c.run('config install "%s" %s' % (zippath, filetype)) self._check(c) assert os.path.exists(zippath) def test_install_config_file(self): """ should install from a settings and remotes file in configuration directory """ import tempfile profile_folder = self._create_profile_folder() assert os.path.isdir(profile_folder) src_setting_file = os.path.join(profile_folder, "settings.yml") src_remote_file = os.path.join(profile_folder, "remotes.json") # Install profile_folder without settings.yml remotes.json in order to install them manually tmp_dir = tempfile.mkdtemp() dest_setting_file = os.path.join(tmp_dir, "settings.yml") dest_remote_file = os.path.join(tmp_dir, "remotes.json") shutil.move(src_setting_file, dest_setting_file) shutil.move(src_remote_file, dest_remote_file) c = TestClient(light=True) c.run('config install "%s"' % profile_folder) shutil.move(dest_setting_file, src_setting_file) shutil.move(dest_remote_file, src_remote_file) shutil.rmtree(tmp_dir) for cmd_option in ["", "--type=file"]: c.run('config install "%s" %s' % (src_setting_file, cmd_option)) c.run('config install "%s" %s' % (src_remote_file, cmd_option)) self._check(c) def test_install_dir(self): """ should install from a dir in current dir """ folder = self._create_profile_folder() assert os.path.isdir(folder) c = TestClient(light=True) for dirtype in ["", "--type=dir"]: c.run('config install "%s" %s' % (folder, dirtype)) self._check(c) def test_install_source_target_folders(self): folder = temp_folder() save_files(folder, {"subf/file.txt": "hello", "subf/subf/file2.txt": "bye"}) c = TestClient(light=True) c.run('config install "%s" -sf=subf -tf=newsubf' % folder) content = c.load_home("newsubf/file.txt") assert content == "hello" content = c.load_home("newsubf/subf/file2.txt") assert content == "bye" def test_install_remotes_json(self): folder = temp_folder() remotes_json = textwrap.dedent(""" { "remotes": [ { "name": "repojson1", "url": "https://repojson1.net", "verify_ssl": false }, { "name": "repojson2", "url": "https://repojson2.com", "verify_ssl": true } ] } """) remotes_txt = textwrap.dedent("""\ repotxt1 https://repotxt1.net False repotxt2 https://repotxt2.com True """) # remotes.txt is ignored save_files(folder, {"remotes.json": remotes_json, "remotes.txt": remotes_txt}) c = TestClient(light=True) c.run(f'config install "{folder}"') assert "Defining remotes from remotes.json" in c.out c.run('remote list') assert "repojson1: https://repojson1.net [Verify SSL: False, Enabled: True]" in c.out assert "repojson2: https://repojson2.com [Verify SSL: True, Enabled: True]" in c.out # We only install remotes.json folder = temp_folder() save_files(folder, {"remotes.json": remotes_json}) c.run(f'config install "{folder}"') assert "Defining remotes from remotes.json" in c.out c.run('remote list') assert "repojson1: https://repojson1.net [Verify SSL: False, Enabled: True]" in c.out assert "repojson2: https://repojson2.com [Verify SSL: True, Enabled: True]" in c.out def test_without_profile_folder(self): c = TestClient(light=True) shutil.rmtree(c.paths.profiles_path) zippath = self._create_zip() c.run('config install "%s"' % zippath) assert sorted(os.listdir(c.paths.profiles_path)) == sorted(["linux", "windows"]) assert c.load_home("profiles/linux") == "#linuxprofile" def test_install_url(self): """ should install from a URL """ c = TestClient(light=True) for origin in ["", "--type=url"]: def my_download(obj, url, file_path, **kwargs): # noqa self._create_zip(file_path) with patch.object(FileDownloader, 'download', new=my_download): c.run("config install http://myfakeurl.com/myconf.zip %s" % origin) self._check(c) # repeat the process to check c.run("config install http://myfakeurl.com/myconf.zip %s" % origin) self._check(c) def test_install_url_query(self): """ should install from a URL """ c = TestClient(light=True) def my_download(obj, url, file_path, **kwargs): # noqa self._create_zip(file_path) with patch.object(FileDownloader, 'download', new=my_download): # repeat the process to check it works with ?args c.run("config install http://myfakeurl.com/myconf.zip?sha=1") self._check(c) def test_install_change_only_verify_ssl(self): def my_download(obj, url, file_path, **kwargs): # noqa self._create_zip(file_path) c = TestClient(light=True) with patch.object(FileDownloader, 'download', new=my_download): c.run("config install http://myfakeurl.com/myconf.zip") self._check(c) # repeat the process to check c.run("config install http://myfakeurl.com/myconf.zip --verify-ssl=False") self._check(c) def test_install_url_tgz(self): """ should install from a URL to tar.gz """ c = TestClient(light=True) def my_download(obj, url, file_path, **kwargs): # noqa self._create_tgz(file_path) with patch.object(FileDownloader, 'download', new=my_download): c.run("config install http://myfakeurl.com/myconf.tar.gz") self._check(c) def test_failed_install_repo(self): """ should install from a git repo """ c = TestClient(light=True) c.run('config install notexistingrepo.git', assert_error=True) assert "ERROR: Failed conan config install: Can't clone repo" in c.out def test_failed_install_http(self): """ should install from a http zip """ c = TestClient(light=True) c.run('config install httpnonexisting', assert_error=True) assert ("ERROR: Failed conan config install: " "Error while installing config from httpnonexisting") in c.out @pytest.mark.tool("git") def test_install_repo(self): """ should install from a git repo """ c = TestClient(light=True) folder = self._create_profile_folder() with c.chdir(folder): c.run_command('git init .') c.run_command('git add .') c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command('git commit -m "mymsg"') c.run('config install "%s/.git"' % folder) self._check(c) @pytest.mark.tool("git") def test_install_repo_relative(self): c = TestClient(light=True) relative_folder = "./config" absolute_folder = os.path.join(c.current_folder, "config") mkdir(absolute_folder) folder = self._create_profile_folder(absolute_folder) with c.chdir(folder): c.run_command('git init .') c.run_command('git add .') c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command('git commit -m "mymsg"') c.run('config install "%s/.git"' % relative_folder) self._check(c) @pytest.mark.tool("git") def test_install_custom_args(self): """ should install from a git repo """ c = TestClient(light=True) folder = self._create_profile_folder() with c.chdir(folder): c.run_command('git init .') c.run_command('git add .') c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command('git commit -m "mymsg"') c.run('config install "%s/.git" --args="-c init.templateDir=value"' % folder) self._check(c) def test_force_git_type(self): client = TestClient(light=True) client.run('config install httpnonexisting --type=git', assert_error=True) assert "Can't clone repo" in client.out def test_force_dir_type(self): c = TestClient(light=True) c.run('config install httpnonexisting --type=dir', assert_error=True) assert "ERROR: Failed conan config install: No such directory: 'httpnonexisting'" in c.out def test_force_file_type(self): client = TestClient(light=True) client.run('config install httpnonexisting --type=file', assert_error=True) assert "No such file or directory: 'httpnonexisting'" in client.out def test_force_url_type(self): client = TestClient(light=True) client.run('config install httpnonexisting --type=url', assert_error=True) assert "Error downloading file httpnonexisting: 'Invalid URL 'httpnonexisting'" in client.out def test_removed_credentials_from_url_unit(self): """ Unit tests to remove credentials in netloc from url when using basic auth # https://github.com/conan-io/conan/issues/2324 """ url_without_credentials = r"https://server.com/resource.zip" url_with_credentials = r"https://test_username:test_password_123@server.com/resource.zip" url_hidden_password = r"https://test_username:@server.com/resource.zip" # Check url is the same when not using credentials assert _hide_password(url_without_credentials) == url_without_credentials # Check password is hidden using url with credentials assert _hide_password(url_with_credentials) == url_hidden_password # Check that it works with other protocols ftp ftp_with_credentials = r"ftp://test_username_ftp:test_password_321@server.com/resurce.zip" ftp_hidden_password = r"ftp://test_username_ftp:@server.com/resurce.zip" assert _hide_password(ftp_with_credentials) == ftp_hidden_password # Check function also works for file paths *unix/windows unix_file_path = r"/tmp/test" assert _hide_password(unix_file_path) == unix_file_path windows_file_path = r"c:\windows\test" assert _hide_password(windows_file_path) == windows_file_path # Check works with empty string assert _hide_password('') == '' def test_remove_credentials_config_installer(self): """ Functional test to check credentials are not displayed in output but are still present in conan configuration # https://github.com/conan-io/conan/issues/2324 """ fake_url_with_credentials = "http://test_user:test_password@myfakeurl.com/myconf.zip" fake_url_hidden_password = "http://test_user:@myfakeurl.com/myconf.zip" def my_download(obj, url, file_path, **kwargs): # noqa assert url == fake_url_with_credentials self._create_zip(file_path) c = TestClient(light=True) with patch.object(FileDownloader, 'download', new=my_download): c.run("config install %s" % fake_url_with_credentials) # Check credentials are not displayed in output assert fake_url_with_credentials not in c.out assert fake_url_hidden_password in c.out # Check credentials still stored in configuration self._check(c) def test_ssl_verify(self): c = TestClient(light=True) fake_url = "https://fakeurl.com/myconf.zip" def download_verify_false(obj, url, file_path, **kwargs): # noqa assert kwargs["verify_ssl"] is False self._create_zip(file_path) def download_verify_true(obj, url, file_path, **kwargs): # noqa assert kwargs["verify_ssl"] is True self._create_zip(file_path) with patch.object(FileDownloader, 'download', new=download_verify_false): c.run("config install %s --verify-ssl=False" % fake_url) with patch.object(FileDownloader, 'download', new=download_verify_true): c.run("config install %s --verify-ssl=True" % fake_url) with patch.object(FileDownloader, 'download', new=download_verify_true): c.run(f"config install {fake_url}") with patch.object(FileDownloader, 'download', new=download_verify_false): c.run(f"config install {fake_url} --insecure") @pytest.mark.tool("git") def test_git_checkout_is_possible(self): folder = self._create_profile_folder() c = TestClient(light=True) with c.chdir(folder): c.run_command('git init .') c.run_command('git checkout -b master') c.run_command('git add .') c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command('git commit -m "mymsg"') c.run_command('git checkout -b other_branch') save(os.path.join(folder, "extensions", "hooks", "cust", "cust.py"), "") c.run_command('git add .') c.run_command('git commit -m "my file"') c.run('config install "%s/.git" --args "-b other_branch"' % folder) self._check(c) file_path = os.path.join(c.paths.hooks_path, "cust", "cust.py") assert load(file_path) == "" # Add changes to that branch and update with c.chdir(folder): save(os.path.join(folder, "extensions", "hooks", "cust", "cust.py"), "new content") c.run_command('git add .') c.run_command('git commit -m "my other file"') c.run_command('git checkout master') c.run('config install "%s/.git" --args "-b other_branch"' % folder) self._check(c) assert load(file_path) == "new content" def test_config_install_requester(self): # https://github.com/conan-io/conan/issues/4169 path = self._create_zip() c = TestClient(light=True) file_server = TestFileServer(os.path.dirname(path)) c.servers["file_server"] = file_server c.run(f"config install {file_server.fake_url}/myconfig.zip") assert "Defining remotes from remotes.json" in c.out assert "Copying file myfuncs.py" in c.out def test_overwrite_read_only_file(self): c = TestClient(light=True) source_folder = self._create_profile_folder() c.run('config install "%s"' % source_folder) # make existing settings.yml read-only make_file_read_only(c.paths.settings_path) assert not os.access(c.paths.settings_path, os.W_OK) # config install should overwrite the existing read-only file c.run('config install "%s"' % source_folder) assert os.access(c.paths.settings_path, os.W_OK) def test_dont_copy_file_permissions(self): source_folder = self._create_profile_folder() # make source settings.yml read-only make_file_read_only(os.path.join(source_folder, 'remotes.json')) c = TestClient(light=True) c.run('config install "%s"' % source_folder) assert os.access(c.paths.settings_path, os.W_OK) class TestConfigInstallSched: def test_execute_more_than_once(self): """ Once executed by the scheduler, conan config install must executed again when invoked manually """ folder = temp_folder(path_with_spaces=False) save_files(folder, {"global.conf": "core.download:parallel=0"}) c = TestClient(light=True) c.run('config install "%s"' % folder) assert "Copying file global.conf" in c.out c.run('config install "%s"' % folder) assert "Copying file global.conf" in c.out @pytest.mark.tool("git") def test_config_install_remove_git_repo(self): """ config_install_interval must break when remote git has been removed """ folder = temp_folder(path_with_spaces=False) save_files(folder, {"global.conf": "core.download:parallel=0"}) c = TestClient(light=True) with c.chdir(folder): c.run_command('git init .') c.run_command('git add .') c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command('git commit -m "mymsg"') c.run('config install "%s/.git" --type git' % folder) assert "Copying file global.conf" in c.out assert "Repo cloned!" in c.out # git clone executed by scheduled task def test_config_fails_git_folder(self): # https://github.com/conan-io/conan/issues/8594 config_folder = temp_folder(path_with_spaces=False) save_files(config_folder, {"global.conf": "core.download:parallel=0"}) folder = os.path.join(temp_folder(), ".gitlab-conan", DEFAULT_CONAN_HOME) client = TestClient(cache_folder=folder) with client.chdir(config_folder): client.run_command('git init .') client.run_command('git add .') client.run_command('git config user.name myname') client.run_command('git config user.email myname@mycompany.com') client.run_command('git commit -m "mymsg"') assert ".gitlab-conan" in client.cache_folder assert os.path.basename(client.cache_folder) == DEFAULT_CONAN_HOME client.run('config install "%s/.git" --type git' % config_folder) client.load_home("global.conf") # check it is there dirs = os.listdir(client.cache_folder) assert ".git" not in dirs class TestConfigInstall2: def test_config_install_reestructuring_source(self): """ https://github.com/conan-io/conan/issues/9885 """ folder = temp_folder() client = TestClient() with client.chdir(folder): client.save({"profiles/debug/address-sanitizer": ""}) client.run("config install .") debug_cache_folder = os.path.join(client.cache_folder, "profiles", "debug") assert os.path.isdir(debug_cache_folder) # Now reestructure the files, what it was already a directory in the cache now we want # it to be a file folder = temp_folder() with client.chdir(folder): client.save({"profiles/debug": ""}) client.run("config install .") assert os.path.isfile(debug_cache_folder) # And now is a directory again folder = temp_folder() with client.chdir(folder): client.save({"profiles/debug/address-sanitizer": ""}) client.run("config install .") assert os.path.isdir(debug_cache_folder) ================================================ FILE: test/functional/command/test_config_install_pkg.py ================================================ import json import os import textwrap import pytest import yaml from conan.api.model import RecipeReference from conan.internal.cache.home_paths import HomePaths from conan.internal.util.files import load from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def servers(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Conf(ConanFile): package_type = "configuration" def package(self): copy(self, "*.conf", src=self.build_folder, dst=self.package_folder) """) c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": conanfile}) for pkg in ("myconf_a", "myconf_b", "myconf_c"): for version in ("0.1", "0.2"): c.save({"global.conf": f"user.myteam:myconf=my_{pkg}/{version}_value"}) c.run(f"export-pkg . --name={pkg} --version={version}") c.run("upload * -r=default -c") c.run("remove * -c") return c.servers def _check_conf(c, pkg): c.run("config show *") assert f"user.myteam:myconf: my_{pkg}_value" in c.out def _check_conf_file(c, refs): content = json.loads(c.load_home("config_version.json"))["config_version"] for a, b in zip(refs, content): assert a in b class TestConfigInstallPkg: def test_install_pkg(self, servers): c = TestClient(servers=servers, light=True) c.run("config install-pkg myconf_a/0.1") assert "Installing new or updating configuration packages" in c.out _check_conf(c, "myconf_a/0.1") _check_conf_file(c, ["myconf_a/0.1"]) cache_files = os.listdir(c.cache_folder) assert "conaninfo.txt" not in cache_files assert "conanmanifest.txt" not in cache_files # skip reinstall c.run("config install-pkg myconf_a/0.1") assert ("The requested configurations are identical to the already installed ones, " "skipping re-installation") in c.out _check_conf(c, "myconf_a/0.1") _check_conf_file(c, ["myconf_a/0.1"]) # forced reinstall c.save_home({"global.conf": ""}) c.run("config install-pkg myconf_a/0.1 --force") assert "forcing re-installation because --force" in c.out _check_conf(c, "myconf_a/0.1") _check_conf_file(c, ["myconf_a/0.1"]) def test_with_url(self, servers): c = TestClient(servers=servers, light=True) url = servers["default"].fake_url c.run("remote remove default") c.run(f"config install-pkg myconf_a/0.1 --url={url}") assert "Installing new or updating configuration packages" in c.out _check_conf(c, "myconf_a/0.1") _check_conf_file(c, ["myconf_a/0.1"]) def test_update(self, servers): c = TestClient(servers=servers, light=True) c.run("config install-pkg myconf_a/0.1") c.run("config install-pkg myconf_a/0.2") assert "Installing new or updating configuration packages" in c.out _check_conf(c, "myconf_a/0.2") _check_conf_file(c, ["myconf_a/0.2"]) def test_addition(self, servers): c = TestClient(servers=servers, light=True) c.run("config install-pkg myconf_a/0.1") c.run("config install-pkg myconf_b/0.1") assert "Installing new or updating configuration packages" in c.out _check_conf(c, "myconf_b/0.1") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1"]) def test_update_first(self, servers): c = TestClient(servers=servers, light=True) c.run("config install-pkg myconf_a/0.1") c.run("config install-pkg myconf_b/0.1") # update of the first fail c.run("config install-pkg myconf_a/[*]", assert_error=True) assert "ERROR: Installing these configuration packages will break" in c.out assert "use 'conan config install-pkg --force' to force" in c.out assert "Use 'conan config clean' first to fully reset your configuration" in c.out _check_conf(c, "myconf_b/0.1") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1"]) # Now force the first c.run("config install-pkg myconf_a/[*] --force") assert "Forcing the installation because --force was defined" in c.out _check_conf(c, "myconf_a/0.2") _check_conf_file(c, ["myconf_b/0.1", "myconf_a/0.2"]) def test_update_second(self, servers): c = TestClient(servers=servers, light=True) c.run("config install-pkg myconf_a/0.1") c.run("config install-pkg myconf_b/0.1") # update of the second works without problem c.run("config install-pkg myconf_b/[*]") assert "Installing new or updating configuration packages" in c.out _check_conf(c, "myconf_b/0.2") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.2"]) def test_error_cant_use_as_dependency(self): c = TestClient(light=True) conanfile = GenConanfile("myconf", "0.1").with_package_type("configuration") c.save({"myconf/conanfile.py": conanfile, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("myconf/0.1")}) c.run("create myconf") c.run("install pkg", assert_error=True) assert "ERROR: Configuration package myconf/0.1 cannot be used as requirement, " \ "but pkg/0.1 is requiring it" in c.out def test_error_cant_use_without_type(self): c = TestClient(light=True) c.save({"myconf/conanfile.py": GenConanfile("myconf", "0.1")}) c.run("create myconf") c.run("config install-pkg myconf/[*]", assert_error=True) assert 'ERROR: myconf/0.1 is not of package_type="configuration"' in c.out def test_create_also(self): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Conf(ConanFile): name = "myconf" version = "0.1" package_type = "configuration" exports_sources = "*.conf" def package(self): copy(self, "*.conf", src=self.build_folder, dst=self.package_folder) """) c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": conanfile, "global.conf": "user.myteam:myconf=my_myconf/0.1_value"}) c.run("create .") c.run("upload * -r=default -c") c.run("remove * -c") c.run("config install-pkg myconf/[*]") _check_conf(c, "myconf/0.1") _check_conf_file(c, ["myconf/0.1"]) def test_lockfile(self, servers): """ it should be able to install the config older version using a lockfile """ c = TestClient(servers=servers, light=True) c.run("config install-pkg myconf_a/0.1 --lockfile-out=config.lock") c.run("config install-pkg myconf_a/[*] --lockfile=config.lock") assert ("The requested configurations are identical to the already installed ones, " "skipping re-installation") in c.out _check_conf(c, "myconf_a/0.1") _check_conf_file(c, ["myconf_a/0.1"]) # Without the lockfile, it is free to update c.run("config install-pkg myconf_a/[*] --lockfile-out=config.lock") assert "Installing new or updating configuration packages" _check_conf(c, "myconf_a/0.2") _check_conf_file(c, ["myconf_a/0.2"]) result = json.loads(c.load("config.lock")) assert "myconf_a/0.2" in result["config_requires"][0] def test_package_id_effect(self): # full integration, as "test_config_package_id.py" tests from hardcoded cache json files c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Conf(ConanFile): name = "myconf" version = "0.1" package_type = "configuration" def package(self): copy(self, "*.conf", src=self.build_folder, dst=self.package_folder) """) c.save({"conanfile.py": conanfile, "global.conf": f"core.package_id:config_mode=minor_mode"}) c.run(f"export-pkg .") c.run("config install-pkg myconf/0.1") c.save({"conanfile.py": GenConanfile("pkg", "0.1")}, clean_first=True) c.run("create .") assert f"pkg/0.1: config_version: myconf/0.1.Z" in c.out c.run("list pkg/0.1:* --format=json") info = json.loads(c.stdout) rrev = info["Local Cache"]["pkg/0.1"]["revisions"]["485dad6cb11e2fa99d9afbe44a57a164"] pkg = rrev["packages"]["b683aa607d81f4a3b6f0c87c8191387133f2ff4a"] assert pkg["info"] == {"config_version": ["myconf/0.1.Z"]} class TestConfigInstallPkgFromFile: def test_install_from_file(self, servers): c = TestClient(servers=servers, light=True) # To make explicit the format of the conanconfig file conanconfig = textwrap.dedent("""\ packages: - myconf_a/0.1 - myconf_b/0.1 """) c.save({"conanconfig.yml": conanconfig}) # Admits the default location . c.run("config install-pkg") _check_conf(c, "myconf_b/0.1") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1"]) # install same file again: c.run("config install-pkg .") assert ("The requested configurations are identical to the already installed ones, " "skipping re-installation") in c.out _check_conf(c, "myconf_b/0.1") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1"]) # Forced re-install c.save_home({"global.conf": ""}) c.run("config install-pkg . --force") assert "forcing re-installation because --force" in c.out _check_conf(c, "myconf_b/0.1") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1"]) def test_install_from_file_with_url(self, servers): c = TestClient(servers=servers, light=True) c.run("remote remove default") server_url = servers["default"].fake_url conanconfig = yaml.dump({"packages": ["myconf_a/0.1"], "urls": [server_url]}) c.save({"conanconfig.yml": conanconfig}) c.run("config install-pkg .") _check_conf(c, "myconf_a/0.1") _check_conf_file(c, ["myconf_a/0.1"]) def test_update_with_file(self, servers): c = TestClient(servers=servers, light=True) conanconfig = yaml.dump({"packages": ["myconf_a/0.1", "myconf_b/0.1"]}) c.save({"conanconfig.yml": conanconfig}) c.run("config install-pkg .") # Now installing "updates" without disrupting the order c.run("config install-pkg myconf_c/0.1") assert "Installing new or updating configuration packages" _check_conf(c, "myconf_c/0.1") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1", "myconf_c/0.1"]) # Now installing "updates" without disrupting the order conanconfig = textwrap.dedent("""\ packages: - myconf_a/0.1 - myconf_b/0.1 - myconf_c/[*] """) c.save({"conanconfig.yml": conanconfig}) c.run("config install-pkg .") assert "Installing new or updating configuration packages" in c.out _check_conf(c, "myconf_c/0.2") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1", "myconf_c/0.2"]) # This is also an update of the existing ones, keeping the order conanconfig = textwrap.dedent("""\ packages: - myconf_a/[*] - myconf_b/0.1 - myconf_c/[*] """) c.save({"conanconfig.yml": conanconfig}) c.run("config install-pkg .") assert "Installing new or updating configuration packages" in c.out _check_conf(c, "myconf_c/0.2") _check_conf_file(c, ["myconf_a/0.2", "myconf_b/0.1", "myconf_c/0.2"]) def test_failed_update_force(self, servers): c = TestClient(servers=servers, light=True) conanconfig = yaml.dump({"packages": ["myconf_a/0.1", "myconf_b/0.1"]}) c.save({"conanconfig.yml": conanconfig}) c.run("config install-pkg .") # Now installing "updates" without disrupting the order conanconfig = textwrap.dedent("""\ packages: - myconf_a/0.2 """) c.save({"conanconfig.yml": conanconfig}) c.run("config install-pkg .", assert_error=True) assert "use 'conan config install-pkg --force' to force" in c.out assert "Use 'conan config clean' first to fully reset" in c.out _check_conf(c, "myconf_b/0.1") _check_conf_file(c, ["myconf_a/0.1", "myconf_b/0.1"]) c.run("config install-pkg . --force") assert "Forcing the installation because --force was defined" in c.out _check_conf(c, "myconf_a/0.2") _check_conf_file(c, ["myconf_b/0.1", "myconf_a/0.2"]) def test_install_from_file_with_lockfile(self, servers): # it should stay in version 0.1 c = TestClient(servers=servers) c.run("config install-pkg myconf_a/0.1 --lockfile-out=conan.lock") conanconfig = yaml.dump({"packages": ["myconf_a/[>=0.1 <1.0]"]}) c.save({"conanconfig.yml": conanconfig}) c.run("config install-pkg .") path = HomePaths(c.cache_folder).config_version_path # First are the newest installed configs = json.loads(load(path))["config_version"] configs = [str(RecipeReference.loads(r)) for r in configs] assert configs == ["myconf_a/0.1"] class TestConfigInstallPkgSettings: conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Conf(ConanFile): name = "myconf" version = "0.1" settings = "os" package_type = "configuration" def package(self): f = "win" if self.settings.os == "Windows" else "nix" copy(self, "*.conf", src=os.path.join(self.build_folder, f), dst=self.package_folder) """) @pytest.fixture() def client(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": self.conanfile, "win/global.conf": "user.myteam:myconf=mywinvalue", "nix/global.conf": "user.myteam:myconf=mynixvalue", }) c.run("export-pkg . -s os=Windows") c.run("export-pkg . -s os=Linux") c.run("upload * -r=default -c") c.run("remove * -c") return c @pytest.mark.parametrize("default_profile", [False, True]) def test_config_install_from_pkg(self, client, default_profile): # Now install it c = client if not default_profile: os.remove(os.path.join(c.cache_folder, "profiles", "default")) c.run("config install-pkg myconf/[*] -s os=Windows") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: mywinvalue" in c.out c.run("config install-pkg myconf/[*] -s os=Linux --force") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: mynixvalue" in c.out def test_error_no_settings_defined(self, client): c = client os.remove(os.path.join(c.cache_folder, "profiles", "default")) c.run("config install-pkg myconf/[*]", assert_error=True) assert "There are invalid packages:" in c.out assert "myconf/0.1: Invalid: 'settings.os' value not defined" in c.out def test_config_install_from_pkg_profile(self, client): # Now install it c = client c.save({"win.profile": "[settings]\nos=Windows", "nix.profile": "[settings]\nos=Linux"}) c.run("config install-pkg myconf/[*] -pr=win.profile") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: mywinvalue" in c.out c.run("config install-pkg myconf/[*] -pr=nix.profile --force") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: mynixvalue" in c.out def test_config_install_from_pkg_profile_default(self, client): # Now install it c = client c.save_home({"profiles/default": "[settings]\nos=Windows"}) c.run("config install-pkg myconf/[*]") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: mywinvalue" in c.out c.save_home({"profiles/default": "[settings]\nos=Linux"}) c.run("config install-pkg myconf/[*] --force") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: mynixvalue" in c.out class TestConfigInstallPkgOptions: conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Conf(ConanFile): name = "myconf" version = "0.1" options = {"project": ["project1", "project2"]} default_options = {"project": "project1"} package_type = "configuration" def package(self): copy(self, "*.conf", src=os.path.join(self.build_folder, str(self.options.project)), dst=self.package_folder) """) @pytest.fixture() def client(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": self.conanfile, "project1/global.conf": "user.myteam:myconf=my1value", "project2/global.conf": "user.myteam:myconf=my2value", }) c.run("export-pkg .") c.run("export-pkg . -o project=project2") c.run("upload * -r=default -c") c.run("remove * -c") return c @pytest.mark.parametrize("default_profile", [False, True]) def test_config_install_from_pkg(self, client, default_profile): # Now install it c = client if not default_profile: os.remove(os.path.join(c.cache_folder, "profiles", "default")) c.run("config install-pkg myconf/[*] -o &:project=project1") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: my1value" in c.out c.run("config install-pkg myconf/[*] -o &:project=project2 --force") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: my2value" in c.out def test_no_option_defined(self, client): c = client os.remove(os.path.join(c.cache_folder, "profiles", "default")) c.run("config install-pkg myconf/[*]") assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: my1value" in c.out def test_config_install_from_pkg_profile(self, client): # Now install it c = client c.save({"win.profile": "[options]\n&:project=project1", "nix.profile": "[options]\n&:project=project2"}) c.run("config install-pkg myconf/[*] -pr=win.profile") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: my1value" in c.out c.run("config install-pkg myconf/[*] -pr=nix.profile --force") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: my2value" in c.out def test_config_install_from_pkg_profile_default(self, client): # Now install it c = client c.save_home({"profiles/default": "[options]\n&:project=project1"}) c.run("config install-pkg myconf/[*]") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: my1value" in c.out c.save_home({"profiles/default": "[options]\n&:project=project2"}) c.run("config install-pkg myconf/[*] --force") assert "myconf/0.1: Downloaded package revision" in c.out assert "Copying file global.conf" in c.out c.run("config show *") assert "user.myteam:myconf: my2value" in c.out ================================================ FILE: test/functional/command/test_custom_symlink_home.py ================================================ import os import platform import pytest from conan.test.utils.scm import create_local_git_repo from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save @pytest.mark.skipif(platform.system() == "Windows", reason="Uses symlinks") def test_custom_symlinked_home_config_install(): base_cache = temp_folder() real_cache = os.path.join(base_cache, "real_cache") os.makedirs(real_cache) symlink_cache = os.path.join(base_cache, "symlink_cache") os.symlink(real_cache, symlink_cache) origin_folder = temp_folder() save(os.path.join(origin_folder, "myfile.txt"), "some contents") create_local_git_repo(folder=origin_folder) c = TestClient(cache_folder=symlink_cache) c.run(f'config install "{origin_folder}" --type=git') assert "Copying file myfile.txt to" in c.out ================================================ FILE: test/functional/command/test_install_deploy.py ================================================ import os import platform import shutil import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save @pytest.fixture() def client(matrix_client_shared): c = matrix_client_shared conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Tool(ConanFile): name = "tool" version = "1.0" def package(self): save(self, os.path.join(self.package_folder, "build", "my_tools.cmake"), 'set(MY_TOOL_VARIABLE "Hello world!")') def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.libdirs = [] self.cpp_info.bindirs = [] path_build_modules = os.path.join("build", "my_tools.cmake") self.cpp_info.set_property("cmake_build_modules", [path_build_modules]) """) c.save({"conanfile.py": conanfile}, clean_first=True) c.run("create .") return c @pytest.mark.tool("cmake") @pytest.mark.parametrize("powershell", [False, True]) def test_install_deploy(client, powershell): c = client custom_content = 'message(STATUS "MY_TOOL_VARIABLE=${MY_TOOL_VARIABLE}!")' cmake = gen_cmakelists(appname="my_app", appsources=["main.cpp"], find_package=["matrix", "tool"], custom_content=custom_content) deploy = textwrap.dedent(""" import os, shutil # USE **KWARGS to be robust against changes def deploy(graph, output_folder, **kwargs): conanfile = graph.root.conanfile for r, d in conanfile.dependencies.items(): new_folder = os.path.join(output_folder, d.ref.name) shutil.copytree(d.package_folder, new_folder) d.set_deploy_folder(new_folder) """) c.save({"conanfile.txt": "[requires]\nmatrix/1.0\ntool/1.0", "deploy.py": deploy, "CMakeLists.txt": cmake, "main.cpp": gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"])}, clean_first=True) pwsh = "-c tools.env.virtualenv:powershell=True" if powershell else "" c.run("install . -o *:shared=True " f"--deployer=deploy.py -of=mydeploy -g CMakeToolchain -g CMakeDeps {pwsh}") c.run("remove * -c") # Make sure the cache is clean, no deps there arch = c.get_default_host_profile().settings['arch'] deps = c.load(f"mydeploy/matrix-release-{arch}-data.cmake") assert 'set(matrix_PACKAGE_FOLDER_RELEASE "${CMAKE_CURRENT_LIST_DIR}/matrix")' in deps assert 'set(matrix_INCLUDE_DIRS_RELEASE "${matrix_PACKAGE_FOLDER_RELEASE}/include")' in deps assert 'set(matrix_LIB_DIRS_RELEASE "${matrix_PACKAGE_FOLDER_RELEASE}/lib")' in deps # We can fully move it to another folder, and still works tmp = os.path.join(temp_folder(), "relocated") shutil.copytree(c.current_folder, tmp) shutil.rmtree(c.current_folder) c2 = TestClient(current_folder=tmp) # I can totally build without errors with deployed c2.run_command("cmake . -DCMAKE_TOOLCHAIN_FILE=mydeploy/conan_toolchain.cmake " "-DCMAKE_BUILD_TYPE=Release") assert "MY_TOOL_VARIABLE=Hello world!!" in c2.out c2.run_command("cmake --build . --config Release") if platform.system() == "Windows": # Only the .bat env-generators are relocatable if powershell: cmd = r"powershell.exe mydeploy\conanrun.ps1 ; Release\my_app.exe" else: cmd = r"mydeploy\conanrun.bat && Release\my_app.exe" # For Lunux: cmd = ". mydeploy/conanrun.sh && ./my_app" c2.run_command(cmd) assert "matrix/1.0: Hello World Release!" in c2.out @pytest.mark.tool("cmake") def test_install_full_deploy_layout(client): c = client custom_content = 'message(STATUS "MY_TOOL_VARIABLE=${MY_TOOL_VARIABLE}!")' cmake = gen_cmakelists(appname="my_app", appsources=["main.cpp"], find_package=["matrix", "tool"], custom_content=custom_content) conanfile = textwrap.dedent(""" [requires] matrix/1.0 tool/1.0 [generators] CMakeDeps CMakeToolchain [layout] cmake_layout """) c.save({"conanfile.txt": conanfile, "CMakeLists.txt": cmake, "main.cpp": gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"])}, clean_first=True) c.run("install . -o *:shared=True --deployer=full_deploy.py") c.run("remove * -c") # Make sure the cache is clean, no deps there arch = c.get_default_host_profile().settings['arch'] folder = "/Release" if platform.system() != "Windows" else "" rel_path = "../../" if platform.system() == "Windows" else "../../../" deps = c.load(f"build{folder}/generators/matrix-release-{arch}-data.cmake") assert 'set(matrix_PACKAGE_FOLDER_RELEASE "${CMAKE_CURRENT_LIST_DIR}/' \ f'{rel_path}full_deploy/host/matrix/1.0/Release/{arch}")' in deps assert 'set(matrix_INCLUDE_DIRS_RELEASE "${matrix_PACKAGE_FOLDER_RELEASE}/include")' in deps assert 'set(matrix_LIB_DIRS_RELEASE "${matrix_PACKAGE_FOLDER_RELEASE}/lib")' in deps # We can fully move it to another folder, and still works tmp = os.path.join(temp_folder(), "relocated") shutil.copytree(c.current_folder, tmp) shutil.rmtree(c.current_folder) c2 = TestClient(current_folder=tmp) with c2.chdir(f"build{folder}"): # I can totally build without errors with deployed cmakelist = "../.." if platform.system() != "Windows" else ".." c2.run_command(f"cmake {cmakelist} -DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake " "-DCMAKE_BUILD_TYPE=Release") assert "MY_TOOL_VARIABLE=Hello world!!" in c2.out c2.run_command("cmake --build . --config Release") if platform.system() == "Windows": # Only the .bat env-generators are relocatable atm cmd = r"generators\conanrun.bat && Release\my_app.exe" # For Lunux: cmd = ". mydeploy/conanrun.sh && ./my_app" c2.run_command(cmd) assert "matrix/1.0: Hello World Release!" in c2.out def test_copy_files_deploy(): c = TestClient() deploy = textwrap.dedent(""" import os, shutil def deploy(graph, output_folder, **kwargs): conanfile = graph.root.conanfile for r, d in conanfile.dependencies.items(): bindir = os.path.join(d.package_folder, "bin") for f in os.listdir(bindir): shutil.copy2(os.path.join(bindir, f), os.path.join(output_folder, f)) """) c.save({"conanfile.txt": "[requires]\nhello/0.1", "deploy.py": deploy, "hello/conanfile.py": GenConanfile("hello", "0.1").with_package_file("bin/file.txt", "content!!")}) c.run("create hello") c.run("install . --deployer=deploy.py -of=mydeploy") def test_multi_deploy(): """ check that we can add more than 1 deployer in the command line, both in local folders and in cache. Also testing that using .py extension or not, is the same Also, the local folder have precedence over the cache extensions """ c = TestClient() deploy1 = textwrap.dedent(""" def deploy(graph, output_folder, **kwargs): conanfile = graph.root.conanfile conanfile.output.info("deploy1!!") """) deploy2 = textwrap.dedent(""" def deploy(graph, output_folder, **kwargs): conanfile = graph.root.conanfile conanfile.output.info("sub/deploy2!!") """) deploy_cache = textwrap.dedent(""" def deploy(graph, output_folder, **kwargs): conanfile = graph.root.conanfile conanfile.output.info("deploy cache!!") """) save(os.path.join(c.cache_folder, "extensions", "deploy", "deploy_cache.py"), deploy_cache) # This should never be called in this test, always the local is found first save(os.path.join(c.cache_folder, "extensions", "deploy", "mydeploy.py"), "CRASH!!!!") c.save({"conanfile.txt": "", "mydeploy.py": deploy1, "sub/mydeploy2.py": deploy2}) c.run("install . --deployer=mydeploy --deployer=sub/mydeploy2 --deployer=deploy_cache") assert "conanfile.txt: deploy1!!" in c.out assert "conanfile.txt: sub/deploy2!!" in c.out assert "conanfile.txt: deploy cache!!" in c.out # Now with .py extension c.run("install . --deployer=mydeploy.py --deployer=sub/mydeploy2.py --deployer=deploy_cache.py") assert "conanfile.txt: deploy1!!" in c.out assert "conanfile.txt: sub/deploy2!!" in c.out assert "conanfile.txt: deploy cache!!" in c.out def test_deploy_local_import(): """ test that deployers can share some Python code with local imports """ c = TestClient() helper = textwrap.dedent(""" def myhelper(conanfile): conanfile.output.info("My HELPER!!") """) deploy_cache = textwrap.dedent(""" from helper import myhelper def deploy(graph, output_folder, **kwargs): myhelper(graph.root.conanfile) """) save(os.path.join(c.cache_folder, "extensions", "deployers", "deploy_cache.py"), deploy_cache) save(os.path.join(c.cache_folder, "extensions", "deployers", "helper.py"), helper) c.save({"conanfile.txt": ""}) c.run("install . --deployer=deploy_cache") assert "conanfile.txt: My HELPER!!" in c.out def test_builtin_full_deploy(): """ check the built-in full_deploy """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): settings = "arch", "build_type" def package(self): content = f"{self.settings.build_type}-{self.settings.arch}" save(self, os.path.join(self.package_folder, "include/hello.h"), content) def package_info(self): path_build_modules = os.path.join("build", "my_tools_{}.cmake".format(self.context)) self.cpp_info.set_property("cmake_build_modules", [path_build_modules]) """) c.save({"conanfile.py": conanfile}) c.run("create . --name=dep --version=0.1") c.run("create . --name=dep --version=0.1 -s build_type=Debug -s arch=x86") c.save({"conanfile.txt": "[requires]\ndep/0.1"}, clean_first=True) c.run("install . --deployer=full_deploy -of=output -g CMakeDeps") assert "Conan built-in full deployer" in c.out c.run("install . --deployer=full_deploy -of=output -g CMakeDeps " "-s build_type=Debug -s arch=x86") host_arch = c.get_default_host_profile().settings['arch'] release = c.load(f"output/full_deploy/host/dep/0.1/Release/{host_arch}/include/hello.h") assert f"Release-{host_arch}" in release debug = c.load("output/full_deploy/host/dep/0.1/Debug/x86/include/hello.h") assert "Debug-x86" in debug cmake_release = c.load(f"output/dep-release-{host_arch}-data.cmake") assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/include")' in cmake_release assert f"${{CMAKE_CURRENT_LIST_DIR}}/full_deploy/host/dep/0.1/Release/{host_arch}" in cmake_release assert 'set(dep_BUILD_MODULES_PATHS_RELEASE ' \ '"${dep_PACKAGE_FOLDER_RELEASE}/build/my_tools_host.cmake")' in cmake_release cmake_debug = c.load("output/dep-debug-x86-data.cmake") assert 'set(dep_INCLUDE_DIRS_DEBUG "${dep_PACKAGE_FOLDER_DEBUG}/include")' in cmake_debug assert "${CMAKE_CURRENT_LIST_DIR}/full_deploy/host/dep/0.1/Debug/x86" in cmake_debug assert 'set(dep_BUILD_MODULES_PATHS_DEBUG ' \ '"${dep_PACKAGE_FOLDER_DEBUG}/build/my_tools_host.cmake")' in cmake_debug def test_deploy_reference(): """ check that we can also deploy a reference """ c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_package_file("include/hi.h", "hi")}) c.run("create .") c.run("install --requires=pkg/1.0 --deployer=full_deploy --output-folder=output") # NOTE: Full deployer always use build_type/arch, even if None/None in the path, same structure header = c.load("output/full_deploy/host/pkg/1.0/include/hi.h") assert "hi" in header # Testing that we can deploy to the current folder too c.save({}, clean_first=True) c.run("install --requires=pkg/1.0 --deployer=full_deploy") # NOTE: Full deployer always use build_type/arch, even if None/None in the path, same structure header = c.load("full_deploy/host/pkg/1.0/include/hi.h") assert "hi" in header def test_deploy_overwrite(): """ calling several times the install --deploy doesn't crash if files already exist """ c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_package_file("include/hi.h", "hi")}) c.run("create .") c.run("install --requires=pkg/1.0 --deployer=full_deploy --output-folder=output") header = c.load("output/full_deploy/host/pkg/1.0/include/hi.h") assert "hi" in header # modify the package c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_package_file("include/hi.h", "bye")}) c.run("create .") c.run("install --requires=pkg/1.0 --deployer=full_deploy --output-folder=output") header = c.load("output/full_deploy/host/pkg/1.0/include/hi.h") assert "bye" in header def test_deploy_editable(): """ when deploying something that is editable, with the full_deploy built-in, it will copy the editable files as-is, but it doesn't fail at this moment """ c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "1.0"), "src/include/hi.h": "hi"}) c.run("editable add .") # If we don't change to another folder, the full_deploy will be recursive and fail with c.chdir(temp_folder()): c.run("install --requires=pkg/1.0 --deployer=full_deploy --output-folder=output") header = c.load("output/full_deploy/host/pkg/1.0/src/include/hi.h") assert "hi" in header def test_deploy_aggregate_components(): """ The caching of aggregated components can be causing issues when deploying and using generators that would still point to the packages with components in the cache https://github.com/conan-io/conan/issues/14022 """ c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.components["mycomp"].libs = ["mycomp"] """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type") .with_requires("dep/0.1") .with_generator("CMakeDeps"), "consumer/conanfile.py": GenConanfile().with_settings("build_type") .with_requires("pkg/0.1") .with_generator("CMakeDeps")}) c.run("export dep") c.run("export pkg") # If we don't change to another folder, the full_deploy will be recursive and fail c.run("install consumer --build=missing --deployer=full_deploy --output-folder=output") data = c.load("output/dep-release-data.cmake") assert 'set(dep_PACKAGE_FOLDER_RELEASE ' \ '"${CMAKE_CURRENT_LIST_DIR}/full_deploy/host/dep/0.1")' in data assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/include")' in data def test_deploy_single_package(): """ Let's try a deploy that executes on a single package reference """ c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_package_file("include/hi.h", "hi"), "consumer/conanfile.txt": "[requires]\npkg/1.0"}) c.run("create .") # if we deploy one --requires, we get that package c.run("install --requires=pkg/1.0 --deployer=direct_deploy --output-folder=output") header = c.load("output/direct_deploy/pkg/include/hi.h") assert "hi" in header # If we deploy a local conanfile.txt, we get deployed its direct dependencies c.run("install consumer/conanfile.txt --deployer=direct_deploy --output-folder=output2") header = c.load("output2/direct_deploy/pkg/include/hi.h") assert "hi" in header def test_deploy_output_locations(): tc = TestClient() deployer = textwrap.dedent(""" def deploy(graph, output_folder, **kwargs): graph.root.conanfile.output.info(f"Deployer output: {output_folder}") """) tc.save({"conanfile.txt": "", "my_deploy.py": deployer}) tmp_folder = temp_folder() tc.run(f"install . --deployer=my_deploy -of='{tmp_folder}'") assert f"Deployer output: {tmp_folder}" in tc.out deployer_output = temp_folder() tc.run(f"install . --deployer=my_deploy -of='{tmp_folder}' --deployer-folder='{deployer_output}'") assert f"Deployer output: {deployer_output}" in tc.out assert f"Deployer output: {tmp_folder}" not in tc.out def test_not_deploy_absolute_paths(): """ Absolute paths, for system packages, don't need to be relativized https://github.com/conan-io/conan/issues/15242 """ c = TestClient() some_abs_path = temp_folder().replace("\\", "/") conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" def package_info(self): self.cpp_info.includedirs = ["{some_abs_path}/myusr/include"] self.cpp_info.libdirs = ["{some_abs_path}/myusr/lib"] self.buildenv_info.define_path("MYPATH", "{some_abs_path}/mypath") """) c.save({"conanfile.py": conanfile}) c.run("create .") # if we deploy one --requires, we get that package c.run("install --requires=pkg/1.0 --deployer=full_deploy -g CMakeDeps -g CMakeToolchain " "-s os=Linux -s:b os=Linux -s arch=x86_64 -s:b arch=x86_64") data = c.load("pkg-release-x86_64-data.cmake") assert f'set(pkg_INCLUDE_DIRS_RELEASE "{some_abs_path}/myusr/include")' in data assert f'set(pkg_LIB_DIRS_RELEASE "{some_abs_path}/myusr/lib")' in data env = c.load("conanbuildenv-release-x86_64.sh") assert f'export MYPATH="{some_abs_path}/mypath"' in env def test_deploy_incorrect_folder(): # https://github.com/conan-io/cmake-conan/issues/658 c = TestClient() c.save({"conanfile.txt": ""}) c.run('install . --deployer=full_deploy --deployer-folder="mydep fold"') assert os.path.exists(os.path.join(c.current_folder, "mydep fold")) if platform.system() == "Windows": # This only fails in Windows c.run(r'install . --deployer=full_deploy --deployer-folder="\"mydep fold\""', assert_error=True) assert "ERROR: Deployer folder cannot be created" in c.out class TestRuntimeDeployer: def test_runtime_deploy(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): package_type = "shared-library" def package(self): copy(self, "*.so", src=self.build_folder, dst=self.package_folder) copy(self, "*.dll", src=self.build_folder, dst=self.package_folder) """) c.save({"pkga/conanfile.py": conanfile, "pkga/lib/pkga.so": "", "pkga/bin/pkga.dll": "", "pkgb/conanfile.py": conanfile, "pkgb/lib/pkgb.so": ""}) c.run("export-pkg pkga --name=pkga --version=1.0") c.run("export-pkg pkgb --name=pkgb --version=1.0") c.run("install --requires=pkga/1.0 --requires=pkgb/1.0 --deployer=runtime_deploy " "--deployer-folder=myruntime -vvv") expected = sorted(["pkga.so", "pkgb.so", "pkga.dll"]) assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime"))) == expected c.run("install --requires=pkga/1.0 --requires=pkgb/1.0 --deployer=runtime_deploy " "--deployer-folder=myruntime -vvv") assert "pkga.dll exists with same contents, skipping copy" in c.out assert "pkga.so exists with same contents, skipping copy" in c.out assert "pkgb.so exists with same contents, skipping copy" in c.out c.save({"myruntime/pkga.dll": "other content"}) c.run("install --requires=pkga/1.0 --requires=pkgb/1.0 --deployer=runtime_deploy " "--deployer-folder=myruntime -vvv") assert "pkga.dll exists and will be overwritten" in c.out def test_runtime_not_deploy(self): # https://github.com/conan-io/conan/issues/16712 # If no run=False (no package-type), then no runtime is deployed c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): def package(self): copy(self, "*.so", src=self.build_folder, dst=self.package_folder) copy(self, "*.dll", src=self.build_folder, dst=self.package_folder) """) c.save({"pkga/conanfile.py": conanfile, "pkga/lib/pkga.so": "", "pkga/bin/pkga.dll": ""}) c.run("export-pkg pkga --name=pkga --version=1.0") c.run("install --requires=pkga/1.0 --deployer=runtime_deploy --deployer-folder=myruntime") assert os.listdir(os.path.join(c.current_folder, "myruntime")) == [] def test_runtime_deploy_components(self): c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): package_type = "shared-library" def package(self): copy(self, "*.so", src=self.build_folder, dst=os.path.join(self.package_folder, "a")) copy(self, "*.dll", src=self.build_folder, dst=os.path.join(self.package_folder, "b")) def package_info(self): self.cpp_info.components["a"].libdirs = ["a"] self.cpp_info.components["b"].bindirs = ["b"] """) c.save({"pkga/conanfile.py": conanfile, "pkga/lib/pkga.so": "", "pkga/bin/pkga.dll": "", "pkgb/conanfile.py": conanfile, "pkgb/lib/pkgb.so": ""}) c.run("export-pkg pkga --name=pkga --version=1.0") c.run("export-pkg pkgb --name=pkgb --version=1.0") c.run("install --requires=pkga/1.0 --requires=pkgb/1.0 --deployer=runtime_deploy " "--deployer-folder=myruntime -vvv") assert (sorted(os.listdir(os.path.join(c.current_folder, "myruntime"))) == sorted(["bin", "lib"])) assert (sorted(os.listdir(os.path.join(c.current_folder, "myruntime", "lib"))) == sorted(['pkga.so', 'pkgb.so'])) assert (sorted(os.listdir(os.path.join(c.current_folder, "myruntime", "bin"))) == sorted(['pkga.dll'])) @pytest.mark.parametrize("symlink, expected", [(True, ["libfoo.so.0.1.0", "libfoo.so.0", "libfoo.so"]), (False, ["libfoo.so.0.1.0"])]) def test_runtime_deploy_symlinks(self, symlink, expected): """ The deployer runtime_deploy should preserve symlinks when deploying shared libraries """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy, chdir import os class Pkg(ConanFile): package_type = "shared-library" def package(self): copy(self, "*.so*", src=self.build_folder, dst=self.package_folder) with chdir(self, os.path.join(self.package_folder, "lib")): os.symlink(src="libfoo.so.0.1.0", dst="libfoo.so.0") os.symlink(src="libfoo.so.0", dst="libfoo.so") """) c.save({"foo/conanfile.py": conanfile, "foo/lib/libfoo.so.0.1.0": ""}) c.run("export-pkg foo/ --name=foo --version=0.1.0") c.run(f"install --requires=foo/0.1.0 --deployer=runtime_deploy --deployer-folder=output " f"-c:a tools.deployer:symlinks={symlink}") sorted_expected = sorted(expected) assert sorted(os.listdir(os.path.join(c.current_folder, "output"))) == sorted_expected link_so_0 = os.path.join(c.current_folder, "output", "libfoo.so.0") link_so = os.path.join(c.current_folder, "output", "libfoo.so") lib = os.path.join(c.current_folder, "output", "libfoo.so.0.1.0") # INFO: This test requires in Windows to have symlinks enabled, otherwise it will fail if symlink and platform.system() != "Windows": assert os.path.islink(link_so_0) assert os.path.islink(link_so) assert not os.path.isabs(os.readlink(link_so_0)) assert not os.path.isabs(os.readlink(os.path.join(link_so))) assert os.path.realpath(link_so) == os.path.realpath(link_so_0) assert os.path.realpath(link_so_0) == os.path.realpath(lib) assert not os.path.islink(lib) else: assert not os.path.islink(lib) def test_runtime_deploy_subfolder(self): """ The deployer runtime_deploy should preserve subfolder structure when deploying shared libraries """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy import os class Pkg(ConanFile): package_type = "shared-library" def package(self): copy(self, "*.so*", src=self.build_folder, dst=self.package_folder, keep_path=True) """) c.save({"foo/conanfile.py": conanfile, "foo/lib/libfoo.so": "", "foo/lib/subfolder/libbar.so": "", "foo/lib/subfolder/subsubfolder/libqux.so": "",}) c.run("export-pkg foo/ --name=foo --version=0.1.0") c.run(f"install --requires=foo/0.1.0 --deployer=runtime_deploy --deployer-folder=output") assert (sorted(os.listdir(os.path.join(c.current_folder, "output"))) == ["libfoo.so", "subfolder"]) assert (sorted(os.listdir(os.path.join(c.current_folder, "output", "subfolder"))) == ["libbar.so", "subsubfolder"]) assert sorted(os.listdir(os.path.join(c.current_folder, "output", "subfolder", "subsubfolder"))) == ["libqux.so"] def test_runtime_deploy_subfolder_symlink(self): """ The deployer runtime_deploy should preserve subfolder structure when deploying shared libraries with symlinks """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy, chdir, mkdir import os class Pkg(ConanFile): package_type = "shared-library" def package(self): copy(self, "*.so*", src=self.build_folder, dst=self.package_folder) with chdir(self, os.path.join(self.package_folder, "lib")): os.symlink(src="subfolder/libfoo.so.1.0", dst="libfoo.so.1") os.symlink(src="libfoo.so.1", dst="libfoo.so") """) c.save({"foo/conanfile.py": conanfile, "foo/lib/subfolder/libfoo.so.1.0": "",}) c.run("export-pkg foo/ --name=foo --version=0.1.0") c.run(f"install --requires=foo/0.1.0 --deployer=runtime_deploy --deployer-folder=output " f"-c:a tools.deployer:symlinks=True") assert os.listdir(os.path.join(c.current_folder, "output", "subfolder")) == ["libfoo.so.1.0"] # INFO: This test requires in Windows to have symlinks enabled, otherwise it will fail if platform.system() != "Windows": assert (sorted(os.listdir(os.path.join(c.current_folder, "output"))) == ["libfoo.so", "libfoo.so.1", "subfolder"]) link_so_0 = os.path.join(c.current_folder, "output", "libfoo.so.1") link_so = os.path.join(c.current_folder, "output", "libfoo.so") lib = os.path.join(c.current_folder, "output", "subfolder", "libfoo.so.1.0") assert os.path.islink(link_so_0) assert os.path.islink(link_so) assert not os.path.isabs(os.readlink(link_so_0)) assert not os.path.isabs(os.readlink(os.path.join(link_so))) assert os.path.realpath(link_so) == os.path.realpath(link_so_0) assert os.path.realpath(link_so_0) == os.path.realpath(lib) def test_deployer_errors(): c = TestClient() c.save({"conanfile.txt": "", "mydeploy.py": "", "mydeploy2.py": "nonsense"}) c.run("install . --deployer=nonexisting.py", assert_error=True) assert "ERROR: Cannot find deployer 'nonexisting.py'" in c.out c.run("install . --deployer=mydeploy.py", assert_error=True) assert "ERROR: Deployer does not contain 'deploy()' function" in c.out c.run("install . --deployer=mydeploy2.py", assert_error=True) # The error message says conanfile, not a big deal still path to file is shown assert "ERROR: Unable to load conanfile" in c.out def test_deploy_relative_paths(): c = TestClient() consumer = textwrap.dedent(""" import os from conan import ConanFile class Consumer(ConanFile): requires = "pkg/0.1" settings = "build_type" os = "build_type" generators = "CMakeDeps" def layout(self): self.folders.build = os.path.join("some/sub/folders") self.folders.generators = os.path.join(self.folders.build, "generators") """) deploy = textwrap.dedent(""" import os, shutil def deploy(graph, output_folder): conanfile = graph.root.conanfile output_folder = os.path.join(conanfile.build_folder, "installed") for dep in conanfile.dependencies.values(): new_folder = os.path.join(output_folder, dep.ref.name) shutil.copytree(dep.package_folder, new_folder, symlinks=True) dep.set_deploy_folder(new_folder) """) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"), "consumer/conanfile.py": consumer, "mydeploy.py": deploy}) c.run("create pkg") # If we don't change to another folder, the full_deploy will be recursive and fail c.run("install consumer --build=missing --deployer=mydeploy.py") data = c.load("consumer/some/sub/folders/generators/pkg-release-data.cmake") assert 'set(pkg_PACKAGE_FOLDER_RELEASE "${CMAKE_CURRENT_LIST_DIR}/../installed/pkg")' in data @pytest.mark.parametrize("absolute_path", [True, False]) def test_deploy_output_absolute(absolute_path): # https://github.com/conan-io/conan/issues/18560 c = TestClient() c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_package_file("myfile.txt", "c"), "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("create pkg") # It is important to use the forward /myout in Windows, this was the breaking input out_path = f"{c.current_folder}/myout" if absolute_path else "myout" c.run("install consumer/conanfile.txt -s arch=x86_64 --deployer=full_deploy -g CMakeDeps " f'-of="{out_path}"') assert c.load("myout/full_deploy/host/pkg/0.1/myfile.txt") == "c" data = c.load("myout/pkg-release-x86_64-data.cmake") assert ('set(pkg_PACKAGE_FOLDER_RELEASE ' '"${CMAKE_CURRENT_LIST_DIR}/full_deploy/host/pkg/0.1")') in data ================================================ FILE: test/functional/command/test_new.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") def test_conan_new_compiles(): # TODO: Maybe add more templates that are not used in the rest of the test suite? tc = TestClient() tc.run("new header_lib -d name=hello -d version=1.0 -o=hello") tc.run("new header_lib -d name=bye -d version=1.0 -d requires=hello/1.0 -o=bye") tc.run("create hello -tf=") tc.run("create bye") @pytest.mark.tool("cmake") def test_conan_new_empty(): c = TestClient() c.run("new") cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) add_executable(example main.cpp) """) main = textwrap.dedent(r""" #include int main() { std::cout << "Hello World!\n"; } """) c.save({ "CMakeLists.txt": cmakelists, "main.cpp": main, }) c.run("build") suffix = ".exe" if platform.system() == "Windows" else "" assert os.path.exists(os.path.join(c.current_folder, "build", "Release", f"example{suffix}")) ================================================ FILE: test/functional/conftest.py ================================================ import os import shutil import textwrap import pytest from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient @pytest.fixture(scope="session") def _matrix_client(): """ matrix/1.0, just static, no test_package """ c = TestClient() c.run("new cmake_lib -d name=matrix -d version=1.0") c.run("create . -tf=") return c @pytest.fixture(scope="session") def _matrix_client_shared(_matrix_client): _matrix_client.run("create . -o *:shared=True -tf=") return _matrix_client @pytest.fixture(scope="session") def _matrix_client_debug(_matrix_client): _matrix_client.run("create . -s build_type=Debug -tf=") return _matrix_client @pytest.fixture() def matrix_client(_matrix_client): c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_matrix_client.cache_folder, c.cache_folder) return c @pytest.fixture() def matrix_client_nospace(_matrix_client): c = TestClient(path_with_spaces=False) c.cache_folder = os.path.join(temp_folder(path_with_spaces=False), ".conan2") shutil.copytree(_matrix_client.cache_folder, c.cache_folder) return c @pytest.fixture() def matrix_client_shared(_matrix_client_shared): c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_matrix_client_shared.cache_folder, c.cache_folder) return c @pytest.fixture() def matrix_client_shared_debug(_matrix_client_shared, _matrix_client_debug): c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_matrix_client_shared.cache_folder, c.cache_folder) return c @pytest.fixture() def matrix_client_debug(_matrix_client_debug): c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_matrix_client_debug.cache_folder, c.cache_folder) return c @pytest.fixture(scope="session") def _transitive_libraries(_matrix_client): """ engine/1.0->matrix/1.0 """ c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_matrix_client.cache_folder, c.cache_folder) c.save({}, clean_first=True) c.run("new cmake_lib -d name=engine -d version=1.0 -d requires=matrix/1.0") # create both static and shared c.run("create . -tf=") c.run("create . -o engine/*:shared=True -tf=") return c @pytest.fixture() def transitive_libraries(_transitive_libraries): c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_transitive_libraries.cache_folder, c.cache_folder) return c @pytest.fixture(scope="session") def _matrix_client_components(): """ 2 components, different than the package name """ c = TestClient() headers_h = textwrap.dedent(""" #include #ifndef MY_MATRIX_HEADERS_DEFINE #error "Fatal error MY_MATRIX_HEADERS_DEFINE not defined" #endif void headers(){ std::cout << "Matrix headers: Release!" << std::endl; #if __cplusplus std::cout << " Matrix headers __cplusplus: __cplusplus" << __cplusplus << std::endl; #endif } """) vector_h = gen_function_h(name="vector") vector_cpp = gen_function_cpp(name="vector", includes=["vector"]) module_h = gen_function_h(name="module") module_cpp = gen_function_cpp(name="module", includes=["module", "vector"], calls=["vector"]) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Matrix(ConanFile): name = "matrix" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" exports_sources = "src/*", "CMakeLists.txt" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.default_components = ["vector", "module"] self.cpp_info.components["headers"].includedirs = ["include/headers"] self.cpp_info.components["headers"].set_property("cmake_target_name", "MatrixHeaders") self.cpp_info.components["headers"].defines = ["MY_MATRIX_HEADERS_DEFINE=1"] # Few flags to cover that CMakeDeps doesn't crash with them if self.settings.compiler == "msvc": self.cpp_info.components["headers"].cxxflags = ["/Zc:__cplusplus"] self.cpp_info.components["headers"].cflags = ["/Zc:__cplusplus"] self.cpp_info.components["headers"].system_libs = ["ws2_32"] else: self.cpp_info.components["headers"].system_libs = ["m"] # Just to verify CMake don't break self.cpp_info.sharedlinkflags = ["-z now", "-z relro"] self.cpp_info.exelinkflags = ["-z now", "-z relro"] self.cpp_info.components["vector"].libs = ["vector"] self.cpp_info.components["vector"].includedirs = ["include"] self.cpp_info.components["vector"].libdirs = ["lib"] self.cpp_info.components["module"].libs = ["module"] self.cpp_info.components["module"].includedirs = ["include"] self.cpp_info.components["module"].libdirs = ["lib"] self.cpp_info.components["module"].requires = ["vector"] """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(matrix CXX) add_library(vector src/vector.cpp) add_library(module src/module.cpp) add_library(headers INTERFACE) target_link_libraries(module PRIVATE vector) set_target_properties(headers PROPERTIES PUBLIC_HEADER "src/headers.h") set_target_properties(module PROPERTIES PUBLIC_HEADER "src/module.h") set_target_properties(vector PROPERTIES PUBLIC_HEADER "src/vector.h") install(TARGETS vector module) install(TARGETS headers PUBLIC_HEADER DESTINATION include/headers) """) c.save({"src/headers.h": headers_h, "src/vector.h": vector_h, "src/vector.cpp": vector_cpp, "src/module.h": module_h, "src/module.cpp": module_cpp, "CMakeLists.txt": cmakelists, "conanfile.py": conanfile}) c.run("create .") return c @pytest.fixture() def matrix_client_components(_matrix_client_components): c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_matrix_client_components.cache_folder, c.cache_folder) return c @pytest.fixture(scope="session") def _matrix_c_interface_client(): c = TestClient() matrix_h = textwrap.dedent("""\ #pragma once #ifdef __cplusplus extern "C" { #endif void matrix(); #ifdef __cplusplus } #endif """) matrix_cpp = textwrap.dedent("""\ #include "matrix.h" #include #include void matrix(){ std::cout<< std::string("Hello Matrix!") < $ ) set_target_properties(matrix PROPERTIES PUBLIC_HEADER "include/matrix.h") install(TARGETS matrix EXPORT matrixConfig) export(TARGETS matrix NAMESPACE matrix:: FILE "${CMAKE_CURRENT_BINARY_DIR}/matrixConfig.cmake" ) install(EXPORT matrixConfig DESTINATION "matrix/cmake" NAMESPACE matrix:: ) """) conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Recipe(ConanFile): name = "matrix" version = "0.1" settings = "os", "compiler", "build_type", "arch" package_type = "static-library" generators = "CMakeToolchain" exports_sources = "CMakeLists.txt", "src/*", "include/*" languages = "C++" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["matrix"] """) c.save({"include/matrix.h": matrix_h, "src/matrix.cpp": matrix_cpp, "conanfile.py": conanfile, "CMakeLists.txt": cmake}) c.run("create .") return c @pytest.fixture() def matrix_c_interface_client(_matrix_c_interface_client): c = TestClient() c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_matrix_c_interface_client.cache_folder, c.cache_folder) return c ================================================ FILE: test/functional/layout/__init__.py ================================================ ================================================ FILE: test/functional/layout/test_build_system_layout_helpers.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save, load @pytest.fixture def conanfile(): conanfile = str(GenConanfile() .with_import("import os") .with_setting("build_type").with_setting("arch") .with_import("from conan.tools.microsoft import vs_layout") .with_import("from conan.tools.files import save, copy")) conanfile += """ def source(self): save(self, "include/myheader.h", "") def build(self): save(self, "{libpath}/mylib.lib", "") def layout(self): vs_layout(self) def package(self): copy(self, "*", self.source_folder, self.package_folder) copy(self, "*", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) """ return conanfile subfolders_arch = {"armv7": "ARM", "armv8": "ARM64", "x86": None, "x86_64": "x64"} @pytest.mark.parametrize("arch", ["x86_64", "x86", "armv7", "armv8"]) @pytest.mark.parametrize("build_type", ["Debug", "Release"]) def test_layout_in_cache(conanfile, build_type, arch): """The layout in the cache is used too, always relative to the "base" folders that the cache requires. But by the default, the "package" is not followed """ client = TestClient() libarch = subfolders_arch.get(arch) libpath = "{}{}".format(libarch + "/" if libarch else "", build_type) client.save({"conanfile.py": conanfile.format(libpath=libpath)}) client.run(f"create . --name=lib --version=1.0 -s build_type={build_type} -s arch={arch}") layout = client.created_layout() bf = layout.build() pf = layout.package() # Check the build folder assert os.path.exists(os.path.join(os.path.join(bf, libpath), "mylib.lib")) # Check the package folder assert os.path.exists(os.path.join(pf, "lib/mylib.lib")) assert os.path.exists(os.path.join(pf, "include", "myheader.h")) @pytest.mark.parametrize("arch", ["x86_64", "x86", "armv7", "armv8"]) @pytest.mark.parametrize("build_type", ["Debug", "Release"]) def test_layout_with_local_methods(conanfile, build_type, arch): """The layout in the cache is used too, always relative to the "base" folders that the cache requires. But by the default, the "package" is not followed """ client = TestClient() libarch = subfolders_arch.get(arch) libpath = "{}{}".format(libarch + "/" if libarch else "", build_type) client.save({"conanfile.py": conanfile.format(libpath=libpath)}) client.run("install . --name=lib --version=1.0 -s build_type={} -s arch={}".format(build_type, arch)) client.run("source .") # Check the source folder (release) assert os.path.exists(os.path.join(client.current_folder, "include", "myheader.h")) client.run("build . --name=lib --version=1.0 -s build_type={} -s arch={}".format(build_type, arch)) # Check the build folder (release) assert os.path.exists(os.path.join(os.path.join(client.current_folder, libpath), "mylib.lib")) @pytest.mark.skipif(platform.system() != "Windows", reason="Removing msvc compiler") def test_error_no_msvc(): # https://github.com/conan-io/conan/issues/9953 conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def layout(self): cmake_layout(self) """) settings_yml = textwrap.dedent(""" os: [Windows] os_build: [Windows] arch_build: [x86_64] compiler: gcc: version: ["8"] build_type: [Release] arch: [x86_64] """) client = TestClient() client.save({"conanfile.py": conanfile}) save(client.paths.settings_path, settings_yml) client.run('install . -s os=Windows -s build_type=Release -s arch=x86_64 ' '-s compiler=gcc -s compiler.version=8 ' '-s:b os=Windows -s:b build_type=Release -s:b arch=x86_64 ' '-s:b compiler=gcc -s:b compiler.version=8') assert "Installing" in client.out def test_error_no_build_type(): # https://github.com/conan-io/conan/issues/9953 conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "arch" def layout(self): cmake_layout(self) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run('install .', assert_error=True) assert " 'build_type' setting not defined, it is necessary for cmake_layout()" in client.out def test_cmake_layout_external_sources(): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout from conan.tools.files import save, copy, load class Pkg(ConanFile): settings = "os", "build_type" exports_sources = "exported.txt" def layout(self): cmake_layout(self, src_folder="src") def generate(self): save(self, "generate.txt", "generate") def source(self): save(self, "source.txt", "foo") def build(self): c1 = load(self, os.path.join(self.source_folder, "source.txt")) c2 = load(self, os.path.join(self.source_folder, "..", "exported.txt")) save(self, "build.txt", c1 + c2) def package(self): copy(self, "build.txt", self.build_folder, os.path.join(self.package_folder, "res")) """) client = TestClient() client.save({"conanfile.py": conanfile, "exported.txt": "exported_contents"}) client.run("create . --name=foo --version=1.0 -s os=Linux") assert "Packaged 1 '.txt' file: build.txt" in client.out # Local flow client.run("install . --name=foo --version=1.0 -s os=Linux") assert os.path.exists(os.path.join(client.current_folder, "build", "Release", "generators", "generate.txt")) client.run("source .") assert os.path.exists(os.path.join(client.current_folder, "src", "source.txt")) client.run("build .") contents = load(os.path.join(client.current_folder, "build", "Release", "build.txt")) assert contents == "fooexported_contents" client.run("export-pkg . --name=foo --version=1.0") assert "Packaged 1 '.txt' file: build.txt" in client.out @pytest.mark.parametrize("with_build_type", [True, False]) def test_basic_layout_external_sources(with_build_type): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.layout import basic_layout from conan.tools.files import save, load, copy class Pkg(ConanFile): settings = "os", "compiler", "arch"{} exports_sources = "exported.txt" def layout(self): basic_layout(self, src_folder="src") def generate(self): save(self, "generate.txt", "generate") def source(self): save(self, "source.txt", "foo") def build(self): c1 = load(self, os.path.join(self.source_folder, "source.txt")) c2 = load(self, os.path.join(self.source_folder, "..", "exported.txt")) save(self, "build.txt", c1 + c2) def package(self): copy(self, "build.txt", self.build_folder, os.path.join(self.package_folder, "res")) """) if with_build_type: conanfile = conanfile.format(', "build_type"') else: conanfile = conanfile.format("") client = TestClient() client.save({"conanfile.py": conanfile, "exported.txt": "exported_contents"}) client.run("create . --name=foo --version=1.0 -s os=Linux") assert "Packaged 1 '.txt' file: build.txt" in client.out # Local flow build_folder = "build-release" if with_build_type else "build" client.run("install . --name=foo --version=1.0 -s os=Linux") assert os.path.exists(os.path.join(client.current_folder, build_folder, "conan", "generate.txt")) client.run("source .") assert os.path.exists(os.path.join(client.current_folder, "src", "source.txt")) client.run("build .") contents = load(os.path.join(client.current_folder, build_folder, "build.txt")) assert contents == "fooexported_contents" client.run("export-pkg . --name=foo --version=1.0") assert "Packaged 1 '.txt' file: build.txt" in client.out @pytest.mark.parametrize("with_build_type", [True, False]) def test_basic_layout_no_external_sources(with_build_type): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.layout import basic_layout from conan.tools.files import save, load, copy class Pkg(ConanFile): settings = "os", "compiler", "arch"{} exports_sources = "exported.txt" def layout(self): basic_layout(self) def generate(self): save(self, "generate.txt", "generate") def build(self): contents = load(self, os.path.join(self.source_folder, "exported.txt")) save(self, "build.txt", contents) def package(self): copy(self, "build.txt", self.build_folder, os.path.join(self.package_folder, "res")) """) if with_build_type: conanfile = conanfile.format(', "build_type"') else: conanfile = conanfile.format("") client = TestClient() client.save({"conanfile.py": conanfile, "exported.txt": "exported_contents"}) client.run("create . --name=foo --version=1.0 -s os=Linux") assert "Packaged 1 '.txt' file: build.txt" in client.out # Local flow client.run("install . --name=foo --version=1.0 -s os=Linux") build_folder = "build-release" if with_build_type else "build" assert os.path.exists(os.path.join(client.current_folder, build_folder, "conan", "generate.txt")) client.run("build .") contents = load(os.path.join(client.current_folder, build_folder, "build.txt")) assert contents == "exported_contents" client.run("export-pkg . --name=foo --version=1.0") assert "Packaged 1 '.txt' file: build.txt" in client.out def test_cmake_layout_custom_build_folder(): # https://github.com/conan-io/conan/issues/11838 conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): settings = "os", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self, src_folder="src", build_folder="mybuild") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("install .") assert os.path.exists(os.path.join(client.current_folder, "mybuild/Release/generators/conan_toolchain.cmake")) ================================================ FILE: test/functional/layout/test_editable_cmake.py ================================================ import os import platform import pytest from conan.test.utils.mocks import ConanFileMock from conan.tools.env.environment import environment_wrap_command from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient def editable_cmake(generator, build_folder=None): c = TestClient() if generator is not None: c.save_home({"global.conf": "tools.cmake.cmaketoolchain:generator={}".format(generator)}) with c.chdir("dep"): c.run("new cmake_lib -d name=dep -d version=0.1") with c.chdir("pkg"): c.run("new cmake_exe -d name=pkg -d version=0.1 -d requires=dep/0.1") output_folder = '--output-folder="{}"'.format(build_folder) if build_folder else "" def build_dep(): c.run("build . {}".format(output_folder)) c.run("build . -s build_type=Debug {}".format(output_folder)) with c.chdir("dep"): c.run("editable add . {}".format(output_folder)) build_dep() def build_pkg(msg, build_editable=False): build_editable = "--build=editable" if build_editable else "" c.run(f"build . {build_editable}") folder = os.path.join("build", "Release") c.run_command(os.sep.join([".", folder, "pkg"])) assert "pkg/0.1: Hello World Release!" in c.out assert "{}: Hello World Release!".format(msg) in c.out c.run(f"build . -s build_type=Debug {build_editable}") folder = os.path.join("build", "Debug") c.run_command(os.sep.join([".", folder, "pkg"])) assert "pkg/0.1: Hello World Debug!" in c.out assert "{}: Hello World Debug!".format(msg) in c.out with c.chdir("pkg"): c.run("install . ") c.run("install . -s build_type=Debug") build_pkg("dep/0.1") # Do a source change in the editable! with c.chdir("dep"): f = c.load("src/dep.cpp") f = f.replace("dep/0.1", "SUPERDEP/0.1") c.save({"src/dep.cpp": f}) build_dep() with c.chdir("pkg"): build_pkg("SUPERDEP/0.1") # Do a source change in the editable, but do NOT build yet with c.chdir("dep"): f = c.load("src/dep.cpp") f = f.replace("SUPERDEP/0.1", "MEGADEP/0.1") c.save({"src/dep.cpp": f}) # And build with --build=editable with c.chdir("pkg"): build_pkg("MEGADEP/0.1", build_editable=True) # Check that create is still possible c.run("editable remove dep") c.run("create dep") c.run("create pkg") # print(c.out) assert "pkg/0.1: Created package" in c.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") @pytest.mark.parametrize("generator", [None, "MinGW Makefiles"]) @pytest.mark.tool("mingw64") def test_editable_cmake_windows(generator): editable_cmake(generator) @pytest.mark.tool("cmake") def test_editable_cmake_windows_folders(): build_folder = temp_folder() editable_cmake(generator=None, build_folder=build_folder) @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux") @pytest.mark.parametrize("generator", [None, "Ninja", "Ninja Multi-Config"]) @pytest.mark.tool("cmake", "3.19") def test_editable_cmake_linux(generator): editable_cmake(generator) @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires Macos") @pytest.mark.parametrize("generator", [None, "Ninja", "Xcode"]) @pytest.mark.tool("cmake", "3.23") def test_editable_cmake_osx(generator): editable_cmake(generator) def editable_cmake_exe(generator): c = TestClient() if generator is not None: c.save_home({"global.conf": "tools.cmake.cmaketoolchain:generator={}".format(generator)}) c.run("new cmake_exe -d name=dep -d version=0.1 -o=dep") def build_dep(): c.run("build .") c.run("build . -s build_type=Debug") with c.chdir("dep"): c.run("editable add .") build_dep() def run_pkg(msg): host_arch = c.get_default_host_profile().settings['arch'] cmd_release = environment_wrap_command(ConanFileMock(), f"conanrunenv-release-{host_arch}", c.current_folder, "dep") c.run_command(cmd_release) assert "{}: Hello World Release!".format(msg) in c.out cmd_release = environment_wrap_command(ConanFileMock(), f"conanrunenv-debug-{host_arch}", c.current_folder, "dep") c.run_command(cmd_release) assert "{}: Hello World Debug!".format(msg) in c.out with c.chdir("pkg"): c.run("install --requires=dep/0.1") c.run("install --requires=dep/0.1 -s build_type=Debug") run_pkg("dep/0.1") # Do a source change in the editable! with c.chdir("dep"): dep = c.load("src/dep.cpp") dep = dep.replace("dep/0.1", "SUPERDEP/0.1") c.save({"src/dep.cpp": dep}) build_dep() with c.chdir("pkg"): run_pkg("SUPERDEP/0.1") @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") @pytest.mark.parametrize("generator", [None, "MinGW Makefiles"]) @pytest.mark.tool("mingw64") def test_editable_cmake_windows_exe(generator): editable_cmake_exe(generator) @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux") @pytest.mark.parametrize("generator", [None, "Ninja", "Ninja Multi-Config"]) @pytest.mark.tool("cmake", "3.19") def test_editable_cmake_linux_exe(generator): editable_cmake_exe(generator) @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires Macos") @pytest.mark.parametrize("generator", [None, "Ninja", "Xcode"]) @pytest.mark.tool("cmake", "3.23") def test_editable_cmake_osx_exe(generator): editable_cmake_exe(generator) ================================================ FILE: test/functional/layout/test_editable_cmake_components.py ================================================ import os import shutil import textwrap import pytest from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") def test_editable_cmake_components(): hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello", includes=["hello"]) bye_h = gen_function_h(name="bye") bye_cpp = gen_function_cpp(name="bye", includes=["bye"]) conanfile_greetings = textwrap.dedent(""" from os.path import join from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.files import copy class GreetingsConan(ConanFile): name = "greetings" version = "0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "src/*" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self, src_folder="src") self.cpp.source.components["hello"].includedirs = ["."] self.cpp.source.components["bye"].includedirs = ["."] bt = "." if self.settings.os != "Windows" else str(self.settings.build_type) self.cpp.build.components["hello"].libdirs = [bt] self.cpp.build.components["bye"].libdirs = [bt] def package(self): copy(self, "*.h", src=self.source_folder, dst=join(self.package_folder, "include")) copy(self, "*.lib", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) def package_info(self): self.cpp_info.components["hello"].libs = ["hello"] self.cpp_info.components["bye"].libs = ["bye"] self.cpp_info.set_property("cmake_file_name", "MYG") self.cpp_info.set_property("cmake_target_name", "MyGreetings::MyGreetings") self.cpp_info.components["hello"].set_property("cmake_target_name", "MyGreetings::MyHello") self.cpp_info.components["bye"].set_property("cmake_target_name", "MyGreetings::MyBye") """) cmakelists_greetings = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.0) project(greetings CXX) add_library(hello hello.cpp) target_include_directories(hello PRIVATE hello) add_library(bye bye.cpp) target_include_directories(bye PRIVATE bye) """) app_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class GreetingsTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires("greetings/0.1") def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.path.join(self.cpp.build.bindirs[0], "example")) self.run(os.path.join(self.cpp.build.bindirs[0], "example2")) """) app_cpp = gen_function_cpp(name="main", includes=["hello/hello", "bye/bye"], calls=["hello", "bye"]) app_cpp2 = gen_function_cpp(name="main", includes=["hello/hello"], calls=["hello"]) app_cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.0) project(PackageTest CXX) find_package(MYG) add_executable(example example.cpp) target_link_libraries(example MyGreetings::MyGreetings) add_executable(example2 example2.cpp) target_link_libraries(example2 MyGreetings::MyHello) """) client = TestClient() client.save({"greetings/conanfile.py": conanfile_greetings, "greetings/src/CMakeLists.txt": cmakelists_greetings, "greetings/src/hello/hello.h": hello_h, "greetings/src/hello.cpp": hello_cpp, "greetings/src/bye/bye.h": bye_h, "greetings/src/bye.cpp": bye_cpp, "app/conanfile.py": app_conanfile, "app/example.cpp": app_cpp, "app/example2.cpp": app_cpp2, "app/CMakeLists.txt": app_cmakelists}) client.run("create greetings") client.run("build app") assert "hello: Release!" in client.out assert "bye: Release!" in client.out client.run("remove * -c") shutil.rmtree(os.path.join(client.current_folder, "app", "build")) client.run("editable add greetings") client.run("build greetings") client.run("build app") assert str(client.out).count("hello: Release!") == 2 assert str(client.out).count("bye: Release!") == 1 # Do a modification to one component, to verify it is used correctly bye_cpp = gen_function_cpp(name="bye", includes=["bye"], msg="Adios") client.save({"greetings/src/bye.cpp": bye_cpp}) client.run("build greetings") client.run("build app") assert str(client.out).count("hello: Release!") == 2 assert str(client.out).count("Adios: Release!") == 1 ================================================ FILE: test/functional/layout/test_editable_msbuild.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") def test_editable_msbuild(): c = TestClient() c.run("new msbuild_lib -d name=dep -d version=0.1 -o dep") c.run("new cmake_exe -d name=pkg -d version=0.1 -d requires=dep/0.1 -o pkg") build_folder = "" output_folder = '--output-folder="{}"'.format(build_folder) if build_folder else "" build_folder = '--build-folder="{}"'.format(build_folder) if build_folder else "" def build_dep(): c.run("install . {}".format(output_folder)) c.run("build . {}".format(build_folder)) c.run("install . -s build_type=Debug {}".format(output_folder)) c.run("build . -s build_type=Debug {}".format(build_folder)) with c.chdir("dep"): c.run("editable add . {}".format(output_folder)) build_dep() def build_pkg(msg): c.run("build . ") folder = os.path.join("build", "Release") c.run_command(os.sep.join([".", folder, "pkg"])) assert "pkg/0.1: Hello World Release!" in c.out assert "{}: Hello World Release!".format(msg) in c.out c.run("build . -s build_type=Debug") folder = os.path.join("build", "Debug") c.run_command(os.sep.join([".", folder, "pkg"])) assert "pkg/0.1: Hello World Debug!" in c.out assert "{}: Hello World Debug!".format(msg) in c.out with c.chdir("pkg"): c.run("install . ") c.run("install . -s build_type=Debug") build_pkg("dep/0.1") # Do a source change in the editable! with c.chdir("dep"): dep_cpp = c.load("src/dep.cpp") dep_cpp = dep_cpp.replace("dep/", "SUPERDEP/") c.save({"src/dep.cpp": dep_cpp}) build_dep() with c.chdir("pkg"): build_pkg("SUPERDEP/0.1") # Check that create is still possible c.run("editable remove dep") c.run("create dep") c.run("create pkg") # print(c.out) assert "pkg/0.1: Created package" in c.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_editable_msbuilddeps(): # https://github.com/conan-io/conan/issues/12521 c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): pass """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type", "arch") .with_requires("dep/0.1") .with_generator("MSBuildDeps")}) c.run("editable add dep") c.run("install pkg") # It doesn't crash anymore assert "dep/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709 - Editable" in c.out ================================================ FILE: test/functional/layout/test_exports_sources.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_exports_sources_patch(): """ tests that using ``self.export_sources_folder`` we can access both from the source() and build() methods the folder where the exported sources (patches, new build files) are. And maintain a local flow without exports copies """ c = TestClient() conanfile = textwrap.dedent(""" import os, shutil from conan import ConanFile from conan.tools.files import load, copy, save class Pkg(ConanFile): name = "pkg" version = "0.1" exports_sources = "CMakeLists.txt", "patches*" def layout(self): self.folders.source = "src" def source(self): save(self, "CMakeLists.txt", "old, bad") # EMULATE A DOWNLOAD! base_source = self.export_sources_folder mypatch = load(self, os.path.join(base_source, "patches/mypatch")) self.output.info("MYPATCH-SOURCE {}".format(mypatch)) shutil.copy(os.path.join(base_source, "CMakeLists.txt"), "CMakeLists.txt") def build(self): path = os.path.join(self.source_folder, "CMakeLists.txt") cmake = load(self, path) self.output.info("MYCMAKE-BUILD: {}".format(cmake)) path = os.path.join(self.export_sources_folder, "patches/mypatch") cmake = load(self, path) self.output.info("MYPATCH-BUILD: {}".format(cmake)) """) c.save({"conanfile.py": conanfile, "patches/mypatch": "mypatch!", "CMakeLists.txt": "mycmake!"}) c.run("create .") assert "pkg/0.1: MYPATCH-SOURCE mypatch!" in c.out assert "pkg/0.1: MYCMAKE-BUILD: mycmake!" in c.out assert "pkg/0.1: MYPATCH-BUILD: mypatch!" in c.out # Local flow c.run("install .") c.run("source .") assert "conanfile.py (pkg/0.1): MYPATCH-SOURCE mypatch!" in c.out assert c.load("CMakeLists.txt") == "mycmake!" # My original one assert c.load("src/CMakeLists.txt") == "mycmake!" # The one patched by "source()" c.run("build .") assert "conanfile.py (pkg/0.1): MYCMAKE-BUILD: mycmake!" in c.out assert "conanfile.py (pkg/0.1): MYPATCH-BUILD: mypatch!" in c.out ================================================ FILE: test/functional/layout/test_in_cache.py ================================================ import os import textwrap import pytest from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID @pytest.fixture def conanfile(): conan_file = str(GenConanfile() .with_import("import os") .with_import("from conan.tools.files import copy, save"). with_require("base/1.0")) conan_file += """ no_copy_sources = True def layout(self): self.folders.source = "my_sources" self.folders.build = "my_build" def source(self): self.output.warning("Source folder: {}".format(self.source_folder)) # The layout describes where the sources are, not force them to be there save(self, "source.h", "foo") def build(self): self.output.warning("Build folder: {}".format(self.build_folder)) save(self, "build.lib", "bar") def package(self): self.output.warning("Package folder: {}".format(self.package_folder)) save(self, os.path.join(self.package_folder, "LICENSE"), "bar") copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include")) copy(self, "*.lib", self.build_folder, os.path.join(self.package_folder, "lib")) def package_info(self): # This will be easier when the layout declares also the includedirs etc self.cpp_info.includedirs = ["include"] self.cpp_info.libdirs = ["lib"] """ return conan_file def test_create_test_package_no_layout(): """The test package using the new generators work (having the generated files in the build folder)""" client = TestClient() conanfile_test = textwrap.dedent(""" import os from conan import ConanFile, tools class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): assert os.path.exists("conan_toolchain.cmake") self.output.warning("hey! building") self.output.warning(os.getcwd()) def test(self): self.output.warning("hey! testing") """) client.save({"conanfile.py": GenConanfile(), "test_package/conanfile.py": conanfile_test}) client.run("create . --name=lib --version=1.0") assert "hey! building" in client.out assert "hey! testing" in client.out def test_create_test_package_with_layout(): """The test package using the new generators work (having the generated files in the build folder)""" client = TestClient() conanfile_test = textwrap.dedent(""" import os from conan import ConanFile, tools from conan.tools.cmake import CMakeToolchain, CMake, CMakeDeps class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def layout(self): self.folders.generators = "my_generators" def build(self): assert os.path.exists("my_generators/conan_toolchain.cmake") self.output.warning("hey! building") self.output.warning(os.getcwd()) def test(self): self.output.warning("hey! testing") """) client.save({"conanfile.py": GenConanfile(), "test_package/conanfile.py": conanfile_test}) client.run("create . --name=lib --version=1.0") assert "hey! building" in client.out assert "hey! testing" in client.out def test_cache_in_layout(conanfile): """The layout in the cache is used too, always relative to the "base" folders that the cache requires. But by the default, the "package" is not followed """ client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=base --version=1.0") client.save({"conanfile.py": conanfile}) client.run("create . --name=lib --version=1.0") pkg_layout = client.created_layout() sf = client.exported_layout().source() bf = pkg_layout.build() pf = pkg_layout.package() source_folder = os.path.join(sf, "my_sources") build_folder = os.path.join(bf, "my_build") # Check folders match with the declared by the layout assert "Source folder: {}".format(source_folder) in client.out assert "Build folder: {}".format(build_folder) in client.out # Check the source folder assert os.path.exists(os.path.join(source_folder, "source.h")) # Check the build folder assert os.path.exists(os.path.join(build_folder, "build.lib")) # Check the conaninfo assert os.path.exists(os.path.join(pf, "conaninfo.txt")) def test_same_conanfile_local(conanfile): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=base --version=1.0") client.save({"conanfile.py": conanfile}) source_folder = os.path.join(client.current_folder, "my_sources") build_folder = os.path.join(client.current_folder, "install", "my_build") client.run("install . --name=lib --version=1.0 -of=install") client.run("source .") assert "Source folder: {}".format(source_folder) in client.out assert os.path.exists(os.path.join(source_folder, "source.h")) client.run("build . -of=install") assert "Build folder: {}".format(build_folder) in client.out assert os.path.exists(os.path.join(build_folder, "build.lib")) def test_cpp_package(): client = TestClient() conan_hello = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): def package(self): save(self, os.path.join(self.package_folder, "foo/include/foo.h"), "") save(self, os.path.join(self.package_folder,"foo/libs/foo.lib"), "") def layout(self): self.cpp.package.includedirs = ["foo/include"] self.cpp.package.libdirs = ["foo/libs"] self.cpp.package.libs = ["foo"] """) client.save({"conanfile.py": conan_hello}) client.run("create . --name=hello --version=1.0") rrev = client.exported_recipe_revision() ref = RecipeReference.loads("hello/1.0") ref.revision = rrev pref = PkgReference(ref, NO_SETTINGS_PACKAGE_ID) package_folder = client.get_latest_pkg_layout(pref).package().replace("\\", "/") + "/" conan_consumer = textwrap.dedent(""" from conan import ConanFile class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "hello/1.0" generators = "CMakeDeps" def generate(self): info = self.dependencies["hello"].cpp_info self.output.warning("**includedirs:{}**".format(info.includedirs)) self.output.warning("**libdirs:{}**".format(info.libdirs)) self.output.warning("**libs:{}**".format(info.libs)) """) client.save({"conanfile.py": conan_consumer}) client.run("install .") out = str(client.out).replace(r"\\", "/").replace(package_folder, "") assert "**includedirs:['foo/include']**" in out assert "**libdirs:['foo/libs']**" in out assert "**libs:['foo']**" in out host_arch = client.get_default_host_profile().settings['arch'] cmake = client.load(f"hello-release-{host_arch}-data.cmake") assert 'set(hello_INCLUDE_DIRS_RELEASE "${hello_PACKAGE_FOLDER_RELEASE}/foo/include")' in cmake assert 'set(hello_LIB_DIRS_RELEASE "${hello_PACKAGE_FOLDER_RELEASE}/foo/libs")' in cmake assert 'set(hello_LIBS_RELEASE foo)' in cmake def test_git_clone_with_source_layout(): client = TestClient() repo = temp_folder() conanfile = textwrap.dedent(""" import os from conan import ConanFile class Pkg(ConanFile): exports_sources = "*.txt" def layout(self): self.folders.source = "src" def source(self): self.run('git clone "{}" .') """).format(repo.replace("\\", "/")) client.save({"conanfile.py": conanfile, "myfile.txt": "My file is copied"}) with client.chdir(repo): client.save({"cloned.txt": "foo"}, repo) client.init_git_repo() client.run("create . --name=hello --version=1.0") sf = client.exported_layout().source() assert os.path.exists(os.path.join(sf, "myfile.txt")) # The conanfile is cleared from the root before cloning assert not os.path.exists(os.path.join(sf, "conanfile.py")) assert not os.path.exists(os.path.join(sf, "cloned.txt")) assert os.path.exists(os.path.join(sf, "src", "cloned.txt")) assert not os.path.exists(os.path.join(sf, "src", "myfile.txt")) ================================================ FILE: test/functional/layout/test_in_subfolder.py ================================================ import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient def test_exports_sources_own_code_in_subfolder(): """ test that we can put the conanfile in a subfolder, and it can work. The key is the exports_sources() method that can do: os.path.join(self.recipe_folder, "..") And the layout: self.folders.root = ".." """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load, copy, save class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.root = ".." self.folders.source = "." self.folders.build = "build" self.folders.generators = "conangen" def generate(self): save(self, "mygen.txt", "mycontent") def export_sources(self): source_folder = os.path.join(self.recipe_folder, "..") copy(self, "*.txt", source_folder, self.export_sources_folder) def source(self): cmake = load(self, "CMakeLists.txt") self.output.info("MYCMAKE-SRC: {}".format(cmake)) def build(self): path = os.path.join(self.source_folder, "CMakeLists.txt") cmake = load(self, path) self.output.info("MYCMAKE-BUILD: {}".format(cmake)) save(self, "mylib.a", "mylib") """) c.save({"conan/conanfile.py": conanfile, "CMakeLists.txt": "mycmake!"}) c.run("create conan") assert "pkg/0.1: MYCMAKE-SRC: mycmake!" in c.out assert "pkg/0.1: MYCMAKE-BUILD: mycmake!" in c.out # Local flow c.run("install conan") assert c.load("conangen/mygen.txt") == "mycontent" # SOURCE NOT CALLED! It doesnt make sense (will fail due to local exports) c.run("build conan") assert "conanfile.py (pkg/0.1): MYCMAKE-BUILD: mycmake!" in c.out assert c.load("build/mylib.a") == "mylib" def test_exports_sources_common_code(): """ very similar to the above, but intended for a multi-package project sharing some common code """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load, copy, save class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.root = ".." self.folders.source = "pkg" self.folders.build = "build" def export_sources(self): source_folder = os.path.join(self.recipe_folder, "..") copy(self, "*.txt", source_folder, self.export_sources_folder) copy(self, "*.cmake", source_folder, self.export_sources_folder) def source(self): cmake = load(self, "CMakeLists.txt") self.output.info("MYCMAKE-SRC: {}".format(cmake)) def build(self): path = os.path.join(self.source_folder, "CMakeLists.txt") cmake = load(self, path) self.output.info("MYCMAKE-BUILD: {}".format(cmake)) path = os.path.join(self.export_sources_folder, "common", "myutils.cmake") cmake = load(self, path) self.output.info("MYUTILS-BUILD: {}".format(cmake)) """) c.save({"pkg/conanfile.py": conanfile, "pkg/CMakeLists.txt": "mycmake!", "common/myutils.cmake": "myutils!"}) c.run("create pkg") assert "pkg/0.1: MYCMAKE-SRC: mycmake!" in c.out assert "pkg/0.1: MYCMAKE-BUILD: mycmake!" in c.out assert "pkg/0.1: MYUTILS-BUILD: myutils!" in c.out # Local flow c.run("install pkg") # SOURCE NOT CALLED! It doesnt make sense (will fail due to local exports) c.run("build pkg") assert "conanfile.py (pkg/0.1): MYCMAKE-BUILD: mycmake!" in c.out assert "conanfile.py (pkg/0.1): MYUTILS-BUILD: myutils!" in c.out @pytest.mark.tool("cmake") def test_exports_sources_common_code_layout(): """ Equal to the previous test, but actually building and using cmake_layout """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout, CMake from conan.tools.files import load, copy, save class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def layout(self): self.folders.root = ".." self.folders.subproject = "pkg" cmake_layout(self) def export_sources(self): source_folder = os.path.join(self.recipe_folder, "..") copy(self, "*", source_folder, self.export_sources_folder) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.path.join(self.cpp.build.bindirs[0], "myapp")) """) cmake_include = "include(${CMAKE_CURRENT_LIST_DIR}/../common/myutils.cmake)" c.save({"pkg/conanfile.py": conanfile, "pkg/app.cpp": gen_function_cpp(name="main", includes=["../common/myheader"], preprocessor=["MYDEFINE"]), "pkg/CMakeLists.txt": gen_cmakelists(appsources=["app.cpp"], custom_content=cmake_include), "common/myutils.cmake": 'message(STATUS "MYUTILS.CMAKE!")', "common/myheader.h": '#define MYDEFINE "MYDEFINEVALUE"'}) c.run("create pkg") assert "MYUTILS.CMAKE!" in c.out assert "main: Release!" in c.out assert "MYDEFINE: MYDEFINEVALUE" in c.out # Local flow c.run("build pkg") assert "MYUTILS.CMAKE!" in c.out assert "main: Release!" in c.out assert "MYDEFINE: MYDEFINEVALUE" in c.out c.run("build pkg -s build_type=Debug") assert "MYUTILS.CMAKE!" in c.out assert "main: Debug!" in c.out assert "MYDEFINE: MYDEFINEVALUE" in c.out ================================================ FILE: test/functional/layout/test_local_commands.py ================================================ import os import platform import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_local_static_generators_folder(): """If we configure a generators folder in the layout, the generator files: - If belong to new generators: go to the specified folder: "my_generators" - If belong to old generators or txt: remains in the install folder """ client = TestClient() conan_file = str(GenConanfile().with_settings("build_type")) conan_file += """ generators = "CMakeToolchain" def layout(self): self.folders.build = "build-{}".format(self.settings.build_type) self.folders.generators = "{}/generators".format(self.folders.build) """ client.save({"conanfile.py": conan_file}) client.run("install . -of=my_install") old_install_folder = os.path.join(client.current_folder, "my_install") cmake_toolchain_generator_path = os.path.join(old_install_folder, "conan_toolchain.cmake") assert not os.path.exists(cmake_toolchain_generator_path) build_folder = os.path.join(client.current_folder, "my_install", "build-Release") generators_folder = os.path.join(build_folder, "generators") cmake_toolchain_generator_path = os.path.join(generators_folder, "conan_toolchain.cmake") assert os.path.exists(cmake_toolchain_generator_path) def test_local_dynamic_generators_folder(): """If we configure a generators folder in the layout, the generator files: - If belong to new generators: go to the specified folder: "my_generators" - "txt" and old ones always to the install folder """ client = TestClient() conan_file = str(GenConanfile().with_settings("build_type"). with_import("from conan.tools.cmake import CMakeToolchain, CMake")) conan_file += """ def generate(self): tc = CMakeToolchain(self) tc.generate() def layout(self): self.folders.build = "build-{}".format(self.settings.build_type) self.folders.generators = "{}/generators".format(self.folders.build) """ client.save({"conanfile.py": conan_file}) client.run("install . -of=my_install") old_install_folder = os.path.join(client.current_folder, "my_install") cmake_toolchain_generator_path = os.path.join(old_install_folder, "conan_toolchain.cmake") assert not os.path.exists(cmake_toolchain_generator_path) build_folder = os.path.join(client.current_folder, "my_install", "build-Release") generators_folder = os.path.join(build_folder, "generators") cmake_toolchain_generator_path = os.path.join(generators_folder, "conan_toolchain.cmake") assert os.path.exists(cmake_toolchain_generator_path) def test_no_layout_generators_folder(): """If we don't configure a generators folder in the layout, the generator files: - all go to the install_folder """ client = TestClient() conan_file = str(GenConanfile().with_settings("build_type"). with_import("from conan.tools.cmake import CMakeToolchain, CMake")) conan_file += """ def generate(self): tc = CMakeToolchain(self) tc.generate() """ client.save({"conanfile.py": conan_file}) client.run("install . -of=my_install") old_install_folder = os.path.join(client.current_folder, "my_install") cmake_toolchain_generator_path = os.path.join(old_install_folder, "conan_toolchain.cmake") # In the install_folder assert os.path.exists(cmake_toolchain_generator_path) # But not in the base folder assert not os.path.exists(os.path.join(client.current_folder, "conan_toolchain.cmake")) def test_local_build(): """If we configure a build folder in the layout, the installed files in a "conan build ." go to the specified folder: "my_build" """ client = TestClient() conan_file = str(GenConanfile().with_import("from conan.tools.files import save")) conan_file += """ def layout(self): self.folders.generators = "my_generators" self.folders.build = "my_build" def build(self): self.output.warning("Generators folder: {}".format(self.folders.generators_folder)) save(self, "build_file.dll", "bar") """ client.save({"conanfile.py": conan_file}) client.run("build . --output-folder=my_install") dll = os.path.join(client.current_folder, "my_install", "my_build", "build_file.dll") assert os.path.exists(dll) def test_local_build_change_base(): """If we configure a build folder in the layout, the build files in a "conan build ." go to the specified folder: "my_build under the modified base one "common" """ client = TestClient() conan_file = str(GenConanfile().with_import("from conan.tools.files import save")) conan_file += """ def layout(self): self.folders.build = "my_build" def build(self): save(self, "build_file.dll", "bar") """ client.save({"conanfile.py": conan_file}) client.run("install . --output-folder=common") client.run("build . --output-folder=common") dll = os.path.join(client.current_folder, "common", "my_build", "build_file.dll") assert os.path.exists(dll) def test_local_source(): """The "conan source" is NOT affected by the --output-folder """ client = TestClient() conan_file = str(GenConanfile().with_import("from conan.tools.files import save")) conan_file += """ def layout(self): self.folders.source = "my_source" def source(self): save(self, "downloaded.h", "bar") """ client.save({"conanfile.py": conan_file}) client.run("install . -of=my_install") client.run("source .") header = os.path.join(client.current_folder, "my_source", "downloaded.h") assert os.path.exists(header) def test_export_pkg(): """The export-pkg, calling the "package" method, follows the layout if `cache_package_layout` """ client = TestClient() conan_file = str(GenConanfile("lib", "1.0") .with_import("from conan.tools.files import copy, save")) conan_file += """ no_copy_source = True def layout(self): self.folders.source = "my_source" self.folders.build = "my_build" def source(self): save(self, "downloaded.h", "bar") def build(self): save(self, "library.lib", "bar") save(self, "generated.h", "bar") def package(self): copy(self, "*.h", self.source_folder, self.package_folder) copy(self, "*.h", self.build_folder, self.package_folder) copy(self, "*.lib", self.build_folder, self.package_folder) """ client.save({"conanfile.py": conan_file}) client.run("source .") client.run("build .") client.run("export-pkg .") pf = client.created_layout().package() # Check the artifacts packaged assert os.path.exists(os.path.join(pf, "downloaded.h")) assert os.path.exists(os.path.join(pf, "generated.h")) assert os.path.exists(os.path.join(pf, "library.lib")) def test_export_pkg_local(): """The export-pkg, without calling "package" method, with local package, follows the layout""" client = TestClient() conan_file = str(GenConanfile("lib", "1.0") .with_import("from conan.tools.files import copy, save")) conan_file += """ no_copy_source = True def layout(self): self.folders.source = "my_source" self.folders.build = "my_build" self.folders.package = "my_package" def source(self): save(self, "downloaded.h", "bar") def build(self): save(self, "library.lib", "bar") save(self, "generated.h", "bar") def package(self): copy(self, "*.h", self.source_folder, self.package_folder) copy(self, "*.h", self.build_folder, self.package_folder) copy(self, "*.lib", self.build_folder, self.package_folder) """ client.save({"conanfile.py": conan_file}) client.run("source .") client.run("build .") # assert "WARN: Package folder: {}".format(pf) in client.out client.run("export-pkg .") pf = client.created_layout().package() # Check the artifacts packaged, THERE IS NO "my_package" in the cache assert "my_package" not in pf assert os.path.exists(os.path.join(pf, "downloaded.h")) assert os.path.exists(os.path.join(pf, "generated.h")) assert os.path.exists(os.path.join(pf, "library.lib")) # Doing a conan create: Same as export-pkg, there is "my_package" in the cache client.run("create . ") pf = client.created_layout().package() assert "my_package" not in pf assert os.path.exists(os.path.join(pf, "downloaded.h")) assert os.path.exists(os.path.join(pf, "generated.h")) assert os.path.exists(os.path.join(pf, "library.lib")) def test_start_dir_failure(): c = TestClient() c.run("new cmake_lib -d name=dep -d version=0.1") c.run("install .") if platform.system() != "Windows": gen_folder = os.path.join(c.current_folder, "build", "Release", "generators") else: gen_folder = os.path.join(c.current_folder, "build", "generators") expected_path = os.path.join(gen_folder, "conan_toolchain.cmake") assert os.path.exists(expected_path) os.unlink(expected_path) with c.chdir("build"): c.run("install ..") assert os.path.exists(expected_path) def test_local_folders_without_layout(): """ Test that the "conan install" can report the local source and build folder """ # https://github.com/conan-io/conan/issues/10566 client = TestClient() conan_file = textwrap.dedent(""" from conan import ConanFile class Test(ConanFile): version = "1.2.3" name = "test" def layout(self): self.folders.source = "." self.folders.build = "build" def generate(self): self.output.info("generate sf: {}".format(self.source_folder)) self.output.info("generate bf: {}".format(self.build_folder)) """) client.save({"conanfile.py": conan_file}) client.run("install .") assert "conanfile.py (test/1.2.3): generate sf: {}".format(client.current_folder) in client.out assert "conanfile.py (test/1.2.3): generate bf: {}".format(client.current_folder) in client.out ================================================ FILE: test/functional/layout/test_source_folder.py ================================================ import os import platform from shutil import copy from unittest import mock import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient, zipdir app_name = "Release/my_app.exe" if platform.system() == "Windows" else "my_app" @pytest.mark.tool("cmake") @pytest.mark.parametrize("no_copy_source", ["False", "True"]) def test_exports_source_with_src_subfolder(no_copy_source): """If we have the sources in a subfolder, specifying it in the self.folders.source will work both locally (conan build) or in the cache (exporting the sources)""" conan_file = GenConanfile() \ .with_name("app").with_version("1.0") \ .with_settings("os", "arch", "build_type", "compiler") \ .with_exports_sources("my_src/*")\ .with_cmake_build()\ .with_class_attribute("no_copy_source={}".format(no_copy_source)) conan_file = str(conan_file) conan_file += """ def layout(self): self.folders.source = "my_src" self.folders.build = str(self.settings.build_type) """ cmake = gen_cmakelists(appname="my_app", appsources=["main.cpp"]) app = gen_function_cpp(name="main") client = TestClient() client.save({"conanfile.py": conan_file, "my_src/main.cpp": app, "my_src/CMakeLists.txt": cmake}) client.run("build .") assert os.path.exists(os.path.join(client.current_folder, "Release", app_name)) client.run("create . ") assert "Created package revision" in client.out def test_exports(): """If we have some sources in the root (like the CMakeLists.txt) we don't declare folders.source""" conan_file = GenConanfile() \ .with_name("app").with_version("1.0") \ .with_settings("os", "arch", "build_type", "compiler") \ .with_exports("*.py") \ .with_import("from my_tools import FOO") conan_file = str(conan_file) conan_file += """ def layout(self): self.folders.source = "my_src" def build(self): # This FOO comes from the my_tools.py self.output.warning("FOO: {}".format(FOO)) """ client = TestClient() client.save({"conanfile.py": conan_file, "my_tools.py": "FOO=1"}) client.run("build .") assert "FOO: 1" in client.out client.run("create . ") assert "FOO: 1" in client.out @pytest.mark.tool("cmake") def test_exports_source_without_subfolder(): """If we have some sources in the root (like the CMakeLists.txt) we don't declare folders.source""" conan_file = GenConanfile() \ .with_name("app").with_version("1.0") \ .with_settings("os", "arch", "build_type", "compiler") \ .with_exports_sources("CMakeLists.txt", "my_src/*")\ .with_cmake_build() conan_file = str(conan_file) conan_file += """ def layout(self): self.folders.build = str(self.settings.build_type) """ cmake = gen_cmakelists(appname="my_app", appsources=["my_src/main.cpp"]) app = gen_function_cpp(name="main") client = TestClient() client.save({"conanfile.py": conan_file, "my_src/main.cpp": app, "CMakeLists.txt": cmake}) client.run("build .") assert os.path.exists(os.path.join(client.current_folder, "Release", app_name)) client.run("create . ") assert "Created package revision" in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("no_copy_source", ["False", "True"]) def test_zip_download_with_subfolder_new_tools(no_copy_source): """If we have a zip with the sources in a subfolder, specifying it in the self.folders.source will unzip in the base and will work both locally (conan build) or in the cache (exporting the sources)""" tmp = TestClient() # Used only to save some files, sorry for the lazyness cmake = gen_cmakelists(appname="my_app", appsources=["main.cpp"]) app = gen_function_cpp(name="main") tmp.save({"subfolder/main.cpp": app, "subfolder/CMakeLists.txt": cmake, "ignored_subfolder/ignored.txt": ""}) zippath = os.path.join(tmp.current_folder, "my_sources.zip") zipdir(tmp.current_folder, zippath) conan_file = GenConanfile() \ .with_import("import os") \ .with_import("from conan.tools.files import get") \ .with_import("from conan.tools.cmake import CMake") \ .with_name("app").with_version("1.0") \ .with_settings("os", "arch", "build_type", "compiler") \ .with_generator("CMakeToolchain") \ .with_class_attribute("no_copy_source={}".format(no_copy_source)) conan_file = str(conan_file) conan_file += """ def source(self): get(self, "http://fake_url/my_sources.zip") def layout(self): self.folders.source = "src" def build(self): assert os.path.exists(os.path.join(self.source_folder, "subfolder", "CMakeLists.txt")) assert os.path.exists(os.path.join(self.source_folder, "ignored_subfolder", "ignored.txt")) cmake = CMake(self) cmake.configure(build_script_folder="subfolder") cmake.build() """ client = TestClient() client.save({"conanfile.py": conan_file}) with mock.patch("conan.tools.files.files.download") as mock_download: def download_zip(*args, **kwargs): copy(zippath, os.getcwd()) mock_download.side_effect = download_zip client.run("create . ") ================================================ FILE: test/functional/revisions_test.py ================================================ import copy import time from collections import OrderedDict import pytest from unittest.mock import patch from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.utils.env import environment_update from conan.internal.errors import RecipeNotFoundException from conan.api.model import RecipeReference from conans.server.revision_list import RevisionList from conan.test.utils.tools import TestServer, GenConanfile, TestClient from conan.internal.util.files import load def _create(c_v2, ref, conanfile=None, args=None, assert_error=False): conanfile = conanfile or GenConanfile() c_v2.save({"conanfile.py": str(conanfile)}) r = ref c_v2.run(f"create . --name {r.name} --version {r.version} " f"--user {r.user} --channel {r.channel} {args or ''}", assert_error=assert_error) if not assert_error: pref = c_v2.created_package_reference(str(ref)) return pref DepsGraphBuilder.ALLOW_ALIAS = True @pytest.mark.artifactory_ready class TestInstallingPackagesWithRevisions: @pytest.fixture(autouse=True) def setup(self): self.server = TestServer() self.server2 = TestServer() self.servers = OrderedDict([("default", self.server), ("remote2", self.server2)]) self.c_v2 = TestClient(servers=self.servers, inputs=2*["admin", "password"]) self.ref = RecipeReference.loads("lib/1.0@conan/testing") def recipe_revision(self, ref): tmp = copy.copy(ref) tmp.revision = None latest_rrev = self.c_v2.cache.get_latest_recipe_revision(tmp) return latest_rrev.revision def package_revision(self, pref): tmp = copy.copy(pref) tmp.revision = None latest_prev = self.c_v2.cache.get_latest_package_revision(tmp) return latest_prev.revision def test_install_binary_iterating_remotes_same_rrev(self): """We have two servers (remote1 and remote2), first with a recipe but the second one with a PREV of the binary. If a client installs without specifying -r remote1, it will iterate remote2 also""" conanfile = GenConanfile().with_package_file("file.txt", env_var="MY_VAR") with environment_update({"MY_VAR": "1"}): pref = _create(self.c_v2, self.ref, conanfile=conanfile) the_time = time.time() with patch.object(RevisionList, '_now', return_value=the_time): self.c_v2.run(f"upload {self.ref} -r=default -c") self.c_v2.run("remove {}#*:{} -c -r default".format(self.ref, pref.package_id)) # Same RREV, different PREV with environment_update({"MY_VAR": "2"}): pref2 = _create(self.c_v2, self.ref, conanfile=conanfile) the_time = the_time + 10.0 with patch.object(RevisionList, '_now', return_value=the_time): self.c_v2.run(f"upload {self.ref} -r=remote2 -c") self.c_v2.remove_all() assert pref.ref.revision == pref2.ref.revision self.c_v2.run("install --requires={}".format(self.ref)) self.c_v2.assert_listed_require({str(self.ref): "Downloaded (default)"}) assert "Retrieving package {} from remote 'remote2'".format(pref.package_id) in self.c_v2.out def test_diamond_revisions_conflict(self): """ If we have a diamond because of pinned conflicting revisions in the requirements, it gives an error""" # Two revisions of "lib1" to the server lib1 = RecipeReference.loads("lib1/1.0@conan/stable") lib1_pref = _create(self.c_v2, lib1) self.c_v2.run(f"upload {lib1} -r=default -c") lib1b_pref = _create(self.c_v2, lib1, conanfile=GenConanfile().with_build_msg("Rev2")) self.c_v2.run(f"upload {lib1} -r=default -c") # Lib2 depending of lib1 self.c_v2.remove_all() lib2 = RecipeReference.loads("lib2/1.0@conan/stable") _create(self.c_v2, lib2, conanfile=GenConanfile().with_requirement(lib1_pref.ref)) self.c_v2.run(f"upload {lib2} -r=default -c") # Lib3 depending of lib1b self.c_v2.remove_all() lib3 = RecipeReference.loads("lib3/1.0@conan/stable") _create(self.c_v2, lib3, conanfile=GenConanfile().with_requirement(lib1b_pref.ref)) self.c_v2.run(f"upload {lib3} -r=default -c") # Project depending on both lib3 and lib2 self.c_v2.remove_all() project = RecipeReference.loads("project/1.0@conan/stable") _create(self.c_v2, project, conanfile=GenConanfile().with_requirement(lib2).with_requirement(lib3), assert_error=True) assert "ERROR: Version conflict" in self.c_v2.out # assert "Different revisions of {} has been requested".format(lib1), self.c_v2.out) def test_alias_to_a_rrev(self): """ If an alias points to a RREV, it resolved that RREV and no other""" # Upload one revision pref = _create(self.c_v2, self.ref) self.c_v2.run(f"upload {self.ref} -r=default -c") # Upload other revision _create(self.c_v2, self.ref, conanfile=GenConanfile().with_build_msg("Build Rev 2")) self.c_v2.run(f"upload {self.ref} -r=default -c") self.c_v2.remove_all() # Create an alias to the first revision self.c_v2.alias("lib/latest@conan/stable", repr(pref.ref)) alias_ref = RecipeReference.loads("lib/latest@conan/stable") exported = load(self.c_v2.get_latest_ref_layout(alias_ref).conanfile()) assert 'alias = "{}"'.format(repr(pref.ref)) in exported self.c_v2.run(f"upload lib/latest@conan/stable -r=default -c") self.c_v2.remove_all() self.c_v2.run("install --requires=lib/(latest)@conan/stable") # Shouldn't be packages in the cache assert "doesn't belong to the installed recipe revision" not in self.c_v2.out # Read current revision assert pref.ref.revision == self.recipe_revision(self.ref) def test_revision_metadata_update_on_install(self): """If a clean v2 client installs a RREV/PREV from a server, it get the revision from upstream""" # Upload with v2 pref = _create(self.c_v2, self.ref) self.c_v2.run(f"upload {self.ref} -r=default -c") # Remove all from c_v2 local self.c_v2.remove_all() assert len(self.c_v2.cache.get_recipe_revisions(self.ref)) == 0 self.c_v2.run("install --requires={}".format(self.ref)) local_rev = self.recipe_revision(self.ref) local_prev = self.package_revision(pref) assert local_rev == pref.ref.revision assert local_prev == pref.revision def test_revision_update_on_package_update(self): """ A client v2 upload RREV with PREV1 Another client v2 upload the same RREV with PREV2 The first client can upgrade from the remote, only in the package, because the recipe is the same and it is not updated""" client = TestClient(servers={"default": self.server}, inputs=["admin", "password"]) client2 = TestClient(servers={"default": self.server}, inputs=["admin", "password"]) conanfile = GenConanfile("pkg", "0.1").with_package_file("file", env_var="MY_VAR") client.save({"conanfile.py": conanfile}) with environment_update({"MY_VAR": "1"}): client.run("create .") pref = client.created_package_reference("pkg/0.1") time.sleep(1) with patch.object(RevisionList, '_now', return_value=time.time()): client.run(f"upload * -r=default -c") client2.save({"conanfile.py": conanfile}) with environment_update({"MY_VAR": "2"}): client2.run("create .") pref2 = client2.created_package_reference("pkg/0.1") with patch.object(RevisionList, '_now', return_value=time.time() + 20.0): client2.run(f"upload * -r=default -c") prev1_time_remote = self.server.package_revision_time(pref) prev2_time_remote = self.server.package_revision_time(pref2) assert prev1_time_remote != prev2_time_remote # Two package revisions client.run("install --requires=pkg/0.1 --update") client.assert_listed_require({"pkg/0.1": "Cache (Updated date) (default)"}) assert "Retrieving package {}".format(pref.package_id) in client.out def test_revision_mismatch_packages_in_local(self): """Test adapted for the new cache: we create a revision but we export again a recipe to create a new revision, then we won't have a package for the latest recipe revision of the cache. TODO: cache2.0 check this case""" client = self.c_v2 pref = _create(self.c_v2, self.ref) ref2 = client.export(self.ref, conanfile=GenConanfile().with_build_msg("REV2")) # Now we have two RREVs and a PREV corresponding to the first one sot1 = copy.copy(pref.ref) sot1.revision = None sot2 = copy.copy(ref2) sot2.revision = None assert sot1 == sot2 assert pref.ref.revision != ref2.revision # Now we try to install the self.ref, the binary is missing when using revisions command = "install --requires={}".format(self.ref) client.run(command, assert_error=True) assert "ERROR: Missing prebuilt package for '{}'".format(self.ref) in client.out def test_revision_install_explicit_mismatch_rrev(self): # If we have a recipe in local, but we request to install a different one with RREV # It fail and won't look the remotes unless --update client = self.c_v2 ref = client.export(self.ref) command = "install --requires={}#fakerevision".format(ref) client.run(command, assert_error=True) assert "Unable to find '{}#fakerevision' in remotes".format(ref) in client.out command = "install --requires={}#fakerevision --update".format(ref) client.run(command, assert_error=True) assert "Unable to find '{}#fakerevision' in remotes".format(ref) in client.out # Now create a new revision with other client and upload it, we will request it new_client = TestClient(servers=self.servers, inputs=["admin", "password"]) new_client.save({"conanfile.py": GenConanfile("lib", "1.0").with_build_msg("Rev2")}) new_client.run("create . --user=conan --channel=testing") pref = new_client.created_package_reference(self.ref) new_client.run(f"upload * -r=default -c") # Repeat the install --update pointing to the new reference client.run("install --requires={} --update".format(repr(pref.ref))) client.assert_listed_require({str(self.ref): "Downloaded (default)"}) def test_revision_mismatch_packages_remote(self): """If we have a recipe that doesn't match a remote recipe: It is not resolved in the remote.""" _create(self.c_v2, self.ref) self.c_v2.run(f"upload {self.ref} -r=default -c") client = self.c_v2 client.remove_all() client.export(self.ref, conanfile=GenConanfile().with_build_msg("REV2")) command = "install --requires={}".format(self.ref) client.run(command, assert_error=True) assert "Can't find a '{}' package".format(self.ref) in client.out def test_revision_build_requires(self): conanfile = GenConanfile() refs = [] for _ in range(1, 4): # create different revisions conanfile.with_build_msg("any change to get another rrev") pref = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run(f"upload {pref.ref} -r=default -c") refs.append(pref.ref) assert refs.count(pref.ref) == 1 # make sure that all revisions are different client = self.c_v2 # revisions enabled client.remove_all() for ref in refs: command = "install --update --tool-require={}".format(repr(ref)) client.run(command) assert "Downloaded recipe revision {}".format(ref.revision) in client.out class TestRemoveWithRevisions: @pytest.fixture(autouse=True) def setup(self): self.server = TestServer() self.c_v2 = TestClient(servers={"default": self.server}, inputs=["admin", "password"]) self.ref = RecipeReference.loads("lib/1.0@conan/testing") def test_remove_local_recipe(self): """Locally: When I remove a recipe with RREV only if the local revision matches is removed""" client = self.c_v2 # If I remove the ref, the revision is gone, of course ref1 = client.export(self.ref) ref1.revision = None client.run("remove {} -c".format(repr(ref1))) assert not client.recipe_exists(self.ref) # If I remove a ref with a wrong revision, the revision is not removed ref1 = client.export(self.ref) fakeref = copy.copy(ref1) fakeref.revision = "fakerev" full_ref = repr(fakeref) client.run("remove {} -c".format(repr(fakeref)), assert_error=True) assert f"ERROR: Recipe revision '{full_ref}' not found" in client.out assert client.recipe_exists(self.ref) def test_remove_local_package(self): """Locally: When I remove a recipe without RREV, the package is removed. When I remove a recipe with RREV only if the local revision matches is removed When I remove a package with PREV and not RREV it raises an error When I remove a package with RREV and PREV only when both matches is removed""" client = self.c_v2 # If I remove the ref without RREV, the packages are also removed pref1 = _create(self.c_v2, self.ref) tmp = copy.copy(pref1.ref) tmp.revision = None client.run("remove {} -c".format(repr(tmp))) assert not client.package_exists(pref1) # If I remove the ref with fake RREV, the packages are not removed pref1 = _create(self.c_v2, self.ref) fakeref = copy.copy(pref1.ref) fakeref.revision = "fakerev" str_ref = repr(fakeref) client.run("remove {} -c".format(repr(fakeref)), assert_error=True) assert client.package_exists(pref1) assert "Recipe revision '{}' not found".format(str_ref) in client.out # If I remove the ref with valid RREV, the packages are removed pref1 = _create(self.c_v2, self.ref) client.run("remove {} -c".format(repr(pref1.ref))) assert not client.package_exists(pref1) # If I remove the ref without RREV but specifying PREV it raises pref1 = _create(self.c_v2, self.ref) tmp = copy.copy(pref1.ref) tmp.revision = None command = "remove {}:{}#{} -c".format(repr(tmp), pref1.package_id, pref1.revision) client.run(command) assert not client.package_exists(pref1) # A wrong PREV doesn't remove the PREV pref1 = _create(self.c_v2, self.ref) command = "remove {}:{}#fakeprev -c".format(repr(pref1.ref), pref1.package_id) client.run(command, assert_error=True) assert client.package_exists(pref1) assert "ERROR: Package revision" in client.out # Everything correct, removes the unique local package revision pref1 = _create(self.c_v2, self.ref) command = "remove {}:{}#{} -c".format(repr(pref1.ref), pref1.package_id, pref1.revision) client.run(command) assert not client.package_exists(pref1) def test_remove_remote_recipe(self): """When a client removes a reference, it removes ALL revisions, no matter if the client is v1 or v2""" pref1 = _create(self.c_v2, self.ref) self.c_v2.run(f"upload {pref1.ref} -r=default -c") pref2 = _create(self.c_v2, self.ref, conanfile=GenConanfile().with_build_msg("RREV 2!")) self.c_v2.run(f"upload {pref2.ref} -r=default -c") assert pref1 != pref2 remover_client = self.c_v2 # Remove ref without revision in a remote remover_client.run("remove {} -c -r default".format(self.ref)) assert not self.server.recipe_exists(self.ref) assert not self.server.recipe_exists(pref1.ref) assert not self.server.recipe_exists(pref2.ref) assert not self.server.package_exists(pref1) assert not self.server.package_exists(pref2) def test_remove_remote_recipe_revision(self): """If a client removes a recipe with revision: - If the client is v2 will remove only that revision""" pref1 = _create(self.c_v2, self.ref) self.c_v2.run(f"upload {pref1.ref} -r=default -c") pref2 = _create(self.c_v2, self.ref, conanfile=GenConanfile().with_build_msg("RREV 2!")) self.c_v2.run(f"upload {pref2.ref} -r=default -c") assert pref1 != pref2 remover_client = self.c_v2 # Remove ref without revision in a remote command = "remove {} -c -r default".format(repr(pref1.ref)) remover_client.run(command) assert not self.server.recipe_exists(pref1.ref) assert self.server.recipe_exists(pref2.ref) def test_remove_remote_package(self): """When a client removes a package, without RREV, it removes the package from ALL RREVs""" pref1 = _create(self.c_v2, self.ref) self.c_v2.run(f"upload {pref1.ref} -r=default -c") pref2 = _create(self.c_v2, self.ref, conanfile=GenConanfile().with_build_msg("RREV 2!")) self.c_v2.run(f"upload {pref2.ref} -r=default -c") assert pref1.package_id == pref2.package_id # Both revisions exist in 2.0 cache assert self.c_v2.package_exists(pref1) assert self.c_v2.package_exists(pref2) remover_client = self.c_v2 # Remove pref without RREV in a remote remover_client.run("remove {}#*:{} -c -r default".format(self.ref, pref2.package_id)) assert self.server.recipe_exists(pref1.ref) assert self.server.recipe_exists(pref2.ref) assert not self.server.package_exists(pref1) assert not self.server.package_exists(pref2) def test_remove_remote_package_revision(self): """When a client removes a package with PREV (conan remove zlib/1.0@conan/stable:12312#PREV) - If not RREV, the client fails - If RREV and PREV: - If v1 it fails in the client (cannot transmit revisions with v1) - If v2 it removes only that PREV """ # First RREV pref1 = _create(self.c_v2, self.ref) self.c_v2.run(f"upload {pref1.ref} -r=default -c") # Second RREV with two PREVS (exactly same conanfile, different package files) rev2_conanfile = GenConanfile().with_build_msg("RREV 2!")\ .with_package_file("file", env_var="MY_VAR") with environment_update({"MY_VAR": "1"}): pref2 = _create(self.c_v2, self.ref, conanfile=rev2_conanfile) self.c_v2.run(f"upload {pref2.ref} -r=default -c") with environment_update({"MY_VAR": "2"}): pref2b = _create(self.c_v2, self.ref, conanfile=rev2_conanfile) self.c_v2.run(f"upload {pref2b.ref} -r=default -c") # Check created revisions assert pref1.package_id == pref2.package_id assert pref2.package_id == pref2b.package_id assert pref2.ref.revision == pref2b.ref.revision assert pref2.revision != pref2b.revision remover_client = self.c_v2 # Remove PREV without RREV in a remote, the client has to fail command = "remove {}:{}#{} -c -r default".format(self.ref, pref2.package_id, pref2.revision) remover_client.run(command) assert self.server.recipe_exists(pref1.ref) assert self.server.recipe_exists(pref2.ref) assert self.server.recipe_exists(pref2b.ref) assert self.server.package_exists(pref1) assert self.server.package_exists(pref2b) assert not self.server.package_exists(pref2) # Try to remove a missing revision command = "remove {}:{}#fakerev -c -r default".format(repr(pref2.ref), pref2.package_id) remover_client.run(command, assert_error=True) fakeref = copy.copy(pref2) fakeref.revision = "fakerev" assert f"ERROR: Package revision '{fakeref.repr_notime()}' not found" in remover_client.out class TestUploadPackagesWithRevisions: @pytest.fixture(autouse=True) def setup(self): self.server = TestServer() self.c_v2 = TestClient(servers={"default": self.server}, inputs=["admin", "password"]) self.ref = RecipeReference.loads("lib/1.0@conan/testing") def test_upload_a_recipe(self): """If we upload a package to a server: Using v2 client it will upload RREV revision to the server. The rev time is NOT updated. """ client = self.c_v2 pref = _create(self.c_v2, self.ref) client.run(f"upload {self.ref} -r=default -c") revs = [r.revision for r in self.server.server_store.get_recipe_revisions_references(self.ref)] assert revs == [pref.ref.revision] def test_upload_no_overwrite_recipes(self): """If we upload a RREV to the server and create a new RREV in the client, when we upload with --no-overwrite Using v2 client it will warn an upload a new revision. """ client = self.c_v2 pref = _create(self.c_v2, self.ref, conanfile=GenConanfile().with_setting("os"), args=" -s os=Windows") client.run(f"upload {self.ref} -r=default -c") pref2 = _create(self.c_v2, self.ref, conanfile=GenConanfile().with_setting("os").with_build_msg("rev2"), args=" -s os=Linux") assert self.server.server_store.get_last_revision(self.ref)[0] == pref.ref.revision client.run(f"upload {self.ref} -r=default -c") assert self.server.server_store.get_last_revision(self.ref)[0] == pref2.ref.revision def test_upload_no_overwrite_packages(self): """If we upload a PREV to the server and create a new PREV in the client, when we upload with --no-overwrite Using v2 client it will warn and upload a new revision. """ client = self.c_v2 conanfile = GenConanfile().with_package_file("file", env_var="MY_VAR") with environment_update({"MY_VAR": "1"}): pref = _create(self.c_v2, self.ref, conanfile=conanfile) client.run(f"upload {self.ref} -r=default -c") with environment_update({"MY_VAR": "2"}): pref2 = _create(self.c_v2, self.ref, conanfile=conanfile) assert pref.revision != pref2.revision assert self.server.server_store.get_last_package_revision(pref2).revision == pref.revision client.run(f"upload {self.ref} -r=default -c") assert self.server.server_store.get_last_package_revision(pref2).revision == pref2.revision def test_server_with_only_v2_capability(): server = TestServer(server_capabilities=[]) c_v2 = TestClient(servers={"default": server}, inputs=["admin", "password"]) c_v2.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c_v2.run("create .") c_v2.run(f"upload * -r=default -c") class TestServerRevisionsIndexes: @pytest.fixture(autouse=True) def setup(self): self.server = TestServer() self.c_v2 = TestClient(servers={"default": self.server}, inputs=["admin", "password"]) self.ref = RecipeReference.loads("lib/1.0@conan/testing") def test_rotation_deleting_recipe_revisions(self): """ - If we have two RREVs in the server and we remove the first one, the last one is the latest - If we have two RREvs in the server and we remove the second one, the first is now the latest """ ref1 = self.c_v2.export(self.ref, conanfile=GenConanfile()) self.c_v2.run(f"upload {ref1} -r=default -c") assert self.server.server_store.get_last_revision(self.ref).revision == ref1.revision ref2 = self.c_v2.export(self.ref, conanfile=GenConanfile().with_build_msg("I'm rev2")) self.c_v2.run(f"upload {ref2} -r=default -c") assert self.server.server_store.get_last_revision(self.ref).revision == ref2.revision ref3 = self.c_v2.export(self.ref, conanfile=GenConanfile().with_build_msg("I'm rev3")) self.c_v2.run(f"upload {ref2} -r=default -c") assert self.server.server_store.get_last_revision(self.ref).revision == ref3.revision revs = [r.revision for r in self.server.server_store.get_recipe_revisions_references(self.ref)] assert revs == [ref3.revision, ref2.revision, ref1.revision] assert self.server.server_store.get_last_revision(self.ref).revision == ref3.revision # Delete the latest from the server self.c_v2.run("remove {} -r default -c".format(repr(ref3))) revs = [r.revision for r in self.server.server_store.get_recipe_revisions_references(self.ref)] assert revs == [ref2.revision, ref1.revision] assert self.server.server_store.get_last_revision(self.ref).revision == ref2.revision def test_rotation_deleting_package_revisions(self): """ - If we have two PREVs in the server and we remove the first one, the last one is the latest - If we have two PREVs in the server and we remove the second one, the first is now the latest """ conanfile = GenConanfile().with_package_file("file", env_var="MY_VAR") with environment_update({"MY_VAR": "1"}): pref1 = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run("upload * -r=default -c") assert self.server.server_store.get_last_package_revision(pref1).revision == pref1.revision with environment_update({"MY_VAR": "2"}): pref2 = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run("upload * -r=default -c") assert self.server.server_store.get_last_package_revision(pref1).revision == pref2.revision with environment_update({"MY_VAR": "3"}): pref3 = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run("upload * -r=default -c") assert self.server.server_store.get_last_package_revision(pref1).revision == pref3.revision assert pref1.ref.revision == pref2.ref.revision assert pref2.ref.revision == pref3.ref.revision assert pref3.ref.revision == pref3.ref.revision pref = copy.copy(pref1) pref.revision = None revs = [r.revision for r in self.server.server_store.get_package_revisions_references(pref)] assert revs == [pref3.revision, pref2.revision, pref1.revision] assert self.server.server_store.get_last_package_revision(pref).revision == pref3.revision # Delete the latest from the server self.c_v2.run("remove {}:{}#{} -r default -c".format(repr(pref3.ref), pref3.package_id, pref3.revision)) revs = [r.revision for r in self.server.server_store.get_package_revisions_references(pref)] assert revs == [pref2.revision, pref1.revision] assert self.server.server_store.get_last_package_revision(pref).revision == pref2.revision def test_deleting_all_rrevs(self): """ If we delete all the recipe revisions in the server. There is no latest. If then a client uploads a RREV it is the latest """ ref1 = self.c_v2.export(self.ref, conanfile=GenConanfile()) self.c_v2.run("upload * -r=default -c") ref2 = self.c_v2.export(self.ref, conanfile=GenConanfile().with_build_msg("I'm rev2")) self.c_v2.run("upload * -r=default -c") ref3 = self.c_v2.export(self.ref, conanfile=GenConanfile().with_build_msg("I'm rev3")) self.c_v2.run("upload * -r=default -c") self.c_v2.run("remove {} -r default -c".format(repr(ref1))) self.c_v2.run("remove {} -r default -c".format(repr(ref2))) self.c_v2.run("remove {} -r default -c".format(repr(ref3))) with pytest.raises(RecipeNotFoundException): self.server.server_store.get_recipe_revisions_references(self.ref) ref4 = self.c_v2.export(self.ref, conanfile=GenConanfile().with_build_msg("I'm rev4")) self.c_v2.run("upload * -r=default -c") revs = [r.revision for r in self.server.server_store.get_recipe_revisions_references(self.ref)] assert revs == [ref4.revision] def test_deleting_all_prevs(self): """ If we delete all the package revisions in the server. There is no latest. If then a client uploads a RREV/PREV it is the latest """ conanfile = GenConanfile().with_package_file("file", env_var="MY_VAR") with environment_update({"MY_VAR": "1"}): pref1 = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run(f"upload {self.ref} -r=default -c") with environment_update({"MY_VAR": "2"}): pref2 = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run(f"upload {self.ref} -r=default -c") with environment_update({"MY_VAR": "3"}): pref3 = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run(f"upload {self.ref} -r=default -c") # Delete the package revisions (all of them have the same ref#rev and id) command = "remove {}:{}#{{}} -r default -c".format(pref3.ref.repr_notime(), pref3.package_id) self.c_v2.run(command.format(pref3.revision)) self.c_v2.run(command.format(pref2.revision)) self.c_v2.run(command.format(pref1.revision)) with environment_update({"MY_VAR": "4"}): pref4 = _create(self.c_v2, self.ref, conanfile=conanfile) self.c_v2.run("upload {} -r default -c".format(pref4.repr_notime())) pref = copy.copy(pref1) pref.revision = None revs = [r.revision for r in self.server.server_store.get_package_revisions_references(pref)] assert revs == [pref4.revision] def test_touching_other_server(): # https://github.com/conan-io/conan/issues/9333 servers = OrderedDict([("remote1", TestServer()), ("remote2", None)]) # None server will crash if touched c = TestClient(servers=servers, inputs=["admin", "password"]) c.save({"conanfile.py": GenConanfile().with_settings("os")}) c.run("create . --name=pkg --version=0.1 --user=conan --channel=channel -s os=Windows") c.run("upload * -c -r=remote1") c.run("remove * -c") # This is OK, binary found c.run("install --requires=pkg/0.1@conan/channel -r=remote1 -s os=Windows") c.run("install --requires=pkg/0.1@conan/channel -r=remote1 -s os=Linux", assert_error=True) assert "ERROR: Missing binary: pkg/0.1@conan/channel" in c.out @pytest.mark.artifactory_ready def test_reupload_older_revision(): """ upload maintains the server history https://github.com/conan-io/conan/issues/7331 """ c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") rrev1 = c.exported_recipe_revision() c.run("upload * -r=default -c") c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("potato = 42")}) c.run("export .") rrev2 = c.exported_recipe_revision() c.run("upload * -r=default -c") def check_order(inverse=False): c.run("list pkg/0.1#* -r=default") out = str(c.out) assert rrev1 in out assert rrev2 in out if inverse: assert out.find(rrev1) > out.find(rrev2) else: assert out.find(rrev1) < out.find(rrev2) check_order() # If we create the same older revision, and upload, still the same order c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") c.run("upload * -r=default -c") check_order() # Force doesn't change it, same order c.run("upload * -r=default -c --force") check_order() # the only way is to remove it, then upload c.run(f"remove pkg/0.1#{rrev1} -r=default -c") c.run("upload * -r=default -c --force") check_order(inverse=True) @pytest.mark.artifactory_ready def test_reupload_older_revision_new_binaries(): """ upload maintains the server history https://github.com/conan-io/conan/pull/16621 """ c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("os")}) c.run("create . -s os=Linux") rrev1 = c.exported_recipe_revision() c.run("upload * -r=default -c") c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("os") .with_class_attribute("potato = 42")}) c.run("create . -s os=Linux") rrev2 = c.exported_recipe_revision() c.run("upload * -r=default -c") def check_order(): c.run("list pkg/0.1#* -r=default") out = str(c.out) assert rrev1 in out assert rrev2 in out assert out.find(rrev1) < out.find(rrev2) check_order() # If we create the same older revision, and upload, still the same order # c.run("remove * -c") # Make sure no other revision c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("os")}) c.run("create . -s os=Windows") rrev3 = c.exported_recipe_revision() assert rrev3 == rrev1 c.run(f"upload pkg*#{rrev3} -r=default -c --force") check_order() ================================================ FILE: test/functional/sbom/__init__.py ================================================ ================================================ FILE: test/functional/sbom/test_cyclonedx.py ================================================ import json import os import pytest from conan.internal.util.files import save from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient # Using the sbom tool with "conan create" sbom_hook_post_package = """ import json import os from conan.errors import ConanException from conan.api.output import ConanOutput from conan.tools.sbom import cyclonedx_1_4, cyclonedx_1_6 def post_package(conanfile): sbom_cyclonedx_1_4 = cyclonedx_1_4(conanfile, add_build=True, add_tests=True) sbom_cyclonedx_1_6 = cyclonedx_1_6(conanfile, add_build=True, add_tests=True) with open(os.path.join(conanfile.package_metadata_folder, "sbom14.cdx.json"), 'w') as f: json.dump(sbom_cyclonedx_1_4, f, indent=4) with open(os.path.join(conanfile.package_metadata_folder, "sbom16.cdx.json"), 'w') as f: json.dump(sbom_cyclonedx_1_6, f, indent=4) """ class TestCyclonedx: @pytest.fixture() def hook_setup_post_package_tl(self, transitive_libraries): tc = transitive_libraries hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_package) return tc @pytest.mark.tool("cmake") def test_sbom_generation_create(self, hook_setup_post_package_tl): # TODO This doesn't need to be a functional test, check why tc = hook_setup_post_package_tl tc.run("new cmake_lib -d name=bar -d version=1.0 -d requires=engine/1.0 -f") # bar -> engine/1.0 -> matrix/1.0 tc.run("create . -tf=") bar_layout = tc.created_layout() assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom14.cdx.json")) assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom16.cdx.json")) @pytest.mark.tool("cmake") @pytest.mark.parametrize("user, channel, user_dep, channel_dep", [("user", None, "user_dep", None), ("user", "channel", "user_dep", "channel_dep")]) def test_sbom_user_path(self, user, channel, user_dep, channel_dep): tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_package) channel_ref = f"/{channel_dep}" if channel_dep else "" tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": GenConanfile("main", "1.0").with_requires( f"dep/1.0@{user_dep}{channel_ref}")}) command = "create dep" if user: command += f" --user={user_dep}" if channel: command += f" --channel={channel_dep}" tc.run(command) command = "create ." if user: command += f" --user={user}" if channel: command += f" --channel={channel}" tc.run(command) for version in ("14", "16"): create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), f"sbom{version}.cdx.json") content = tc.load(cyclone_path) content_json = json.loads(content) assert content_json["components"][0]["bom-ref"].split("&user=")[ 1] == f"{user}&channel={channel}" if channel else user assert content_json["dependencies"][0]["dependsOn"][0].split("&user=")[ 1] == f"{user_dep}&channel={channel_dep}" if channel_dep else user_dep ================================================ FILE: test/functional/subsystems_build_test.py ================================================ import platform import tempfile import pytest import textwrap from conan.test.assets.autotools import gen_makefile from conan.test.assets.sources import gen_function_cpp from test.functional.utils import check_exe_run, check_vs_runtime from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update @pytest.mark.skipif(platform.system() != "Windows", reason="Tests Windows Subsystems") class TestSubsystems: @pytest.mark.tool("msys2") def test_msys2_available(self): """ Msys2 needs to be installed: - Go to https://www.msys2.org/, download the exe installer and run it - Follow instructions in https://www.msys2.org/ to update the package DB - Install msys2 autotools "pacman -S autotools" - Make sure the entry in conftest_user.py of msys2 points to the right location """ client = TestClient() client.run_command('uname') assert "MSYS" in client.out @pytest.mark.tool("cygwin") def test_cygwin_available(self): """ Cygwin is necessary - Install from https://www.cygwin.com/install.html, use the default packages - Install automake 1.16, gcc-g++, make and binutils packages (will add autoconf and more) - Make sure that the path in conftest_user.py is pointing to cygwin "bin" folder """ client = TestClient() client.run_command('uname') assert "CYGWIN" in client.out @pytest.mark.tool("msys2") @pytest.mark.tool("mingw32") def test_mingw32_available(self): """ Mingw32 needs to be installed. We use msys2, don't know if others work - Inside msys2, install pacman -S mingw-w64-i686-toolchain (all pkgs) """ client = TestClient() client.run_command('uname') assert "MINGW32_NT" in client.out @pytest.mark.tool("msys2") @pytest.mark.tool("ucrt64") def test_ucrt64_available(self): """ ucrt64 needs to be installed. We use msys2, don't know if others work - Inside msys2, install pacman -S mingw-w64-ucrt-x86_64-toolchain (all pkgs) """ client = TestClient() client.run_command('uname') assert "MINGW64_NT" in client.out @pytest.mark.tool("msys2") @pytest.mark.tool("msys2_clang64") def test_clang64_available(self): client = TestClient() client.run_command('uname') assert "MINGW64_NT" in client.out @pytest.mark.tool("msys2") @pytest.mark.tool("mingw64") def test_mingw64_available(self): """ Mingw64 needs to be installed. We use msys2, don't know if others work - Inside msys2, install pacman -S mingw-w64-x86_64-toolchain (all pkgs) """ client = TestClient() client.run_command('uname') assert "MINGW64_NT" in client.out # It's important not to have uname in Path, that could # mean that we have Git bash or MingW in the Path and # we mistakenly use tools from there when we want to use msys2 tools def test_tool_not_available(self): client = TestClient() client.run_command('uname', assert_error=True) assert "'uname' is not recognized as an internal or external command" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Tests Windows Subsystems") class TestSubsystemsBuild: @staticmethod def _build(client, static_runtime=None, make="make"): makefile = gen_makefile(apps=["app"], static_runtime=static_runtime) main_cpp = gen_function_cpp(name="main") client.save({"Makefile": makefile, "app.cpp": main_cpp}) client.run_command(make) client.run_command("app") @pytest.mark.tool("msys2") @pytest.mark.parametrize("static", [True, False]) def test_msys2(self, static): """ native MSYS environment, binaries depend on MSYS runtime (msys-2.0.dll) Install: - pacman -S gcc posix-compatible, intended to be run only in MSYS environment (not in pure Windows) """ client = TestClient() self._build(client, static_runtime=static) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="msys2") assert "_M_X64" not in client.out # TODO: Do not hardcode the visual version check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="msys2") @pytest.mark.parametrize("static", [True, False]) @pytest.mark.tool("mingw") def test_mingw(self, static): """ This will work if you installed the Mingw toolchain outside msys2, from https://sourceforge.net/projects/mingw/, and installed gcc, autotools, mingw32-make, etc But this doesn't contain "make", only "mingw32-make" """ client = TestClient() self._build(client, static_runtime=static, make="mingw32-make") check_exe_run(client.out, "main", "gcc", None, "Debug", "x86", None, subsystem="mingw32") check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="mingw64") @pytest.mark.parametrize("static", [True, False]) @pytest.mark.tool("msys2") @pytest.mark.tool("mingw64") def test_mingw64(self, static): """ This will work if you installed the Mingw toolchain inside msys2 as TestSubystems above 64-bit GCC, binaries """ client = TestClient() # pacman -S mingw-w64-x86_64-gcc self._build(client, static_runtime=static) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="mingw64") # it also defines the VS 64 bits macro assert "main _M_X64 defined" in client.out check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="mingw64") @pytest.mark.parametrize("static", [True, False]) @pytest.mark.tool("msys2") @pytest.mark.tool("msys2_clang64") def test_msys2_clang64(self, static): """ in msys2 $ pacman -S mingw-w64-x86_64-clang (NO, this is the mingw variant in ming64) $ pacman -S mingw-w64-clang-x86_64-toolchain """ client = TestClient() # Not defined in msys2 clang by default with environment_update({"CC": "clang", "CXX": "clang++"}): self._build(client, static_runtime=static) check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem="mingw64") # it also defines the VS 64 bits macro assert "main _M_X64 defined" in client.out check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="clang64") @pytest.mark.tool("msys2") @pytest.mark.tool("mingw64") def test_mingw64_recipe(self): """ A recipe with self.run_bash=True and msys2 configured, using mingw to build stuff with make from the subsystem """ client = TestClient() makefile = gen_makefile(apps=["app"]) main_cpp = gen_function_cpp(name="main") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import Autotools from conan.tools.layout import basic_layout from conan.tools.files import copy class HelloConan(ConanFile): exports_sources = "*.cpp", "Makefile" generators = "AutotoolsToolchain" win_bash = True def build(self): self.output.warning(self.build_folder) auto = Autotools(self) auto.make() def package(self): copy(self, "app*", self.build_folder, os.path.join(self.package_folder, "bin")) """) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile class TestConan(ConanFile): def requirements(self): self.tool_requires(self.tested_reference_str) def test(self): self.run("app") """) profile = textwrap.dedent(""" include(default) [conf] tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path=bash """) client.save({"conanfile.py": conanfile, "Makefile": makefile, "app.cpp": main_cpp, "test_package/conanfile.py": test_conanfile, "myprofile": profile}) client.run("create . --name foo --version 1.0 --profile:build myprofile --build-require") assert "__MINGW64__" in client.out assert "__CYGWIN__" not in client.out @pytest.mark.tool("msys2") @pytest.mark.tool("msys2_mingw64_clang64") def test_msys2_mingw64_clang64(self): """ in msys2 $ pacman -S mingw-w64-x86_64-clang $ pacman -S mingw-w64-clang-x86_64-toolchain (NO, this is the clang) """ client = TestClient() static = False # This variant needs --static-glibc -static-libstdc++ (weird) to link static # Need to redefine CXX otherwise is gcc with environment_update({"CXX": "clang++"}): self._build(client, static_runtime=static) check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem="mingw64") # it also defines the VS 64 bits macro assert "main _M_X64 defined" in client.out check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="mingw64") @pytest.mark.parametrize("static", [True, False]) @pytest.mark.tool("msys2") @pytest.mark.tool("mingw32") def test_mingw32(self, static): """ This will work if you installed the Mingw toolchain inside msys2 as TestSubystems above 32-bit GCC, binaries for generic Windows (no dependency on MSYS runtime) """ client = TestClient() # pacman -S mingw-w64-i686-gcc self._build(client, static_runtime=static) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86", None, subsystem="mingw32") # It also defines the VS flag assert "main _M_IX86 defined" in client.out check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="mingw32") @pytest.mark.parametrize("static", [True, False]) @pytest.mark.tool("msys2") @pytest.mark.tool("ucrt64") def test_ucrt64(self, static): """ This will work if you installed the Mingw toolchain inside msys2 as TestSubystems above """ client = TestClient() self._build(client, static_runtime=static) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="mingw32") # it also defines the VS macro assert "main _M_X64 defined" in client.out check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="ucrt64") @pytest.mark.parametrize("static", [True, False]) @pytest.mark.tool("cygwin") def test_cygwin(self, static): """ Cygwin environment, binaries depend on Cygwin runtime (cygwin1.dll) posix-compatible, intended to be run only in Cygwin environment (not in pure Windows) """ client = TestClient() self._build(client, static_runtime=static) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="cygwin") check_vs_runtime("app.exe", client, "15", "Debug", static_runtime=static, subsystem="cygwin") @pytest.mark.skipif(platform.system() != "Windows", reason="Tests Windows Subsystems") class TestSubsystemsAutotoolsBuild: configure_ac = textwrap.dedent(""" AC_INIT([Tutorial Program], 1.0) AM_INIT_AUTOMAKE([foreign]) AC_PROG_CXX AC_CONFIG_FILES(Makefile) AC_OUTPUT """) # newline is important makefile_am = textwrap.dedent(""" bin_PROGRAMS = app app_SOURCES = main.cpp """) def _build(self, client): main_cpp = gen_function_cpp(name="main") client.save({"configure.ac": self.configure_ac, "Makefile.am": self.makefile_am, "main.cpp": main_cpp}) path = client.current_folder # Seems unix_path not necessary for this to pass client.run_command('bash -lc "cd \\"%s\\" && autoreconf -fiv"' % path) client.run_command('bash -lc "cd \\"%s\\" && ./configure"' % path) client.run_command("make") client.run_command("app") @pytest.mark.tool("msys2") def test_msys(self): """ native MSYS environment, binaries depend on MSYS runtime (msys-2.0.dll) posix-compatible, intended to be run only in MSYS environment (not in pure Windows) """ client = TestClient() # pacman -S gcc self._build(client) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="msys2") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="msys2") @pytest.mark.tool("msys2") @pytest.mark.tool("mingw64") def test_mingw64(self): """ 64-bit GCC, binaries for generic Windows (no dependency on MSYS runtime) """ client = TestClient() # pacman -S mingw-w64-x86_64-gcc self._build(client) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="mingw64") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="mingw64") @pytest.mark.tool("msys2") @pytest.mark.tool("mingw32") def test_mingw32(self): """ 32-bit GCC, binaries for generic Windows (no dependency on MSYS runtime) """ client = TestClient() # pacman -S mingw-w64-i686-gcc self._build(client) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86", None, subsystem="mingw32") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="mingw32") @pytest.mark.tool("cygwin") def test_cygwin(self): """ Cygwin environment, binaries depend on Cygwin runtime (cygwin1.dll) posix-compatible, intended to be run only in Cygwin environment (not in pure Windows) # install autotools, autoconf, libtool, "gcc-c++" and "make" packages """ client = TestClient() self._build(client) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="cygwin") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="cygwin") @pytest.mark.skipif(platform.system() != "Windows", reason="Tests Windows Subsystems") class TestSubsystemsCMakeBuild: """ These tests are running the CMake INSIDE THE subsystem, not the Windows native one The results are basically the same if CMake is outside the subsystem, but it is NOT enough to define CMAKE_CXX_COMPILER full path to the compiler, but it must be in the path """ cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) message(STATUS "MYCMAKE VERSION=${CMAKE_VERSION}") add_executable(app main.cpp) """) def _build(self, client, generator="Unix Makefiles", compiler=None, toolset=None): main_cpp = gen_function_cpp(name="main") client.save({"CMakeLists.txt": self.cmakelists, "main.cpp": main_cpp}) cmake_compiler = "" if compiler: cmake_compiler += " -DCMAKE_C_COMPILER={}".format(compiler) compilerpp = "clang++" if compiler == "clang" else "g++" cmake_compiler += " -DCMAKE_CXX_COMPILER={}".format(compilerpp) cmake_compiler += " -DCMAKE_RC_COMPILER={}".format(compiler) toolset = "-T {}".format(toolset) if toolset else "" client.run_command("cmake {} {}" " -DCMAKE_SH=\"CMAKE_SH-NOTFOUND\" -G \"{}\" .".format(cmake_compiler, toolset, generator)) build_out = client.out client.run_command("cmake --build .") app = "app" if "Visual" not in generator else r"Debug\app" client.run_command(app) return build_out @pytest.mark.tool("msys2") def test_msys(self): """ pacman -S cmake """ client = TestClient() self._build(client) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="msys2") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="msys2") @pytest.mark.tool("msys2") @pytest.mark.tool("mingw64") def test_mingw64(self): """ $ pacman -S mingw-w64-x86_64-cmake """ client = TestClient() self._build(client, generator="MinGW Makefiles") check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="mingw64") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="mingw64") @pytest.mark.tool("msys2") @pytest.mark.tool("msys2_clang64") @pytest.mark.skip(reason="This doesn't work, seems CMake issue") def test_msys2_clang64(self): """ FAILS WITH: System is unknown to cmake, create: Platform/MINGW64_NT-10.0-19044 to use this system, """ client = TestClient() self._build(client, generator="Unix Makefiles") check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem="mingw64") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="clang64") @pytest.mark.tool("msys2") @pytest.mark.tool("msys2_clang64") @pytest.mark.tool("cmake", "3.19") def test_msys2_clang64_external(self): """ Exactly the same as the previous tests, but with a native cmake 3.19 (higher priority) """ client = TestClient() build_out = self._build(client) assert "MYCMAKE VERSION=3.19" in build_out check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem="mingw64") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="clang64") @pytest.mark.tool("msys2") @pytest.mark.tool("msys2_mingw64_clang64") def test_msys2_mingw64_clang64(self): """ """ client = TestClient() # IMPORTANT: Need to redefine the CXX, otherwise CMake will use GCC by default with environment_update({"CXX": "clang++"}): self._build(client, generator="MinGW Makefiles") check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem="mingw64") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="mingw64") @pytest.mark.tool("msys2") @pytest.mark.tool("mingw32") def test_mingw32(self): """ $ pacman -S mingw-w64-i686-cmake """ client = TestClient() self._build(client, generator="MinGW Makefiles") check_exe_run(client.out, "main", "gcc", None, "Debug", "x86", None, subsystem="mingw32") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="mingw32") @pytest.mark.tool("cygwin") def test_cygwin(self): """ Needs to install cmake from the cygwin setup.exe """ client = TestClient() # install "gcc-c++" and "make" packages self._build(client) check_exe_run(client.out, "main", "gcc", None, "Debug", "x86_64", None, subsystem="cygwin") check_vs_runtime("app.exe", client, "15", "Debug", subsystem="cygwin") @pytest.mark.tool("ninja") @pytest.mark.tool("clang", "20") def test_clang(self): """ native, LLVM/Clang compiler Installing the binary from LLVM site https://github.com/llvm/llvm-project/releases/tag/llvmorg-14.0.6 """ client = TestClient() self._build(client, generator="Ninja", compiler="clang") check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem=None) check_vs_runtime("app.exe", client, "15", "Debug", subsystem=None) @pytest.mark.tool("cmake", "3.23") @pytest.mark.tool("visual_studio", "17") def test_vs_clang(self): """ native, LLVM/Clang compiler installed with VS 2022 -T ClangCL """ # IMPORTANT: VS CLang not found if in another unit folder = tempfile.mkdtemp(suffix='conans') client = TestClient(current_folder=folder) self._build(client, generator="Visual Studio 17 2022", toolset="ClangCL") check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem=None) check_vs_runtime("Debug/app.exe", client, "15", "Debug", subsystem=None) @pytest.mark.tool("msys2") def test_msys2_env_vars_paths(): c = TestClient() # A tool-requires injecting PATHs for native, should not use "_path" calls, and use # 'separator=;' explicitly tool = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "tool" version = "0.1" def package_info(self): self.buildenv_info.append("INCLUDE", "C:/mytool/path", separator=";") """) conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): win_bash = True tool_requires = "tool/0.1" def build(self): self.run('echo "INCLUDE=$INCLUDE"') """) profile = textwrap.dedent(""" [conf] tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path=bash [buildenv] INCLUDE=+(sep=;)C:/prepended/path INCLUDE+=(sep=;)C:/appended/path """) c.save({"tool/conanfile.py": tool, "consumer/conanfile.py": conanfile, "profile": profile}) c.run("create tool") with environment_update({"INCLUDE": "C:/my/abs path/folder;C:/other path/subfolder"}): c.run("build consumer -pr=profile") # Check the profile is outputed correctly assert "INCLUDE=+(sep=;)C:/prepended/path" in c.out assert "INCLUDE+=(sep=;)C:/appended/path" in c.out # check the composition is correct assert "INCLUDE=C:/prepended/path;C:/my/abs path/folder;C:/other path/subfolder;" \ "C:/mytool/path;C:/appended/path" in c.out ================================================ FILE: test/functional/test_local_recipes_index.py ================================================ import os import textwrap from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, zipdir from conan.internal.util.files import save_files, sha256sum class TestLocalRecipeIndexNew: def test_conan_new_local_recipes_index(self): # Setup the release pkg0.1.zip http server file_server = TestFileServer() zippath = os.path.join(file_server.store, "pkg0.1.zip") repo_folder = temp_folder() cmake = gen_cmakelists(libname="pkg", libsources=["pkg.cpp"], install=True, public_header="pkg.h") save_files(repo_folder, {"pkg/CMakeLists.txt": cmake, "pkg/pkg.h": gen_function_h(name="pkg"), "pkg/pkg.cpp": gen_function_cpp(name="pkg")}) zipdir(repo_folder, zippath) sha256 = sha256sum(zippath) url = f"{file_server.fake_url}/pkg0.1.zip" c0 = TestClient() c0.servers["file_server"] = file_server c0.run(f"new local_recipes_index -d name=pkg -d version=0.1 -d url={url} -d sha256={sha256}") # A local create is possible, and it includes a test_package c0.run("create recipes/pkg/all --version=0.1") assert "pkg: Release!" in c0.out remote_folder = c0.current_folder c = TestClient() c.servers["file_server"] = file_server c.run(f"remote add local '{remote_folder}'") c.run("new cmake_exe -d name=app -d version=0.1 -d requires=pkg/0.1") c.run("create . --version=0.1 --build=missing") assert "pkg: Release!" in c.out class TestInRepo: def test_in_repo(self): """testing that it is possible to put a "recipes" folder inside a source repo, and use it as local-recipes-index repository, exporting the source from itself """ repo_folder = temp_folder() cmake = gen_cmakelists(libname="pkg", libsources=["pkg.cpp"], install=True, public_header="pkg.h") config_yml = textwrap.dedent("""\ versions: "0.1": folder: all """) conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.files import copy class PkgRecipe(ConanFile): name = "pkg" package_type = "library" # Binary configuration settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} generators = "CMakeToolchain" def export_sources(self): src = os.path.dirname(os.path.dirname(os.path.dirname(self.recipe_folder))) copy(self, "*", src=src, dst=self.export_sources_folder, excludes=["recipes*"]) def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = [self.name] """) save_files(repo_folder, {"recipes/pkg/config.yml": config_yml, "recipes/pkg/all/conanfile.py": conanfile, "CMakeLists.txt": cmake, "pkg.h": gen_function_h(name="pkg"), "pkg.cpp": gen_function_cpp(name="pkg")}) c = TestClient() c.run(f"remote add local '{repo_folder}'") c.run("new cmake_exe -d name=app -d version=0.1 -d requires=pkg/0.1") c.run("create . --build=missing") assert "pkg: Release!" in c.out # Of course the recipe can also be created locally path = os.path.join(repo_folder, "recipes/pkg/all") c.run(f'create "{path}" --version=0.1') assert "pkg/0.1: Created package" in c.out # Finally lets remove the remote, check that the clone is cleared c.run('remote remove local') assert "Removing temporary files for 'local' local-recipes-index remote" in c.out def test_not_found(self): """testing that the correct exception is raised when a recipe is not found """ repo1_folder = temp_folder() repo2_folder = temp_folder() config_yml = textwrap.dedent("""\ versions: "0.1": folder: all """) conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.files import copy class PkgRecipe(ConanFile): name = "pkg" def export_sources(self): copy(self, "*", src=self.recipe_folder, dst=self.export_sources_folder) """) save_files(repo2_folder, {"recipes/pkg/config.yml": config_yml, "recipes/pkg/all/conanfile.py": conanfile, "recipes/pkg/all/pkg.h": gen_function_h(name="pkg")}) c = TestClient() c.run(f"remote add local1 '{repo1_folder}'") c.run(f"remote add local2 '{repo2_folder}'") c.run("install --requires=pkg/0.1 --build=missing") assert "Install finished successfully" in c.out ================================================ FILE: test/functional/test_profile_detect_api.py ================================================ import platform import textwrap import pytest from conan.internal.api.detect import detect_api from conan.test.utils.tools import TestClient class TestProfileDetectAPI: @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") @pytest.mark.tool("visual_studio", "17") def test_profile_detect_compiler(self): client = TestClient() tpl1 = textwrap.dedent(""" {% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %} {% set runtime, _ = detect_api.default_msvc_runtime(compiler) %} [settings] compiler={{compiler}} compiler.version={{detect_api.default_compiler_version(compiler, version)}} compiler.runtime={{runtime}} compiler.cppstd={{detect_api.default_cppstd(compiler, version)}} # detect_msvc_update returns the real update, like 12 for VS 17.12 so # we have to convert to the setting that's 0-10 compiler.update={{ (detect_api.detect_msvc_update(version) | int) % 10 }} [conf] tools.microsoft.msbuild:vs_version={{detect_api.default_msvc_ide_version(version)}} """) client.save({"profile1": tpl1}) client.run("profile show -pr=profile1 --context=host") # FIXME: check update setting update = str(int(detect_api.detect_msvc_update("194")) % 10) expected = textwrap.dedent(f"""\ [settings] compiler=msvc compiler.cppstd=14 compiler.runtime=dynamic compiler.runtime_type=Release compiler.update={update} compiler.version=194 [conf] tools.microsoft.msbuild:vs_version=17 """) assert expected in client.out @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux") def test_profile_detect_libc(self): client = TestClient() tpl1 = textwrap.dedent(""" {% set compiler, version, _ = detect_api.detect_gcc_compiler() %} {% set libc, libc_version = detect_api.detect_libc() %} [settings] os=Linux compiler={{compiler}} compiler.version={{version}} [conf] user.confvar:libc={{libc}} user.confvar:libc_version={{libc_version}} """) client.save({"profile1": tpl1}) client.run("profile show -pr=profile1 --context=host") libc_name, libc_version = detect_api.detect_libc() assert libc_name is not None assert libc_version is not None _, version, _ = detect_api.detect_gcc_compiler() expected = textwrap.dedent(f"""\ [settings] compiler=gcc compiler.version={version} os=Linux [conf] user.confvar:libc={libc_name} user.confvar:libc_version={libc_version} """) assert expected in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_profile_detect_darwin_sdk(self): client = TestClient() tpl1 = textwrap.dedent("""\ [settings] os = "Macos" os.sdk_version = {{ detect_api.detect_sdk_version(sdk="macosx") }} """) client.save({"profile1": tpl1}) client.run("profile show -pr=profile1") sdk_version = detect_api.detect_sdk_version(sdk="macosx") assert f"os.sdk_version={sdk_version}" in client.out @pytest.mark.parametrize("context", [None, "host", "build"]) @pytest.mark.parametrize("f", ["json", "text"]) def test_profile_show_aggregate_usecase(context, f): tc = TestClient(light=True) context_arg = f"--context {context}" if context else "" tc.run(f'profile show {context_arg} -s:h="os=Windows" -s:b="os=Linux" --format={f}') if context == "host": if f == "text": assert "Host profile:" not in tc.stdout assert "Host profile:" in tc.stderr assert "Linux" not in tc.out if context == "build": if f == "text": assert "Build profile:" not in tc.stdout assert "Build profile:" in tc.stderr assert "Windows" not in tc.out if context in (None, "host"): assert "Windows" in tc.out if context in (None, "build"): assert "Linux" in tc.out ================================================ FILE: test/functional/test_third_party_patch_flow.py ================================================ import os import textwrap from conan.test.utils.scm import create_local_git_repo from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import mkdir def test_third_party_patch_flow(): """ this test emulates the work of a developer contributing recipes to ConanCenter, and having to do multiple patches to the original library source code: - Everything is local, not messing with the cache - Using layout() to define location of things """ conanfile = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, load, apply_conandata_patches class Pkg(ConanFile): name = "mypkg" version = "1.0" exports_sources = "*" def layout(self): self.folders.source = "src" self.folders.build = "build" def source(self): # emulate a download from web site save(self, "myfile.cpp", "mistake1\nsomething\nmistake2\nmistake3\nsome\n") apply_conandata_patches(self) def build(self): content = load(self, os.path.join(self.source_folder, "myfile.cpp")) for i in (1, 2, 3): if "mistake{}".format(i) in content: raise Exception("MISTAKE{} BUILD!".format(i)) """) client = TestClient() client.save({"conanfile.py": conanfile, "conandata.yml": ""}) client.run("install .") client.run("source .") assert "apply_conandata_patches(): No patches defined in conandata" in client.out client.save({"conandata.yml": "patches: {}"}) client.run("source .") client.run("build .", assert_error=True) assert "MISTAKE1 BUILD!" in client.out # user decides to create patches, first init the repo client.init_git_repo(folder="src") # Using helper for user/email repo init client.save({"src/myfile.cpp": "correct1\nsomething\nmistake2\nmistake3\nsome\n"}) # compute the patch mkdir(os.path.join(client.current_folder, "patches")) client.run_command("cd src && git diff > ../patches/patch1") client.run_command("cd src && git add . && git commit -m patch1") conandata = textwrap.dedent(""" patches: "1.0": - patch_file: "patches/patch1" """) client.save({"conandata.yml": conandata}) client.run("source .") client.run("build .", assert_error=True) assert "MISTAKE2 BUILD!" in client.out client.save({"src/myfile.cpp": "correct1\nsomething\ncorrect2\nmistake3\nsome\n"}) # compute the patch mkdir(os.path.join(client.current_folder, "patches")) client.run_command("cd src && git diff > ../patches/patch2") client.run_command("cd src && git add . && git commit -m patch1") conandata = textwrap.dedent(""" patches: "1.0": - patch_file: "patches/patch1" - patch_file: "patches/patch2" """) client.save({"conandata.yml": conandata}) client.run("source .") client.run("build .", assert_error=True) assert "MISTAKE3 BUILD!" in client.out client.save({"src/myfile.cpp": "correct1\nsomething\ncorrect2\ncorrect3\nsome\n"}) # compute the patch mkdir(os.path.join(client.current_folder, "patches")) client.run_command("cd src && git diff > ../patches/patch3") client.run_command("cd src && git add . && git commit -m patch1") conandata = textwrap.dedent(""" patches: "1.0": - patch_file: "patches/patch1" - patch_file: "patches/patch2" - patch_file: "patches/patch3" """) client.save({"conandata.yml": conandata}) client.run("source .") client.run("build .") assert "conanfile.py (mypkg/1.0): Calling build()" in client.out # of course create should work too client.run("create .") assert "mypkg/1.0: Created package" in client.out def test_third_party_overwrite_build_file(): """ this test emulates the work of a developer contributing recipes to ConanCenter, and replacing the original build script with your one one. The export_sources is actually copying CMakeLists.txt into the "src" folder, but the 'download' will overwrite it, so it is necessary to copy it again """ conanfile = textwrap.dedent(r""" import os, shutil from conan import ConanFile from conan.tools.files import save, load class Pkg(ConanFile): name = "mypkg" version = "1.0" exports_sources = "CMakeLists.txt" def layout(self): self.folders.source = "src" self.folders.build = "build" def source(self): # emulate a download from web site save(self, "CMakeLists.txt", "MISTAKE: Very old CMakeLists to be replaced") # Now I fix it with one of the exported files shutil.copy("../CMakeLists.txt", ".") def build(self): if "MISTAKE" in load(self, os.path.join(self.source_folder, "CMakeLists.txt")): raise Exception("MISTAKE BUILD!") """) client = TestClient() client.save({"conanfile.py": conanfile, "conandata.yml": "", "CMakeLists.txt": "My better cmake"}) client.run("install .") client.run("source .") client.run("build .") assert "conanfile.py (mypkg/1.0): Calling build()" in client.out # of course create should work too client.run("create .") assert "mypkg/1.0: Created package" in client.out def test_third_party_git_overwrite_build_file(): """ Same as the above, but using git clone The trick: "git clone ." needs an empty directory. No reason why the ``src`` folder should be polluted automatically with exports, so just removing things works """ git_repo = temp_folder().replace("\\", "/") create_local_git_repo({"CMakeLists.txt": "MISTAKE Cmake"}, folder=git_repo) conanfile = textwrap.dedent(r""" import os, shutil from conan import ConanFile from conan.tools.files import save, load class Pkg(ConanFile): name = "mypkg" version = "1.0" exports_sources = "CMakeLists.txt" def layout(self): self.folders.source = "src" self.folders.build = "build" def source(self): self.output.info("CWD: {{}}!".format(os.getcwd())) self.output.info("FILES: {{}}!".format(sorted(os.listdir(".")))) self.run('git clone "{}" .') # Now I fix it with one of the exported files shutil.copy("../CMakeLists.txt", ".") def build(self): if "MISTAKE" in load(self, os.path.join(self.source_folder, "CMakeLists.txt")): raise Exception("MISTAKE BUILD!") """.format(git_repo)) client = TestClient() client.save({"conanfile.py": conanfile, "conandata.yml": "", "CMakeLists.txt": "My better cmake"}) client.run("install .") client.run("source .") assert "FILES: []!" in client.out client.run("build .") assert "conanfile.py (mypkg/1.0): Calling build()" in client.out # of course create should work too client.run("create .") assert "mypkg/1.0: Created package" in client.out ================================================ FILE: test/functional/toolchains/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/android/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/android/test_using_cmake.py ================================================ import platform import tempfile import textwrap import pytest from test.conftest import tools_locations from conan.test.utils.tools import TestClient @pytest.mark.slow @pytest.mark.tool("cmake", "3.23") # Android complains if <3.19 @pytest.mark.tool("ninja") # so it easily works in Windows too @pytest.mark.tool("android_ndk") @pytest.mark.skipif(platform.system() != "Darwin", reason="NDK only installed on MAC") def test_use_cmake_toolchain(): """ This is the naive approach, we follow instruction from CMake in its documentation https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling-for-android """ # Overriding the default folders, so they are in the same unit drive in Windows # otherwise AndroidNDK FAILS to build, it needs using the same unit drive c = TestClient(cache_folder=tempfile.mkdtemp(), current_folder=tempfile.mkdtemp()) c.run("new cmake_lib -d name=hello -d version=0.1") ndk_path = tools_locations["android_ndk"]["system"]["path"][platform.system()] android = textwrap.dedent(f""" [settings] os=Android os.api_level=23 arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=c++_shared build_type=Release [conf] tools.android:ndk_path={ndk_path} tools.cmake.cmaketoolchain:generator=Ninja """) c.save({"android": android}) c.run('create . --profile:host=android') assert "hello/0.1 (test package): Running test()" in c.out # Build locally c.run('build . --profile:host=android') assert "conanfile.py (hello/0.1): Running CMake.build()" in c.out ================================================ FILE: test/functional/toolchains/apple/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/apple/test_xcodebuild.py ================================================ import platform import re import textwrap import pytest from conan.test.utils.tools import TestClient xcode_project = textwrap.dedent(""" name: app targets: app: type: tool platform: macOS sources: - app configFiles: Debug: conan_config.xcconfig Release: conan_config.xcconfig """) xcode_project_bare = textwrap.dedent(""" name: app targets: app: type: tool platform: macOS sources: - app """) main = textwrap.dedent(""" #include #include "hello.h" int main(int argc, char *argv[]) { hello(); #ifndef DEBUG std::cout << "App Release!" << std::endl; #else std::cout << "App Debug!" << std::endl; #endif } """) test = textwrap.dedent(""" import os from conan import ConanFile, tools from conan.tools.build import cross_building class TestApp(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if not cross_building(self): self.run("app", env="conanrun") """) @pytest.fixture(scope="module") def client(): client = TestClient() client.run("new cmake_lib -d name=hello -d version=0.1") client.run("create . -s build_type=Release") client.run("create . -s build_type=Debug") return client @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") def test_project_xcodebuild(client): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.apple import XcodeBuild from conan.tools.files import copy class MyApplicationConan(ConanFile): name = "myapplication" version = "1.0" requires = "hello/0.1" settings = "os", "compiler", "build_type", "arch" generators = "XcodeDeps" exports_sources = "app.xcodeproj/*", "app/*" package_type = "application" def build(self): xcode = XcodeBuild(self) xcode.build("app.xcodeproj") def package(self): copy(self, "build/{}/app".format(self.settings.build_type), self.source_folder, os.path.join(self.package_folder, "bin"), keep_path=False) def package_info(self): self.cpp_info.bindirs = ["bin"] """) client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test, "app/main.cpp": main, "project.yml": xcode_project}, clean_first=True) client.run("install . --build=missing") client.run("install . -s build_type=Debug --build=missing") client.run_command("xcodegen generate") client.run("create . --build=missing -s os.version=15.0 -c tools.build:verbosity=verbose -c tools.compilation:verbosity=verbose") assert "MACOSX_DEPLOYMENT_TARGET=15.0" in client.out assert "xcodebuild: error: invalid option" not in client.out assert "hello/0.1: Hello World Release!" in client.out assert "App Release!" in client.out client.run("create . -s build_type=Debug -s os.version=15.0 --build=missing") assert "hello/0.1: Hello World Debug!" in client.out assert "App Debug!" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") @pytest.mark.skip(reason="Different sdks not installed in CI") def test_xcodebuild_test_different_sdk(client): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.apple import XcodeBuild class MyApplicationConan(ConanFile): name = "myapplication" version = "1.0" requires = "hello/0.1" settings = "os", "compiler", "build_type", "arch" generators = "XcodeDeps" exports_sources = "app.xcodeproj/*", "app/*" def build(self): xcode = XcodeBuild(self) xcode.build("app.xcodeproj") self.run("otool -l build/Release/app") """) client.save({"conanfile.py": conanfile, "app/main.cpp": main, "project.yml": xcode_project}, clean_first=True) client.run("install . --build=missing") client.run("install . -s build_type=Debug --build=missing") client.run_command("xcodegen generate") client.run("create . --build=missing -s os.sdk=macosx -s os.sdk_version=10.15 " "-c tools.apple:sdk_path='/Applications/Xcode11.7.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk'") assert "sdk 10.15.6" in client.out client.run("create . --build=missing -s os.sdk_version=11.3 " "-c tools.apple:sdk_path='/Applications/Xcode12.5.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk'") assert "sdk 11.3" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") def test_missing_sdk(client): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.apple import XcodeBuild class MyApplicationConan(ConanFile): name = "myapplication" version = "1.0" requires = "hello/0.1" settings = "os", "compiler", "build_type", "arch" generators = "XcodeDeps" exports_sources = "app.xcodeproj/*", "app/*" def build(self): xcode = XcodeBuild(self) xcode.build("app.xcodeproj") """) client.save({"conanfile.py": conanfile, "app/main.cpp": main, "project.yml": xcode_project}, clean_first=True) client.run("install . --build=missing") client.run("install . -s build_type=Debug --build=missing") client.run_command("xcodegen generate") client.run("create . --build=missing -s os.sdk=macosx -s os.sdk_version=12.0 " "-c tools.apple:sdk_path=notexistingsdk", assert_error=True) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") @pytest.mark.parametrize("no_copy_source", [True, False]) def test_project_xcodebuild_cli_args(client, no_copy_source): conanfile = textwrap.dedent(f""" import os from conan import ConanFile from conan.tools.apple import XcodeBuild from conan.tools.files import copy class MyApplicationConan(ConanFile): name = "myapplication" version = "1.0" requires = "hello/0.1" settings = "os", "compiler", "build_type", "arch" generators = "XcodeDeps" exports_sources = "app.xcodeproj/*", "app/*" package_type = "application" no_copy_source = {str(no_copy_source)} def build(self): xb = XcodeBuild(self) proj = os.path.join(self.source_folder, "app.xcodeproj") xc = os.path.join(self.build_folder, "conan_config.xcconfig") xb.build(proj, cli_args=["-xcconfig", xc, f"SYMROOT={{self.build_folder}}", f"OBJROOT={{self.build_folder}}"]) def package(self): copy(self, "{{}}/app".format(self.settings.build_type), self.build_folder, os.path.join(self.package_folder, "bin"), keep_path=False) def package_info(self): self.cpp_info.bindirs = ["bin"] """) client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test, "app/main.cpp": main, "project.yml": xcode_project_bare}, clean_first=True) client.run_command("xcodegen generate") for build_type in ["Release", "Debug"]: client.run(f"create . --build=missing -s build_type={build_type} -c tools.build:verbosity=verbose -c tools.compilation:verbosity=verbose") build_folder = re.search(r"Building your package in (/.+)", client.out).group(1) assert f"OBJROOT = {build_folder}" assert f"SYMROOT = {build_folder}" assert "-xcconfig" in client.out assert f"App {build_type}!" in client.out ================================================ FILE: test/functional/toolchains/apple/test_xcodebuild_targets.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient xcode_project = textwrap.dedent(""" name: HelloLibrary targets: hello-static: type: library.static platform: macOS sources: - src configFiles: Debug: static.xcconfig Release: static.xcconfig hello-dynamic: type: library.dynamic platform: macOS sources: - src configFiles: Debug: dynamic.xcconfig Release: dynamic.xcconfig """) hello_cpp = textwrap.dedent(""" #include "hello.hpp" #include void hellofunction(){ #ifndef DEBUG std::cout << "Hello Release!" << std::endl; #else std::cout << "Hello Debug!" << std::endl; #endif } """) hello_hpp = textwrap.dedent(""" #ifndef hello_hpp #define hello_hpp void hellofunction(); #endif /* hello_hpp */ """) test = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.build import cross_building class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain", "VirtualBuildEnv", "VirtualRunEnv" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if not cross_building(self): cmd = os.path.join(self.cpp.build.bindirs[0], "example") self.run(cmd, env="conanrun") if self.options.shared: self.run("otool -l {}".format(os.path.join(self.cpp.build.bindirs[0], "example"))) else: self.run("nm {}".format(os.path.join(self.cpp.build.bindirs[0], "example"))) """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) find_package(hello CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example hello::hello) """) test_src = textwrap.dedent(""" #include "hello.hpp" int main() { hellofunction(); } """) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.apple import XcodeBuild from conan.tools.files import copy class HelloLib(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "XcodeToolchain" exports_sources = "HelloLibrary.xcodeproj/*", "src/*", "static.xcconfig", "dynamic.xcconfig" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def build(self): xcode = XcodeBuild(self) if self.options.shared: xcode.build("HelloLibrary.xcodeproj", target="hello-dynamic") else: xcode.build("HelloLibrary.xcodeproj", target="hello-static") def package(self): name = "hello-dynamic.dylib" if self.options.shared else "libhello-static.a" copy(self, "build/{}/{}".format(self.settings.build_type, name), src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False) copy(self, "*/*.hpp", src=self.build_folder, dst=os.path.join(self.package_folder, "include"), keep_path=False) def package_info(self): self.cpp_info.libs = ["hello-{}".format("dynamic.dylib" if self.options.shared else "static")] """) static_xcconfig = textwrap.dedent(""" #include \"conan_config.xcconfig\" LD_DYLIB_INSTALL_NAME = @rpath/libhello-static.dylib """) dynamic_xcconfig = textwrap.dedent(""" #include \"conan_config.xcconfig\" LD_DYLIB_INSTALL_NAME = @rpath/hello-dynamic.dylib """) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") def test_shared_static_targets(): """ The pbxproj has defined two targets, one for static and one for dynamic libraries, in the XcodeBuild build helper we pass the target we want to build depending on the shared option """ client = TestClient() client.save({"conanfile.py": conanfile, "src/hello.cpp": hello_cpp, "src/hello.hpp": hello_hpp, "project.yml": xcode_project, "test_package/conanfile.py": test, "test_package/src/example.cpp": test_src, "test_package/CMakeLists.txt": cmakelists, "conan_config.xcconfig": "", "static.xcconfig": static_xcconfig, "dynamic.xcconfig": dynamic_xcconfig}) client.run_command("xcodegen generate") client.run("create . -o *:shared=True -tf=\"\"") assert "Packaged 1 '.dylib' file: hello-dynamic.dylib" in client.out client.run("test test_package hello/1.0@ -o *:shared=True") assert "@rpath/hello-dynamic.dylib" in client.out client.run("create . -tf=\"\"") assert "Packaged 1 '.a' file: libhello-static.a" in client.out client.run("test test_package hello/1.0@") # check the symbol hellofunction in in the executable assert "hellofunction" in client.out ================================================ FILE: test/functional/toolchains/apple/test_xcodedeps_build_configs.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient xcode_project = textwrap.dedent(""" name: app targets: app: type: tool platform: macOS sources: - main.cpp configFiles: Debug: conan_config.xcconfig Release: conan_config.xcconfig """) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("cmake") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") def test_xcodedeps_build_configurations(): client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("export .") client.run("new cmake_lib -d name=bye -d version=0.1 -f") client.run("export .") main = textwrap.dedent(""" #include #include "hello.h" #include "bye.h" int main(int argc, char *argv[]) { hello(); bye(); #ifndef DEBUG std::cout << "App Release!" << std::endl; #else std::cout << "App Debug!" << std::endl; #endif } """) client.save({ "conanfile.txt": "[requires]\nhello/0.1\nbye/0.1\n", "main.cpp": main, "project.yml": xcode_project, }, clean_first=True) for config in ["Release", "Debug"]: client.run("install . -s build_type={} -s arch=x86_64 --build=missing -g XcodeDeps".format(config)) client.run_command("xcodegen generate") for config in ["Release", "Debug"]: client.run_command("xcodebuild -project app.xcodeproj -configuration {} -arch x86_64".format(config)) client.run_command("./build/{}/app".format(config)) assert "App {}!".format(config) in client.out assert "hello/0.1: Hello World {}!".format(config).format(config) in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("cmake") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") def test_frameworks(): client = TestClient(path_with_spaces=False) client.save({"hello.py": GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_package_info(cpp_info={"frameworks": ['CoreFoundation']})}) client.run("export hello.py --name=hello --version=0.1") main = textwrap.dedent(""" #include int main(int argc, char *argv[]) { CFShow(CFSTR("Hello!")); } """) project_name = "app" client.save({"conanfile.txt": "[requires]\nhello/0.1\n", "main.cpp": main, "project.yml": xcode_project}, clean_first=True) client.run("install . -s build_type=Release -s arch=x86_64 --build=missing -g XcodeDeps") client.run_command("xcodegen generate") client.run_command("xcodebuild -project app.xcodeproj -configuration Release -arch x86_64") client.run_command("./build/Release/{}".format(project_name)) assert "Hello!" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") def test_xcodedeps_dashes_names_and_arch(): # https://github.com/conan-io/conan/issues/9949 client = TestClient(path_with_spaces=False) client.save({"conanfile.py": GenConanfile().with_name("hello-dashes").with_version("0.1")}) client.run("export .") main = "int main(int argc, char *argv[]) { return 0; }" client.save({"conanfile.txt": "[requires]\nhello-dashes/0.1\n", "main.cpp": main, "project.yml": xcode_project}, clean_first=True) client.run("install . -s arch=armv8 --build=missing -g XcodeDeps") assert os.path.exists(os.path.join(client.current_folder, "conan_hello_dashes_hello_dashes_release_arm64.xcconfig")) client.run_command("xcodegen generate") client.run_command("xcodebuild -project app.xcodeproj -arch arm64") assert "BUILD SUCCEEDED" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") @pytest.mark.tool("xcodegen") def test_xcodedeps_definitions_escape(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloLib(ConanFile): def package_info(self): self.cpp_info.defines.append("USER_CONFIG=\\"user_config.h\\"") self.cpp_info.defines.append('OTHER="other.h"') ''') client.save({"conanfile.py": conanfile}) client.run("export . --name=hello --version=1.0") main = textwrap.dedent(""" #include #define STR(x) #x #define SHOW_DEFINE(x) printf("%s=%s", #x, STR(x)) int main(int argc, char *argv[]) { SHOW_DEFINE(USER_CONFIG); SHOW_DEFINE(OTHER); return 0; } """) client.save({"conanfile.txt": "[requires]\nhello/1.0\n", "main.cpp": main, "project.yml": xcode_project}, clean_first=True) client.run("install . --build=missing -g XcodeDeps") client.run_command("xcodegen generate") client.run_command("xcodebuild -project app.xcodeproj -configuration Release") client.run_command("./build/Release/app") assert 'USER_CONFIG="user_config.h"' in client.out assert 'OTHER="other.h"' in client.out ================================================ FILE: test/functional/toolchains/apple/test_xcodedeps_components.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("cmake") def test_xcodedeps_components(): """ tcp/1.0 is a lib without components network/1.0 lib that requires tcp/1.0 and has three components: - core - client -> requires core component and tcp::tcp - server -> requires core component and tcp::tcp chat/1.0 -> lib that requires network::client component We create an application called ChatApp that uses XcodeDeps for chat/1.0 dependency And check that we are not requiring more than the needed components, nothing from network::server for example """ client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=tcp -d version=1.0") client.run("create . -tf=\"\"") header = textwrap.dedent(""" #pragma once void {name}(); """) source = textwrap.dedent(""" #include #include "{name}.h" {include} void {name}(){{ {call} #ifdef NDEBUG std::cout << "{name}/1.0: Hello World Release!" << std::endl; #else std::cout << "{name}/1.0: Hello World Debug!" << std::endl; #endif }} """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(myproject CXX) find_package(tcp REQUIRED CONFIG) add_library(core src/core.cpp include/core.h) add_library(client src/client.cpp include/client.h) add_library(server src/server.cpp include/server.h) target_include_directories(core PUBLIC include) target_include_directories(client PUBLIC include) target_include_directories(server PUBLIC include) set_target_properties(core PROPERTIES PUBLIC_HEADER "include/core.h") set_target_properties(client PROPERTIES PUBLIC_HEADER "include/client.h") set_target_properties(server PROPERTIES PUBLIC_HEADER "include/server.h") target_link_libraries(client core tcp::tcp) target_link_libraries(server core tcp::tcp) install(TARGETS core client server) """) conanfile_py = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "CMakeLists.txt", "src/*", "include/*" {requires} generators = "CMakeToolchain", "CMakeDeps" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() {package_info} """) network_pi = """ def package_info(self): self.cpp_info.components["core"].libs = ["core"] self.cpp_info.components["core"].includedirs.append("include") self.cpp_info.components["core"].libdirs.append("lib") self.cpp_info.components["client-test++"].libs = ["client"] self.cpp_info.components["client-test++"].includedirs.append("include") self.cpp_info.components["client-test++"].libdirs.append("lib") self.cpp_info.components["client-test++"].requires.extend(["core", "tcp::tcp"]) self.cpp_info.components["server"].libs = ["server"] self.cpp_info.components["server"].includedirs.append("include") self.cpp_info.components["server"].libdirs.append("lib") self.cpp_info.components["server"].requires.extend(["core", "tcp::tcp"]) """ client.save({ "include/core.h": header.format(name="core"), "include/server.h": header.format(name="server"), "include/client.h": header.format(name="client"), "src/core.cpp": source.format(name="core", include="", call=""), "src/server.cpp": source.format(name="server", include='#include "core.h"\n#include "tcp.h"', call="core(); tcp();"), "src/client.cpp": source.format(name="client", include='#include "core.h"\n#include "tcp.h"', call="core(); tcp();"), "conanfile.py": conanfile_py.format(requires='requires= "tcp/1.0"', package_info=network_pi), "CMakeLists.txt": cmakelists, }, clean_first=True) client.run("create . --name=network --version=1.0") chat_pi = """ def package_info(self): self.cpp_info.libs = ["chat"] self.cpp_info.includedirs.append("include") self.cpp_info.libdirs.append("lib") self.cpp_info.requires.append("network::client-test++") """ cmakelists_chat = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(chat CXX) find_package(network REQUIRED CONFIG) add_library(chat src/chat.cpp include/chat.h) target_include_directories(chat PUBLIC include) set_target_properties(chat PROPERTIES PUBLIC_HEADER "include/chat.h") target_link_libraries(chat network::client-test++) install(TARGETS chat) """) client.save({ "include/chat.h": header.format(name="chat"), "src/chat.cpp": source.format(name="chat", include='#include "client.h"', call="client();"), "conanfile.py": conanfile_py.format(requires='requires= "network/1.0"', package_info=chat_pi), "CMakeLists.txt": cmakelists_chat, }, clean_first=True) client.run("create . --name=chat --version=1.0") xcode_project = textwrap.dedent(""" name: ChatApp fileGroups: - conan targets: chat: type: tool platform: macOS sources: - src configFiles: Debug: conan/conan_config.xcconfig Release: conan/conan_config.xcconfig """) client.save({ "src/main.cpp": '#include "chat.h"\nint main(){chat();return 0;}', "project.yml": xcode_project }, clean_first=True) client.run("install --requires=chat/1.0@ -g XcodeDeps --output-folder=conan") client.run("install --requires=chat/1.0@ -g XcodeDeps --output-folder=conan " "-s build_type=Debug --build=missing") chat_xcconfig = client.load(os.path.join("conan", "conan_chat_chat.xcconfig")) assert '#include "conan_network_client_test__.xcconfig"' in chat_xcconfig assert '#include "conan_network_server.xcconfig"' not in chat_xcconfig assert '#include "conan_network_network.xcconfig"' not in chat_xcconfig host_arch = client.get_default_host_profile().settings['arch'] arch = "arm64" if host_arch == "armv8" else host_arch client.run_command("xcodegen generate") client.run_command(f"xcodebuild -project ChatApp.xcodeproj -configuration Release -arch {arch}") client.run_command(f"xcodebuild -project ChatApp.xcodeproj -configuration Debug -arch {arch}") client.run_command("build/Debug/chat") assert "core/1.0: Hello World Debug!" in client.out assert "tcp/1.0: Hello World Debug!" in client.out assert "client/1.0: Hello World Debug!" in client.out assert "chat/1.0: Hello World Debug!" in client.out client.run_command("build/Release/chat") assert "core/1.0: Hello World Release!" in client.out assert "tcp/1.0: Hello World Release!" in client.out assert "client/1.0: Hello World Release!" in client.out assert "chat/1.0: Hello World Release!" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("cmake") def test_cpp_info_require_whole_package(): """ https://github.com/conan-io/conan/issues/12089 liba has two components liba::cmp1 liba::cmp2 libb has not components and requires liba::liba so should generate the same info as if it was requiring liba::cmp1 and liba::cmp2 libc has components and one component requires liba::liba so should generate the same info as if it was requiring liba::cmp1 and liba::cmp2 """ client = TestClient(path_with_spaces=False) liba = textwrap.dedent(""" import os from conan import ConanFile class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" name = "liba" version = "1.0" def package_info(self): self.cpp_info.components["cmp1"].includedirs.append("cmp1") self.cpp_info.components["cmp2"].includedirs.append("cmp2") """) libb = textwrap.dedent(""" import os from conan import ConanFile class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" name = "libb" version = "1.0" requires = "liba/1.0" def package_info(self): self.cpp_info.requires = ["liba::liba"] """) libc = textwrap.dedent(""" import os from conan import ConanFile class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" name = "libc" version = "1.0" requires = "liba/1.0" def package_info(self): self.cpp_info.components["cmp1"].includedirs.append("cmp1") self.cpp_info.components["cmp2"].includedirs.append("cmp2") self.cpp_info.components["cmp1"].requires = ["liba::liba"] """) client.save({"liba.py": liba, "libb.py": libb, "libc.py": libc}) client.run("create liba.py") client.run("create libb.py") client.run("create libc.py") client.run("install --requires=libb/1.0 -g XcodeDeps -of=libb") libb_xcconfig = client.load(os.path.join("libb", "conan_libb_libb.xcconfig")) assert '#include "conan_liba.xcconfig"' in libb_xcconfig assert '#include "conan_liba_liba.xcconfig"' not in libb_xcconfig client.run("install --requires=libc/1.0 -g XcodeDeps -of=libc") libc_comp1_xcconfig = client.load(os.path.join("libc", "conan_libc_cmp1.xcconfig")) assert '#include "conan_liba.xcconfig"' in libc_comp1_xcconfig assert '#include "conan_liba_liba.xcconfig"' not in libc_comp1_xcconfig @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("cmake") def test_xcodedeps_test_require(): client = TestClient() client.run("new cmake_lib -d name=gtest -d version=1.0") client.run("create . -tf=\"\"") # Create library having build and test requires conanfile = textwrap.dedent(r''' from conan import ConanFile class HelloLib(ConanFile): settings = "os", "compiler", "build_type", "arch" def build_requirements(self): self.test_requires('gtest/1.0') ''') client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -g XcodeDeps") host_arch = client.get_default_host_profile().settings['arch'] arch = "arm64" if host_arch == "armv8" else host_arch assert os.path.isfile(os.path.join(client.current_folder, "conan_gtest.xcconfig")) assert os.path.isfile(os.path.join(client.current_folder, "conan_gtest_gtest.xcconfig")) assert os.path.isfile(os.path.join(client.current_folder, f"conan_gtest_gtest_release_{arch}.xcconfig")) assert '#include "conan_gtest.xcconfig"' in client.load("conandeps.xcconfig") ================================================ FILE: test/functional/toolchains/apple/test_xcodetoolchain.py ================================================ import platform import textwrap import pytest from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient test = textwrap.dedent(""" import os from conan import ConanFile, tools from conan.tools.build import can_run class TestApp(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if can_run(self): self.run("app", env="conanrun") """) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool("xcodebuild") @pytest.mark.parametrize("cppstd, cppstd_output, min_version", [ ("gnu14", "__cplusplus201402", "11.0"), ("gnu17", "__cplusplus201703", "11.0"), ("gnu17", "__cplusplus201703", "10.15") ]) def test_project_xcodetoolchain(cppstd, cppstd_output, min_version): client = TestClient() client.run("new cmake_lib -d name=hello -d version=0.1") client.run("export .") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.apple import XcodeBuild from conan.tools.files import copy class MyApplicationConan(ConanFile): name = "myapplication" version = "1.0" requires = "hello/0.1" settings = "os", "compiler", "build_type", "arch" generators = "XcodeDeps", "XcodeToolchain" exports_sources = "app.xcodeproj/*", "app/*" package_type = "application" def build(self): xcode = XcodeBuild(self) xcode.build("app.xcodeproj") self.run("otool -l build/{}/app".format(self.settings.build_type)) def package(self): copy(self, "build/{}/app".format(self.settings.build_type), self.build_folder, os.path.join(self.package_folder, "bin"), keep_path=False) def package_info(self): self.cpp_info.bindirs = ["bin"] """) xcode_project = textwrap.dedent(""" name: app targets: app: type: tool platform: macOS sources: - app configFiles: Debug: conan_config.xcconfig Release: conan_config.xcconfig """) client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test, "app/main.cpp": gen_function_cpp(name="main", includes=["hello"], calls=["hello"]), "project.yml": xcode_project, "conan_config.xcconfig": ""}, clean_first=True) client.run_command("xcodegen generate") sdk_version = "15.5" settings = "-s arch=x86_64 -s os.sdk_version={} -s compiler.cppstd={} " \ "-s compiler.libcxx=libc++ -s os.version={} " \ "-c tools.build.cross_building:can_run=True " \ "-c 'tools.build:cflags=[\"-fstack-protector-strong\"]'".format(sdk_version, cppstd, min_version) client.run("create . -s build_type=Release {} --build=missing".format(settings)) assert "main __x86_64__ defined" in client.out assert "main {}".format(cppstd_output) in client.out assert "minos {}".format(min_version) in client.out assert "sdk {}".format(sdk_version) in client.out assert "libc++" in client.out assert " -fstack-protector-strong" in client.out ================================================ FILE: test/functional/toolchains/autotools/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/autotools/test_universal_binaries.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_autotools_universal_binary(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools from conan.tools.layout import basic_layout class mylibraryRecipe(ConanFile): name = "mylibrary" version = "1.0" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "configure.ac", "Makefile.am", "src/*" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): basic_layout(self) def generate(self): at_toolchain = AutotoolsToolchain(self) at_toolchain.generate() def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def package(self): autotools = Autotools(self) autotools.install() self.run(f"lipo -info {os.path.join(self.package_folder, 'lib', 'libmylibrary.a')}") def package_info(self): self.cpp_info.libs = ["mylibrary"] """) client.run('new autotools_lib -d name=mylibrary -d version=0.1') client.save({"conanfile.py": conanfile}) client.run('create . -s="arch=armv8|x86_64" -tf=""') assert "libmylibrary.a are: x86_64 arm64" in client.out ================================================ FILE: test/functional/toolchains/cmake/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/cmake/cmakeconfigdeps/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_aliases.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient new_value = "will_break_next" consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Consumer(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = ["CMakeLists.txt"] requires = "hello/1.0" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) @pytest.mark.tool("cmake", "3.27") def test_global_alias(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): # the default global target is "hello::hello" self.cpp_info.set_property("cmake_target_aliases", ["hello"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(_aliased_target hello ALIASED_TARGET) message("hello aliased target: ${_aliased_target}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") assert "hello aliased target: hello::hello" in client.out @pytest.mark.tool("cmake", "3.27") def test_component_alias(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["buy"].set_property("cmake_target_aliases", ["hola::adios"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(_aliased_target hola::adios ALIASED_TARGET) message("hola::adios aliased target: ${_aliased_target}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") assert "hola::adios aliased target: hello::buy" in client.out @pytest.mark.tool("cmake", "3.27") def test_global_and_component_alias(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.set_property("cmake_target_aliases", ["hola::hola"]) self.cpp_info.components["buy"].set_property("cmake_target_aliases", ["hola::adios"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(_aliased_target hola::hola ALIASED_TARGET) message("hola::hola aliased target: ${_aliased_target}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") assert "hola::hola aliased target: hello::hello" in client.out @pytest.mark.tool("cmake", "3.27") def test_custom_name(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.set_property("cmake_target_name", "ola::comprar") self.cpp_info.set_property("cmake_target_aliases", ["hello"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(_aliased_target hello ALIASED_TARGET) message("hello aliased target: ${_aliased_target}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") assert "hello aliased target: ola::comprar" in client.out @pytest.mark.tool("cmake", "3.27") def test_collide_component_alias(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["buy"].set_property("cmake_target_aliases", ["hello::buy"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert "Alias 'hello::buy' already defined as a target in hello/1.0" in client.out @pytest.mark.tool("cmake", "3.27") def test_collide_component_alias_to_alias(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["ola"].set_property("cmake_target_aliases", ["hello::foo"]) self.cpp_info.components["buy"].set_property("cmake_target_aliases", ["hello::foo"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert "Alias 'hello::foo' already defined in hello/1.0" in client.out @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("root_target", ["hello::custom", None]) def test_skip_global_if_aliased(root_target): target_line = f'self.cpp_info.set_property("cmake_target_name", "{root_target}")' if root_target else "" target_name = root_target or "hello::hello" conanfile = textwrap.dedent(f""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): {target_line} self.cpp_info.components["foo"].set_property("cmake_target_aliases", ["{target_name}"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(_aliased_target hello::hello ALIASED_TARGET) message("hello::hello aliased target: ${_aliased_target}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert f"Can't define an alias '{target_name}' for the root target '{target_name}' in hello/1.0" in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py ================================================ import platform import textwrap import pytest from conan.internal.model.pkg_type import PackageType from conan.test.utils.tools import TestClient new_value = "will_break_next" @pytest.mark.parametrize("shared", [True, False]) @pytest.mark.tool("cmake", "3.27") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_osx_frameworks(shared): """ Testing custom package frameworks + system frameworks + requirements """ client = TestClient() client.run("new cmake_lib -d name=dep -d version=1.0") client.run(f"create . -tf='' -o '&:shared={shared}'") cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyFramework CXX) find_package(dep CONFIG REQUIRED) add_library(MyFramework frame.cpp frame.h) set_target_properties(MyFramework PROPERTIES FRAMEWORK TRUE FRAMEWORK_VERSION C # Version "A" is macOS convention MACOSX_FRAMEWORK_IDENTIFIER MyFramework PUBLIC_HEADER frame.h ) target_link_libraries(MyFramework PRIVATE dep::dep) if(BUILD_SHARED_LIBS) target_link_libraries(MyFramework PRIVATE "-framework CoreFoundation") endif() install(TARGETS MyFramework FRAMEWORK DESTINATION .) """) frame_cpp = textwrap.dedent(""" #include "frame.h" #include "dep.h" #include #include void greet() { // CoreFoundation CFTypeRef keys[] = {CFSTR("key")}; CFTypeRef values[] = {CFSTR("value")}; CFDictionaryRef dict = CFDictionaryCreate(kCFAllocatorDefault, keys, values, sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (dict) CFRelease(dict); // MyFramework std::cout << "Hello from MyFramework!" << std::endl; // dep requirement dep(); } """) frame_h = textwrap.dedent(""" #pragma once #include #include void greet(); """) cpp_info_type = PackageType.SHARED if shared else PackageType.STATIC conanfile = textwrap.dedent(f""" import os from conan import ConanFile from conan.tools.cmake import CMake class MyFramework(ConanFile): name = "frame" version = "1.0" settings = "os", "arch", "compiler", "build_type" options = {{"shared": [True, False], "fPIC": [True, False]}} default_options = {{"shared": False, "fPIC": True}} exports_sources = "frame.cpp", "frame.h", "CMakeLists.txt" generators = "CMakeToolchain", "CMakeConfigDeps" requires = "dep/1.0" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.type = "{cpp_info_type}" self.cpp_info.package_framework = "MyFramework" self.cpp_info.location = os.path.join(self.package_folder, "MyFramework.framework", "MyFramework") # Using also a system framework self.cpp_info.frameworks = ["CoreFoundation"] """) test_main_cpp = textwrap.dedent(""" #include int main() { greet(); } """) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.build import can_run class LibTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeConfigDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """) test_cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) find_package(frame CONFIG REQUIRED) add_executable(example main.cpp) target_link_libraries(example frame::frame) """) client.save({ 'test_package/main.cpp': test_main_cpp, 'test_package/CMakeLists.txt': test_cmakelists, 'test_package/conanfile.py': test_conanfile, 'CMakeLists.txt': cmakelists, 'frame.cpp': frame_cpp, 'frame.h': frame_h, 'conanfile.py': conanfile }, clean_first=True) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value} -o '*:shared={shared}'") assert "Hello from MyFramework!" in client.out assert "dep/1.0: Hello World" in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new.py ================================================ import os import platform import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.tools import TestClient new_value = "will_break_next" @pytest.mark.tool("cmake") class TestExes: @pytest.mark.parametrize("editable", [False, True]) def test_exe(self, editable): conanfile = textwrap.dedent(r""" import os, platform from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Test(ConanFile): name = "mytool" version = "0.1" package_type = "application" settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" exports_sources = "*" def layout(self): cmake_layout(self) self.cpp.build.exe = "mytool" name = "mytool.exe" if platform.system() == "Windows" else "mytool" app_loc = os.path.join("build", str(self.settings.build_type), name) self.cpp.build.location = app_loc def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.exe = "mytool" self.cpp_info.set_property("cmake_target_name", "MyTool::myexe") self.cpp_info.location = os.path.join("bin", "mytool") """) main = textwrap.dedent(""" #include #include int main() { std::cout << "Mytool generating out.c!!!!!" << std::endl; std::ofstream f("out.c"); } """) c = TestClient() c.run("new cmake_exe -d name=mytool -d version=0.1") c.save({"conanfile.py": conanfile, "src/main.cpp": main}) if editable: c.run("editable add .") c.run("create .") for requires in ("tool_requires", "requires"): consumer = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" {requires} = "mytool/0.1" def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = textwrap.dedent(""" set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(consumer C) find_package(mytool) add_custom_command(OUTPUT out.c COMMAND MyTool::myexe) add_library(myLib out.c) """) c.save({f"consumer_{requires}/conanfile.py": consumer, f"consumer_{requires}/CMakeLists.txt": cmake}) c.run(f"build consumer_{requires} -c tools.cmake.cmakedeps:new={new_value}") assert "find_package(mytool)" in c.out assert "target_link_libraries(..." not in c.out assert "Conan: Target declared imported executable 'MyTool::myexe'" in c.out assert "Mytool generating out.c!!!!!" in c.out def test_exe_components(self): conanfile = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.cmake import CMake class Test(ConanFile): name = "mytool" version = "0.1" package_type = "application" settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" exports_sources = "*" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.components["my1exe"].exe = "mytool1" self.cpp_info.components["my1exe"].set_property("cmake_target_name", "MyTool::my1exe") self.cpp_info.components["my1exe"].location = os.path.join("bin", "mytool1") self.cpp_info.components["my2exe"].exe = "mytool2" self.cpp_info.components["my2exe"].set_property("cmake_target_name", "MyTool::my2exe") self.cpp_info.components["my2exe"].location = os.path.join("bin", "mytool2") """) main = textwrap.dedent(""" #include #include int main() {{ std::cout << "Mytool{number} generating out{number}.c!!!!!" << std::endl; std::ofstream f("out{number}.c"); }} """) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(proj CXX) add_executable(mytool1 src/main1.cpp) add_executable(mytool2 src/main2.cpp) install(TARGETS mytool1 DESTINATION "." RUNTIME DESTINATION bin) install(TARGETS mytool2 DESTINATION "." RUNTIME DESTINATION bin) """) c = TestClient() c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "src/main1.cpp": main.format(number=1), "src/main2.cpp": main.format(number=2) }) c.run("create .") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" tool_requires = "mytool/0.1" def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = textwrap.dedent(""" set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(consumer C) find_package(mytool) add_custom_command(OUTPUT out1.c COMMAND MyTool::my1exe) add_custom_command(OUTPUT out2.c COMMAND MyTool::my2exe) add_library(myLib out1.c out2.c) """) c.save({"conanfile.py": consumer, "CMakeLists.txt": cmake}, clean_first=True) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "Conan: Target declared imported executable 'MyTool::my1exe'" in c.out assert "Mytool1 generating out1.c!!!!!" in c.out assert "Conan: Target declared imported executable 'MyTool::my2exe'" in c.out assert "Mytool2 generating out2.c!!!!!" in c.out @pytest.mark.tool("cmake") class TestLibs: def test_libs(self, matrix_client): c = matrix_client c.run("new cmake_lib -d name=app -d version=0.1 -d requires=matrix/1.0") c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "find_package(matrix)" in c.out assert "target_link_libraries(... matrix::matrix)" in c.out assert "Conan: Target declared imported STATIC library 'matrix::matrix'" in c.out @pytest.mark.parametrize("shared", [False, True]) def test_libs_transitive(self, transitive_libraries, shared): c = transitive_libraries c.run("new cmake_lib -d name=app -d version=0.1 -d requires=engine/1.0") shared = "-o engine/*:shared=True" if shared else "" c.run(f"build . {shared} -c tools.cmake.cmakedeps:new={new_value}") assert "find_package(engine)" in c.out assert "target_link_libraries(... engine::engine)" in c.out if shared: assert "matrix::matrix" not in c.out # It is hidden as static behind the engine assert "Conan: Target declared imported SHARED library 'engine::engine'" in c.out else: assert "Conan: Target declared imported STATIC library 'matrix::matrix'" in c.out assert "Conan: Target declared imported STATIC library 'engine::engine'" in c.out # if not using cmake >= 3.23 the intermediate gamelib_test linkage fail @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("shared", [False, True]) def test_multilevel(self, shared): # TODO: make this shared fixtures in conftest for multi-level shared testing c = TestClient(default_server_user=True) c.run("new cmake_lib -d name=matrix -d version=0.1") c.run(f"create . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}") c.save({}, clean_first=True) c.run("new cmake_lib -d name=engine -d version=0.1 -d requires=matrix/0.1") c.run(f"create . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}") c.save({}, clean_first=True) c.run("new cmake_lib -d name=gamelib -d version=0.1 -d requires=engine/0.1") # This specific CMake fails for shared libraries with old CMakeDeps in Linux cmake = textwrap.dedent("""\ set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(gamelib CXX) find_package(engine CONFIG REQUIRED) add_library(gamelib src/gamelib.cpp) target_include_directories(gamelib PUBLIC include) target_link_libraries(gamelib PRIVATE engine::engine) add_executable(gamelib_test src/gamelib_test.cpp) target_link_libraries(gamelib_test PRIVATE gamelib) set_target_properties(gamelib PROPERTIES PUBLIC_HEADER "include/gamelib.h") install(TARGETS gamelib) """) # Testing that a local test executable links correctly with the new CMakeDeps # It fails with the old CMakeDeps c.save({"CMakeLists.txt": cmake, "src/gamelib_test.cpp": '#include "gamelib.h"\nint main() { gamelib(); }'}) c.run(f"create . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}") c.save({}, clean_first=True) c.run("new cmake_exe -d name=game -d version=0.1 -d requires=gamelib/0.1") c.run(f"create . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}") assert "matrix/0.1: Hello World Release!" assert "engine/0.1: Hello World Release!" assert "gamelib/0.1: Hello World Release!" assert "game/0.1: Hello World Release!" # Make sure that transitive headers are private, fails to include, traits work game_cpp = c.load("src/game.cpp") for header in ("matrix", "engine"): new_game_cpp = f"#include <{header}.h>\n" + game_cpp c.save({"src/game.cpp": new_game_cpp}) c.run(f"build . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert f"{header}.h" in c.out # Make sure it works downloading to another cache c.run("upload * -r=default -c") c.run("remove * -c") c2 = TestClient(servers=c.servers) c2.run("new cmake_exe -d name=game -d version=0.1 -d requires=gamelib/0.1") c2.run(f"create . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}") assert "matrix/0.1: Hello World Release!" assert "engine/0.1: Hello World Release!" assert "gamelib/0.1: Hello World Release!" assert "game/0.1: Hello World Release!" class TestLibsIntegration: def test_libs_no_location(self): # Integration test # https://github.com/conan-io/conan/issues/17256 c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "0.1" settings = "build_type" def package_info(self): self.cpp_info.libs = ["dep"] """) c.save({"dep/conanfile.py": dep, "app/conanfile.py": GenConanfile().with_requires("dep/0.1") .with_settings("build_type")}) c.run("create dep") c.run(f"install app -c tools.cmake.cmakedeps:new={new_value} -g CMakeDeps", assert_error=True) assert "ERROR: Error in generator 'CMakeDeps': dep/0.1: Cannot obtain 'location' " \ "for library 'dep'" in c.out def test_custom_file_targetname(self): # Integration test c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "0.1" settings = "build_type" def package_info(self): self.cpp_info.set_property("cmake_file_name", "MyDep") self.cpp_info.set_property("cmake_target_name", "MyTargetDep") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1"), "app/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_settings("build_type")}) c.run("create dep") c.run("create pkg") c.run(f"install app -c tools.cmake.cmakedeps:new={new_value} -g CMakeDeps") targets_cmake = c.load("app/pkg-Targets-release.cmake") assert "find_dependency(MyDep REQUIRED CONFIG)" in targets_cmake assert 'set_property(TARGET pkg::pkg APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n' \ ' "$<$:MyTargetDep>")' in targets_cmake class TestLibsLinkageTraits: def test_linkage_shared_static(self): """ the static library is skipped """ c = TestClient() c.run("new cmake_lib -d name=matrix -d version=0.1") c.run(f"create . -c tools.cmake.cmakedeps:new={new_value} -tf=") c.save({}, clean_first=True) c.run("new cmake_lib -d name=engine -d version=0.1 -d requires=matrix/0.1") c.run(f"create . -o engine/*:shared=True -c tools.cmake.cmakedeps:new={new_value} -tf=") c.save({}, clean_first=True) c.run("new cmake_exe -d name=game -d version=0.1 -d requires=engine/0.1") c.run(f"create . -o engine/*:shared=True -c tools.cmake.cmakedeps:new={new_value} " "-c tools.compilation:verbosity=verbose") assert re.search(r"Skipped binaries(\s*)matrix/0.1", c.out) assert "matrix/0.1: Hello World Release!" assert "engine/0.1: Hello World Release!" assert "game/0.1: Hello World Release!" @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("shared", [False, True]) def test_transitive_headers(self, shared): c = TestClient() c.run("new cmake_lib -d name=matrix -d version=0.1") c.run(f"create . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value} -tf=") c.save({}, clean_first=True) c.run("new cmake_lib -d name=engine -d version=0.1 -d requires=matrix/0.1") engine_h = c.load("include/engine.h") engine_h = "#include \n" + engine_h c.save({"include/engine.h": engine_h}) conanfile = c.load("conanfile.py") conanfile = conanfile.replace('self.requires("matrix/0.1")', 'self.requires("matrix/0.1", transitive_headers=True)') c.save({"conanfile.py": conanfile}) c.run(f"create . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value} -tf=") c.save({}, clean_first=True) c.run("new cmake_exe -d name=game -d version=0.1 -d requires=engine/0.1") c.run(f"build . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}") # it works @pytest.mark.tool("cmake", "3.27") def test_link_features(self): tc = TestClient() tc.run("new cmake_lib -d name=matrix -d version=0.1") conanfile = tc.load("conanfile.py") # So that the custom link feature works conanfile += (" self.cpp_info.set_property('cmake_extra_variables', " "{'CMAKE_LINK_LIBRARY_USING_MYFET_SUPPORTED': True," "'CMAKE_LINK_LIBRARY_USING_MYFET': ''})\n") # And set this library to use the custom link feature conanfile += " self.cpp_info.set_property('cmake_link_feature', 'MYFET')" tc.save({"conanfile.py": conanfile}) tc.run(f"create -c tools.cmake.cmakedeps:new={new_value}") tc.save({}, clean_first=True) tc.run("new cmake_lib -d name=lib -d version=0.1 -d requires=matrix/0.1") tc.run(f"create -c tools.cmake.cmakedeps:new={new_value}") test_build_folder = tc.created_test_build_folder("lib/0.1") test_generators_folder = os.path.join("test_package", test_build_folder, "generators") libs_targets = tc.load(os.path.join(test_generators_folder, "lib-Targets-release.cmake")) assert '"$:matrix::matrix>>"' in libs_targets @pytest.mark.tool("cmake") class TestLibsComponents: def test_libs_components(self, matrix_client_components): """ explicit usage of components """ c = matrix_client_components # TODO: Check that find_package(.. COMPONENTS nonexisting) fails c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp) # standard one, and a custom cmake_target_name one target_link_libraries(app PRIVATE matrix::module MatrixHeaders) """) app_cpp = textwrap.dedent(""" #include "module.h" #include "headers.h" int main() { module(); headers();} """) c.save({"CMakeLists.txt": cmake, "src/app.cpp": app_cpp}) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "find_package(matrix)" in c.out assert "target_link_libraries(... matrix::matrix)" in c.out assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out if platform.system() == "Windows": c.run_command(r".\build\Release\app.exe") assert "Matrix headers __cplusplus: __cplusplus2014" in c.out def test_libs_components_default(self, matrix_client_components): """ Test that the default components are used when no component is specified """ c = matrix_client_components c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") app_cpp = textwrap.dedent(""" #include "module.h" int main() { module();} """) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp) target_link_libraries(app PRIVATE matrix::matrix) """) c.save({"src/app.cpp": app_cpp, "CMakeLists.txt": cmake}) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out assert "Conan: Target declared imported INTERFACE library 'MatrixHeaders'" in c.out def test_libs_components_default_error(self, matrix_client_components): """ Same as above, but it fails, because headers is not in the default components """ c = matrix_client_components c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") app_cpp = textwrap.dedent(""" #include "module.h" #include "headers.h" int main() { module();} """) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp) target_link_libraries(app PRIVATE matrix::matrix) """) c.save({"src/app.cpp": app_cpp, "CMakeLists.txt": cmake}) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert "Error in build() method, line 35" in c.out assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp) target_link_libraries(app PRIVATE matrix::matrix MatrixHeaders) """) c.save({"CMakeLists.txt": cmake}) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "Running CMake.build()" in c.out # Now it doesn't fail def test_libs_components_transitive(self, matrix_client_components): """ explicit usage of components matrix::module -> matrix::vector engine::bots -> engine::physix engine::physix -> matrix::vector engine::world -> engine::physix, matrix::module """ c = matrix_client_components bots_h = gen_function_h(name="bots") bots_cpp = gen_function_cpp(name="bots", includes=["bots", "physix"], calls=["physix"]) physix_h = gen_function_h(name="physix") physix_cpp = gen_function_cpp(name="physix", includes=["physix", "vector"], calls=["vector"]) world_h = gen_function_h(name="world") world_cpp = gen_function_cpp(name="world", includes=["world", "physix", "module"], calls=["physix", "module"]) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class Engine(ConanFile): name = "engine" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" exports_sources = "src/*", "CMakeLists.txt" requires = "matrix/1.0" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.set_property("cmake_file_name", "MyEngine") self.cpp_info.components["bots"].libs = ["bots"] self.cpp_info.components["bots"].includedirs = ["include"] self.cpp_info.components["bots"].libdirs = ["lib"] self.cpp_info.components["bots"].requires = ["physix"] self.cpp_info.components["physix"].libs = ["physix"] self.cpp_info.components["physix"].includedirs = ["include"] self.cpp_info.components["physix"].libdirs = ["lib"] self.cpp_info.components["physix"].requires = ["matrix::vector"] self.cpp_info.components["world"].libs = ["world"] self.cpp_info.components["world"].includedirs = ["include"] self.cpp_info.components["world"].libdirs = ["lib"] self.cpp_info.components["world"].requires = ["physix", "matrix::module"] """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(matrix CXX) find_package(matrix CONFIG REQUIRED) add_library(physix src/physix.cpp) add_library(bots src/bots.cpp) add_library(world src/world.cpp) target_link_libraries(physix PRIVATE matrix::vector) target_link_libraries(bots PRIVATE physix) target_link_libraries(world PRIVATE physix matrix::module) set_target_properties(bots PROPERTIES PUBLIC_HEADER "src/bots.h") set_target_properties(physix PROPERTIES PUBLIC_HEADER "src/physix.h") set_target_properties(world PROPERTIES PUBLIC_HEADER "src/world.h") install(TARGETS physix bots world) """) c.save({"src/physix.h": physix_h, "src/physix.cpp": physix_cpp, "src/bots.h": bots_h, "src/bots.cpp": bots_cpp, "src/world.h": world_h, "src/world.cpp": world_cpp, "CMakeLists.txt": cmakelists, "conanfile.py": conanfile}) c.run("create .") c.save({}, clean_first=True) c.run("new cmake_exe -d name=app -d version=0.1 -d requires=engine/1.0") cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(MyEngine CONFIG REQUIRED) add_executable(app src/app.cpp) target_link_libraries(app PRIVATE engine::bots) install(TARGETS app) """) app_cpp = textwrap.dedent(""" #include "bots.h" int main() { bots();} """) c.save({"CMakeLists.txt": cmake, "src/app.cpp": app_cpp}) c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") assert "find_package(MyEngine)" in c.out assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out assert "Conan: Target declared imported INTERFACE library 'matrix::matrix'" in c.out assert "Conan: Target declared imported STATIC library 'engine::bots'" in c.out assert "Conan: Target declared imported STATIC library 'engine::physix'" in c.out assert "Conan: Target declared imported STATIC library 'engine::world'" in c.out assert "Conan: Target declared imported INTERFACE library 'engine::engine'" in c.out assert "bots: Release!" in c.out assert "physix: Release!" in c.out assert "vector: Release!" in c.out def test_libs_components_multilib(self): """ cpp_info.libs = ["lib1", "lib2"] """ c = TestClient() vector_h = gen_function_h(name="vector") vector_cpp = gen_function_cpp(name="vector", includes=["vector"]) module_h = gen_function_h(name="module") module_cpp = gen_function_cpp(name="module", includes=["module"]) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Matrix(ConanFile): name = "matrix" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" exports_sources = "src/*", "CMakeLists.txt" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.set_property("cmake_target_name", "MyMatrix::MyMatrix") self.cpp_info.libs = ["module", "vector"] """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(matrix CXX) add_library(module src/module.cpp) add_library(vector src/vector.cpp) set_target_properties(vector PROPERTIES PUBLIC_HEADER "src/vector.h") set_target_properties(module PROPERTIES PUBLIC_HEADER "src/module.h") install(TARGETS module vector) """) c.save({"src/module.h": module_h, "src/module.cpp": module_cpp, "src/vector.h": vector_h, "src/vector.cpp": vector_cpp, "CMakeLists.txt": cmakelists, "conanfile.py": conanfile}) c.run("create .") c.save({}, clean_first=True) c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp) target_link_libraries(app PRIVATE MyMatrix::MyMatrix) install(TARGETS app) """) app_cpp = textwrap.dedent(""" #include "vector.h" #include "module.h" int main() { vector();module();} """) c.save({"CMakeLists.txt": cmake, "src/app.cpp": app_cpp}) c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") assert "Conan: Target declared imported STATIC library 'matrix::_vector'" in c.out assert "Conan: Target declared imported STATIC library 'matrix::_module'" in c.out assert "Conan: Target declared imported INTERFACE library 'MyMatrix::MyMatrix'" in c.out assert "matrix::matrix" not in c.out assert "vector: Release!" in c.out assert "module: Release!" in c.out assert "vector: Release!" in c.out def test_libs_components_multilib_component(self): """ cpp_info.components["mycomp"].libs = ["lib1", "lib2"] """ c = TestClient() vector_h = gen_function_h(name="vector") vector_cpp = gen_function_cpp(name="vector", includes=["vector"]) module_h = gen_function_h(name="module") module_cpp = gen_function_cpp(name="module", includes=["module"]) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Matrix(ConanFile): name = "matrix" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" exports_sources = "src/*", "CMakeLists.txt" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.set_property("cmake_target_name", "MyMatrix::MyMatrix") self.cpp_info.components["mycomp"].set_property("cmake_target_name", "MyComp::MyComp") self.cpp_info.components["mycomp"].libs = ["module", "vector"] """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(matrix CXX) add_library(module src/module.cpp) add_library(vector src/vector.cpp) set_target_properties(vector PROPERTIES PUBLIC_HEADER "src/vector.h") set_target_properties(module PROPERTIES PUBLIC_HEADER "src/module.h") install(TARGETS module vector) """) c.save({"src/module.h": module_h, "src/module.cpp": module_cpp, "src/vector.h": vector_h, "src/vector.cpp": vector_cpp, "CMakeLists.txt": cmakelists, "conanfile.py": conanfile}) c.run("create .") c.save({}, clean_first=True) c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp) target_link_libraries(app PRIVATE MyMatrix::MyMatrix) install(TARGETS app) """) app_cpp = textwrap.dedent(""" #include "vector.h" #include "module.h" int main() { vector();module();} """) c.save({"CMakeLists.txt": cmake, "src/app.cpp": app_cpp}) c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") assert "Conan: Target declared imported STATIC library 'matrix::_mycomp_vector'" in c.out assert "Conan: Target declared imported STATIC library 'matrix::_mycomp_module'" in c.out assert "Conan: Target declared imported INTERFACE library 'MyMatrix::MyMatrix'" in c.out assert "matrix::matrix" not in c.out assert "vector: Release!" in c.out assert "module: Release!" in c.out assert "vector: Release!" in c.out @pytest.mark.tool("cmake") class TestHeaders: def test_header_lib(self, matrix_client): c = matrix_client conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class EngineHeader(ConanFile): name = "engine" version = "1.0" requires = "matrix/1.0" exports_sources = "*.h" settings = "compiler" def package(self): copy(self, "*.h", src=self.source_folder, dst=self.package_folder) def package_id(self): self.info.clear() def package_info(self): self.cpp_info.defines = ["MY_MATRIX_HEADERS_DEFINE=1", "MY_MATRIX_HEADERS_DEFINE2=1"] # Few flags to cover that CMakeDeps doesn't crash with them if self.settings.compiler == "msvc": self.cpp_info.cxxflags = ["/Zc:__cplusplus"] self.cpp_info.cflags = ["/Zc:__cplusplus"] self.cpp_info.system_libs = ["ws2_32"] else: self.cpp_info.system_libs = ["m", "dl"] # Just to verify CMake don't break if self.settings.compiler == "gcc": # This triggers errors when not quoted and list ";" separated self.cpp_info.sharedlinkflags = ["SHELL:-z lazy", "SHELL:-u symbol1", "SHELL:-u symbol2", "-s"] self.cpp_info.exelinkflags = ["SHELL:-z lazy", "SHELL:-u symbol1", "SHELL:-u symbol2", "-s"] """) engine_h = textwrap.dedent(""" #pragma once #include #include "matrix.h" #ifndef MY_MATRIX_HEADERS_DEFINE #error "Fatal error MY_MATRIX_HEADERS_DEFINE not defined" #endif #ifndef MY_MATRIX_HEADERS_DEFINE2 #error "Fatal error MY_MATRIX_HEADERS_DEFINE2 not defined" #endif void engine(){ std::cout << "Engine!" < #ifndef MY_MATRIX_HEADERS_{version}_DEFINE #error "Fatal error MY_MATRIX_HEADERS_{version}_DEFINE not defined" #endif void engine(){{ std::cout << "Engine {version}!" < #include #include "protobuf.h" int main() { protobuf(); #ifdef NDEBUG std::cout << "Protoc RELEASE generating out.c!!!!!" << std::endl; #else std::cout << "Protoc DEBUG generating out.c!!!!!" << std::endl; #endif std::ofstream f("out.c"); } """) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(protobuf CXX) add_library(protobuf src/protobuf.cpp) add_executable(protoc src/main.cpp) target_link_libraries(protoc PRIVATE protobuf) set_target_properties(protobuf PROPERTIES PUBLIC_HEADER "src/protobuf.h") install(TARGETS protoc protobuf) """) c = TestClient() c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "src/protobuf.h": gen_function_h(name="protobuf"), "src/protobuf.cpp": gen_function_cpp(name="protobuf", includes=["protobuf"]), "src/main.cpp": main}) c.run("export .") consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "protobuf/0.1" generators = "CMakeToolchain", "CMakeDeps" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.path.join(self.cpp.build.bindir, "myapp")) """) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(consumer CXX) find_package(MyProtobuf CONFIG REQUIRED) add_custom_command(OUTPUT out.c COMMAND Protobuf::Protocompile) add_executable(myapp myapp.cpp out.c) target_link_libraries(myapp PRIVATE protobuf::protobuf) get_target_property(imported_configs Protobuf::Protocompile IMPORTED_CONFIGURATIONS) message(STATUS "Protoc imported configurations: ${imported_configs}!!!") """) myapp = textwrap.dedent(""" #include #include "protobuf.h" int main() { protobuf(); std::cout << "MyApp" << std::endl; } """) c.save({"conanfile.py": consumer, "CMakeLists.txt": cmake, "myapp.cpp": myapp}, clean_first=True) return c def test_requires(self, protobuf): c = protobuf c.run(f"build . --build=missing -c tools.cmake.cmakedeps:new={new_value}") assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out assert "Protoc RELEASE generating out.c!!!!!" in c.out assert 'Protoc imported configurations: RELEASE!!!' in c.out def test_both(self, protobuf): consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "protobuf/0.1" tool_requires = "protobuf/0.1" generators = "CMakeToolchain", "CMakeDeps" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) c = protobuf c.save({"conanfile.py": consumer}) c.run("build . -s:h build_type=Debug --build=missing " f"-c tools.cmake.cmakedeps:new={new_value}") assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out assert "Protoc RELEASE generating out.c!!!!!" in c.out assert "protobuf: Release!" in c.out assert "protobuf: Debug!" not in c.out assert 'Protoc imported configurations: RELEASE!!!' in c.out cmd = "./build/Debug/myapp" if platform.system() != "Windows" else r"build\Debug\myapp" c.run_command(cmd) assert "protobuf: Debug!" in c.out assert "protobuf: Release!" not in c.out c.run("build . --build=missing " f"-c tools.cmake.cmakedeps:new={new_value}") assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out assert "Protoc RELEASE generating out.c!!!!!" in c.out assert "protobuf: Release!" in c.out assert "protobuf: Debug!" not in c.out assert 'Protoc imported configurations: RELEASE!!!' in c.out cmd = "./build/Release/myapp" if platform.system() != "Windows" else r"build\Release\myapp" c.run_command(cmd) assert "protobuf: Debug!" not in c.out assert "protobuf: Release!" in c.out @pytest.mark.tool("cmake", "3.27") class TestConfigs: @pytest.mark.skipif(platform.system() != "Windows", reason="Only MSVC multi-conf") def test_multi_config(self, matrix_client): c = matrix_client c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") c.run("install . -s build_type=Debug --build=missing " f"-c tools.cmake.cmakedeps:new={new_value}") c.run_command("cmake --preset conan-default") c.run_command("cmake --build --preset conan-release") c.run_command("cmake --build --preset conan-debug") c.run_command("build\\Release\\app") assert "matrix/1.0: Hello World Release!" in c.out assert "app/0.1: Hello World Release!" in c.out c.run_command("build\\Debug\\app") assert "matrix/1.0: Hello World Debug!" in c.out assert "app/0.1: Hello World Debug!" in c.out def test_cross_config(self, matrix_client): # Release dependencies, but compiling app in Debug c = matrix_client c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") c.run(f"install . -s &:build_type=Debug -c tools.cmake.cmakedeps:new={new_value}") # With modern CMake > 3.26 not necessary set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.27) project(app CXX) # set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp src/main.cpp) target_link_libraries(app PRIVATE matrix::matrix) """) c.save({"CMakeLists.txt": cmake}) preset = "conan-default" if platform.system() == "Windows" else "conan-debug" c.run_command(f"cmake --preset {preset}") c.run_command("cmake --build --preset conan-debug") c.run_command(os.path.join("build", "Debug", "app")) assert "matrix/1.0: Hello World Release!" in c.out assert "app/0.1: Hello World Debug!" in c.out def test_cross_config_components(self, matrix_client_components): # Release dependencies, but compiling app in Debug c = matrix_client_components c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") app_cpp = textwrap.dedent(""" #include "app.h" #include "module.h" void app(){ module(); }""") c.save({"src/app.cpp": app_cpp, "src/main.cpp": gen_function_cpp(name="main", includes=["app"], calls=["app"])}) c.run(f"install . -s &:build_type=Debug -c tools.cmake.cmakedeps:new={new_value}") # With modern CMake > 3.26 not necessary set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.27) project(app CXX) # set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp src/main.cpp) target_link_libraries(app PRIVATE matrix::matrix) """) c.save({"CMakeLists.txt": cmake}) preset = "conan-default" if platform.system() == "Windows" else "conan-debug" c.run_command(f"cmake --preset {preset}") c.run_command("cmake --build --preset conan-debug") c.run_command(os.path.join("build", "Debug", "app")) assert "main: Debug!" in c.out assert "module: Release!" in c.out assert "vector: Release!" in c.out @pytest.mark.skipif(platform.system() == "Windows", reason="This doesn't work in MSVC") def test_cross_config_implicit(self, matrix_client): # Release dependencies, but compiling app in Debug, without specifying it c = matrix_client c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") # With modern CMake > 3.26 not necessary set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.27) project(app CXX) # set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) find_package(matrix CONFIG REQUIRED) add_executable(app src/app.cpp src/main.cpp) target_link_libraries(app PRIVATE matrix::matrix) """) c.save({"CMakeLists.txt": cmake}) # Now we can force the Debug build, even if dependencies are Release c.run_command("cmake . -DCMAKE_BUILD_TYPE=Debug -B build " "-DCMAKE_PREFIX_PATH=build/Release/generators") c.run_command("cmake --build build") c.run_command("./build/app") assert "matrix/1.0: Hello World Release!" in c.out assert "app/0.1: Hello World Debug!" in c.out @pytest.mark.tool("cmake", "3.23") class TestCMakeTry: def test_check_c_source_compiles(self, matrix_client): """ https://github.com/conan-io/conan/issues/12012 """ c = matrix_client # it brings the "matrix" package dependency pre-built consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps class PkgConan(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "matrix/1.0" generators = "CMakeToolchain", def generate(self): deps = CMakeDeps(self) deps.set_property("matrix", "cmake_additional_variables_prefixes", ["MyMatrix"]) deps.generate() """) cmakelist = textwrap.dedent("""\ set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Hello LANGUAGES CXX) find_package(matrix CONFIG REQUIRED) include(CheckCXXSourceCompiles) set(CMAKE_REQUIRED_INCLUDES ${MyMatrix_INCLUDE_DIRS}) set(CMAKE_REQUIRED_LIBRARIES ${MyMatrix_LIBRARIES}) check_cxx_source_compiles("#include int main(void) { matrix();return 0; }" IT_COMPILES) """) c.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelist}, clean_first=True) c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") preset = "conan-default" if platform.system() == "Windows" else "conan-release" c.run_command(f"cmake --preset {preset} ") assert "Performing Test IT_COMPILES - Success" in c.out class TestCMakeComponents: @pytest.mark.tool("cmake") @pytest.mark.parametrize("components, found", [("comp1", True), ("compX", False)]) def test_components(self, components, found): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_components", ["comp1", "comp2"]) """) c.save({"conanfile.py": dep}) c.run("create .") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class PkgConan(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "dep/0.1" generators = "CMakeToolchain", "CMakeDeps" def build(self): deps = CMake(self) deps.configure() """) cmakelist = textwrap.dedent(f"""\ cmake_minimum_required(VERSION 3.15) project(Hello LANGUAGES NONE) find_package(dep CONFIG REQUIRED COMPONENTS {components}) """) c.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelist}, clean_first=True) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}", assert_error=not found) if not found: assert f"Conan: Error: 'dep' required COMPONENT '{components}' not found" in c.out def test_components_default_definition(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): # private component that should be skipped self.cpp_info.components["_private"].includedirs = ["include"] self.cpp_info.components["c1"].set_property("cmake_target_name", "MyC1") self.cpp_info.components["c2"].set_property("cmake_target_name", "Dep::MyC2") self.cpp_info.components["c3"].includedirs = ["include"] """) c.save({"conanfile.py": dep}) c.run("create .") c.run(f"install --requires=dep/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}") cmake = c.load("dep-config.cmake") assert 'set(dep_PACKAGE_PROVIDED_COMPONENTS MyC1 MyC2 c3)' in cmake def test_components_individual_names(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.components["c1"].set_property("cmake_target_name", "MyC1") self.cpp_info.components["c1"].set_property("cmake_components", ["MyCompC1"]) self.cpp_info.components["c2"].set_property("cmake_target_name", "Dep::MyC2") self.cpp_info.components["c3"].includedirs = ["include"] """) c.save({"conanfile.py": dep}) c.run("create .") c.run(f"install --requires=dep/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}") cmake = c.load("dep-config.cmake") assert 'set(dep_PACKAGE_PROVIDED_COMPONENTS MyCompC1 MyC2 c3)' in cmake class TestCppInfoChecks: def test_check_exe_libs(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.libs = ["mylib"] self.cpp_info.exe = "myexe" """) c.save({"conanfile.py": dep}) c.run("create .") args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" c.run(f"install --requires=dep/0.1 {args}", assert_error=True) assert "Error in generator 'CMakeDeps': dep/0.1 " 'cpp_info has both .exe and .libs' in c.out def test_exe_no_location(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.exe = "myexe" """) c.save({"conanfile.py": dep}) c.run("create .") args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" c.run(f"install --requires=dep/0.1 {args}", assert_error=True) assert "Error in generator 'CMakeDeps': dep/0.1 cpp_info has .exe and no .location" in c.out def test_check_exe_wrong_type(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.type = "shared-library" self.cpp_info.exe = "myexe" """) c.save({"conanfile.py": dep}) c.run("create .") args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" c.run(f"install --requires=dep/0.1 {args}", assert_error=True) assert "dep/0.1 cpp_info incorrect .type shared-library for .exe myexe" in c.out def test_multiple_find_package_subfolder(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestPackage(ConanFile): name = "matrix" version = "1.0" def package_info(self): self.cpp_info.system_libs = ["mysystemlib"] """) c.save({"conanfile.py": conanfile}) c.run("create .") cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(app NONE) find_package(matrix CONFIG REQUIRED) add_subdirectory(subdir) """) subcmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(subdir NONE) find_package(matrix CONFIG REQUIRED) """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): requires = "matrix/1.0" generators = "CMakeToolchain", "CMakeDeps" settings = "os", "compiler", "build_type", "arch" def build(self): cmake = CMake(self) cmake.configure() """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "subdir/CMakeLists.txt": subcmake}, clean_first=True) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "find_package(matrix)" in c.out assert "target_link_libraries(... matrix::matrix)" in c.out assert "Conan: Target declared imported INTERFACE library 'matrix::matrix'" in c.out @pytest.mark.tool("cmake", "3.27") def test_find_package_casing_non_fallback(): """ CMakeConfigDeps creates hello_DIR by default, but CMake looks for the package name as given in find_package, so casing matters, and it won't look for hello_DIR, but to HellO_DIR. Note that this would work without the layout, because CMake would fallback to find the lowercase hello-config.cmake file in the CMAKE_INSTALL_PREFIX, which happens to be the same folder the generated config file ends up without the layout.""" cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) # As long as the package generates hello-config.cmake lowercase, it works find_package(HellO REQUIRED) # Casing!!!!! """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Pkg(ConanFile): requires = "hello/1.0" generators = "CMakeToolchain", "CMakeDeps" settings = "os", "compiler", "build_type", "arch" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() """) client = TestClient() client.save({"conanfile.py": GenConanfile("hello", "1.0")}) client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run(f"build . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert 'Could not find a package configuration file provided by "HellO"' in client.out @pytest.mark.tool("cmake", "3.27") def test_find_package_extra_variants(): """ Producers can now specify extra casing names for find_package. If a consumer uses yet a different casing, it should directly change the cmake_file_name property instead.""" cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) # As long as the package generates hello-config.cmake lowercase, it works find_package(HellO REQUIRED) # Casing!!!!! if (HellO_FOUND) message(STATUS "Found HellO!") endif() if (hello_FOUND) message(STATUS "Found hello!") endif() """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Pkg(ConanFile): requires = "hello/1.0" generators = "CMakeToolchain", "CMakeConfigDeps" settings = "os", "compiler", "build_type", "arch" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() """) client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "1.0" def package_info(self): self.cpp_info.set_property("cmake_file_name_variants", ["HellO"]) """) client.save({"conanfile.py": conanfile}) client.run("create") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run("build") assert 'Conan: Configuring Targets for hello/1.0' in client.out # And this follows the expected found variable generation assert "Found HellO!" in client.out assert "Found hello!" not in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_cpp_linkage.py ================================================ import platform import textwrap import pytest from conan.test.assets.sources import gen_function_c, gen_function_h from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") def test_cxx_only_project_links_c_library(): """ When the consumer enables only CXX (project(myapp CXX)), the generated CMake config must not SEND_ERROR for a dependency with C linkage. CXX implies C linkage support, so the template adds "C" to the enabled languages check when "CXX" is present. """ c = TestClient() # C library package: builds a static C lib, declares languages = "C" so link_languages = ["C"] hello_c = gen_function_c(name="hello") hello_h = gen_function_h(name="hello") hello_cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(hello C) add_library(hello STATIC src/hello.c) target_include_directories(hello PUBLIC $ $) install(TARGETS hello ARCHIVE DESTINATION lib INCLUDES DESTINATION include) install(FILES include/hello.h DESTINATION include) """) hello_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Clib(ConanFile): name = "hello" version = "0.1" settings = "os", "compiler", "build_type", "arch" package_type = "static-library" generators = "CMakeToolchain" exports_sources = "CMakeLists.txt", "src/*", "include/*" languages = "C" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["hello"] """) c.save({ "hello/conanfile.py": hello_conanfile, "hello/CMakeLists.txt": hello_cmake, "hello/src/hello.c": hello_c, "hello/include/hello.h": hello_h, }) c.run("create hello") # Consumer: project(CXX) only - no C in project(). Links to C library. consumer_cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(myapp CXX) find_package(hello CONFIG REQUIRED) add_executable(app main.cpp) target_link_libraries(app PRIVATE hello::hello) """) main_cpp = textwrap.dedent(""" extern "C" { #include "hello.h" } int main() { hello(); } """) consumer_conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Recipe(ConanFile): settings = "os", "compiler", "build_type", "arch" package_type = "application" generators = "CMakeToolchain", "CMakeConfigDeps" requires = "hello/0.1" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.path.join(self.cpp.build.bindir, "app"), env="conanrun") """) c.save({ "consumer/conanfile.py": consumer_conanfile, "consumer/CMakeLists.txt": consumer_cmake, "consumer/main.cpp": main_cpp, }) c.run("build consumer") # If the fix were missing, CMake configure would SEND_ERROR: "Target hello::hello has C # linkage but C not enabled in project()". So reaching here and running the app proves the fix. assert "hello: Release!" in c.out @pytest.mark.skipif(platform.system() == "Windows", reason="Windows doesn't fail to link") def test_auto_cppstd(matrix_c_interface_client): c = matrix_c_interface_client # IMPORTANT: This must be a C and CXX CMake project!! consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(myapp C CXX) find_package(matrix REQUIRED) add_executable(app app.c) target_link_libraries(app PRIVATE matrix::matrix) """) conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Recipe(ConanFile): settings = "os", "compiler", "build_type", "arch" package_type = "application" generators = "CMakeToolchain", "CMakeConfigDeps" requires = "matrix/0.1" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.path.join(self.cpp.build.bindir, "app"), env="conanrun") """) app = textwrap.dedent(""" #include "matrix.h" int main(){ matrix(); return 0; } """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer, "app.c": app}, clean_first=True) c.run(f"build .") assert "Hello Matrix!" in c.out ================================================ FILE: test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_paths.py ================================================ import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient new_value = "will_break_next" @pytest.fixture def client(): c = TestClient() pkg = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout import os class Pkg(ConanFile): settings = "build_type", "os", "arch", "compiler" requires = "dep/0.1" generators = "CMakeDeps", "CMakeToolchain" def layout(self): # Necessary to force config files in another location cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure(variables={"CMAKE_FIND_DEBUG_MODE": "ON"}) """) cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(pkgb LANGUAGES NONE) find_package(dep CONFIG REQUIRED) """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": pkg, "pkg/CMakeLists.txt": cmake}) return c @pytest.mark.tool("cmake") def test_cmake_generated(client): c = client c.run("create dep") c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") assert "Conan toolchain: Including CMakeDeps generated conan_cmakedeps_paths.cmake" in c.out assert "Conan: Target declared imported INTERFACE library 'dep::dep'" in c.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("lowercase", [False, True]) def test_cmake_in_package(client, lowercase): c = client # same, but in-package f = "dep-config" if lowercase else "depConfig" dep = textwrap.dedent(f""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "dep" version = "0.1" def package(self): content = 'message(STATUS "Hello from dep dep-Config.cmake!!!!!")' save(self, os.path.join(self.package_folder, "cmake", "{f}.cmake"), content) def package_info(self): self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.builddirs = ["cmake"] """) c.save({"dep/conanfile.py": dep}) c.run("create dep") c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") assert "Conan toolchain: Including CMakeDeps generated conan_cmakedeps_paths.cmake" in c.out assert "Hello from dep dep-Config.cmake!!!!!" in c.out class TestRuntimeDirs: def test_runtime_lib_dirs_multiconf(self): client = TestClient() app = GenConanfile().with_requires("dep/1.0").with_generator("CMakeDeps")\ .with_settings("build_type") client.save({"lib/conanfile.py": GenConanfile(), "dep/conanfile.py": GenConanfile("dep").with_requires("onelib/1.0", "twolib/1.0"), "app/conanfile.py": app}) client.run("create lib --name=onelib --version=1.0") client.run("create lib --name=twolib --version=1.0") client.run("create dep --version=1.0") client.run(f'install app -s build_type=Release -c tools.cmake.cmakedeps:new={new_value}') client.run(f'install app -s build_type=Debug -c tools.cmake.cmakedeps:new={new_value}') contents = client.load("app/conan_cmakedeps_paths.cmake") pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1) assert "" in runtime_lib_dirs assert "" in runtime_lib_dirs # too simple of a check, but this is impossible to test automatically assert "set(CMAKE_VS_DEBUGGER_ENVIRONMENT" in contents @pytest.mark.tool("cmake") class TestCMakeDepsPaths: @pytest.mark.parametrize("requires, tool_requires", [(True, False), (False, True), (True, True)]) def test_find_program_path(self, requires, tool_requires): """Test that executables in bindirs of tool_requires can be found with find_program() in consumer CMakeLists. """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): name = "tool" version = "1.0" exports_sources = "*" def package(self): copy(self, "*", self.source_folder, os.path.join(self.package_folder, "bin")) """) c.save({"conanfile.py": conanfile, "hello": "", "hello.exe": ""}) c.run("create .") requires = 'requires = "tool/1.0"' if requires else "" tool_requires = 'tool_requires = "tool/1.0"' if tool_requires else "" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake class PkgConan(ConanFile): {requires} {tool_requires} settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain", "CMakeDeps" def build(self): cmake = CMake(self) cmake.configure() """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_program(HELLOPROG hello) if(HELLOPROG) message(STATUS "Found hello prog: ${HELLOPROG}") endif() """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "Found hello prog" in c.out if requires and tool_requires: assert "There is already a 'tool/1.0' package contributing to CMAKE_PROGRAM_PATH" in c.out def test_find_include_and_lib_paths(self): c = TestClient() conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): name = "hello" version = "1.0" exports_sources = "*" def package(self): copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include")) copy(self, "*.lib", self.source_folder, os.path.join(self.package_folder, "lib")) copy(self, "*.a", self.source_folder, os.path.join(self.package_folder, "lib")) copy(self, "*.so", self.source_folder, os.path.join(self.package_folder, "lib")) copy(self, "*.dll", self.source_folder, os.path.join(self.package_folder, "lib")) """) c.save({"conanfile.py": conanfile, "hello.h": "", "hello.lib": "", "libhello.a": "", "libhello.so": "", "libhello.dll": ""}) c.run("create .") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake class PkgConan(ConanFile): requires = "hello/1.0" settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain", "CMakeDeps" def build(self): cmake = CMake(self) cmake.configure() """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_file(HELLOINC hello.h) find_library(HELLOLIB hello) if(HELLOINC) message(STATUS "Found hello header: ${HELLOINC}") endif() if(HELLOLIB) message(STATUS "Found hello lib: ${HELLOLIB}") endif() """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "Found hello header" in c.out assert "Found hello lib" in c.out @pytest.mark.parametrize("require_type", ["requires", "tool_requires"]) def test_include_modules(self, require_type): """Test that cmake module files in builddirs of requires and tool_requires are accessible with include() in consumer CMakeLists """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): exports_sources = "*" def package(self): copy(self, "*", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.builddirs.append("cmake") """) c.save({"conanfile.py": conanfile, "cmake/myowncmake.cmake": 'MESSAGE("MYOWNCMAKE FROM hello!")'}) c.run("create . --name=hello --version=0.1") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake class PkgConan(ConanFile): settings = "os", "compiler", "arch", "build_type" {require_type} = "hello/0.1" generators = "CMakeToolchain", "CMakeConfigDeps" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) include(myowncmake) """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "MYOWNCMAKE FROM hello!" in c.out def test_include_modules_both_build_host(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): exports_sources = "*" def package(self): copy(self, "*", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.builddirs.append("cmake") """) c.save({"conanfile.py": conanfile, "cmake/myowncmake.cmake": 'MESSAGE("MYOWNCMAKE FROM hello!")'}) c.run("create . --name=hello --version=0.1") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake class PkgConan(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "hello/0.1" tool_requires = "hello/0.1" generators = "CMakeToolchain", "CMakeConfigDeps" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) include(myowncmake) """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") assert "conanfile.py: There is already a 'hello/0.1' package " \ "contributing to CMAKE_MODULE_PATH" in c.out assert "MYOWNCMAKE FROM hello!" in c.out ================================================ FILE: test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_sources.py ================================================ import os import platform import textwrap import pytest from conan.tools.files import replace_in_file from conan.test.utils.mocks import ConanFileMock from conan.test.utils.tools import TestClient new_value = "will_break_next" @pytest.mark.skipif(platform.system() != "Linux", reason="No OS specific test") @pytest.mark.tool("cmake") def test_cpp_info_sources(): c = TestClient() c.run("new cmake_lib -d name=hello -d version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class HelloConan(ConanFile): name = "hello" version = "1.0" exports_sources = "src/*", "include/*" package_type = "header-library" def package(self): copy(self, "*.h", self.source_folder, self.package_folder) copy(self, "*.cpp", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.sources = ["src/hello.cpp"] """) c.save({"conanfile.py": conanfile}) # Check that the hello library builds in test_package c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") # Check content of the generated files c.run(f"install --requires=hello/1.0 -g=CMakeConfigDeps " f"-c tools.cmake.cmakedeps:new={new_value}") cmake = c.load("hello-Targets-release.cmake") assert "add_library(hello::hello INTERFACE IMPORTED)" in cmake assert "set_property(TARGET hello::hello APPEND PROPERTY INTERFACE_SOURCES\n"\ " $<$:${hello_PACKAGE_FOLDER_RELEASE}/src/hello.cpp>)" in cmake @pytest.mark.skipif(platform.system() != "Linux", reason="No OS specific test") @pytest.mark.tool("cmake") def test_cpp_info_component_sources(): c = TestClient() c.run("new cmake_lib -d name=hello -d version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class HelloConan(ConanFile): name = "hello" version = "1.0" exports_sources = "src/*", "include/*" package_type = "header-library" def package(self): copy(self, "*.h", self.source_folder, self.package_folder) copy(self, "*.cpp", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.components["my_comp"].sources = ["src/hello.cpp", "src/other.cpp"] """) c.save({ "conanfile.py": conanfile, "src/other.cpp": "", }) # Make test_package link with component's target test_package_cmakelists_path = os.path.join(c.current_folder, "test_package", "CMakeLists.txt") replace_in_file(ConanFileMock(), test_package_cmakelists_path, "hello::hello", "hello::my_comp") test_package_cmakelists_content = c.load(test_package_cmakelists_path) assert "target_link_libraries(example hello::my_comp)" in test_package_cmakelists_content # Check that the hello library builds in test_package c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") # Check the content of the generated files c.run(f"install --requires=hello/1.0 -g=CMakeConfigDeps " f"-c tools.cmake.cmakedeps:new={new_value}") cmake = c.load("hello-Targets-release.cmake") assert "add_library(hello::hello INTERFACE IMPORTED)" in cmake assert "add_library(hello::my_comp INTERFACE IMPORTED)" in cmake assert "set_property(TARGET hello::my_comp APPEND PROPERTY INTERFACE_SOURCES\n"\ " $<$:${hello_PACKAGE_FOLDER_RELEASE}/src/hello.cpp"\ " ${hello_PACKAGE_FOLDER_RELEASE}/src/other.cpp>)" in cmake ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_apple_frameworks.py ================================================ import platform import textwrap import pytest from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient @pytest.fixture def client(): lib_conanfile = textwrap.dedent(""" from conan import ConanFile class FooLib(ConanFile): name = "foolib" version = "1.0" def package_info(self): self.cpp_info.frameworks.extend(['Foundation', 'CoreServices', 'CoreFoundation']) """) t = TestClient() t.save({'conanfile.py': lib_conanfile}) t.run("create .") return t app_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class App(ConanFile): requires = "foolib/1.0" generators = "CMakeDeps", "CMakeToolchain" settings = "build_type", "os", "arch" def build(self): cmake = CMake(self) cmake.configure() """) # needs at least 3.23.3 because of error with "empty identity" # https://stackoverflow.com/questions/72746725/xcode-14-beta-cmake-not-able-to-resolve-cmake-c-compiler-and-cmake-cxx-compiler @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.tool("cmake", "3.23") def test_apple_framework_xcode(client): app_cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(Testing NONE) find_package(foolib REQUIRED) message(">>> foolib_FRAMEWORKS_FOUND_DEBUG: ${foolib_FRAMEWORKS_FOUND_DEBUG}") message(">>> foolib_FRAMEWORKS_FOUND_RELEASE: ${foolib_FRAMEWORKS_FOUND_RELEASE}") """) client.save({'conanfile.py': app_conanfile, 'CMakeLists.txt': app_cmakelists}) client.run("build . -c tools.cmake.cmaketoolchain:generator=Xcode") assert "/System/Library/Frameworks/Foundation.framework;" in client.out assert "/System/Library/Frameworks/CoreServices.framework;" in client.out assert "/System/Library/Frameworks/CoreFoundation.framework" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class AppleframeworkConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "src/*" name = "mylibrary" version = "1.0" def layout(self): self.folders.source = "src" def build(self): cmake = CMake(self) cmake.configure() cmake.build() cmake.install() self.run("otool -L '%s/hello.framework/hello'" % self.build_folder) self.run("otool -L '%s/hello.framework/hello'" % self.package_folder) def package_info(self): self.cpp_info.frameworkdirs.append(self.package_folder) self.cpp_info.frameworks.append("hello") self.cpp_info.includedirs = [] """) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyHello CXX) # set @rpaths for libraries to link against SET(CMAKE_SKIP_RPATH FALSE) #SET(CMAKE_SKIP_BUILD_RPATH FALSE) #SET(CMAKE_INSTALL_RPATH "@rpath/") #SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) add_library(hello SHARED hello.cpp hello.h) set_target_properties(hello PROPERTIES FRAMEWORK TRUE FRAMEWORK_VERSION A MACOSX_FRAMEWORK_IDENTIFIER com.cmake.hello MACOSX_FRAMEWORK_INFO_PLIST src/Info.plist # "current version" in semantic format in Mach-O binary file VERSION 1.6.0 # "compatibility version" in semantic format in Mach-O binary file SOVERSION 1.6.0 PUBLIC_HEADER hello.h INSTALL_NAME_DIR "@rpath" MACOSX_RPATH TRUE ) install(TARGETS hello DESTINATION ".") """) hello_h = textwrap.dedent(""" #pragma once #ifdef WIN32 #define HELLO_EXPORT __declspec(dllexport) #else #define HELLO_EXPORT __attribute__((visibility("default"))) #endif #ifdef __cplusplus extern "C" { #endif class HELLO_EXPORT Hello { public: static void hello(); }; #ifdef __cplusplus } #endif """) hello_cpp = textwrap.dedent(""" #include #include "hello.h" void Hello::hello(){ #ifdef NDEBUG std::cout << "Hello World Release!" < CFBundleDisplayName hello CFBundleExecutable hello CFBundleIdentifier com.test.hello CFBundleInfoDictionaryVersion 6.0 CFBundleName hello CFBundlePackageType FMWK CFBundleShortVersionString 1.6.0 CFBundleVersion 1.6.0 Flavor_ID 0 NSAppTransportSecurity NSAllowsArbitraryLoads NSPrincipalClass """) timer_cpp = textwrap.dedent(""" #include int main(){ Hello::hello(); } """) @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("settings", ['', '-s os=iOS -s os.sdk=iphoneos -s os.version=10.0 -s arch=armv8', "-s os=tvOS -s os.sdk=appletvos -s os.version=11.0 -s arch=armv8"]) def test_apple_own_framework_cross_build(settings): client = TestClient() test_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Testing CXX) find_package(mylibrary REQUIRED) add_executable(timer timer.cpp) target_link_libraries(timer mylibrary::mylibrary) """) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout from conan.tools.build import cross_building class TestPkg(ConanFile): generators = "CMakeToolchain" settings = "os", "arch", "compiler", "build_type" def layout(self): cmake_layout(self) def requirements(self): self.requires(self.tested_reference_str) self.tool_requires(self.tested_reference_str) def generate(self): cmake = CMakeDeps(self) cmake.build_context_activated = ["mylibrary"] cmake.build_context_suffix = {"mylibrary": "_BUILD"} cmake.generate() def build(self): self.output.warning("Building test package at: {}".format(self.build_folder)) cmake = CMake(self) cmake.configure() cmake.build() def test(self): if not cross_building(self): cmd = os.path.join(self.cpp.build.bindirs[0], "timer") self.run(cmd, env="conanrunenv") """) client.save({'conanfile.py': conanfile, "src/CMakeLists.txt": cmake, "src/hello.h": hello_h, "src/hello.cpp": hello_cpp, "src/Info.plist": infoplist, "test_package/conanfile.py": test_conanfile, 'test_package/CMakeLists.txt': test_cmake, "test_package/timer.cpp": timer_cpp}) # First build it as build_require in the build-context, no testing # the UX could be improved, but the simplest could be: # - Have users 2 test_packages, one for the host and other for the build, with some naming # convention. CI launches one after the other if found client.run("create . %s -tf=\"\" --build-require" % settings) client.run("create . %s" % settings) if not len(settings): assert "Hello World Release!" in client.out @pytest.mark.xfail(reason="run_environment=True no longer works") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.tool("cmake", "3.19") def test_apple_own_framework_cmake_deps(): client = TestClient() test_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Testing CXX) message(STATUS "CMAKE_BINARY_DIR ${CMAKE_BINARY_DIR}") find_package(mylibrary REQUIRED) message(">>> MYLIBRARY_FRAMEWORKS_FOUND_DEBUG: ${MYLIBRARY_FRAMEWORKS_FOUND_DEBUG}") message(">>> MYLIBRARY_FRAMEWORKS_FOUND_RELEASE: ${MYLIBRARY_FRAMEWORKS_FOUND_RELEASE}") add_executable(timer timer.cpp) target_link_libraries(timer mylibrary::mylibrary) """) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake class TestPkg(ConanFile): generators = "CMakeToolchain" name = "app" version = "1.0" requires = "mylibrary/1.0" exports_sources = "CMakeLists.txt", "timer.cpp" settings = "os", "arch", "compiler", "build_type" def requirements(self): self.tool_requires(self.tested_reference_str) def generate(self): cmake = CMakeDeps(self) cmake.build_context_activated = ["mylibrary"] cmake.build_context_suffix = {"mylibrary": "_BUILD"} cmake.generate() def layout(self): self.folders.build = str(self.settings.build_type) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def test(self): self.run(os.path.join(str(self.settings.build_type), "timer"), env="conanrunenv") """) client.save({'conanfile.py': conanfile, "src/CMakeLists.txt": cmake, "src/hello.h": hello_h, "src/hello.cpp": hello_cpp, "src/Info.plist": infoplist}) client.run("export . --name=mylibrary --version=1.0") client.run("create . --name=mylibrary --version=1.0 -s build_type=Debug") client.run("create . --name=mylibrary --version=1.0 -s build_type=Release") profile = textwrap.dedent(""" include(default) [conf] tools.cmake.cmaketoolchain:generator=Xcode """) client.save({"conanfile.py": test_conanfile, 'CMakeLists.txt': test_cmake, "timer.cpp": timer_cpp, "profile": profile}) client.run("install . -s build_type=Debug -pr=profile") client.run("install . -s build_type=Release -pr=profile") client.run("test . mylibrary/1.0@ -pr=profile") assert "Hello World Release!" in client.out client.run("test . mylibrary/1.0@ -s:b build_type=Debug -pr=profile") assert "Hello World Debug!" in client.out @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_apple_own_framework_cmake_find_package_multi(): client = TestClient() test_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Testing CXX) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/bin") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/bin") find_package(mylibrary REQUIRED) message(">>> CONAN_FRAMEWORKS_FOUND_MYLIBRARY: ${CONAN_FRAMEWORKS_FOUND_MYLIBRARY}") add_executable(timer timer.cpp) target_link_libraries(timer mylibrary::mylibrary) """) test_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class TestPkg(ConanFile): generators = "CMakeDeps", "CMakeToolchain" settings = "build_type", "os", "arch" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def test(self): self.run("bin/timer", env="conanrunenv") """) client.save({'conanfile.py': conanfile, "src/CMakeLists.txt": cmake, "src/hello.h": hello_h, "src/hello.cpp": hello_cpp, "src/Info.plist": infoplist, "test_package/conanfile.py": test_conanfile, 'test_package/CMakeLists.txt': test_cmake, "test_package/timer.cpp": timer_cpp}) client.run("create .") assert "Hello World Release!" in client.out @pytest.mark.tool("cmake", "3.19") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_component_uses_apple_framework(): conanfile_py = textwrap.dedent(""" from conan import ConanFile, tools from conan.tools.cmake import CMake class HelloConan(ConanFile): name = "hello" description = "example" topics = ("conan",) url = "https://github.com/conan-io/conan-center-index" homepage = "https://www.example.com" license = "MIT" exports_sources = ["hello.cpp", "hello.h", "CMakeLists.txt"] generators = "CMakeDeps", "CMakeToolchain" settings = "os", "arch", "compiler", "build_type" def build(self): cmake = CMake(self) cmake.configure() cmake.build() cmake.install() def package_info(self): self.cpp_info.set_property("cmake_file_name", "HELLO") self.cpp_info.components["libhello"].set_property("cmake_target_name", "hello::libhello") self.cpp_info.components["libhello"].libs = ["hello"] # We need to add the information about the lib/include directories to be able to find them self.cpp_info.components["libhello"].libdirs = ["lib"] self.cpp_info.components["libhello"].includedirs = ["include"] self.cpp_info.components["libhello"].frameworks.extend(["CoreFoundation"]) """) hello_cpp_core = textwrap.dedent(""" #include void hello_api() { CFTypeRef keys[] = {CFSTR("key")}; CFTypeRef values[] = {CFSTR("value")}; CFDictionaryRef dict = CFDictionaryCreate(kCFAllocatorDefault, keys, values, sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (dict) CFRelease(dict); } """) hello_h_core = textwrap.dedent(""" void hello_api(); """) cmakelists_txt = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(hello) include(GNUInstallDirs) file(GLOB SOURCES *.cpp) file(GLOB HEADERS *.h) add_library(${PROJECT_NAME} ${SOURCES} ${HEADERS}) set_target_properties(${PROJECT_NAME} PROPERTIES PUBLIC_HEADER ${HEADERS}) install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include) """) test_conanfile_py = textwrap.dedent(""" import os from conan import ConanFile, tools from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps from conan.tools.build import cross_building class TestPackageConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) self.tool_requires(self.tested_reference_str) def generate(self): cmake = CMakeDeps(self) cmake.build_context_activated = ["hello"] cmake.build_context_suffix = {"hello": "_BUILD"} cmake.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def test(self): if not cross_building(self): self.run("./test_package", env="conanrunenv") """) test_test_package_cpp = textwrap.dedent(""" #include "hello.h" int main() { hello_api(); } """) test_cmakelists_txt = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(test_package CXX) find_package(HELLO REQUIRED CONFIG) add_executable(${PROJECT_NAME} test_package.cpp) target_link_libraries(${PROJECT_NAME} hello::libhello) """) t = TestClient() t.save({'conanfile.py': conanfile_py, 'hello.cpp': hello_cpp_core, 'hello.h': hello_h_core, 'CMakeLists.txt': cmakelists_txt, 'test_package/conanfile.py': test_conanfile_py, 'test_package/CMakeLists.txt': test_cmakelists_txt, 'test_package/test_package.cpp': test_test_package_cpp}) t.run("create . --name=hello --version=1.0") @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_iphoneos_crossbuild(): profile = textwrap.dedent(""" include(default) [settings] os=iOS os.version=12.0 os.sdk=iphoneos arch=armv8 """).format() client = TestClient(path_with_spaces=False) client.save({"ios-armv8": profile}, clean_first=True) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("create . --profile:build=default --profile:host=ios-armv8 -tf=\"\"") main = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) # FIXME: The crossbuild for iOS etc is failing with find_package because cmake ignore the # cmake_prefix_path to point only to the Frameworks of the system. The only fix found # would require to introduce something like "set (mylibrary_DIR "${CMAKE_BINARY_DIR}")" # at the toolchain (but it would require the toolchain to know about the deps) # https://stackoverflow.com/questions/65494246/cmakes-find-package-ignores-the-paths-option-when-building-for-ios# cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) set(hello_DIR "${CMAKE_BINARY_DIR}") find_package(hello) add_executable(main main.cpp) target_link_libraries(main hello::hello) """) hello = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class TestConan(ConanFile): requires = "hello/0.1" settings = "os", "compiler", "arch", "build_type" exports_sources = "CMakeLists.txt", "main.cpp" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) client.save({"conanfile.py": hello, "CMakeLists.txt": cmakelists, "main.cpp": main, "ios-armv8": profile}, clean_first=True) client.run("install . --profile:build=default --profile:host=ios-armv8") client.run("build . --profile:build=default --profile:host=ios-armv8") main_path = "./main.app/main" client.run_command("lipo -info {}".format(main_path)) assert "Non-fat file" in client.out assert "is architecture: arm64" in client.out client.run_command(f"vtool -show-build {main_path}") assert "platform IOS" in client.out assert "minos 12.0" in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_build_context_protobuf.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.fixture def client(): c = TestClient() conanfile = textwrap.dedent(''' from conan import ConanFile from conan.tools.files import save, chdir import os class Protobuf(ConanFile): settings = "build_type", "os", "arch", "compiler" def package(self): my_cmake_module = """ function(foo_generate) write_file(foo_generated.h "int from_context = %s;") endfunction() """ with chdir(self, self.package_folder): save(self, "include_build/protobuf.h", "int protubuff_stuff(){ return 1; }") save(self, "include_host/protobuf.h", "int protubuff_stuff(){ return 2; }") save(self, "build/my_tools_build.cmake", my_cmake_module % "1") save(self, "build/my_tools_host.cmake", my_cmake_module % "2") def package_info(self): # This info depends on self.context !! self.cpp_info.includedirs = ["include_{}".format(self.context)] path_build_modules = os.path.join("build", "my_tools_{}.cmake".format(self.context)) self.cpp_info.set_property("cmake_build_modules", [path_build_modules]) ''') c.save({"conanfile.py": conanfile}) c.run("create . --name=protobuf --version=1.0") return c main = textwrap.dedent(""" #include #include "protobuf.h" #include "foo_generated.h" int main(){ int ret = protubuff_stuff(); if(ret == 1){ std::cout << " Library from build context!" << std::endl; } else if(ret == 2){ std::cout << " Library from host context!" << std::endl; } // Variable declared at the foo_generated if(from_context == 1){ std::cout << " Generated code in build context!" << std::endl; } else if(from_context == 2){ std::cout << " Generated code in host context!" << std::endl; } return 0; } """) consumer_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps class Consumer(ConanFile): settings = "build_type", "os", "arch", "compiler" exports_sources = "CMakeLists.txt", "main.cpp" requires = "protobuf/1.0" def build_requirements(self): self.requires("protobuf/1.0", visible=False, build=True) def generate(self): toolchain = CMakeToolchain(self) toolchain.generate() deps = CMakeDeps(self) {} deps.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() folder = str(self.settings.build_type) if self.settings.os == "Windows" else "." self.run(os.sep.join([folder, "app"])) """) @pytest.mark.tool("cmake") def test_build_modules_from_build_context(client): consumer_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) find_package(protobuf) find_package(protobuf_BUILD) add_executable(app main.cpp) foo_generate() target_link_libraries(app protobuf::protobuf) """) cmake_deps_conf = """ deps.build_context_activated = ["protobuf"] deps.build_context_build_modules = ["protobuf"] deps.build_context_suffix = {"protobuf": "_BUILD"} """ client.save({"conanfile.py": consumer_conanfile.format(cmake_deps_conf), "CMakeLists.txt": consumer_cmake, "main.cpp": main}) client.run("create . --name=app --version=1.0 -pr:b default -pr:h default") assert "Library from host context!" in client.out assert "Generated code in build context!" in client.out def test_build_modules_and_target_from_build_context(client): consumer_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) find_package(protobuf) find_package(protobuf_BUILD) add_executable(app main.cpp) foo_generate() target_link_libraries(app protobuf_BUILD::protobuf_BUILD) """) cmake_deps_conf = """ deps.build_context_activated = ["protobuf"] deps.build_context_build_modules = ["protobuf"] deps.build_context_suffix = {"protobuf": "_BUILD"} """ client.save({"conanfile.py": consumer_conanfile.format(cmake_deps_conf), "CMakeLists.txt": consumer_cmake, "main.cpp": main}) client.run("create . --name=app --version=1.0 -pr:b default -pr:h default") assert "Library from build context!" in client.out assert "Generated code in build context!" in client.out def test_build_modules_from_host_and_target_from_build_context(client): consumer_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) find_package(protobuf) find_package(protobuf_BUILD) add_executable(app main.cpp) foo_generate() target_link_libraries(app protobuf_BUILD::protobuf_BUILD) """) cmake_deps_conf = """ deps.build_context_activated = ["protobuf"] deps.build_context_suffix = {"protobuf": "_BUILD"} """ client.save({"conanfile.py": consumer_conanfile.format(cmake_deps_conf), "CMakeLists.txt": consumer_cmake, "main.cpp": main}) client.run("create . --name=app --version=1.0 -pr:b default -pr:h default") assert "Library from build context!" in client.out assert "Generated code in host context!" in client.out @pytest.mark.tool("cmake") def test_build_modules_and_target_from_host_context(client): consumer_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) find_package(protobuf) find_package(protobuf_BUILD) add_executable(app main.cpp) foo_generate() target_link_libraries(app protobuf::protobuf) """) cmake_deps_conf = """ deps.build_context_activated = ["protobuf"] deps.build_context_build_modules = [] deps.build_context_suffix = {"protobuf": "_BUILD"} """ client.save({"conanfile.py": consumer_conanfile.format(cmake_deps_conf), "CMakeLists.txt": consumer_cmake, "main.cpp": main}) client.run("create . --name=app --version=1.0 -pr:b default -pr:h default") assert "Conan: Target declared 'protobuf::protobuf'" in client.out assert "Conan: Target declared 'protobuf_BUILD::protobuf_BUILD'" in client.out assert "Library from host context!" in client.out assert "Generated code in host context!" in client.out def test_exception_when_not_prefix_specified(client): cmake_deps_conf = """ deps.build_context_activated = ["protobuf"] """ client.save({"conanfile.py": consumer_conanfile.format(cmake_deps_conf), "main.cpp": main}) client.run("create . --name=app --version=1.0 -pr:b default -pr:h default", assert_error=True) assert "The package 'protobuf' exists both as 'require' and as 'build require'. " \ "You need to specify a suffix using the 'build_context_suffix' attribute at the " \ "CMakeDeps generator." in client.out @pytest.mark.tool("cmake") def test_not_activated_not_fail(client): consumer_cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) find_package(protobuf) add_executable(app main.cpp) foo_generate() target_link_libraries(app protobuf::protobuf) """) client.save({"conanfile.py": consumer_conanfile.format(""), "CMakeLists.txt": consumer_cmake, "main.cpp": main}) client.run("create . --name=app --version=1.0 -pr:b default -pr:h default") assert "app/1.0: Created package" in client.out assert "Library from host context!" in client.out assert "Generated code in host context!" in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_build_context_transitive_build.py ================================================ import os import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient """ If we have a BR with transitive requires we won't generate 'xxx-config.cmake' files for them but neither will be included as find_dependencies() """ @pytest.fixture def client(): c = TestClient() zlib_conanfile = textwrap.dedent(''' from conan import ConanFile class Zlib(ConanFile): name = "zlib" version = "1.2.11" def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.cxxflags = ["foo"] ''') c.save({"conanfile.py": zlib_conanfile}) c.run("create . ") doxygen_conanfile = textwrap.dedent(''' from conan import ConanFile from conan.tools.files import save, chdir import os class Doxygen(ConanFile): settings = "build_type", "os", "arch", "compiler" requires = "zlib/1.2.11" def package(self): with chdir(self, self.package_folder): save(self, "include/doxygen.h", "int foo=1;") ''') c.save({"conanfile.py": doxygen_conanfile}) c.run("create . --name=doxygen --version=1.0") return c @pytest.mark.tool("cmake") def test_zlib_not_included(client): main = gen_function_cpp(name="main", includes=["doxygen.h"]) cmake = gen_cmakelists(find_package=["doxygen"], appsources=["main.cpp"], appname="main") conanfile_consumer = textwrap.dedent(''' from conan import ConanFile from conan.tools.cmake import CMakeDeps class Consumer(ConanFile): settings = "build_type", "os", "arch", "compiler" build_requires = ["doxygen/1.0"] generators = "CMakeToolchain" def generate(self): d = CMakeDeps(self) d.build_context_activated = ["doxygen"] d.generate() ''') client.save({"main.cpp": main, "CMakeLists.txt": cmake, "conanfile.py": conanfile_consumer}, clean_first=True) client.run("install . -pr:h=default -pr:b=default") # The compilation works, so it finds the doxygen without transitive failures client.run_command("cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release") # Assert there is no zlib target assert "Target declared 'zlib::zlib'" not in client.out # Of course we find the doxygen-config.cmake assert os.path.exists(os.path.join(client.current_folder, "doxygen-config.cmake")) # The -config files for zlib are not there assert not os.path.exists(os.path.join(client.current_folder, "zlib-config.cmake")) def test_error_cmakedeps_transitive_build_requires(): """ CMakeDeps when building an intermediate "tool_requires" that has a normal "requires" to other package, needs to use ``require.build`` trait instead of the more global "dep.is_build_context" We do build "protobuf" in the "build" context to make sure the whole CMakeDeps is working correctly """ c = TestClient() c.run("new cmake_lib -d name=zlib -d version=0.1") c.run("create . -tf=") c.save({}, clean_first=True) c.run("new cmake_lib -d name=openssl -d version=0.1 -d requires=zlib/0.1") c.run("create . -tf=") # Protobuf binary is missing, to force a build below with ``--build=missing`` c.save({}, clean_first=True) c.run("new cmake_exe -d name=protobuf -d version=0.1 -d requires=openssl/0.1") c.run("export .") # This conanfile used to fail, creating protobuf_mybuild*.cmake files even if for "tool" the # protobuf/0.1 is a regular "requires" and it is in its host context tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMakeDeps class Tool(ConanFile): name = "tool" version = "0.1" settings = "os", "compiler", "build_type", "arch" requires = "protobuf/0.1" def generate(self): deps = CMakeDeps(self) deps.build_context_activated = ["protobuf"] deps.build_context_suffix = {"protobuf": "_mybuild"} deps.generate() def build(self): assert os.path.exists("protobufTargets.cmake") assert os.path.exists("protobuf-Target-release.cmake") """) c.save({"tool/conanfile.py": tool, "consumer/conanfile.py": GenConanfile().with_build_requires("tool/0.1")}, clean_first=True) c.run("export tool") c.run("install consumer --build=missing -s:b build_type=Release -s:h build_type=Debug") assert "tool/0.1: Created package" in c.out def test_transitive_tool_requires_visible(): # https://github.com/conan-io/conan/issues/16058 # The "tool/0.1", even if visible doesn't generate any files in the "app" consumer side, # even if the "app" declares it in build_context_activated, because it only works for direct # dependencies. tool_requires(..., visible=True) only affects at the moment and version conflict # detection c = TestClient() c.save({"tool/conanfile.py": GenConanfile("tool", "0.1").with_package_type("application"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_tool_requirement("tool/0.1", visible=True), "app/conanfile.py": GenConanfile("app", "0.1").with_requires("pkgb/0.1") .with_settings("build_type") .with_generator("CMakeDeps")}) c.run("create tool") c.run("create pkgb") c.run("install app") cmake = c.load("app/pkgb-release-data.cmake") # pkgb doesn't deepnd on tool, because it is a tool-require and not build-context activated assert "list(APPEND pkgb_FIND_DEPENDENCY_NAMES )" in cmake ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py ================================================ import os import platform import shutil import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.mocks import ConanFileMock from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID from conan.tools.files import replace_in_file @pytest.fixture def client(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class Pkg(ConanFile): settings = "build_type", "os", "arch", "compiler" {} def package(self): save(self, os.path.join(self.package_folder, "include", "%s.h" % self.name), '#define MYVAR%s "%s"' % (self.name, self.settings.build_type)) """) c.save({"conanfile.py": conanfile.format("")}) c.run("create . --name=liba --version=0.1 -s build_type=Release") c.run("create . --name=liba --version=0.1 -s build_type=Debug") c.save({"conanfile.py": conanfile.format("requires = 'liba/0.1'")}) c.run("create . --name=libb --version=0.1 -s build_type=Release") c.run("create . --name=libb --version=0.1 -s build_type=Debug") return c @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Windows", reason="Windows only multi-config") def test_transitive_multi_windows(client): # Save conanfile and example conanfile = textwrap.dedent(""" [requires] libb/0.1 [generators] CMakeDeps CMakeToolchain """) example_cpp = gen_function_cpp(name="main", includes=["libb", "liba"], preprocessor=["MYVARliba", "MYVARlibb"]) client.save({"conanfile.txt": conanfile, "CMakeLists.txt": gen_cmakelists(appname="example", appsources=["example.cpp"], find_package=["libb"]), "example.cpp": example_cpp}, clean_first=True) with client.chdir("build"): for bt in ("Debug", "Release"): # NOTE: -of=. otherwise the output files are located in the parent directory client.run("install .. --user=user --channel=channel -s build_type={} -of=.".format(bt)) # Test that we are using find_dependency with the NO_MODULE option # to skip finding first possible FindBye somewhere assert "find_dependency(${_DEPENDENCY} REQUIRED ${${_DEPENDENCY}_FIND_MODE})" \ in client.load("libb-config.cmake") assert 'set(liba_FIND_MODE "NO_MODULE")' in client.load("libb-release-x86_64-data.cmake") if platform.system() == "Windows": client.run_command('cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake') client.run_command('cmake --build . --config Debug') client.run_command('cmake --build . --config Release') client.run_command('Debug\\example.exe') assert "main: Debug!" in client.out assert "MYVARliba: Debug" in client.out assert "MYVARlibb: Debug" in client.out client.run_command('Release\\example.exe') assert "main: Release!" in client.out assert "MYVARliba: Release" in client.out assert "MYVARlibb: Release" in client.out @pytest.mark.tool("cmake") def test_system_libs(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class Test(ConanFile): name = "test" version = "2.0" settings = "build_type" def package(self): save(self, os.path.join(self.package_folder, "lib/lib1.lib"), "") save(self, os.path.join(self.package_folder, "lib/liblib1.a"), "") def package_info(self): self.cpp_info.libs = ["lib1"] if self.settings.build_type == "Debug": self.cpp_info.system_libs.append("sys1d") else: self.cpp_info.system_libs.append("sys1") self.cpp_info.set_property("cmake_config_version_compat", "AnyNewerVersion") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . -s build_type=Release") client.run("create . -s build_type=Debug") conanfile = textwrap.dedent(""" [requires] test/2.0 [generators] CMakeDeps """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}) find_package(test 1.0) message("System libs release: ${test_SYSTEM_LIBS_RELEASE}") message("Libraries to Link release: ${test_LIBS_RELEASE}") message("System libs debug: ${test_SYSTEM_LIBS_DEBUG}") message("Libraries to Link debug: ${test_LIBS_DEBUG}") get_target_property(tmp test::test INTERFACE_LINK_LIBRARIES) message("Target libs: ${tmp}") get_target_property(tmp CONAN_LIB::test_lib1_%s INTERFACE_LINK_LIBRARIES) message("Micro-target libs: ${tmp}") get_target_property(tmp test_DEPS_TARGET INTERFACE_LINK_LIBRARIES) message("Micro-target deps: ${tmp}") """) for build_type in ["Release", "Debug"]: client.save({"conanfile.txt": conanfile, "CMakeLists.txt": cmakelists % build_type.upper()}, clean_first=True) client.run("install conanfile.txt -s build_type=%s" % build_type) client.run_command('cmake . -DCMAKE_BUILD_TYPE={0}'.format(build_type)) library_name = "sys1d" if build_type == "Debug" else "sys1" # FIXME: Note it is CONAN_LIB::test_lib1_RELEASE, not "lib1" as cmake_find_package if build_type == "Release": assert "System libs release: %s" % library_name in client.out assert "Libraries to Link release: lib1" in client.out else: assert "System libs debug: %s" % library_name in client.out assert "Libraries to Link debug: lib1" in client.out assert (f"Target libs: $<$:>;" f"$<$:CONAN_LIB::test_lib1_{build_type.upper()}>") in client.out assert "Micro-target libs: test_DEPS_TARGET" in client.out micro_target_deps = f"Micro-target deps: $<$:>;" \ f"$<$:{library_name}>;" \ f"$<$:>" assert micro_target_deps in client.out @pytest.mark.tool("cmake") def test_system_libs_no_libs(): """If the recipe doesn't declare cpp_info.libs then the target with the system deps, frameworks and transitive deps has to be linked to the global target""" conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class Test(ConanFile): name = "test" version = "0.1.1" settings = "build_type" def package_info(self): if self.settings.build_type == "Debug": self.cpp_info.system_libs.append("sys1d") else: self.cpp_info.system_libs.append("sys1") self.cpp_info.set_property("cmake_config_version_compat", "SameMinorVersion") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . -s build_type=Release") client.run("create . -s build_type=Debug") conanfile = textwrap.dedent(""" [requires] test/0.1.1 [generators] CMakeDeps """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}) find_package(test 0.1) message("System libs Release: ${test_SYSTEM_LIBS_RELEASE}") message("Libraries to Link release: ${test_LIBS_RELEASE}") message("System libs Debug: ${test_SYSTEM_LIBS_DEBUG}") message("Libraries to Link debug: ${test_LIBS_DEBUG}") get_target_property(tmp test::test INTERFACE_LINK_LIBRARIES) message("Target libs: ${tmp}") get_target_property(tmp test_DEPS_TARGET INTERFACE_LINK_LIBRARIES) message("DEPS TARGET: ${tmp}") """) for build_type in ["Release", "Debug"]: client.save({"conanfile.txt": conanfile, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("install conanfile.txt -s build_type=%s" % build_type) client.run_command('cmake . -DCMAKE_BUILD_TYPE={0}'.format(build_type)) library_name = "sys1d" if build_type == "Debug" else "sys1" assert f"System libs {build_type}: {library_name}" in client.out assert f"Target libs: $<$:>;" \ f"$<$:>;test_DEPS_TARGET" in client.out assert f"DEPS TARGET: $<$:>;" \ f"$<$:{library_name}>" in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("policy", ["AnyNewerVersion", "SameMajorVersion", "SameMinorVersion", "ExactVersion"]) def test_cmake_config_version_compat_rejected(policy): conanfile = textwrap.dedent(f""" from conan import ConanFile class Test(ConanFile): def package_info(self): self.cpp_info.set_property("cmake_config_version_compat", "{policy}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=mytest --version=2.2.1") conanfile = textwrap.dedent(""" [requires] mytest/2.2.1 [generators] CMakeDeps CMakeToolchain """) version_to_reject = {"AnyNewerVersion": "3.0", "SameMajorVersion": "1.0", "SameMinorVersion": "2.1", "ExactVersion": "2.2.0"}[policy] cmakelists = textwrap.dedent(f""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) message(STATUS "CMAKE VERSION=${{CMAKE_VERSION}}") find_package(mytest {version_to_reject} CONFIG REQUIRED) """) client.save({"conanfile.txt": conanfile, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("install .") client.run_command('cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake', assert_error=True) assert "The following configuration files were considered but not accepted" in client.out assert "2.2.1" in client.out @pytest.mark.tool("cmake") def test_system_libs_components_no_libs(): """If the recipe doesn't declare cpp_info.libs then the target with the system deps, frameworks and transitive deps has to be linked to the component target""" conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class Test(ConanFile): name = "test" version = "0.1" settings = "build_type" def package_info(self): if self.settings.build_type == "Debug": self.cpp_info.components["foo"].system_libs.append("sys1d") else: self.cpp_info.components["foo"].system_libs.append("sys1") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . -s build_type=Release") client.run("create . -s build_type=Debug") conanfile = textwrap.dedent(""" [requires] test/0.1 [generators] CMakeDeps """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}) find_package(test) message("System libs Release: ${test_test_foo_SYSTEM_LIBS_RELEASE}") message("Libraries to Link release: ${test_test_foo_LIBS_RELEASE}") message("System libs Debug: ${test_test_foo_SYSTEM_LIBS_DEBUG}") message("Libraries to Link debug: ${test_test_foo_LIBS_DEBUG}") get_target_property(tmp test::foo INTERFACE_LINK_LIBRARIES) message("Target libs: ${tmp}") get_target_property(tmp test_test_foo_DEPS_TARGET INTERFACE_LINK_LIBRARIES) message("DEPS TARGET: ${tmp}") """) for build_type in ["Release", "Debug"]: client.save({"conanfile.txt": conanfile, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("install conanfile.txt -s build_type=%s" % build_type) client.run_command('cmake . -DCMAKE_BUILD_TYPE={0}'.format(build_type)) library_name = "sys1d" if build_type == "Debug" else "sys1" assert f"System libs {build_type}: {library_name}" in client.out assert (f"Target libs: $<$:>;" f"$<$:>;test_test_foo_DEPS_TARGET") in client.out assert f"DEPS TARGET: $<$:>;" \ f"$<$:{library_name}>" in client.out @pytest.mark.tool("cmake") def test_do_not_mix_cflags_cxxflags(): # TODO: Verify with components too client = TestClient() cpp_info = {"cflags": ["one", "two"], "cxxflags": ["three", "four"]} client.save({"conanfile.py": GenConanfile("upstream", "1.0").with_package_info(cpp_info=cpp_info)}) client.run("create .") consumer_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Consumer(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "arch", "build_type" exports_sources = "CMakeLists.txt" requires = "upstream/1.0" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) find_package(upstream CONFIG REQUIRED) get_target_property(tmp upstream::upstream INTERFACE_COMPILE_OPTIONS) message("compile options: ${tmp}") message("cflags: ${upstream_COMPILE_OPTIONS_C_RELEASE}") message("cxxflags: ${upstream_COMPILE_OPTIONS_CXX_RELEASE}") """) client.save({"conanfile.py": consumer_conanfile, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("create .") assert "compile options: $<$:" \ "$<$:three;four>;$<$:one;two>>" in client.out assert "cflags: one;two" in client.out assert "cxxflags: three;four" in client.out def test_custom_configuration(client): """ The configuration in the build context is still the same than the host context""" conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps class Consumer(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "arch", "build_type" requires = "liba/0.1" build_requires = "liba/0.1" generators = "CMakeToolchain" def generate(self): cmake = CMakeDeps(self) cmake.configuration = "Debug" cmake.build_context_activated = ["liba"] cmake.build_context_suffix["liba"] = "_build" cmake.generate() """) host_arch = client.get_default_host_profile().settings['arch'] client.save({"conanfile.py": conanfile}) client.run("install . -pr:h default -s:b build_type=RelWithDebInfo" " -pr:b default -s:b arch=x86 --build missing") curdir = client.current_folder data_name_context_build = f"liba_build-debug-{host_arch}-data.cmake" data_name_context_host = f"liba-debug-{host_arch}-data.cmake" assert os.path.exists(os.path.join(curdir, data_name_context_build)) assert os.path.exists(os.path.join(curdir, data_name_context_host)) assert "set(liba_build_INCLUDE_DIRS_DEBUG" in \ open(os.path.join(curdir, data_name_context_build)).read() assert "set(liba_INCLUDE_DIRS_DEBUG" in \ open(os.path.join(curdir, data_name_context_host)).read() @pytest.mark.tool("cmake") def test_buildirs_working(): """ If a recipe declares cppinfo.buildirs those dirs will be exposed to be consumer to allow a cmake "include" function call after a find_package""" c = TestClient() conanfile = str(GenConanfile().with_name("my_lib").with_version("1.0") .with_import("import os") .with_import("from conan.tools.files import save")) conanfile += """ def package(self): save(self, os.path.join(self.package_folder, "my_build_dir", "my_cmake_script.cmake"), 'set(MYVAR "Like a Rolling Stone")') def package_info(self): self.cpp_info.builddirs=["my_build_dir"] """ c.save({"conanfile.py": conanfile}) c.run("create .") consumer_conanfile = GenConanfile().with_name("consumer").with_version("1.0")\ .with_cmake_build().with_require("my_lib/1.0") \ .with_settings("os", "arch", "build_type", "compiler") \ .with_exports_sources("*.txt") cmake = gen_cmakelists(find_package=["my_lib"]) cmake += """ message("CMAKE_MODULE_PATH: ${CMAKE_MODULE_PATH}") include("my_cmake_script") message("MYVAR=>${MYVAR}") """ c.save({"conanfile.py": consumer_conanfile, "CMakeLists.txt": cmake}) c.run("create .") assert "MYVAR=>Like a Rolling Stone" in c.out @pytest.mark.tool("cmake") def test_cpp_info_link_objects(): client = TestClient() obj_ext = "obj" if platform.system() == "Windows" else "o" cpp_info = {"objects": [os.path.join("lib", "myobject.{}".format(obj_ext))]} object_cpp = gen_function_cpp(name="myobject") object_h = gen_function_h(name="myobject") cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyObject C CXX) file(GLOB HEADERS *.h) add_library(myobject OBJECT myobject.cpp) if( WIN32 ) set(OBJ_PATH "myobject.dir/Release/myobject${CMAKE_C_OUTPUT_EXTENSION}") else() set(OBJ_PATH "CMakeFiles/myobject.dir/myobject.cpp${CMAKE_C_OUTPUT_EXTENSION}") endif() install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${OBJ_PATH} DESTINATION ${CMAKE_INSTALL_PREFIX}/lib RENAME myobject${CMAKE_C_OUTPUT_EXTENSION}) install(FILES ${HEADERS} DESTINATION ${CMAKE_INSTALL_PREFIX}/include) """) test_package_cpp = gen_function_cpp(name="main", includes=["myobject"], calls=["myobject"]) test_package_cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(example CXX) find_package(myobject REQUIRED) add_executable(example example.cpp) target_link_libraries(example myobject::myobject) """) test_conanfile = (GenConanfile().with_cmake_build().with_import("import os") .with_test('path = "{}".format(self.settings.build_type) ' 'if self.settings.os == "Windows" else "."') .with_test('self.run("{}{}example".format(path, os.sep))')) client.save({"CMakeLists.txt": cmakelists, "conanfile.py": GenConanfile("myobject", "1.0").with_package_info(cpp_info=cpp_info) .with_exports_sources("*") .with_cmake_build() .with_package("cmake = CMake(self)", "cmake.install()"), "myobject.cpp": object_cpp, "myobject.h": object_h, "test_package/conanfile.py": test_conanfile, "test_package/example.cpp": test_package_cpp, "test_package/CMakeLists.txt": test_package_cmakelists}) client.run("create . -s build_type=Release") assert "myobject: Release!" in client.out def test_private_transitive(): # https://github.com/conan-io/conan/issues/9514 client = TestClient() client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requirement("dep/0.1", visible=False), "consumer/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_settings("os", "build_type", "arch")}) client.run("create dep --name=dep --version=0.1") client.run("create pkg --name=pkg --version=0.1") client.run("install consumer -g CMakeDeps -s arch=x86_64 -s build_type=Release -of=. -v") client.assert_listed_binary({"dep/0.1": (NO_SETTINGS_PACKAGE_ID, "Skip")}) data_cmake = client.load("pkg-release-x86_64-data.cmake") assert 'list(APPEND pkg_FIND_DEPENDENCY_NAMES )' in data_cmake @pytest.mark.tool("cmake") def test_system_dep(): """This test creates a zlib package and use the installation CMake FindZLIB.cmake to locate the library of the package. That happens because: - The package declares: self.cpp_info.set_property("cmake_find_mode", "none") so CMakeDeps does nothing - The toolchain set the CMAKE_LIBRARY_PATH to the "lib" of the package, so the library file is found """ client = TestClient() client.run("new cmake_lib -d name=zlib -d version=0.1 -o zlib") conanfile = client.load("zlib/conanfile.py") conanfile += textwrap.indent(textwrap.dedent(""" # This will use the FindZLIB from CMake but will find this library package self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") self.cpp_info.set_property("cmake_find_mode", "none") """), " ") client.save({"zlib/conanfile.py": conanfile}) client.run("new cmake_lib -d name=mylib -d version=0.1 -d requires=zlib/0.1 -o mylib") cmake = client.load("mylib/CMakeLists.txt") cmake = cmake.replace("find_package(zlib CONFIG", "find_package(ZLIB") cmake = cmake.replace("zlib::zlib", "ZLIB::ZLIB") client.save({"mylib/CMakeLists.txt": cmake}) client.run("new cmake_lib -d name=consumer -d version=0.1 -d requires=mylib/0.1 -o=consumer") client.run("create zlib -tf=") client.run("create mylib -tf=") client.run("create consumer -tf=") assert "Found ZLIB:" in client.out client.run("install consumer") if platform.system() != "Windows": host_arch = client.get_default_host_profile().settings['arch'] data = f"consumer/build/Release/generators/mylib-release-{host_arch}-data.cmake" contents = client.load(data) assert 'set(ZLIB_FIND_MODE "")' in contents # needs at least 3.23.3 because of error with "empty identity" @pytest.mark.tool("cmake", "3.23") def test_error_missing_build_type(matrix_client): # https://github.com/conan-io/conan/issues/11168 c = matrix_client conanfile = textwrap.dedent(""" [requires] matrix/1.0 [generators] CMakeDeps CMakeToolchain """) main = textwrap.dedent(""" #include int main() {matrix();return 0;} """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) find_package(matrix REQUIRED) add_executable(app) target_link_libraries(app matrix::matrix) target_sources(app PRIVATE main.cpp) """) if platform.system() != "Windows": c.save({ "conanfile.txt": conanfile, "main.cpp": main, "CMakeLists.txt": cmakelists }, clean_first=True) c.run("install .") c.run_command("cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -G 'Unix Makefiles'", assert_error=True) assert "Please, set the CMAKE_BUILD_TYPE variable when calling to CMake" in c.out c.save({ "conanfile.txt": conanfile, "main.cpp": main, "CMakeLists.txt": cmakelists }, clean_first=True) c.run("install .") generator = { "Windows": '-G "Visual Studio 15 2017"', "Darwin": '-G "Xcode"', "Linux": '-G "Ninja Multi-Config"' }.get(platform.system()) c.run_command("cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake {}".format(generator)) c.run_command("cmake --build . --config Release") run_app = r".\Release\app.exe" if platform.system() == "Windows" else "./Release/app" c.run_command(run_app) assert "matrix/1.0: Hello World Release!" in c.out @pytest.mark.tool("cmake") def test_map_imported_config(transitive_libraries): # https://github.com/conan-io/conan/issues/12041 c = transitive_libraries conanfile = textwrap.dedent(""" [requires] engine/1.0 [generators] CMakeDeps CMakeToolchain """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(app CXX) set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) find_package(engine REQUIRED) add_executable(app main.cpp) target_link_libraries(app engine::engine) """) c.save({ "conanfile.txt": conanfile, "main.cpp": gen_function_cpp(name="main", includes=["engine"], calls=["engine"]), "CMakeLists.txt": cmakelists }, clean_first=True) c.run("install . -s build_type=Release") if platform.system() != "Windows": c.run_command("cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake " "-DCMAKE_BUILD_TYPE=DEBUG") c.run_command("cmake --build .") c.run_command("./app") else: c.run_command("cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake") c.run_command("cmake --build . --config Debug") c.run_command("Debug\\app.exe") assert "matrix/1.0: Hello World Release!" in c.out assert "engine/1.0: Hello World Release!" in c.out assert "main: Debug!" in c.out @pytest.mark.tool("cmake", "3.23") @pytest.mark.skipif(platform.system() != "Windows", reason="Windows DLL specific") def test_cmake_target_runtime_dlls(transitive_libraries): # https://github.com/conan-io/conan/issues/13504 c = transitive_libraries c.run("new cmake_exe -d name=foo -d version=1.0 -d requires=engine/1.0 -f") cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(foo CXX) find_package(engine CONFIG REQUIRED) add_executable(foo src/foo.cpp src/main.cpp) target_link_libraries(foo PRIVATE engine::engine) # Make sure CMake copies DLLs from dependencies, next to the executable # in this case it should copy engine.dll add_custom_command(TARGET foo POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ $ COMMAND_EXPAND_LISTS) """) c.save({"CMakeLists.txt": cmakelists}) c.run('install . -s build_type=Release -o "engine/*":shared=True') c.run_command("cmake -S . -B build/ " "-DCMAKE_TOOLCHAIN_FILE=build/generators/conan_toolchain.cmake") c.run_command("cmake --build build --config Release") c.run_command("build\\Release\\foo.exe") assert os.path.exists(os.path.join(c.current_folder, "build", "Release", "engine.dll")) assert "engine/1.0: Hello World Release!" in c.out # if the DLL wasn't copied, the application would not run and show output @pytest.mark.tool("cmake") def test_quiet(): conanfile = textwrap.dedent(""" from conan import ConanFile class Test(ConanFile): name = "test" version = "0.1" def package_info(self): self.cpp_info.system_libs = ["lib1"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") conanfile = textwrap.dedent(""" [requires] test/0.1 [generators] CMakeDeps """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}) find_package(test QUIET) """) client.save({"conanfile.txt": conanfile, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("install .") client.run_command('cmake . -DCMAKE_BUILD_TYPE=Release') # Because we used QUIET, not in output assert "Target declared 'test::test'" not in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only Windows and MSVC") @pytest.mark.tool("meson") @pytest.mark.tool("pkg_config") @pytest.mark.tool("ninja") @pytest.mark.tool("cmake", "3.23") def test_meson_and_cmakedeps_and_static_builds(): """ Testing when library is built with Meson + MSVC as a static library (lib + name + .a) and consumed using CMakeDeps (and CMakeConfigDeps) or PkgConfigDeps. Issues related: - https://github.com/conan-io/conan/issues/11866 - https://github.com/mesonbuild/meson/issues/7378 """ client = TestClient() profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release """) client.run("new meson_lib -d name=hello -d version=1.0") cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) find_package(hello CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example hello::hello) """) cmake_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.build import can_run class pkgTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """) client.save({ "win": profile, "test_package_cmake/conanfile.py": cmake_conanfile, "test_package_cmake/CMakeLists.txt": cmakelists }) shutil.copytree(os.path.join(client.current_folder, "test_package", "src"), os.path.join(client.current_folder, "test_package_cmake", "src")) client.run("create . -pr:a win") # meson + pkgconfigdeps (test_package) runs OK # meson + CMakeDeps (test_package_cmake) runs OK client.run("test --profile:all=win test_package_cmake hello/1.0") # Now, let's use the CMakeConfigDeps to demonstrate that it works with/without changing replace_in_file(ConanFileMock(), os.path.join(client.current_folder, "test_package_cmake", "conanfile.py"), '"CMakeDeps"', '"CMakeConfigDeps"') # meson + CMakeConfigDeps (test_package_cmake) runs OK client.run("test --profile:all=win test_package_cmake hello/1.0 " "-c tools.cmake.cmakedeps:new=will_break_next") ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_aggregator.py ================================================ import textwrap from conan.test.assets.sources import gen_function_cpp def test_aggregator(transitive_libraries): c = transitive_libraries conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "engine/1.0" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "*" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.path.join(self.cpp.build.bindir, "app")) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) project(app CXX) include(${CMAKE_BINARY_DIR}/generators/conandeps_legacy.cmake) add_executable(app main.cpp) target_link_libraries(app ${CONANDEPS_LEGACY}) """) c.save({ "conanfile.py": conanfile, "main.cpp": gen_function_cpp(name="main", includes=["engine"], calls=["engine"]), "CMakeLists.txt": cmakelists }, clean_first=True) c.run("build .") assert "matrix/1.0: Hello World Release!" in c.out assert "engine/1.0: Hello World Release!" in c.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_aliases.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Consumer(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = ["CMakeLists.txt"] requires = "hello/1.0" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) @pytest.mark.tool("cmake") def test_global_alias(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): # the default global target is "hello::hello" self.cpp_info.set_property("cmake_target_aliases", ["hello"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(link_libraries hello INTERFACE_LINK_LIBRARIES) message("hello link libraries: ${link_libraries}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run("create .") assert "hello link libraries: hello::hello" in client.out @pytest.mark.tool("cmake") def test_component_alias(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["buy"].set_property("cmake_target_aliases", ["hola::adios"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(link_libraries hola::adios INTERFACE_LINK_LIBRARIES) message("hola::adios link libraries: ${link_libraries}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run("create .") assert "hola::adios link libraries: hello::buy" in client.out @pytest.mark.tool("cmake") def test_custom_name(): conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.set_property("cmake_target_name", "ola::comprar") self.cpp_info.set_property("cmake_target_aliases", ["hello"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) get_target_property(link_libraries hello INTERFACE_LINK_LIBRARIES) message("hello link libraries: ${link_libraries}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run("create .") assert "hello link libraries: ola::comprar" in client.out @pytest.mark.tool("cmake") def test_collide_global_alias(): """ FIXME: right now, having multiple aliases with same name doesn't emit any warning/error. Possible alias collisions should be checked in CMakeDeps generator """ conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): # the default global target is "hello::hello" self.cpp_info.set_property("cmake_target_aliases", ["hello::hello"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run("create .") # assert "Target name 'hello::hello' already exists." in client.out @pytest.mark.tool("cmake") def test_collide_component_alias(): """ FIXME: right now, having multiple aliases with same name doesn't emit any warning/error. Possible alias collisions should be checked in CMakeDeps generator """ conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["buy"].set_property("cmake_target_aliases", ["hello::buy"]) """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(hello REQUIRED) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run("create .") # assert "Target name 'hello::buy' already exists." in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Linux", reason="Only Linux") @pytest.mark.tool("cmake") def test_shared_link_flags(): """ Testing CMakeDeps and linker flags injection Issue: https://github.com/conan-io/conan/issues/9936 """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class HelloConan(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False]} default_options = {"shared": False} exports_sources = "CMakeLists.txt", "src/*", "include/*" generators = "CMakeDeps", "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["hello"] self.cpp_info.sharedlinkflags = ["-z now", "-z relro"] self.cpp_info.exelinkflags = ["-z now", "-z relro"] """) client = TestClient() client.run("new cmake_lib -d name=hello -d version=1.0") client.save({"conanfile.py": conanfile}) client.run("create .") host_arch = client.get_default_host_profile().settings['arch'] build_folder = client.created_test_build_folder("hello/1.0") t = os.path.join("test_package", build_folder, "generators", f"hello-release-{host_arch}-data.cmake") target_data_cmake_content = client.load(t) assert 'set(hello_SHARED_LINK_FLAGS_RELEASE "-z now;-z relro")' in target_data_cmake_content assert 'set(hello_EXE_LINK_FLAGS_RELEASE "-z now;-z relro")' in target_data_cmake_content assert "hello/1.0: Hello World Release!" in client.out def test_not_mixed_configurations(): # https://github.com/conan-io/conan/issues/11852 client = TestClient() client.run("new cmake_lib -d name=foo -d version=1.0") conanfile = client.load("conanfile.py") conanfile.replace("package_info(self)", "invalid(self)") conanfile += """ def package_info(self): self.cpp_info.libs = ["foo" if self.settings.build_type == "Release" else "foo_d"] """ cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(foo CXX) add_library(foo "src/foo.cpp") target_include_directories(foo PUBLIC include) set_target_properties(foo PROPERTIES PUBLIC_HEADER "include/foo.h") # Different name for Release or Debug set_target_properties(foo PROPERTIES OUTPUT_NAME_DEBUG foo_d) set_target_properties(foo PROPERTIES OUTPUT_NAME_RELEASE foo) install(TARGETS foo) """) client.save({"CMakeLists.txt": cmake, "conanfile.py": conanfile}) client.run("create .") client.run("create . -s build_type=Debug") if platform.system() != "Windows": assert "libfoo_d.a" in client.out # Just to make sure we built the foo_d # Now create a consumer of foo with CMakeDeps locally conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout class ConsumerConan(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" requires = "foo/1.0" generators = "CMakeDeps" def layout(self): cmake_layout(self) def generate(self): tc = CMakeToolchain(self) tc.cache_variables["CMAKE_VERBOSE_MAKEFILE:BOOL"] = "ON" tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(consumer CXX) find_package(foo) add_executable(consumer src/consumer.cpp) target_include_directories(consumer PUBLIC include) target_link_libraries(consumer foo::foo) set_target_properties(consumer PROPERTIES PUBLIC_HEADER "include/consumer.h")""") consumer_cpp = gen_function_cpp(name="main", includes=["foo"], calls=["foo"]) consumer_h = gen_function_h(name="consumer") client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "src/consumer.cpp": consumer_cpp, "src/consumer.h": consumer_h}) client.run("install . -s build_type=Debug") client.run("install . -s build_type=Release") # With the bug, this build only fail on windows client.run("build .") # But we inspect the output for Macos/Linux to check that the library is not linked if platform.system() != "Windows": assert "libfoo_d.a" not in client.out assert "libfoo.a" in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_build_modules.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") def test_build_modules_alias_target(): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Conan(ConanFile): name = "hello" version = "1.0" settings = "os", "arch", "compiler", "build_type" exports_sources = ["target-alias.cmake"] def package(self): copy(self, "target-alias.cmake", self.source_folder, os.path.join(self.package_folder, "share/cmake")) def package_info(self): module = os.path.join("share", "cmake", "target-alias.cmake") self.cpp_info.set_property("cmake_build_modules", [module]) """) target_alias = textwrap.dedent(""" add_library(otherhello INTERFACE IMPORTED) target_link_libraries(otherhello INTERFACE hello::hello) """) client.save({"conanfile.py": conanfile, "target-alias.cmake": target_alias}) client.run("create .") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Conan(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" exports_sources = ["CMakeLists.txt"] generators = "CMakeDeps", "CMakeToolchain" requires = "hello/1.0" def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.0) project(test NONE) find_package(hello CONFIG) get_target_property(tmp otherhello INTERFACE_LINK_LIBRARIES) message("otherhello link libraries: ${tmp}") """) client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) client.run("create .") assert "otherhello link libraries: hello::hello" in client.out @pytest.mark.tool("cmake") def test_build_modules_custom_script(): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Conan(ConanFile): name = "myfunctions" version = "1.0" exports_sources = ["*.cmake"] def package(self): copy(self, "*.cmake", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.set_property("cmake_build_modules", ["myfunction.cmake"]) """) myfunction = textwrap.dedent(""" function(myfunction) message("Hello myfunction!!!!") endfunction() """) client.save({"conanfile.py": conanfile, "myfunction.cmake": myfunction}) client.run("create .") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeDeps class Conan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" tool_requires = "myfunctions/1.0" def generate(self): deps = CMakeDeps(self) deps.build_context_activated = ["myfunctions"] deps.build_context_build_modules = ["myfunctions"] deps.set_property("myfunctions", "cmake_find_mode", "module", build_context=True) deps.generate() def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.0) project(test NONE) find_package(myfunctions MODULE REQUIRED) myfunction() """) client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("build .") assert "Hello myfunction!!!!" in client.out @pytest.mark.tool("cmake") def test_build_modules_components_is_not_possible(): """ The "cmake_build_module" property declared in the components is useless """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Conan(ConanFile): name = "openssl" version = "1.0" settings = "os", "arch", "compiler", "build_type" exports_sources = ["crypto.cmake", "root.cmake"] def package(self): copy(self, "*.cmake", self.source_folder, os.path.join(self.package_folder, "share/cmake")) def package_info(self): crypto_module = os.path.join("share", "cmake", "crypto.cmake") self.cpp_info.components["crypto"].set_property("cmake_build_modules", [crypto_module]) root_module = os.path.join("share", "cmake", "root.cmake") self.cpp_info.set_property("cmake_build_modules", [root_module]) """) crypto_cmake = textwrap.dedent(""" function(crypto_message MESSAGE_OUTPUT) message("CRYPTO MESSAGE:${ARGV${0}}") endfunction() """) root_cmake = textwrap.dedent(""" function(root_message MESSAGE_OUTPUT) message("ROOT MESSAGE:${ARGV${0}}") endfunction() """) client.save({"conanfile.py": conanfile, "crypto.cmake": crypto_cmake, "root.cmake": root_cmake}) client.run("create .") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Conan(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" exports_sources = ["CMakeLists.txt"] generators = "CMakeDeps", "CMakeToolchain" requires = "openssl/1.0" def build(self): cmake = CMake(self) cmake.configure() def package_info(self): self.cpp_info.requires = ["openssl::crypto"] """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.0) project(test NONE) find_package(openssl CONFIG) crypto_message("hello!") root_message("hello!") """) client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) # As we are requiring only "crypto" but it doesn't matter, it is not possible to include # only crypto build_modules client.run("create .", assert_error=True) assert 'Unknown CMake command "crypto_message"' in client.out # Comment the function call client.save({"CMakeLists.txt": cmakelists.replace("crypto", "#crypto")}) assert "ROOT MESSAGE:hello!" not in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("editable", [True, False]) def test_build_modules_custom_script_editable(editable): c = TestClient() conanfile = textwrap.dedent(r""" import os, glob from conan import ConanFile from conan.tools.cmake import cmake_layout from conan.tools.files import copy, save class Conan(ConanFile): name = "myfunctions" version = "1.0" exports_sources = ["src/*.cmake"] settings = "build_type", "arch" def build(self): cmake = 'set(MY_CMAKE_PATH ${CMAKE_CURRENT_LIST_DIR})\n'\ 'macro(otherfunc)\n'\ 'file(READ "${MY_CMAKE_PATH}/my.txt" c)\n'\ 'message("Hello ${c}!!!!")\nendmacro()' save(self, "otherfuncs.cmake", cmake) save(self, "my.txt", "contents of text file!!!!") def layout(self): cmake_layout(self, src_folder="src") src = glob.glob(os.path.join(self.recipe_folder, self.folders.source, "*.cmake")) build = glob.glob(os.path.join(self.recipe_folder, self.folders.build, "*.cmake")) self.cpp.source.set_property("cmake_build_modules", src) self.cpp.build.set_property("cmake_build_modules", build) def package(self): copy(self, "*.cmake", self.source_folder, os.path.join(self.package_folder, "mods"), keep_path=False) copy(self, "*.cmake", self.build_folder, os.path.join(self.package_folder, "mods"), keep_path=False) copy(self, "*.txt", self.build_folder, os.path.join(self.package_folder, "mods"), keep_path=False) def package_info(self): self.cpp_info.set_property("cmake_build_modules", glob.glob("mods/*.cmake")) """) myfunction = textwrap.dedent(""" function(myfunction) message("Hello myfunction!!!!") endfunction() """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Conan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain", "CMakeDeps" requires = "myfunctions/1.0" def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(myfunctions CONFIG REQUIRED) myfunction() otherfunc() """) c.save({"functions/conanfile.py": conanfile, "functions/src/myfunction.cmake": myfunction, "app/conanfile.py": consumer, "app/CMakeLists.txt": cmakelists}) if editable: c.run("editable add functions") c.run('build functions -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"') else: c.run("create functions") c.run('build app -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"') assert "Hello myfunction!!!!" in c.out assert "Hello contents of text file!!!!" in c.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("editable", [True, False]) def test_build_modules_custom_script_editable_package(editable): # same as above, but with files from the package folder c = TestClient() conanfile = textwrap.dedent(r""" import os, glob from conan import ConanFile from conan.tools.cmake import cmake_layout from conan.tools.files import copy, save class Conan(ConanFile): name = "myfunctions" version = "1.0" exports_sources = ["src/*.cmake", "src/vis/*.vis"] settings = "build_type", "arch" def build(self): config = str(self.settings.build_type).upper() cmake = f'set(MY_CMAKE_PATH ${{{self.name}_RES_DIRS_{config}}})\n'\ 'macro(otherfunc)\n'\ 'file(READ "${MY_CMAKE_PATH}/my.vis" c)\n'\ 'message("Hello ${c}!!!!")\nendmacro()' save(self, "otherfuncs.cmake", cmake) def layout(self): cmake_layout(self, src_folder="src") src = glob.glob(os.path.join(self.recipe_folder, self.folders.source, "*.cmake")) build = glob.glob(os.path.join(self.recipe_folder, self.folders.build, "*.cmake")) self.cpp.source.set_property("cmake_build_modules", src) self.cpp.build.set_property("cmake_build_modules", build) self.cpp.source.resdirs = ["vis"] def package(self): copy(self, "*.cmake", self.source_folder, os.path.join(self.package_folder, "mods"), keep_path=False) copy(self, "*.cmake", self.build_folder, os.path.join(self.package_folder, "mods"), keep_path=False) copy(self, "*.vis", self.source_folder, os.path.join(self.package_folder, "mods"), keep_path=False) def package_info(self): self.cpp_info.resdirs = ["mods"] self.cpp_info.set_property("cmake_build_modules", glob.glob("mods/*.cmake")) """) myfunction = textwrap.dedent(""" function(myfunction) message("Hello myfunction!!!!") endfunction() """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Conan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain", "CMakeDeps" requires = "myfunctions/1.0" def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(test NONE) find_package(myfunctions CONFIG REQUIRED) myfunction() otherfunc() """) c.save({"functions/conanfile.py": conanfile, "functions/src/myfunction.cmake": myfunction, "functions/src/vis/my.vis": "contents of text file!!!!", "app/conanfile.py": consumer, "app/CMakeLists.txt": cmakelists}) if editable: c.run("editable add functions") c.run('build functions -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"') else: c.run("create functions -vv") c.run('build app -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"') assert "Hello myfunction!!!!" in c.out assert "Hello contents of text file!!!!" in c.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient class TestPropagateSpecificComponents: """ Feature: recipes can declare the components they are consuming from their requirements, only those components should be propagated to their own consumers. If required components doesn't exist, Conan will fail: * Resolved versions/revisions of the requirement might provide different components or no components at all. """ top = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): name = "top" def package_info(self): self.cpp_info.components["cmp1"].libs = ["top_cmp1"] self.cpp_info.components["cmp2"].libs = ["top_cmp2"] """) middle = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): name = "middle" requires = "top/version" def package_info(self): self.cpp_info.requires = ["top::cmp1"] """) app = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): settings = "os", "compiler", "build_type", "arch" name = "app" requires = "middle/version" """) @pytest.fixture(autouse=True) def setup(self): client = TestClient() client.save({ 'top.py': self.top, 'middle.py': self.middle, 'app.py': self.app }) client.run('create top.py --name=top --version=version') client.run('create middle.py --name=middle --version=version') self.cache_folder = client.cache_folder def test_cmakedeps_app(self): t = TestClient(cache_folder=self.cache_folder) t.save({'conanfile.py': self.app}) t.run("install . -g CMakeDeps") config = t.load("middle-Target-release.cmake") assert 'top::cmp1' in config assert "top::top" not in config def test_cmakedeps_multi(self): t = TestClient(cache_folder=self.cache_folder) t.run('install --requires=middle/version@ -g CMakeDeps') host_arch = t.get_default_host_profile().settings['arch'] content = t.load(f'middle-release-{host_arch}-data.cmake') assert "list(APPEND middle_FIND_DEPENDENCY_NAMES top)" in content content = t.load('middle-Target-release.cmake') assert "top::top" not in content assert "top::cmp2" not in content assert "top::cmp1" in content @pytest.fixture def top_conanfile(): return textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.cpp_info.components["cmp1"].libs = ["top_cmp1"] self.cpp_info.components["cmp2"].libs = ["top_cmp2"] """) @pytest.mark.parametrize("from_component", [False, True]) def test_wrong_component(top_conanfile, from_component): """ If the requirement doesn't provide the component, it fails. We can only raise this error after the graph is fully resolved, it is when we know the actual components that the requirement is going to provide. """ # TODO: This test is specific of CMakeDeps, could be made general? consumer = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): requires = "top/version" def package_info(self): self.cpp_info.{}requires = ["top::not-existing"] """).format("components['foo']." if from_component else "") t = TestClient() t.save({'top.py': top_conanfile, 'consumer.py': consumer}) t.run('create top.py --name=top --version=version') t.run('create consumer.py --name=wrong --version=version') t.run('install --requires=wrong/version@ -g CMakeDeps', assert_error=True) assert "Component 'top::not-existing' not found in 'top' package requirement" in t.out @pytest.mark.tool("cmake") def test_components_system_libs(): conanfile = textwrap.dedent(""" from conan import ConanFile class Requirement(ConanFile): name = "requirement" version = "system" settings = "os", "arch", "compiler", "build_type" def package_info(self): self.cpp_info.components["component"].system_libs = ["system_lib_component"] """) t = TestClient() t.save({"conanfile.py": conanfile}) t.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Consumer(ConanFile): name = "consumer" version = "0.1" requires = "requirement/system" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "CMakeLists.txt" settings = "os", "arch", "compiler", "build_type" def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) find_package(requirement) get_target_property(tmp_libs requirement::component INTERFACE_LINK_LIBRARIES) get_target_property(tmp_options requirement::component INTERFACE_LINK_OPTIONS) get_target_property(tmp_deps requirement_requirement_component_DEPS_TARGET INTERFACE_LINK_LIBRARIES) message("component libs: ${tmp_libs}") message("component options: ${tmp_options}") message("component deps: ${tmp_deps}") """) t.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists}) t.run("create . --build missing -s build_type=Release") assert ('component libs: $<$:>;$<$:>;' 'requirement_requirement_component_DEPS_TARGET') in t.out assert ('component deps: $<$:>;$<$:' 'system_lib_component>;') in t.out assert ('component options: ' '$<$:' '$<$,SHARED_LIBRARY>:>;' '$<$,MODULE_LIBRARY>:>;' '$<$,EXECUTABLE>:>>') in t.out # NOTE: If there is no "conan install -s build_type=Debug", the properties won't contain the # @pytest.mark.tool("cmake") def test_components_exelinkflags(): conanfile = textwrap.dedent(""" from conan import ConanFile class Requirement(ConanFile): name = "requirement" version = "system" settings = "os", "arch", "compiler", "build_type" def package_info(self): self.cpp_info.components["component"].exelinkflags = ["-Wl,-link1", "-Wl,-link2"] """) t = TestClient() t.save({"conanfile.py": conanfile}) t.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Consumer(ConanFile): name = "consumer" version = "0.1" requires = "requirement/system" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "CMakeLists.txt" settings = "os", "arch", "compiler", "build_type" def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) find_package(requirement) get_target_property(tmp_options requirement::component INTERFACE_LINK_OPTIONS) message("component options: ${tmp_options}") """) t.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists}) t.run("create . --build missing -s build_type=Release") assert ('component options: ' '$<$:' '$<$,SHARED_LIBRARY>:>;' '$<$,MODULE_LIBRARY>:>;' '$<$,EXECUTABLE>:-Wl,-link1;-Wl,-link2>>') in t.out # NOTE: If there is no "conan install -s build_type=Debug", the properties won't contain the # @pytest.mark.tool("cmake") def test_components_sharedlinkflags(): conanfile = textwrap.dedent(""" from conan import ConanFile class Requirement(ConanFile): name = "requirement" version = "system" settings = "os", "arch", "compiler", "build_type" def package_info(self): self.cpp_info.components["component"].sharedlinkflags = ["-Wl,-link1", "-Wl,-link2"] """) t = TestClient() t.save({"conanfile.py": conanfile}) t.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Consumer(ConanFile): name = "consumer" version = "0.1" requires = "requirement/system" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "CMakeLists.txt" settings = "os", "arch", "compiler", "build_type" def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) find_package(requirement) get_target_property(tmp_options requirement::component INTERFACE_LINK_OPTIONS) message("component options: ${tmp_options}") """) t.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists}) t.run("create . --build missing -s build_type=Release") assert ('component options: ' '$<$:' '$<$,SHARED_LIBRARY>:-Wl,-link1;-Wl,-link2>;' '$<$,MODULE_LIBRARY>:-Wl,-link1;-Wl,-link2>;' '$<$,EXECUTABLE>:>>') in t.out # NOTE: If there is no "conan install -s build_type=Debug", the properties won't contain the # @pytest.mark.tool("cmake") def test_cmake_add_subdirectory(): """https://github.com/conan-io/conan/issues/11743 https://github.com/conan-io/conan/issues/11755""" t = TestClient() boost = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): name = "boost" version = "1.0" def package_info(self): self.cpp_info.set_property("cmake_file_name", "Boost") self.cpp_info.components["A"].system_libs = ["A_1", "A_2"] self.cpp_info.components["B"].system_libs = ["B_1", "B_2"] """) t.save({"conanfile.py": boost}) t.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Consumer(ConanFile): name = "consumer" version = "0.1" requires = "boost/1.0" generators = "CMakeDeps", "CMakeToolchain" settings = "os", "arch", "compiler", "build_type" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(hello NONE) find_package(Boost CONFIG) add_subdirectory(src) """) sub_cmakelists = textwrap.dedent(""" find_package(Boost REQUIRED COMPONENTS exception headers) message("AGGREGATED LIBS: ${Boost_LIBRARIES}") get_target_property(tmp boost::boost INTERFACE_LINK_LIBRARIES) message("AGGREGATED LINKED: ${tmp}") get_target_property(tmp boost::B INTERFACE_LINK_LIBRARIES) message("BOOST_B LINKED: ${tmp}") get_target_property(tmp boost::A INTERFACE_LINK_LIBRARIES) message("BOOST_A LINKED: ${tmp}") get_target_property(tmp boost_boost_B_DEPS_TARGET INTERFACE_LINK_LIBRARIES) message("BOOST_B_DEPS LINKED: ${tmp}") get_target_property(tmp boost_boost_A_DEPS_TARGET INTERFACE_LINK_LIBRARIES) message("BOOST_A_DEPS LINKED: ${tmp}") """) t.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists, "src/CMakeLists.txt": sub_cmakelists}) t.run("install .") # only doing the configure failed before #11743 fix t.run("build .") # The boost::boost target has linked the two components assert "AGGREGATED LIBS: boost::boost" in t.out assert "AGGREGATED LINKED: boost::B;boost::A" in t.out assert ("BOOST_B LINKED: $<$:>;$<$:>;" "boost_boost_B_DEPS_TARGET") in t.out assert ("BOOST_A LINKED: $<$:>;$<$:>;" "boost_boost_A_DEPS_TARGET") in t.out assert "BOOST_B_DEPS LINKED: $<$:>;$<$:B_1;B_2>" in t.out assert "BOOST_A_DEPS LINKED: $<$:>;$<$:A_1;A_2>;" in t.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components_names.py ================================================ import os import shutil import textwrap import pytest from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") @pytest.fixture(scope="module") def setup_client_with_greetings(): """ creates a multi-component package with 2 components "hello" and "bye """ hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello", includes=["hello"]) bye_h = gen_function_h(name="bye") bye_cpp = gen_function_cpp(name="bye", includes=["bye"]) conanfile_greetings = textwrap.dedent(""" from os.path import join from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class GreetingsConan(ConanFile): name = "greetings" version = "0.0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "src/*" options = {"components": ["standard", "custom", "none"]} default_options = {"components": "standard"} def build(self): cmake = CMake(self) cmake.configure(build_script_folder="src") cmake.build() def package(self): copy(self, "*.h", src=join(self.source_folder, "src"), dst=join(self.package_folder, "include")) copy(self, "*.lib", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) def package_info(self): def set_comp_default_dirs(): self.cpp_info.components["hello"].includedirs = ["include"] self.cpp_info.components["hello"].libdirs = ["lib"] self.cpp_info.components["bye"].includedirs = ["include"] self.cpp_info.components["bye"].libdirs = ["lib"] if self.options.components == "standard": self.cpp_info.components["hello"].libs = ["hello"] self.cpp_info.components["bye"].libs = ["bye"] set_comp_default_dirs() elif self.options.components == "custom": self.cpp_info.set_property("cmake_file_name", "MYG") self.cpp_info.set_property("cmake_target_name", "MyGreetings::MyGreetings") self.cpp_info.components["hello"].set_property("cmake_target_name", "MyGreetings::MyHello") self.cpp_info.components["bye"].set_property("cmake_target_name", "MyGreetings::MyBye") self.cpp_info.components["hello"].libs = ["hello"] self.cpp_info.components["bye"].libs = ["bye"] set_comp_default_dirs() else: self.cpp_info.libs = ["hello", "bye"] def package_id(self): del self.info.options.components """) cmakelists_greetings = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.0) project(greetings CXX) add_library(hello hello.cpp) add_library(bye bye.cpp) """) test_package_greetings_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake class GreetingsTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def test(self): path = "{}".format(self.settings.build_type) if self.settings.os == "Windows" else "." self.run("{}{}example".format(path, os.sep)) """) test_package_greetings_cpp = gen_function_cpp(name="main", includes=["hello", "bye"], calls=["hello", "bye"]) test_package_greetings_cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.0) project(PackageTest CXX) find_package(greetings) add_executable(example example.cpp) target_link_libraries(example greetings::greetings) """) client = TestClient() client.save({"conanfile.py": conanfile_greetings, "src/CMakeLists.txt": cmakelists_greetings, "src/hello.h": hello_h, "src/hello.cpp": hello_cpp, "src/bye.h": bye_h, "src/bye.cpp": bye_cpp, "test_package/conanfile.py": test_package_greetings_conanfile, "test_package/example.cpp": test_package_greetings_cpp, "test_package/CMakeLists.txt": test_package_greetings_cmakelists}) client.run("create . -s build_type=Release") assert "hello: Release!" in client.out assert "bye: Release!" in client.out client.run("create . -s build_type=Debug") assert "hello: Debug!" in client.out assert "bye: Debug!" in client.out return client def create_chat(client, components, package_info, cmake_find, test_cmake_find): conanfile = textwrap.dedent(""" from os.path import join from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class Chat(ConanFile): name = "chat" version = "0.0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "src/*" requires = "greetings/0.0.1" default_options = {{"greetings*:components": "{}"}} def build(self): cmake = CMake(self) cmake.configure(build_script_folder="src") cmake.build() def package(self): copy(self, "*.h", src=join(self.source_folder, "src"), dst=join(self.package_folder, "include")) copy(self, "*.lib", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) def package_info(self): {} """).format(components, "\n ".join(package_info.splitlines())) sayhello_h = gen_function_h(name="sayhello") sayhello_cpp = gen_function_cpp(name="sayhello", includes=["hello"], calls=["hello"]) sayhellobye_h = gen_function_h(name="sayhellobye") sayhellobye_cpp = gen_function_cpp(name="sayhellobye", includes=["sayhello", "bye"], calls=["sayhello", "bye"]) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.0) project(world CXX) %s """ % cmake_find) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake class WorldTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def test(self): path = "{}".format(self.settings.build_type) if self.settings.os == "Windows" else "." self.run("{}{}example".format(path, os.sep)) self.run("{}{}example2".format(path, os.sep)) """) test_example_cpp = gen_function_cpp(name="main", includes=["sayhellobye"], calls=["sayhellobye"]) test_cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) %s """ % test_cmake_find) client.save({"conanfile.py": conanfile, "src/CMakeLists.txt": cmakelists, "src/sayhello.h": sayhello_h, "src/sayhello.cpp": sayhello_cpp, "src/sayhellobye.h": sayhellobye_h, "src/sayhellobye.cpp": sayhellobye_cpp, "test_package/conanfile.py": test_conanfile, "test_package/CMakeLists.txt": test_cmakelists, "test_package/example.cpp": test_example_cpp}, clean_first=True) client.run("create . -s build_type=Release") assert "sayhellobye: Release!" in client.out assert "sayhello: Release!" in client.out assert "hello: Release!" in client.out assert "bye: Release!" in client.out client.run("create . -s build_type=Debug") assert "sayhellobye: Debug!" in client.out assert "sayhello: Debug!" in client.out assert "hello: Debug!" in client.out assert "bye: Debug!" in client.out @pytest.mark.tool("cmake") def test_standard_names(setup_client_with_greetings): client = setup_client_with_greetings package_info = textwrap.dedent(""" self.cpp_info.components["sayhello"].requires = ["greetings::hello"] self.cpp_info.components["sayhello"].libs = ["sayhello"] self.cpp_info.components["sayhello"].includedirs = ["include"] self.cpp_info.components["sayhello"].libdirs = ["lib"] self.cpp_info.components["sayhellobye"].requires = ["sayhello", "greetings::bye"] self.cpp_info.components["sayhellobye"].libs = ["sayhellobye"] self.cpp_info.components["sayhellobye"].includedirs = ["include"] self.cpp_info.components["sayhellobye"].libdirs = ["lib"] """) cmake_find = textwrap.dedent(""" find_package(greetings COMPONENTS hello bye) add_library(sayhello sayhello.cpp) target_link_libraries(sayhello greetings::hello) add_library(sayhellobye sayhellobye.cpp) target_link_libraries(sayhellobye sayhello greetings::bye) """) test_cmake_find = textwrap.dedent(""" find_package(chat) add_executable(example example.cpp) target_link_libraries(example chat::sayhellobye) add_executable(example2 example.cpp) target_link_libraries(example2 chat::chat) """) create_chat(client, "standard", package_info, cmake_find, test_cmake_find) @pytest.mark.tool("cmake") def test_custom_names(setup_client_with_greetings): client = setup_client_with_greetings package_info = textwrap.dedent(""" # NOTE: For the new CMakeDeps only filenames mean filename, it is not using the "names" field self.cpp_info.set_property("cmake_target_name", "MyChat::MyChat") self.cpp_info.set_property("cmake_file_name", "MyChat") self.cpp_info.components["sayhello"].set_property("cmake_target_name", "MyChat::MySay") self.cpp_info.components["sayhello"].requires = ["greetings::hello"] self.cpp_info.components["sayhello"].libs = ["sayhello"] self.cpp_info.components["sayhellobye"].set_property("cmake_target_name", "MyChat::MySayBye") self.cpp_info.components["sayhellobye"].requires = ["sayhello", "greetings::bye"] self.cpp_info.components["sayhellobye"].libs = ["sayhellobye"] self.cpp_info.components["sayhello"].libdirs = ["lib"] self.cpp_info.components["sayhello"].includedirs = ["include"] self.cpp_info.components["sayhellobye"].libdirs = ["lib"] self.cpp_info.components["sayhellobye"].includedirs = ["include"] """) cmake_find = textwrap.dedent(""" find_package(MYG COMPONENTS MyHello MyBye) add_library(sayhello sayhello.cpp) target_link_libraries(sayhello MyGreetings::MyHello) add_library(sayhellobye sayhellobye.cpp) target_link_libraries(sayhellobye sayhello MyGreetings::MyBye) """) test_cmake_find = textwrap.dedent(""" find_package(MyChat) add_executable(example example.cpp) target_link_libraries(example MyChat::MySayBye) add_executable(example2 example.cpp) target_link_libraries(example2 MyChat::MyChat) """) create_chat(client, "custom", package_info, cmake_find, test_cmake_find) @pytest.mark.tool("cmake") def test_different_namespace(setup_client_with_greetings): client = setup_client_with_greetings package_info = textwrap.dedent(""" self.cpp_info.set_property("cmake_target_name", "MyChat::MyGlobalChat") self.cpp_info.set_property("cmake_file_name", "MyChat") self.cpp_info.components["sayhello"].set_property("cmake_target_name", "MyChat::MySay") self.cpp_info.components["sayhello"].requires = ["greetings::hello"] self.cpp_info.components["sayhello"].libs = ["sayhello"] self.cpp_info.components["sayhellobye"].set_property("cmake_target_name", "MyChat::MySayBye") self.cpp_info.components["sayhellobye"].requires = ["sayhello", "greetings::bye"] self.cpp_info.components["sayhellobye"].libs = ["sayhellobye"] self.cpp_info.components["sayhello"].libdirs = ["lib"] self.cpp_info.components["sayhello"].includedirs = ["include"] self.cpp_info.components["sayhellobye"].libdirs = ["lib"] self.cpp_info.components["sayhellobye"].includedirs = ["include"] """) cmake_find = textwrap.dedent(""" find_package(MYG COMPONENTS MyHello MyBye) add_library(sayhello sayhello.cpp) target_link_libraries(sayhello MyGreetings::MyHello) add_library(sayhellobye sayhellobye.cpp) target_link_libraries(sayhellobye sayhello MyGreetings::MyBye) """) test_cmake_find = textwrap.dedent(""" find_package(MyChat) add_executable(example example.cpp) target_link_libraries(example MyChat::MySayBye) add_executable(example2 example.cpp) target_link_libraries(example2 MyChat::MyGlobalChat) """) create_chat(client, "custom", package_info, cmake_find, test_cmake_find) @pytest.mark.tool("cmake") def test_no_components(setup_client_with_greetings): client = setup_client_with_greetings package_info = textwrap.dedent(""" self.cpp_info.components["sayhello"].requires = ["greetings::greetings"] self.cpp_info.components["sayhello"].libs = ["sayhello"] self.cpp_info.components["sayhellobye"].requires = ["sayhello", "greetings::greetings"] self.cpp_info.components["sayhellobye"].libs = ["sayhellobye"] self.cpp_info.components["sayhello"].libdirs = ["lib"] self.cpp_info.components["sayhello"].includedirs = ["include"] self.cpp_info.components["sayhellobye"].libdirs = ["lib"] self.cpp_info.components["sayhellobye"].includedirs = ["include"] """) cmake_find = textwrap.dedent(""" find_package(greetings) add_library(sayhello sayhello.cpp) target_link_libraries(sayhello greetings::greetings) add_library(sayhellobye sayhellobye.cpp) target_link_libraries(sayhellobye sayhello greetings::greetings) """) test_cmake_find = textwrap.dedent(""" find_package(chat) add_executable(example example.cpp) target_link_libraries(example chat::sayhellobye) add_executable(example2 example.cpp) target_link_libraries(example2 chat::chat) """) create_chat(client, "none", package_info, cmake_find, test_cmake_find) @pytest.mark.tool("cmake") def test_same_names(): client = TestClient() conanfile_greetings = textwrap.dedent(""" from os.path import join from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class HelloConan(ConanFile): name = "hello" version = "0.0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "src/*" def build(self): cmake = CMake(self) cmake.configure(build_script_folder="src") cmake.build() def package(self): copy(self, "*.h", src=join(self.source_folder, "src"), dst=join(self.package_folder, "include")) copy(self, "*.lib", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) def package_info(self): self.cpp_info.components["global"].name = "hello" self.cpp_info.components["global"].libs = ["hello"] self.cpp_info.components["global"].includedirs = ["include"] self.cpp_info.components["global"].libdirs = ["lib"] """) hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello", includes=["hello"]) cmakelists_greetings = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.0) project(greetings CXX) add_library(hello hello.cpp) """) test_package_greetings_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def test(self): path = "{}".format(self.settings.build_type) if self.settings.os == "Windows" else "." self.run("{}{}example".format(path, os.sep)) """) test_package_greetings_cpp = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) test_package_greetings_cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.0) project(PackageTest CXX) find_package(hello) add_executable(example example.cpp) target_link_libraries(example hello::hello) """) client.save({"conanfile.py": conanfile_greetings, "src/CMakeLists.txt": cmakelists_greetings, "src/hello.h": hello_h, "src/hello.cpp": hello_cpp, "test_package/conanfile.py": test_package_greetings_conanfile, "test_package/example.cpp": test_package_greetings_cpp, "test_package/CMakeLists.txt": test_package_greetings_cmakelists}) client.run("create .") assert "hello: Release!" in client.out @pytest.mark.tool("cmake") class TestComponentsCMakeGenerators: def test_component_not_found(self): conanfile = textwrap.dedent(""" from conan import ConanFile class GreetingsConan(ConanFile): def package_info(self): self.cpp_info.components["hello"].libs = ["hello"] self.cpp_info.components["hello"].libdirs = ["lib"] self.cpp_info.components["hello"].includedirs = ["include"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=greetings --version=0.0.1") conanfile = textwrap.dedent(""" from conan import ConanFile class WorldConan(ConanFile): requires = "greetings/0.0.1" def package_info(self): self.cpp_info.components["helloworld"].requires = ["greetings::non-existent"] self.cpp_info.components["helloworld"].libdirs = ["lib"] self.cpp_info.components["helloworld"].includedirs = ["include"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=world --version=0.0.1") client.run("install --requires=world/0.0.1@ -g CMakeDeps", assert_error=True) assert ("Component 'greetings::non-existent' not found in 'greetings' " "package requirement" in client.out) def test_component_not_found_same_name_as_pkg_require(self): zlib = GenConanfile("zlib", "0.1").with_setting("build_type").with_generator("CMakeDeps") mypkg = GenConanfile("mypkg", "0.1").with_setting("build_type").with_generator("CMakeDeps") final = GenConanfile("final", "0.1").with_setting("build_type").with_generator("CMakeDeps")\ .with_require(RecipeReference("zlib", "0.1", None, None))\ .with_require(RecipeReference("mypkg", "0.1", None, None))\ .with_package_info(cpp_info={"components": {"cmp": {"requires": ["mypkg::zlib", "zlib::zlib"]}}}) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps class HelloConan(ConanFile): name = 'consumer' version = '0.1' def generate(self): deps = CMakeDeps(self) deps.check_components_exist = True deps.generate() def requirements(self): self.requires("final/0.1") settings = "build_type" """) client = TestClient() client.save({"zlib.py": zlib, "mypkg.py": mypkg, "final.py": final, "consumer.py": consumer}) client.run("create zlib.py") client.run("create mypkg.py") client.run("create final.py") client.run("install consumer.py", assert_error=True) assert "Component 'mypkg::zlib' not found in 'mypkg' package requirement" in client.out def test_same_name_global_target_collision(self): # https://github.com/conan-io/conan/issues/7889 conanfile_tpl = textwrap.dedent(""" from os.path import join from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class Conan(ConanFile): name = "{name}" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "src/*", "include/*" def build(self): cmake = CMake(self) cmake.configure(build_script_folder="src") cmake.build() def package(self): copy(self, "*.h", src=join(self.source_folder, "include"), dst=join(self.package_folder, "include")) copy(self, "*.lib", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) def package_info(self): self.cpp_info.set_property("cmake_target_name", "nonstd::nonstd" ) self.cpp_info.set_property("cmake_file_name", "{name}") self.cpp_info.components["1"].set_property("cmake_target_name", "nonstd::{name}") self.cpp_info.components["1"].libs = ["{name}"] self.cpp_info.components["1"].includedirs = ["include"] self.cpp_info.components["1"].libdirs = ["lib"] """) basic_cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) project(middle CXX) add_library({name} {name}.cpp) target_include_directories({name} PUBLIC ../include) """) client = TestClient() for name in ["expected", "variant"]: client.run("new cmake_lib -d name={name} -d version=1.0 -f".format(name=name)) client.save({"conanfile.py": conanfile_tpl.format(name=name), "src/CMakeLists.txt": basic_cmake.format(name=name)}) shutil.rmtree(os.path.join(client.current_folder, "test_package")) client.run("create .") middle_cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) project(middle CXX) find_package(expected) find_package(variant) add_library(middle middle.cpp) target_link_libraries(middle nonstd::nonstd) """) middle_h = gen_function_h(name="middle") middle_cpp = gen_function_cpp(name="middle", includes=["middle", "expected", "variant"], calls=["expected", "variant"]) middle_conanfile = textwrap.dedent(""" from os.path import join from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class Conan(ConanFile): name = "middle" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "src/*" requires = "expected/1.0", "variant/1.0" def build(self): cmake = CMake(self) cmake.configure(build_script_folder="src") cmake.build() def package(self): copy(self, "*.h", src=join(self.source_folder, "src"), dst=join(self.package_folder, "include")) copy(self, "*.lib", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", src=self.build_folder, dst=join(self.package_folder, "lib"), keep_path=False) def package_info(self): self.cpp_info.libs = ["middle"] """) client.save({"conanfile.py": middle_conanfile, "src/CMakeLists.txt": middle_cmakelists, "src/middle.h": middle_h, "src/middle.cpp": middle_cpp}, clean_first=True) client.run("create . --name=middle --version=1.0") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Conan(ConanFile): name = "consumer" version = "1.0" generators = "CMakeDeps", "CMakeToolchain" settings = "os", "compiler", "build_type", "arch" exports_sources = "src/*" requires = "middle/1.0" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure(build_script_folder="src") cmake.build() cmd = os.path.join(self.cpp.build.bindirs[0], "main") self.run(cmd, env="conanrun") """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) project(consumer CXX) find_package(middle) get_target_property(tmp middle::middle INTERFACE_LINK_LIBRARIES) message("Middle link libraries: ${tmp}") add_executable(main main.cpp) target_link_libraries(main middle::middle) """) main_cpp = gen_function_cpp(name="main", includes=["middle"], calls=["middle"]) client.save({"conanfile.py": conanfile, "src/CMakeLists.txt": cmakelists, "src/main.cpp": main_cpp}, clean_first=True) client.run("create . --name=consumer --version=1.0") assert 'main: Release!' in client.out assert 'middle: Release!' in client.out assert 'expected/1.0: Hello World Release!' in client.out assert 'variant/1.0: Hello World Release!' in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("check_components_exist", [False, True, None]) def test_targets_declared_in_build_modules(check_components_exist): """If a require is declaring the component targets in a build_module, CMakeDeps is fine with it, not needed to locate it as a conan declared component""" client = TestClient() conanfile_hello = str(GenConanfile().with_name("hello").with_version("1.0") .with_exports_sources("*.cmake", "*.h") .with_import("from conan.tools.files import copy") .with_import("from os.path import join")) conanfile_hello += """ def package(self): copy(self, "*.h", src=self.source_folder, dst=join(self.package_folder, "include")) copy(self, "*.cmake", src=self.build_folder, dst=join(self.package_folder, "cmake")) def package_info(self): self.cpp_info.set_property("cmake_build_modules", ["cmake/my_modules.cmake"]) """ my_modules = textwrap.dedent(""" add_library(cool_component INTERFACE) target_include_directories(cool_component INTERFACE ${CMAKE_CURRENT_LIST_DIR}/../include/) add_library(hello::invented ALIAS cool_component) """) hello_h = "int cool_header_only=1;" client.save({"conanfile.py": conanfile_hello, "my_modules.cmake": my_modules, "hello.h": hello_h}) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeDeps class HelloConan(ConanFile): name = 'app' version = '1.0' exports_sources = "*.txt", "*.cpp" generators = "CMakeToolchain" requires = ("hello/1.0", ) settings = "os", "compiler", "arch", "build_type" def generate(self): deps = CMakeDeps(self) {} deps.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) if check_components_exist is False: conanfile = conanfile.format("deps.check_components_exist=False") elif check_components_exist: conanfile = conanfile.format("deps.check_components_exist=True") else: conanfile = conanfile.format("") main_cpp = textwrap.dedent(""" #include #include "hello.h" int main(){ std::cout << "cool header value: " << cool_header_only; return 0; } """) cmakelist = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(project CXX) find_package(hello COMPONENTS hello::invented missing) add_executable(myapp main.cpp) target_link_libraries(myapp hello::invented) """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist, "main.cpp": main_cpp}) client.run("create .", assert_error=check_components_exist) assert bool(check_components_exist) == ("Conan: Component 'missing' NOT found in package " "'hello'" in client.out) assert "Conan: Including build module" in client.out assert "my_modules.cmake" in client.out assert bool(check_components_exist) == ("Conan: Component 'hello::invented' found in package 'hello'" in client.out) @pytest.mark.tool("cmake") def test_cmakedeps_targets_no_namespace(): """ This test is checking that when we add targets with no namespace for the root cpp_info and the components, the targets are correctly generated. Before Conan 1.43, Conan only generated targets with namespace """ client = TestClient() my_pkg = textwrap.dedent(""" from conan import ConanFile class MyPkg(ConanFile): name = "my_pkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" def package_info(self): self.cpp_info.set_property("cmake_target_name", "nonamespacepkg") self.cpp_info.components["MYPKGCOMP"].set_property("cmake_target_name", "MYPKGCOMPNAME") """) client.save({"my_pkg/conanfile.py": my_pkg}, clean_first=True) client.run("create my_pkg") conanfile = textwrap.dedent(""" from conan import ConanFile class LibcurlConan(ConanFile): name = "libcurl" version = "0.1" requires = "my_pkg/0.1" settings = "os", "arch", "compiler", "build_type" def package_info(self): self.cpp_info.set_property("cmake_target_name", "CURL") self.cpp_info.set_property("cmake_file_name", "CURLFILENAME") self.cpp_info.components["curl"].set_property("cmake_target_name", "libcurl") self.cpp_info.components["curl2"].set_property("cmake_target_name", "libcurl2") self.cpp_info.components["curl2"].requires.extend(["curl", "my_pkg::MYPKGCOMP"]) """) client.save({"libcurl/conanfile.py": conanfile}) client.run("create libcurl") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps, CMake, CMakeToolchain class Consumer(ConanFile): name = "consumer" version = "0.1" requires = "libcurl/0.1" settings = "os", "arch", "compiler", "build_type" exports_sources = "CMakeLists.txt" def generate(self): deps = CMakeDeps(self) deps.check_components_exist=True deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmakelists = textwrap.dedent("""cmake_minimum_required(VERSION 3.15) project(Consumer) find_package(CURLFILENAME CONFIG REQUIRED COMPONENTS libcurl libcurl2) """) client.save({"consumer/conanfile.py": conanfile, "consumer/CMakeLists.txt": cmakelists}) client.run("create consumer") assert "Component target declared 'libcurl'" in client.out assert "Component target declared 'libcurl2'" in client.out assert "Target declared 'CURL'" in client.out assert "Component target declared 'MYPKGCOMPNAME'" in client.out assert "Target declared 'nonamespacepkg'" in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_custom_configs.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient from conan.internal.util.files import save @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_custom_config_generator_multi(): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMakeDeps from conan.tools.files import copy class App(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello/0.1" def generate(self): cmake = CMakeDeps(self) if self.dependencies["hello"].options.shared: cmake.configuration = "ReleaseShared" cmake.generate() config = str(self.settings.build_type) if self.dependencies["hello"].options.shared: config = "ReleaseShared" src = os.path.join(self.dependencies["hello"].package_folder, "bin") dst = os.path.join(self.build_folder, config) copy(self, "*.dll", src, dst, keep_path=False) """) app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) cmakelist = textwrap.dedent(""" set(CMAKE_CONFIGURATION_TYPES Debug Release ReleaseShared CACHE STRING "Available build-types: Debug, Release and ReleaseShared") set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 2.8) project(App C CXX) set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR} ${CMAKE_PREFIX_PATH}) set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_MODULE_PATH}) set(CMAKE_CXX_FLAGS_RELEASESHARED ${CMAKE_CXX_FLAGS_RELEASE}) set(CMAKE_C_FLAGS_RELEASESHARED ${CMAKE_C_FLAGS_RELEASE}) set(CMAKE_EXE_LINKER_FLAGS_RELEASESHARED ${CMAKE_EXE_LINKER_FLAGS_RELEASE}) find_package(hello REQUIRED) add_executable(app app.cpp) target_link_libraries(app PRIVATE hello::hello) """) c = TestClient(path_with_spaces=False) c.run("new cmake_lib -d name=hello -d version=0.1") c.run("create . -s compiler.version=191 " "-s build_type=Release -o hello/*:shared=True -tf=\"\"") c.run("create . --name=hello --version=0.1 -s compiler.version=191 " "-s build_type=Release -tf=\"\"") # Prepare the actual consumer package c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist, "app.cpp": app}) settings = {"compiler": "msvc", "compiler.version": "191", "compiler.runtime": "dynamic", "arch": "x86_64", "build_type": "Release", } settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v) # Run the configure corresponding to this test case with c.chdir('build'): c.run("install .. %s -o hello/*:shared=True -of=." % settings) c.run("install .. %s -o hello/*:shared=False -of=." % settings) assert os.path.isfile(os.path.join(c.current_folder, "hello-Target-releaseshared.cmake")) assert os.path.isfile(os.path.join(c.current_folder, "hello-Target-release.cmake")) c.run_command('cmake .. -G "Visual Studio 15 Win64" --loglevel=DEBUG') assert "Found DLL and STATIC" in c.out c.run_command('cmake --build . --config Release') c.run_command(r"Release\\app.exe") assert "hello/0.1: Hello World Release!" in c.out assert "main: Release!" in c.out c.run_command('cmake --build . --config ReleaseShared') c.run_command(r"ReleaseShared\\app.exe") assert "hello/0.1: Hello World Release!" in c.out assert "main: Release!" in c.out @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_custom_config_settings(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps class App(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello/0.1" generators = "CMakeToolchain" def generate(self): cmake = CMakeDeps(self) #cmake.configurations.append("MyRelease") # NOT NECESSARY!!! cmake.generate() """) app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) cmakelist = textwrap.dedent(""" set(CMAKE_CONFIGURATION_TYPES Debug Release MyRelease CACHE STRING "Available build-types: Debug, Release and MyRelease") set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(App C CXX) set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR} ${CMAKE_PREFIX_PATH}) set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_MODULE_PATH}) set(CMAKE_CXX_FLAGS_MYRELEASE ${CMAKE_CXX_FLAGS_RELEASE}) set(CMAKE_C_FLAGS_MYRELEASE ${CMAKE_C_FLAGS_RELEASE}) set(CMAKE_EXE_LINKER_FLAGS_MYRELEASE ${CMAKE_EXE_LINKER_FLAGS_RELEASE}) find_package(hello REQUIRED) add_executable(app app.cpp) target_link_libraries(app PRIVATE hello::hello) """) c = TestClient(path_with_spaces=False) settings = "build_type: [MyRelease]" save(c.paths.settings_path_user, settings) c.run("new cmake_lib -d name=hello -d version=0.1") cmake = c.load("CMakeLists.txt") cmake = cmake.replace("cmake_minimum_required", """ set(CMAKE_CONFIGURATION_TYPES Debug MyRelease Release CACHE STRING "Types") cmake_minimum_required""") cmake = cmake.replace("add_library", textwrap.dedent(""" set(CMAKE_CXX_FLAGS_MYRELEASE ${CMAKE_CXX_FLAGS_RELEASE}) set(CMAKE_C_FLAGS_MYRELEASE ${CMAKE_C_FLAGS_RELEASE}) set(CMAKE_EXE_LINKER_FLAGS_MYRELEASE ${CMAKE_EXE_LINKER_FLAGS_RELEASE}) add_library""")) cmake = cmake.replace("PUBLIC_HEADER", "CONFIGURATIONS MyRelease\nPUBLIC_HEADER") c.save({"CMakeLists.txt": cmake}) c.run("create . --name=hello --version=0.1 -s compiler.version=191 " "-s build_type=MyRelease -s:b build_type=MyRelease -tf=\"\"") # Prepare the actual consumer package c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist, "app.cpp": app}) settings = {"compiler": "msvc", "compiler.version": "191", "compiler.runtime": "dynamic", "arch": "x86_64", "build_type": "MyRelease", } settings_h = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items()) settings_b = " ".join('-s:b %s="%s"' % (k, v) for k, v in settings.items()) # Run the configure corresponding to this test case build_directory = os.path.join(c.current_folder, "build").replace("\\", "/") with c.chdir(build_directory): c.run("install .. %s %s -of=." % (settings_h, settings_b)) toolchain = c.load("conan_toolchain.cmake") # As it is a custom configuration, the TRY_COMPILE_CONFIFURATION not defined assert "CMAKE_TRY_COMPILE_CONFIGURATION" not in toolchain assert os.path.isfile(os.path.join(c.current_folder, "hello-Target-myrelease.cmake")) c.run_command('cmake .. -G "Visual Studio 15" ' '-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake') c.run_command('cmake --build . --config MyRelease') c.run_command(r"MyRelease\\app.exe") assert "hello/0.1: Hello World Release!" in c.out assert "main: Release!" in c.out @pytest.mark.tool("cmake") def test_changing_build_type(): client = TestClient(path_with_spaces=False) dep_conanfile = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import copy, save class Dep(ConanFile): settings = "build_type" def build(self): save(self, "hello.h", '# include \n' 'void hello(){{std::cout<<"BUILD_TYPE={}!!";}}'.format(self.settings.build_type)) def package(self): copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include")) """) client.save({"conanfile.py": dep_conanfile}) client.run("create . --name=dep --version=0.1 -s build_type=Release") client.run("create . --name=dep --version=0.1 -s build_type=Debug") cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(App CXX) # NOTE: NO MAP necessary!!! find_package(dep REQUIRED) add_executable(app app.cpp) target_link_libraries(app PRIVATE dep::dep) """) app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) pkg_conanfile = GenConanfile("pkg", "0.1").with_requires("dep/0.1").\ with_generator("CMakeDeps").with_generator("CMakeToolchain").\ with_settings("os", "compiler", "arch", "build_type") client.save({"conanfile.py": pkg_conanfile, "CMakeLists.txt": cmakelists, "app.cpp": app}, clean_first=True) # in MSVC multi-config -s pkg/*:build_type=Debug is not really necesary, toolchain do nothing # TODO: Challenge how to define consumer build_type for conanfile.txt client.run("install . -s pkg*:build_type=Debug -s build_type=Release") client.run_command("cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake " "-DCMAKE_BUILD_TYPE=Debug") client.run_command("cmake --build . --config Debug") cmd = os.path.join(".", "Debug", "app") if platform.system() == "Windows" else "./app" client.run_command(cmd) assert "main: Debug!" in client.out assert "BUILD_TYPE=Release!!" in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_find_module_and_config.py ================================================ import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def client(): t = TestClient() cpp = gen_function_cpp(name="mydep") h = gen_function_h(name="mydep") cmake = gen_cmakelists(libname="mydep", libsources=["mydep.cpp"]) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class Conan(ConanFile): name = "mydep" version = "1.0" settings = "os", "arch", "compiler", "build_type" exports_sources = "*.cpp", "*.h", "CMakeLists.txt" generators = "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include")) copy(self, "*.lib", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) copy(self, "*.dll", self.build_folder, os.path.join(self.package_folder, "bin"), keep_path=False) copy(self, "*.dylib*", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) copy(self, "*.so", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) def package_info(self): self.cpp_info.set_property("cmake_find_mode", "both") self.cpp_info.set_property("cmake_file_name", "MyDep") self.cpp_info.set_property("cmake_target_name", "MyDepTarget::MyDepTarget") self.cpp_info.set_property("cmake_module_file_name", "mi_dependencia") self.cpp_info.set_property("cmake_module_target_name", "mi_dependencia_namespace::mi_dependencia_target") self.cpp_info.components["crispin"].libs = ["mydep"] self.cpp_info.components["crispin"].libdirs = ["lib"] self.cpp_info.components["crispin"].includedirs = ["include"] self.cpp_info.components["crispin"].set_property("cmake_target_name", "MyDepTarget::MyCrispinTarget") self.cpp_info.components["crispin"].set_property("cmake_module_target_name", "mi_dependencia_namespace::mi_crispin_target") """) t.save({"conanfile.py": conanfile, "mydep.cpp": cpp, "mydep.h": h, "CMakeLists.txt": cmake}) t.run("create .") return t @pytest.mark.tool("cmake") def test_reuse_with_modules_and_config(client): cpp = gen_function_cpp(name="main") cmake_exe_config = """ add_executable(myapp main.cpp) find_package(MyDep) # This one will find the config target_link_libraries(myapp MyDepTarget::MyCrispinTarget) """ cmake_exe_module = """ add_executable(myapp2 main.cpp) find_package(mi_dependencia) # This one will find the module target_link_libraries(myapp2 mi_dependencia_namespace::mi_crispin_target) """ cmake = """ set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(project CXX) {} """ # test config conanfile = GenConanfile().with_name("myapp")\ .with_cmake_build().with_exports_sources("*.cpp", "*.txt").with_require("mydep/1.0") client.save({"conanfile.py": conanfile, "main.cpp": cpp, "CMakeLists.txt": cmake.format(cmake_exe_config)}) client.run("install .") client.run("build .") # test modules conanfile = GenConanfile().with_name("myapp")\ .with_cmake_build().with_exports_sources("*.cpp", "*.txt").with_require("mydep/1.0") client.save({"conanfile.py": conanfile, "main.cpp": cpp, "CMakeLists.txt": cmake.format(cmake_exe_module)}, clean_first=True) client.run("install .") client.run("build .") find_modes = [ ("both", "both", ""), ("both", "both", "MODULE"), ("config", "config", ""), ("module", "module", ""), (None, "both", ""), (None, "both", "MODULE") ] @pytest.mark.tool("cmake") @pytest.mark.parametrize("find_mode_pkga, find_mode_pkgb, find_mode_consumer", find_modes) def test_transitive_modules_found(find_mode_pkga, find_mode_pkgb, find_mode_consumer): """ related to https://github.com/conan-io/conan/issues/10224 modules files variables were set with the pkg_name_FOUND or pkg_name_VERSION instead of using filename_*, also there was missing doing a find_dependency of the requires packages to find_package transitive dependencies """ client = TestClient() conan_pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): {requires} def package_info(self): if "{mode}" != "None": self.cpp_info.set_property("cmake_find_mode", "{mode}") self.cpp_info.set_property("cmake_file_name", "{filename}") self.cpp_info.set_property("cmake_module_file_name", "{module_filename}") self.cpp_info.defines.append("DEFINE_{filename}") """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "pkgb/1.0" generators = "CMakeDeps", "CMakeToolchain" exports_sources = "CMakeLists.txt" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 3.1) project(test_package NONE) find_package(MYPKGB REQUIRED {find_mode}) message("MYPKGB_VERSION: ${{MYPKGB_VERSION}}") message("MYPKGB_VERSION_STRING: ${{MYPKGB_VERSION_STRING}}") message("MYPKGB_INCLUDE_DIRS: ${{MYPKGB_INCLUDE_DIRS}}") message("MYPKGB_INCLUDE_DIR: ${{MYPKGB_INCLUDE_DIR}}") message("MYPKGB_LIBRARIES: ${{MYPKGB_LIBRARIES}}") get_target_property(linked_libs pkgb::pkgb INTERFACE_LINK_LIBRARIES) message("MYPKGB_LINKED_LIBRARIES: '${{linked_libs}}'") get_target_property(linked_deps pkgb_DEPS_TARGET INTERFACE_LINK_LIBRARIES) message("MYPKGB_DEPS_LIBRARIES: '${{linked_deps}}'") message("MYPKGB_DEFINITIONS: ${{MYPKGB_DEFINITIONS}}") """) client.save({"pkgb.py": conan_pkg.format(requires='requires="pkga/1.0"', filename='MYPKGB', module_filename='MYPKGB', mode=find_mode_pkgb), "pkga.py": conan_pkg.format(requires='', filename='MYPKGA', module_filename='unicorns', mode=find_mode_pkga), "consumer.py": consumer, "CMakeLists.txt": cmakelist.format(find_mode=find_mode_consumer)}) client.run("create pkga.py --name=pkga --version=1.0") client.run("create pkgb.py --name=pkgb --version=1.0") client.run("create consumer.py --name=consumer --version=1.0") assert "MYPKGB_VERSION: 1.0" in client.out assert "MYPKGB_VERSION_STRING: 1.0" in client.out assert "MYPKGB_INCLUDE_DIRS:" in client.out assert "MYPKGB_INCLUDE_DIR:" in client.out # The MYPKG_LIBRARIES contains the target for the current package, but the target is linked # with the dependencies also: assert "MYPKGB_LIBRARIES: pkgb::pkgb" in client.out assert "MYPKGB_LINKED_LIBRARIES: '$<$:>;$<$:>;pkgb_DEPS_TARGET'" in client.out assert "MYPKGB_DEPS_LIBRARIES: '$<$:>;$<$:>;" \ "$<$:pkga::pkga>'" in client.out assert "MYPKGB_DEFINITIONS: -DDEFINE_MYPKGB" in client.out assert "Conan: Target declared 'pkga::pkga'" if find_mode_pkga == "module": assert 'Found unicorns: 1.0 (found version "1.0")' in client.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_transitivity.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.mocks import ConanFileMock from conan.tools.env.environment import environment_wrap_command from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") @pytest.mark.tool("cmake") def test_transitive_headers_not_public(transitive_libraries): c = transitive_libraries conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): exports = "*" requires = "engine/1.0" settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain", "CMakeDeps" def layout(self): self.folders.source = "src" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = gen_cmakelists(appsources=["main.cpp"], find_package=["engine"]) main = gen_function_cpp(name="main", includes=["engine"], calls=["engine"]) c.save({"src/main.cpp": main, "src/CMakeLists.txt": cmake, "conanfile.py": conanfile}, clean_first=True) c.run("build .") c.run_command(".\\Release\\myapp.exe") assert "matrix/1.0: Hello World Release!" in c.out # If we try to include transitivity matrix headers, it will fail!! main = gen_function_cpp(name="main", includes=["engine", "matrix"], calls=["engine"]) c.save({"src/main.cpp": main}) c.run("build .", assert_error=True) assert "Conan: Target declared 'matrix::matrix'" in c.out assert "Cannot open include file: 'matrix.h'" in c.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") @pytest.mark.tool("cmake") def test_shared_requires_static(transitive_libraries): c = transitive_libraries conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): exports = "*" requires = "engine/1.0" default_options = {"engine/*:shared": True} settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain", "CMakeDeps", "VirtualBuildEnv", "VirtualRunEnv" def layout(self): self.folders.source = "src" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = gen_cmakelists(appsources=["main.cpp"], find_package=["engine"]) main = gen_function_cpp(name="main", includes=["engine"], calls=["engine"]) c.save({"src/main.cpp": main, "src/CMakeLists.txt": cmake, "conanfile.py": conanfile}, clean_first=True) c.run("build .") command = environment_wrap_command(ConanFileMock(), "conanrun", c.current_folder, ".\\Release\\myapp.exe") c.run_command(command) assert "matrix/1.0: Hello World Release!" in c.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") @pytest.mark.tool("cmake") def test_transitive_binary_skipped(transitive_libraries): c = transitive_libraries # IMPORTANT: matrix binary can be removed, no longer necessary c.run("remove matrix*:* -c") conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): exports = "*" requires = "engine/1.0" default_options = {"engine/*:shared": True} settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain", "CMakeDeps" def layout(self): self.folders.source = "src" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = gen_cmakelists(appsources=["main.cpp"], find_package=["engine"]) main = gen_function_cpp(name="main", includes=["engine"], calls=["engine"]) c.save({"src/main.cpp": main, "src/CMakeLists.txt": cmake, "conanfile.py": conanfile}, clean_first=True) c.run("build . ") command = environment_wrap_command(ConanFileMock(), "conanrun", c.current_folder, ".\\Release\\myapp.exe") c.run_command(command) assert "matrix/1.0: Hello World Release!" in c.out # If we try to include transitivity matrix headers, it will fail!! main = gen_function_cpp(name="main", includes=["engine", "matrix"], calls=["engine"]) c.save({"src/main.cpp": main}) c.run("build .", assert_error=True) assert "Cannot open include file: 'matrix.h'" in c.out @pytest.mark.tool("cmake") def test_shared_requires_static_build_all(transitive_libraries): c = transitive_libraries conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): requires = "engine/1.0" settings = "os", "compiler", "arch", "build_type" generators = "CMakeDeps" """) c.save({"conanfile.py": conanfile}, clean_first=True) arch = c.get_default_host_profile().settings['arch'] c.run("install . -o engine*:shared=True") assert not os.path.exists(os.path.join(c.current_folder, f"matrix-release-{arch}-data.cmake")) cmake = c.load(f"engine-release-{arch}-data.cmake") assert 'list(APPEND engine_FIND_DEPENDENCY_NAMES )' in cmake c.run("install . -o engine*:shared=True --build=engine*") assert not os.path.exists(os.path.join(c.current_folder, f"matrix-release-{arch}-data.cmake")) cmake = c.load(f"engine-release-{arch}-data.cmake") assert 'list(APPEND engine_FIND_DEPENDENCY_NAMES )' in cmake ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_versions.py ================================================ import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def hello_client(): client = TestClient() client.save({"conanfile.py": GenConanfile("hello", "1.1")}) client.run("create .") return client @pytest.mark.parametrize("name, version, params, cmake_fails, package_found", [ ("hello", "1.0", "", False, True), ("Hello", "1.0", "", False, True), ("HELLO", "1.0", "", False, True), ("hello", "1.1", "", False, True), ("hello", "1.2", "", False, False), ("hello", "1.0", "EXACT", False, False), ("hello", "1.1", "EXACT", False, True), ("hello", "1.2", "EXACT", False, False), ("hello", "0.1", "", False, False), ("hello", "2.0", "", False, False), ("hello", "1.0", "REQUIRED", False, True), ("hello", "1.1", "REQUIRED", False, True), ("hello", "1.2", "REQUIRED", True, False), ("hello", "1.0", "EXACT REQUIRED", True, False), ("hello", "1.1", "EXACT REQUIRED", False, True), ("hello", "1.2", "EXACT REQUIRED", True, False), ("hello", "0.1", "REQUIRED", True, False), ("hello", "2.0", "REQUIRED", True, False) ]) @pytest.mark.tool("cmake") def test_version(hello_client, name, version, params, cmake_fails, package_found): client = hello_client cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer NONE) find_package({name} {version} {params}) message(STATUS "hello found: ${{{name}_FOUND}}") """).format(name=name, version=version, params=params) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Conan(ConanFile): requires = "hello/1.1" settings = "os", "compiler", "arch", "build_type" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists}, clean_first=True) exit_code = client.run("build .", assert_error=cmake_fails) if cmake_fails: assert exit_code != 0 elif package_found: assert "hello found: 1" in client.out else: assert "hello found: 0" in client.out @pytest.mark.tool("cmake") def test_no_version_file(hello_client): client = hello_client cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.1) project(consumer NONE) find_package(hello 1.0 REQUIRED) message(STATUS "hello found: ${{hello_FOUND}}") """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Conan(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "hello/1.1" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("install .") os.unlink(os.path.join(client.current_folder, "hello-config-version.cmake")) exit_code = client.run("build .", assert_error=True) assert 0 != exit_code ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_conditional_build_type.py ================================================ import textwrap def test_conditional_build_type(matrix_client_debug): # https://github.com/conan-io/conan/issues/15851 c = matrix_client_debug # A header-only library can't be used for testing, it doesn't fail c.save({}, clean_first=True) c.run("new cmake_lib -d name=pkgb -d version=0.1 -d requires=matrix/1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps class pkgbRecipe(ConanFile): name = "pkgb" version = "0.1" package_type = "static-library" settings = "os", "compiler", "build_type", "arch" exports_sources = "CMakeLists.txt", "src/*", "include/*" def layout(self): cmake_layout(self) def generate(self): deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) if self.settings.build_type == "Debug": tc.cache_variables["USE_MATRIX"] = 1 tc.preprocessor_definitions["USE_MATRIX"] = 1 tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["pkgb"] def requirements(self): if self.settings.build_type == "Debug": self.requires("matrix/1.0") """) cmake = textwrap.dedent("""\ set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(pkgb CXX) add_library(pkgb src/pkgb.cpp) target_include_directories(pkgb PUBLIC include) if(USE_MATRIX) find_package(matrix CONFIG REQUIRED) target_link_libraries(pkgb PRIVATE matrix::matrix) endif() set_target_properties(pkgb PROPERTIES PUBLIC_HEADER "include/pkgb.h") install(TARGETS pkgb) """) pkgb_cpp = textwrap.dedent(r""" #include #include "pkgb.h" #ifdef USE_MATRIX #include "matrix.h" #endif void pkgb(){ #ifdef USE_MATRIX matrix(); #endif #ifdef NDEBUG std::cout << "pkgb/0.1: Hello World Release!\n"; #else std::cout << "pkgb/0.1: Hello World Debug!\n"; #endif } """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "src/pkgb.cpp": pkgb_cpp}) c.run("create . -s build_type=Debug -tf=") assert "matrix/1.0" in c.out c.run("create . -s build_type=Release -tf=") # without dep to matrix assert "matrix" not in c.out c.save({}, clean_first=True) c.run("new cmake_lib -d name=pkgc -d version=0.1 -d requires=pkgb/0.1") c.run("build . -s build_type=Debug") c.run("build . -s build_type=Release") # This used to crash because "matrix::matrix" assert "conanfile.py (pkgc/0.1): Running CMake.build()" in c.out ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_link_order.py ================================================ import os import platform import re import textwrap import pytest from jinja2 import Template from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient """ Check that the link order of libraries is preserved when using CMake generators https://github.com/conan-io/conan/issues/6280 """ conanfile = Template(textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Recipe(ConanFile): name = "{{ref.name}}" version = "{{ref.version}}" {% if requires %} requires = {%- for req in requires -%} "{{ req }}"{% if not loop.last %}, {% endif %} {%- endfor -%} {% endif %} def build(self): with open("lib" + self.name + ".a", "w+") as f: f.write("fake library content") with open(self.name + ".lib", "w+") as f: f.write("fake library content") {% for it in libs_extra %} with open("lib{{ it }}.a", "w+") as f: f.write("fake library content") with open("{{ it }}.lib", "w+") as f: f.write("fake library content") {% endfor %} def package(self): copy(self, "*.a", self.build_folder, os.path.join(self.package_folder, "lib")) copy(self, "*.lib", self.build_folder, os.path.join(self.package_folder, "lib")) def package_info(self): self.cpp_info.includedirs = [] # Libraries self.cpp_info.libs = ["{{ ref.name }}"] {% for it in libs_extra %} self.cpp_info.libs.append("{{ it }}") {% endfor %} {% for it in libs_system %} self.cpp_info.libs.append("{{ it }}") {% endfor %} {% if system_libs %} self.cpp_info.system_libs = [ {%- for it in system_libs -%} "{{ it }}"{% if not loop.last %}, {% endif %} {%- endfor -%}] {% endif %} {% if frameworks %} self.cpp_info.frameworks.extend([ {%- for it in frameworks -%} "{{ it }}"{% if not loop.last %}, {% endif %} {%- endfor -%}]) {% endif %} """)) conanfile_headeronly = Template(textwrap.dedent(""" from conan import ConanFile class HeaderOnly(ConanFile): name = "{{ref.name}}" version = "{{ref.version}}" {% if requires %} requires = {%- for req in requires -%} "{{ req }}"{% if not loop.last %}, {% endif %} {%- endfor -%} {% endif %} def package_id(self): self.info.clear() def package_info(self): self.cpp_info.includedirs = [] # It may declare system libraries {% for it in libs_system %} self.cpp_info.libs.append("{{ it }}") {% endfor %} {% if system_libs %} self.cpp_info.system_libs = [ {%- for it in system_libs -%} "{{ it }}"{% if not loop.last %}, {% endif %} {%- endfor -%}] {% endif %} {% if frameworks %} self.cpp_info.frameworks.extend([ {%- for it in frameworks -%} "{{ it }}"{% if not loop.last %}, {% endif %} {%- endfor -%}]) {% endif %} """)) main_cpp = textwrap.dedent(""" int main() {return 0;} """) @pytest.fixture(scope="module") def client(): libz_ref = RecipeReference.loads("libz/version") libh2_ref = RecipeReference.loads("header2/version") libh_ref = RecipeReference.loads("header/version") liba_ref = RecipeReference.loads("liba/version") libb_ref = RecipeReference.loads("libb/version") libc_ref = RecipeReference.loads("libc/version") libd_ref = RecipeReference.loads("libd/version") t = TestClient(path_with_spaces=False) t.save({ 'libz/conanfile.py': conanfile.render( ref=libz_ref, libs_extra=["Z2"], system_libs=["system_lib"], frameworks=["Carbon"]), 'libh2/conanfile.py': conanfile_headeronly.render( ref=libh2_ref, system_libs=["header2_system_lib"], frameworks=["Security"]), 'libh/conanfile.py': conanfile_headeronly.render( ref=libh_ref, requires=[libh2_ref, libz_ref], system_libs=["header_system_lib"], frameworks=["CoreAudio"]), 'liba/conanfile.py': conanfile.render( ref=liba_ref, requires=[libh_ref], libs_extra=["A2"], system_libs=["system_lib"], frameworks=["Carbon"]), 'libb/conanfile.py': conanfile.render( ref=libb_ref, requires=[liba_ref], libs_extra=["B2"], system_libs=["system_lib"], frameworks=["Carbon"]), 'libc/conanfile.py': conanfile.render( ref=libc_ref, requires=[liba_ref], libs_extra=["C2"], system_libs=["system_lib"], frameworks=["Carbon"]), 'libd/conanfile.py': conanfile.render( ref=libd_ref, requires=[libb_ref, libc_ref], libs_extra=["D2"], system_libs=["system_lib"], frameworks=["Carbon"]), }) # Create all of them t.run("create libz") t.run("create libh2") t.run("create libh") t.run("create liba") t.run("create libb") t.run("create libc") t.run("create libd") return t def _validate_link_order(libs): # Check that all the libraries are there: assert len(libs) == 16 if platform.system() == "Darwin" else (13 if platform.system() == "Linux" else 23) # - Regular libs ext = ".lib" if platform.system() == "Windows" else ".a" prefix = "" if platform.system() == "Windows" else "lib" expected_libs = {prefix + it + ext for it in ['libd', 'D2', 'libb', 'B2', 'libc', 'C2', 'liba', 'A2', 'libz', 'Z2']} # - System libs ext_system = ".lib" if platform.system() == "Windows" else "" expected_libs.update([it + ext_system for it in ['header_system_lib', 'header2_system_lib', 'system_lib']]) # - Add MacOS frameworks if platform.system() == "Darwin": expected_libs.update(['CoreAudio', 'Security', 'Carbon']) # - Add Windows libs if platform.system() == "Windows": expected_libs.update(['kernel32.lib', 'user32.lib', 'gdi32.lib', 'winspool.lib', 'shell32.lib', 'ole32.lib', 'oleaut32.lib', 'uuid.lib', 'comdlg32.lib', 'advapi32.lib']) assert set(libs) == expected_libs # These are the first libraries and order is mandatory mandatory_1 = [prefix + it + ext for it in ['libd', 'D2', 'libb', 'B2', 'libc', 'C2', 'liba', 'A2', ]] assert mandatory_1 == libs[:len(mandatory_1)] # Then, libz ones must be before system libraries that are consuming assert libs.index(prefix + 'libz' + ext) < libs.index('system_lib' + ext_system) assert libs.index(prefix + 'Z2' + ext) < libs.index('system_lib' + ext_system) if platform.system() == "Darwin": assert libs.index('liblibz.a') < libs.index('Carbon') assert libs.index('libZ2.a') < libs.index('Carbon') def _get_link_order_from_cmake(content): libs = [] for it in content.splitlines(): # This is for Linux and Mac # Remove double spaces from output that appear in some platforms line = ' '.join(it.split()) if 'main.cpp.o -o example' in line: _, links = line.split("main.cpp.o -o example") for it_lib in links.split(): if it_lib.startswith("-L") or it_lib.startswith("-Wl,-rpath"): continue elif it_lib.startswith("-l"): libs.append(it_lib[2:]) elif it_lib == "-framework": continue else: try: _, libname = it_lib.rsplit('/', 1) except ValueError: libname = it_lib finally: libs.append(libname) break # Windows if 'example.exe" /INCREMENTAL:NO /NOLOGO' in it: for it_lib in it.split(): it_lib = it_lib.strip() if it_lib.endswith(".lib"): try: _, libname = it_lib.rsplit('\\', 1) except ValueError: libname = it_lib finally: libs.append(libname) break return libs def _get_link_order_from_xcode(content): libs = [] # Find the right Release block in the XCode file results = re.finditer(r'/\* Release \*/ = {', content) header_found = False for r in results: release_section = content[r.start():].split("name = Release;", 1)[0] if "-headerpad_max_install_names" in release_section: header_found = True break assert header_found, "Cannot find the Release block linking the expected libraries" start_key = '-Wl,-headerpad_max_install_names' end_key = ');' libs_content = release_section.split(start_key, 1)[1].split(end_key, 1)[0] libs_unstripped = libs_content.split(",") for lib in libs_unstripped: if ".a" in lib: libs.append(lib.strip('"').rsplit('/', 1)[1]) elif "-l" in lib: libs.append(lib.strip('"')[2:]) elif "-framework" in lib: libs.append(lib.strip('"')[11:]) return libs def _create_find_package_project(client): t = TestClient(cache_folder=client.cache_folder) t.save({ 'conanfile.txt': textwrap.dedent(""" [requires] libd/version [generators] CMakeDeps CMakeToolchain """), 'CMakeLists.txt': textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(executable CXX) find_package(libd) add_executable(example main.cpp) target_link_libraries(example libd::libd) """), 'main.cpp': main_cpp }) t.run("install . -s build_type=Release") return t def _run_and_get_lib_order(t, generator): if generator == "Xcode": t.run_command("cmake . -G Xcode -DCMAKE_VERBOSE_MAKEFILE:BOOL=True" " -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake") # This is building by default the Debug configuration that contains nothing, so it works t.run_command("cmake --build .") # This is building the release and fails because invented system libraries are missing t.run_command("cmake --build . --config Release", assert_error=True) # Get the actual link order from the CMake call libs = _get_link_order_from_xcode(t.load(os.path.join('executable.xcodeproj', 'project.pbxproj'))) else: t.run_command("cmake . -DCMAKE_VERBOSE_MAKEFILE:BOOL=True" " -DCMAKE_BUILD_TYPE=Release" " -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake") extra_build = "--config Release" if platform.system() == "Windows" else "" # Windows VS t.run_command("cmake --build . {}".format(extra_build), assert_error=True) # Get the actual link order from the CMake call libs = _get_link_order_from_cmake(str(t.out)) return libs # needs at least 3.23.3 because of error with "empty identity" @pytest.mark.parametrize("generator", [None, "Xcode"]) @pytest.mark.tool("cmake", "3.23") def test_cmake_deps(client, generator): if generator == "Xcode" and platform.system() != "Darwin": pytest.skip("Xcode is needed") t = _create_find_package_project(client) libs = _run_and_get_lib_order(t, generator) _validate_link_order(libs) ================================================ FILE: test/functional/toolchains/cmake/cmakedeps/test_weird_library_names.py ================================================ import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def client_weird_lib_name(): c = TestClient() conanfile = textwrap.dedent(""" import os, platform from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.files import copy class Pkg(ConanFile): exports_sources = "CMakeLists.txt", "src/*" settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain", "CMakeDeps" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): copy(self, "*.h", os.path.join(self.source_folder, "src"), os.path.join(self.package_folder, "include")) copy(self, "*.lib", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) copy(self, "*.a", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) ext = "a" if platform.system() != "Windows" else "lib" prefix = "lib" if platform.system() != "Windows" else "" os.chdir(os.path.join(self.package_folder, "lib")) os.rename("{}hello_0.1.{}".format(prefix, ext), "{}he!llo@0.1.{}".format(prefix, ext)) def package_info(self): self.cpp_info.libs = ["he!llo@0.1"] """) hdr = gen_function_h(name="hello") src = gen_function_cpp(name="hello") cmake = gen_cmakelists(libname="hello_0.1", libsources=["src/hello.cpp"]) c.save({"src/hello.h": hdr, "src/hello.cpp": src, "CMakeLists.txt": cmake, "conanfile.py": conanfile}) c.run("create . --name=hello --version=0.1") return c @pytest.mark.tool("cmake") def test_cmakedeps(client_weird_lib_name): c = client_weird_lib_name c.save({}, clean_first=True) c.run("new cmake_lib -d name=chat -d version=0.1 -d requires=hello/0.1") c.run("create .") assert "chat/0.1: Created package" in c.out ================================================ FILE: test/functional/toolchains/cmake/test_cmake.py ================================================ import os import platform import textwrap import time import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp, gen_function_h from test.functional.utils import check_vs_runtime, check_exe_run from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake", "3.15") @pytest.mark.tool("mingw64") @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows") def test_simple_cmake_mingw(): client = TestClient() client.run("new cmake_lib -d name=hello -d version=1.0") client.save({"mingw": """ [settings] os=Windows arch=x86_64 build_type=Release compiler=gcc compiler.exception=seh compiler.libcxx=libstdc++11 compiler.threads=win32 compiler.version=11.2 compiler.cppstd=17 """}) client.run("create . --profile=mingw") build_folder = client.created_test_build_folder("hello/1.0") # FIXME: Note that CI contains 10.X, so it uses another version rather than the profile one # and no one notices. It would be good to have some details in confuser.py to be consistent check_exe_run(client.out, "hello/1.0:", "gcc", None, "Release", "x86_64", "17", subsystem="mingw64", extra_msg="Hello World", cxx11_abi="1") check_vs_runtime(f"test_package/{build_folder}/example.exe", client, "15", build_type="Release", static_runtime=False, subsystem="mingw64") # TODO: How to link with mingw statically? @pytest.mark.tool("cmake") class Base: conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello/0.1" generators = "CMakeDeps" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} implements = ["auto_shared_fpic", "auto_header_only"] def generate(self): tc = CMakeToolchain(self) tc.variables["MYVAR"] = "MYVAR_VALUE" tc.variables["MYVAR2"] = "MYVAR_VALUE2" tc.variables.debug["MYVAR_CONFIG"] = "MYVAR_DEBUG" tc.variables.release["MYVAR_CONFIG"] = "MYVAR_RELEASE" tc.variables.debug["MYVAR2_CONFIG"] = "MYVAR2_DEBUG" tc.variables.release["MYVAR2_CONFIG"] = "MYVAR2_RELEASE" tc.preprocessor_definitions["MYDEFINE"] = "\"MYDEF_VALUE\"" tc.preprocessor_definitions["MYDEFINEINT"] = 42 tc.preprocessor_definitions.debug["MYDEFINE_CONFIG"] = "\"MYDEF_DEBUG\"" tc.preprocessor_definitions.release["MYDEFINE_CONFIG"] = "\"MYDEF_RELEASE\"" tc.preprocessor_definitions.debug["MYDEFINEINT_CONFIG"] = 421 tc.preprocessor_definitions.release["MYDEFINEINT_CONFIG"] = 422 tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) lib_h = gen_function_h(name="app") lib_cpp = gen_function_cpp(name="app", msg="App", includes=["hello"], calls=["hello"], preprocessor=["MYVAR", "MYVAR_CONFIG", "MYDEFINE", "MYDEFINE_CONFIG", "MYDEFINEINT", "MYDEFINEINT_CONFIG"]) main = gen_function_cpp(name="main", includes=["app"], calls=["app"]) cmakelist = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(App C CXX) if(NOT CMAKE_TOOLCHAIN_FILE) message(FATAL ">> Not using toolchain") endif() message(">> CMAKE_GENERATOR_PLATFORM: ${CMAKE_GENERATOR_PLATFORM}") message(">> CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") message(">> CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") message(">> CMAKE_CXX_FLAGS_DEBUG: ${CMAKE_CXX_FLAGS_DEBUG}") message(">> CMAKE_CXX_FLAGS_RELEASE: ${CMAKE_CXX_FLAGS_RELEASE}") message(">> CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}") message(">> CMAKE_C_FLAGS_DEBUG: ${CMAKE_C_FLAGS_DEBUG}") message(">> CMAKE_C_FLAGS_RELEASE: ${CMAKE_C_FLAGS_RELEASE}") message(">> CMAKE_SHARED_LINKER_FLAGS: ${CMAKE_SHARED_LINKER_FLAGS}") message(">> CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}") message(">> CMAKE_CXX_STANDARD: ${CMAKE_CXX_STANDARD}") message(">> CMAKE_CXX_EXTENSIONS: ${CMAKE_CXX_EXTENSIONS}") message(">> CMAKE_POSITION_INDEPENDENT_CODE: ${CMAKE_POSITION_INDEPENDENT_CODE}") message(">> CMAKE_SKIP_RPATH: ${CMAKE_SKIP_RPATH}") message(">> CMAKE_INSTALL_NAME_DIR: ${CMAKE_INSTALL_NAME_DIR}") message(">> CMAKE_MODULE_PATH: ${CMAKE_MODULE_PATH}") message(">> CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}") message(">> BUILD_SHARED_LIBS: ${BUILD_SHARED_LIBS}") get_directory_property(_COMPILE_DEFS DIRECTORY ${CMAKE_SOURCE_DIR} COMPILE_DEFINITIONS) message(">> COMPILE_DEFINITIONS: ${_COMPILE_DEFS}") find_package(hello REQUIRED) add_library(app_lib app_lib.cpp) target_link_libraries(app_lib PRIVATE hello::hello) target_compile_definitions(app_lib PRIVATE MYVAR="${MYVAR}") target_compile_definitions(app_lib PRIVATE MYVAR_CONFIG="${MYVAR_CONFIG}") add_executable(app app.cpp) target_link_libraries(app PRIVATE app_lib) """) @pytest.fixture(autouse=True) def setup(self): self.client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class Pkg(ConanFile): settings = "build_type" def package(self): save(self, os.path.join(self.package_folder, "include/hello.h"), '''#include void hello(){std::cout<< "Hello: %s" <>", "++>>") self.client.save({"CMakeLists.txt": content}) def _incremental_build(self, build_type=None): build_directory = os.path.join(self.client.current_folder, "build").replace("\\", "/") with self.client.chdir(build_directory): config = "--config %s" % build_type if build_type else "" self.client.run_command("cmake --build . %s" % config) def _run_app(self, build_type, bin_folder=False, msg="App", dyld_path=None): if dyld_path: build_directory = os.path.join(self.client.current_folder, "build").replace("\\", "/") command_str = 'DYLD_LIBRARY_PATH="%s" build/app' % build_directory else: command_str = "build/%s/app.exe" % build_type if bin_folder else "build/app" if platform.system() == "Windows": command_str = command_str.replace("/", "\\") self.client.run_command(command_str) assert "Hello: %s" % build_type in self.client.out assert "%s: %s!" % (msg, build_type) in self.client.out assert "MYVAR: MYVAR_VALUE" in self.client.out assert "MYVAR_CONFIG: MYVAR_%s" % build_type.upper() in self.client.out assert "MYDEFINE: MYDEF_VALUE" in self.client.out assert "MYDEFINE_CONFIG: MYDEF_%s" % build_type.upper() in self.client.out assert "MYDEFINEINT: 42" in self.client.out value = 421 if build_type == "Debug" else 422 assert f"MYDEFINEINT_CONFIG: {value}" in self.client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") class TestWin(Base): @pytest.mark.parametrize("compiler, build_type, runtime, version, cppstd, arch, shared", [("msvc", "Debug", "static", "191", "14", "x86", True), ("msvc", "Release", "dynamic", "191", "17", "x86_64", False)] ) def test_toolchain_win(self, compiler, build_type, runtime, version, cppstd, arch, shared): settings = {"compiler": compiler, "compiler.version": version, "compiler.runtime": runtime, "compiler.cppstd": cppstd, "arch": arch, "build_type": build_type, } options = {"shared": shared} self.client.save_home({"global.conf": "tools.build:jobs=1"}) self._run_build(settings, options) assert ('cmake -G "Visual Studio 15 2017" ' '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"') in self.client.out generator_platform = "x64" if arch == "x86_64" else "Win32" arch_flag = "x64" if arch == "x86_64" else "X86" shared_str = "ON" if shared else "OFF" vals = {"CMAKE_GENERATOR_PLATFORM": generator_platform, "CMAKE_BUILD_TYPE": "", "CMAKE_CXX_FLAGS": "/MP1 /DWIN32 /D_WINDOWS /GR /EHsc", "CMAKE_CXX_FLAGS_DEBUG": "/Zi /Ob0 /Od /RTC1", "CMAKE_CXX_FLAGS_RELEASE": "/O2 /Ob2 /DNDEBUG", "CMAKE_C_FLAGS": "/MP1 /DWIN32 /D_WINDOWS", "CMAKE_C_FLAGS_DEBUG": "/Zi /Ob0 /Od /RTC1", "CMAKE_C_FLAGS_RELEASE": "/O2 /Ob2 /DNDEBUG", "CMAKE_SHARED_LINKER_FLAGS": "/machine:%s" % arch_flag, "CMAKE_EXE_LINKER_FLAGS": "/machine:%s" % arch_flag, "CMAKE_CXX_STANDARD": cppstd, "CMAKE_CXX_EXTENSIONS": "OFF", "BUILD_SHARED_LIBS": shared_str} def _verify_out(marker=">>"): if shared: assert "app_lib.dll" in self.client.out else: assert "app_lib.dll" not in self.client.out out = str(self.client.out).splitlines() for k, v in vals.items(): assert "%s %s: %s" % (marker, k, v) in out _verify_out() opposite_build_type = "Release" if build_type == "Debug" else "Debug" settings["build_type"] = opposite_build_type self._run_build(settings, options) self._run_app("Release", bin_folder=True) check_exe_run(self.client.out, "main", "msvc", version, "Release", arch, cppstd, {"MYVAR": "MYVAR_VALUE", "MYVAR_CONFIG": "MYVAR_RELEASE", "MYDEFINE": "MYDEF_VALUE", "MYDEFINE_CONFIG": "MYDEF_RELEASE" }) self._run_app("Debug", bin_folder=True) check_exe_run(self.client.out, "main", "msvc", version, "Debug", arch, cppstd, {"MYVAR": "MYVAR_VALUE", "MYVAR_CONFIG": "MYVAR_DEBUG", "MYDEFINE": "MYDEF_VALUE", "MYDEFINE_CONFIG": "MYDEF_DEBUG" }) static_runtime = True if runtime == "static" or "MT" in runtime else False check_vs_runtime("build/Release/app.exe", self.client, "15", build_type="Release", static_runtime=static_runtime) check_vs_runtime("build/Debug/app.exe", self.client, "15", build_type="Debug", static_runtime=static_runtime) self._modify_code() time.sleep(1) self._incremental_build(build_type=build_type) _verify_out(marker="++>>") self._run_app(build_type, bin_folder=True, msg="AppImproved") self._incremental_build(build_type=opposite_build_type) self._run_app(opposite_build_type, bin_folder=True, msg="AppImproved") @pytest.mark.parametrize("build_type, libcxx, version, cppstd, arch, shared", [("Debug", "libstdc++", "4.9", "98", "x86_64", True), ("Release", "libstdc++", "4.9", "11", "x86_64", False)]) @pytest.mark.tool("mingw64") @pytest.mark.tool("cmake", "3.15") def test_toolchain_mingw_win(self, build_type, libcxx, version, cppstd, arch, shared): # FIXME: The version and cppstd are wrong, toolchain doesn't enforce it settings = {"compiler": "gcc", "compiler.version": version, "compiler.libcxx": libcxx, "compiler.cppstd": cppstd, "arch": arch, "build_type": build_type, } options = {"shared": shared} self._run_build(settings, options) assert "The C compiler identification is GNU" in self.client.out assert ('cmake -G "MinGW Makefiles" ' '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"') in self.client.out assert '-DCMAKE_SH="CMAKE_SH-NOTFOUND"' in self.client.out def _verify_out(marker=">>"): cmake_vars = {"CMAKE_GENERATOR_PLATFORM": "", "CMAKE_BUILD_TYPE": build_type, "CMAKE_CXX_FLAGS": "-m64", "CMAKE_CXX_FLAGS_DEBUG": "-g", "CMAKE_CXX_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_C_FLAGS": "-m64", "CMAKE_C_FLAGS_DEBUG": "-g", "CMAKE_C_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_SHARED_LINKER_FLAGS": "-m64", "CMAKE_EXE_LINKER_FLAGS": "-m64", "CMAKE_CXX_STANDARD": cppstd, "CMAKE_CXX_EXTENSIONS": "OFF", "BUILD_SHARED_LIBS": "ON" if shared else "OFF"} if shared: assert "app_lib.dll" in self.client.out else: assert "app_lib.dll" not in self.client.out out = str(self.client.out).splitlines() for k, v in cmake_vars.items(): assert "%s %s: %s" % (marker, k, v) in out _verify_out() self._run_app(build_type) check_exe_run(self.client.out, "main", "gcc", None, build_type, arch, None, {"MYVAR": "MYVAR_VALUE", "MYVAR_CONFIG": "MYVAR_{}".format(build_type.upper()), "MYDEFINE": "MYDEF_VALUE", "MYDEFINE_CONFIG": "MYDEF_{}".format(build_type.upper()) }, subsystem="mingw64") self._modify_code() time.sleep(2) self._incremental_build() _verify_out(marker="++>>") self._run_app(build_type, msg="AppImproved") @pytest.mark.skipif(platform.system() != "Linux", reason="Only for Linux") class TestLinux(Base): @pytest.mark.parametrize("build_type, cppstd, arch, libcxx, shared", [("Debug", "14", "x86", "libstdc++", True), ("Release", "gnu14", "x86_64", "libstdc++11", False)]) def test_toolchain_linux(self, build_type, cppstd, arch, libcxx, shared): settings = {"compiler": "gcc", "compiler.cppstd": cppstd, "compiler.libcxx": libcxx, "arch": arch, "build_type": build_type} self._run_build(settings, {"shared": shared}) assert ('cmake -G "Unix Makefiles" ' '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"') in self.client.out extensions_str = "ON" if "gnu" in cppstd else "OFF" arch_str = "-m32" if arch == "x86" else "-m64" cxx11_abi_str = "_GLIBCXX_USE_CXX11_ABI=0;" if libcxx == "libstdc++" else "" defines = '%sMYDEFINE="MYDEF_VALUE";MYDEFINEINT=42;' \ '$<$:MYDEFINE_CONFIG="MYDEF_DEBUG">' \ '$<$:MYDEFINE_CONFIG="MYDEF_RELEASE">;' \ '$<$:MYDEFINEINT_CONFIG=421>' \ '$<$:MYDEFINEINT_CONFIG=422>' % cxx11_abi_str vals = {"CMAKE_CXX_STANDARD": "14", "CMAKE_CXX_EXTENSIONS": extensions_str, "CMAKE_BUILD_TYPE": build_type, "CMAKE_CXX_FLAGS": arch_str, "CMAKE_CXX_FLAGS_DEBUG": "-g", "CMAKE_CXX_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_C_FLAGS": arch_str, "CMAKE_C_FLAGS_DEBUG": "-g", "CMAKE_C_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_SHARED_LINKER_FLAGS": arch_str, "CMAKE_EXE_LINKER_FLAGS": arch_str, "COMPILE_DEFINITIONS": defines, # fPIC is managed automatically depending on the shared option value # if implements = ["auto_shared_fpic", "auto_header_only"] "CMAKE_POSITION_INDEPENDENT_CODE": "ON" if not shared else "" } def _verify_out(marker=">>"): if shared: assert "libapp_lib.so" in self.client.out else: assert "libapp_lib.a" in self.client.out out = str(self.client.out).splitlines() for k, v in vals.items(): assert "%s %s: %s" % (marker, k, v) in out _verify_out() self._run_app(build_type) self._modify_code() self._incremental_build() _verify_out(marker="++>>") self._run_app(build_type, msg="AppImproved") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for Apple") class TestApple(Base): @pytest.mark.parametrize("build_type, cppstd, shared", [("Debug", "14", True), ("Release", "", False)]) def test_toolchain_apple(self, build_type, cppstd, shared): settings = {"compiler": "apple-clang", "compiler.cppstd": cppstd, "build_type": build_type} self._run_build(settings, {"shared": shared}) assert ('cmake -G "Unix Makefiles" ' '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"') in self.client.out extensions_str = "OFF" if cppstd else "" vals = {"CMAKE_CXX_STANDARD": cppstd, "CMAKE_CXX_EXTENSIONS": extensions_str, "CMAKE_BUILD_TYPE": build_type, "CMAKE_CXX_FLAGS_DEBUG": "-g", "CMAKE_CXX_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_C_FLAGS_DEBUG": "-g", "CMAKE_C_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_INSTALL_NAME_DIR": "" } arch_flags = { "CMAKE_C_FLAGS": "-m64", "CMAKE_CXX_FLAGS": "-m64 -stdlib=libc++", "CMAKE_SHARED_LINKER_FLAGS": "-m64", "CMAKE_EXE_LINKER_FLAGS": "-m64", } host_profile = self.client.get_default_host_profile() if host_profile.settings.get("arch") != "x86_64": arch_flags = {"CMAKE_CXX_FLAGS": "-stdlib=libc++"} vals.update(arch_flags) def _verify_out(marker=">>"): if shared: assert "libapp_lib.dylib" in self.client.out else: if marker == ">>": assert "libapp_lib.a" in self.client.out else: # Incremental build not the same msg assert "Built target app_lib" in self.client.out out = str(self.client.out).splitlines() for k, v in vals.items(): assert "%s %s: %s" % (marker, k, v) in out _verify_out() self._run_app(build_type, dyld_path=shared) self._modify_code() time.sleep(1) self._incremental_build() _verify_out(marker="++>>") self._run_app(build_type, dyld_path=shared, msg="AppImproved") @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_msvc_vs_versiontoolset(): settings = {"compiler": "msvc", "compiler.version": "191", "compiler.runtime": "static", "compiler.cppstd": "14", "arch": "x86_64", "build_type": "Release", } client = TestClient() client.save_home({"global.conf": "tools.microsoft.msbuild:vs_version=15"}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class App(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "*" def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(r"Release\\myapp.exe") """) cmakelists = gen_cmakelists(appname="myapp", appsources=["app.cpp"]) main = gen_function_cpp(name="main") client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists, "app.cpp": main, }) settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v) client.run("create . --name=app --version=1.0 {}".format(settings)) assert '-G "Visual Studio 15 2017"' in client.out check_exe_run(client.out, "main", "msvc", "191", "Release", "x86_64", "14") @pytest.mark.tool("cmake") class TestCMakeInstall: def test_install(self): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" exports_sources = "CMakeLists.txt", "header.h" def generate(self): tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() def package(self): cmake = CMake(self) cmake.install() """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 2.8) project(App NONE) install(FILES header.h DESTINATION include) """) client = TestClient(path_with_spaces=False) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist, "header.h": "# my header file"}) # The create flow must work client.run("create . --name=pkg --version=0.1 -c tools.build:verbosity=verbose " "-c tools.compilation:verbosity=verbose") assert "--loglevel=VERBOSE" in client.out assert "unrecognized option" not in client.out assert "--verbose" in client.out assert "pkg/0.1: package(): Packaged 1 '.h' file: header.h" in client.out package_folder = client.created_layout().package() assert os.path.exists(os.path.join(package_folder, "include", "header.h")) def test_install_in_build(self): """ test that we can do a ``cmake.install()`` inside the ``build()`` method without crashing """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.install() """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 2.8) project(App NONE) install(FILES header.h DESTINATION include) """) client = TestClient(path_with_spaces=False) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist, "header.h": "# my header file"}) # The create flow must work client.run("build .") assert "conanfile.py: RUN: cmake --install" in client.out @pytest.mark.tool("cmake") class TestCmakeTestMethod: """ test the cmake.test() helper """ def test_test(self, matrix_client_shared): c = matrix_client_shared conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class App(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps", "CMakeToolchain", "VirtualBuildEnv", "VirtualRunEnv" exports_sources = "CMakeLists.txt", "example.cpp" def build_requirements(self): self.test_requires("matrix/1.0") def build(self): cmake = CMake(self) cmake.configure() cmake.build() cmake.test() cmake.ctest() """) cmakelist = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(App CXX) find_package(matrix CONFIG REQUIRED) add_executable(example example.cpp) target_link_libraries(example matrix::matrix) enable_testing() add_test(NAME example COMMAND example) """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist, "example.cpp": gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"])}, clean_first=True) # The create flow must work c.run("create . --name=pkg --version=0.1 -pr:b=default -o matrix*:shared=True") assert str(c.out).count("1/1 Test #1: example .......................... Passed") == 2 assert "pkg/0.1: RUN: ctest --build-config Release --parallel" @pytest.mark.tool("cmake") class TestCMakeOverrideCache: def test_cmake_cache_variables(self): # https://github.com/conan-io/conan/issues/7832 conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" exports_sources = "CMakeLists.txt" def generate(self): toolchain = CMakeToolchain(self) toolchain.variables["my_config_string"] = "my new value" toolchain.generate() def build(self): cmake = CMake(self) cmake.configure() """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 3.7) project(my_project NONE) set(my_config_string "default value" CACHE STRING "my config string") message(STATUS "VALUE OF CONFIG STRING: ${my_config_string}") """) client = TestClient() client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist}) client.run("build .") assert "VALUE OF CONFIG STRING: my new value" in client.out @pytest.mark.tool("cmake") class TestCMakeFindPackagePreferConfig: def test_prefer_config(self): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class App(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(my_project NONE) find_package(Comandante REQUIRED) """) find = 'message(STATUS "using FindComandante.cmake")' config = 'message(STATUS "using ComandanteConfig.cmake")' profile = textwrap.dedent(""" include(default) [conf] tools.cmake.cmaketoolchain:find_package_prefer_config={} """) client = TestClient() client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelist, "FindComandante.cmake": find, "ComandanteConfig.cmake": config, "profile_true": profile.format(True), "profile_false": profile.format(False)}) client.run("build .") assert "using ComandanteConfig.cmake" in client.out client.run("build . --profile=profile_true") assert "using ComandanteConfig.cmake" in client.out client.run("build . --profile=profile_false") assert "using FindComandante.cmake" in client.out ================================================ FILE: test/functional/toolchains/cmake/test_cmake_and_no_soname_flag.py ================================================ import os import platform import shutil import pytest from conan.tools.env.environment import environment_wrap_command from conan.tools.files import replace_in_file from conan.test.utils.mocks import ConanFileMock from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Linux", reason="Only Linux") @pytest.mark.tool("cmake") @pytest.mark.parametrize("nosoname_property", [ True, # without SONAME False # By default, with SONAME ]) def test_no_soname_flag(nosoname_property): """ This test case is testing this graph structure: * 'Executable' -> 'LibB' -> 'LibNoSoname' Where: * LibNoSoname: is a package built as shared and without the SONAME flag. * LibB: is a package which requires LibNoSoname. * Executable: is the final consumer building an application and depending on OtherLib. How: 1- Creates LibNoSoname and upload it to remote server 2- Creates LibB and upload it to remote server 3- Remove the Conan cache folder 4- Creates an application and consume LibB Goal: * If `self.cpp_info.set_property("nosoname", True), then the `Executable` runs OK. * If `self.cpp_info.set_property("nosoname", False), then the `Executable` fails. """ client = TestClient(default_server_user=True) # Creating nosoname/0.1 library client.run("new cmake_lib -d name=nosoname -d version=0.1") replace_in_file(ConanFileMock(), os.path.join(client.current_folder, "conanfile.py"), 'self.cpp_info.libs = ["nosoname"]', f'self.cpp_info.libs = ["nosoname"]\n self.cpp_info.set_property("nosoname", {nosoname_property})') replace_in_file(ConanFileMock(), os.path.join(client.current_folder, "CMakeLists.txt"), 'target_include_directories(nosoname PUBLIC include)', 'target_include_directories(nosoname PUBLIC include)\nset_target_properties(nosoname PROPERTIES NO_SONAME 1)') client.run("create . -o nosoname/*:shared=True -tf=") # Creating lib_b/0.1 library (depends on nosoname/0.1) client.save({}, clean_first=True) client.run("new cmake_lib -d name=lib_b -d version=0.1 -d requires=nosoname/0.1") client.run("create . -o lib_b/*:shared=True -o nosoname/*:shared=True -tf=") # Creating app/0.1 application (depends on lib_b/0.1) client.save({}, clean_first=True) client.run("new cmake_exe -d name=app -d version=0.1 -d requires=lib_b/0.1") client.run("create . -o nosoname/*:shared=True -o lib_b/*:shared=True -tf=") client.run("upload * -c -r default") # Removing everything from the .conan2/p to ensure that we don't have anything saved in the cache shutil.rmtree(client.cache.store) client = TestClient(servers=client.servers) client.run("install --requires=app/0.1@ -o nosoname*:shared=True -o lib_b/*:shared=True") # This only finds "app" executable because the "app/0.1" is declaring package_type="application" # otherwise, run=None and nothing can tell us if the conanrunenv should have the PATH. command = environment_wrap_command(ConanFileMock(), "conanrun", client.current_folder, "app") # If `nosoname_property` is False, and we have a library without the SONAME flag, # then it should fail if not nosoname_property: client.run_command(command, assert_error=True) assert "libnosoname.so: cannot open shared object file: " \ "No such file or directory" in client.out else: client.run_command(command) assert "nosoname/0.1: Hello World Release!" in client.out assert "lib_b/0.1: Hello World Release!" in client.out assert "app/0.1: Hello World Release!" in client.out ================================================ FILE: test/functional/toolchains/cmake/test_cmake_extra_variables.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient new_value = "will_break_next" @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("generator", ["CMakeDeps", "CMakeConfigDeps"]) def test_package_info_extra_variables(generator): """ The dependencies can define extra variables to be used in CMake, but if the user is setting the cmake_extra_variables conf, those should have precedence. """ client = TestClient() dep_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_extra_variables", {"FOO": 42}) """) client.save({"dep/conanfile.py": dep_conanfile}) client.run("create dep") cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.27) project(myproject CXX) find_package(dep CONFIG REQUIRED) message(STATUS "FOO=${FOO}") message(STATUS "BAR=${BAR}") message(STATUS "BAZ=${BAZ}") message(STATUS "BAR_CACHE=${BAR_CACHE}") message(STATUS "BAZ_CACHE=${BAZ_CACHE}") message(STATUS "BAR_CACHE_FORCE=${BAR_CACHE_FORCE}") """) conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake, {generator}, CMakeToolchain class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "dep/0.1" def generate(self): deps = {generator}(self) deps.generate() tc = CMakeToolchain(self) tc.cache_variables["BAR"] = "42" tc.variables["BAZ"] = "42" tc.cache_variables["BAR_CACHE"] = "42" tc.variables["BAZ_CACHE"] = "42" tc.cache_variables["BAR_CACHE_FORCE"] = "42" tc.generate() def build(self): cmake = CMake(self) cmake.configure() """) client.save({"CMakeLists.txt": cmakelists, "conanfile.py": conanfile}) conf = f"-c tools.cmake.cmakedeps:new={new_value}" if generator == "CMakeConfigDeps" else "" client.run(f"build . {conf} " """-c tools.cmake.cmaketoolchain:extra_variables="{'FOO': '9', 'BAR': '9', 'BAZ': '9', 'BAR_CACHE': {'value': '9', 'cache': True, 'type': 'STRING'}, 'BAZ_CACHE': {'value': '9', 'cache': True, 'type': 'STRING'}, 'BAR_CACHE_FORCE': {'value': '9', 'cache': True, 'type': 'STRING', 'force': True}}" """) assert "-- FOO=9" in client.out assert "-- BAR=9" in client.out assert "-- BAZ=9" in client.out # No overwrite in this case. Cache - Cache keeps the first occurrence in the file, can't override # from the profile assert "-- BAR_CACHE=42" in client.out assert "-- BAZ_CACHE=9" in client.out assert "-- BAR_CACHE_FORCE=9" in client.out ================================================ FILE: test/functional/toolchains/cmake/test_cmake_find_none.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient def test_cmake_find_none_transitive(): c = TestClient() qt = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class pkgRecipe(ConanFile): name = "qt" version = "0.1" package_type = "static-library" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "CMakeLists.txt", "src/*", "include/*" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.builddirs = ["qt/cmake"] self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.set_property("cmake_file_name", "Qt5") """) cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) add_library(qt INTERFACE) install(TARGETS qt EXPORT Qt5Config) export(TARGETS qt NAMESPACE qt:: FILE "${CMAKE_CURRENT_BINARY_DIR}/Qt5Config.cmake" ) install(EXPORT Qt5Config DESTINATION "qt/cmake" NAMESPACE qt:: ) """) c.save({"conanfile.py": qt, "CMakeLists.txt": cmake}) c.run("create .") karchive = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class pkgRecipe(ConanFile): name = "karchive" version = "0.1" package_type = "static-library" requires = "qt/0.1" """) c.save({"conanfile.py": karchive}, clean_first=True) c.run("create .") consumer_cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_package(karchive CONFIG REQUIRED) """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class pkgRecipe(ConanFile): package_type = "static-library" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain", "CMakeDeps" def requirements(self): self.requires("karchive/0.1") self.requires("qt/0.1") def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) c.save({"conanfile.py": consumer, "CMakeLists.txt": consumer_cmake}, clean_first=True) c.run("build .") assert "Conan: Target declared 'karchive::karchive'" in c.out # And it doesn't fail to find transitive qt def test_cmake_find_none_relocation(): c = TestClient(default_server_user=True) c.run("new cmake_lib -d name=pkg -d version=0.1") conanfile = c.load("conanfile.py") conanfile = conanfile + ' self.cpp_info.set_property("cmake_find_mode", "none")' conanfile = conanfile + '\n self.cpp_info.builddirs = ["pkg/cmake"]' cmake_export = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello CXX) add_library(pkg src/pkg.cpp) target_include_directories(pkg PUBLIC $ $ ) set_target_properties(pkg PROPERTIES PUBLIC_HEADER "include/pkg.h") install(TARGETS pkg EXPORT pkgConfig) export(TARGETS pkg NAMESPACE pkg:: FILE "${CMAKE_CURRENT_BINARY_DIR}/pkgConfig.cmake" ) install(EXPORT pkgConfig DESTINATION "pkg/cmake" NAMESPACE pkg:: ) """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake_export}) c.run("create . ") c.run("upload * -r=default -c") c.run("remove * -c") c2 = TestClient(servers=c.servers) c2.run("new cmake_exe -d name=myapp -d version=0.1 -d requires=pkg/0.1") c2.run('build .') # Now it builds correctly without failing, because package is relocatable @pytest.mark.tool("ninja") # It is IMPORTANT to do cmake-3.27 AFTER ninja, otherwise CI injects another cmake @pytest.mark.tool("cmake", "3.27") def test_cmake_find_none_relocation_multi(): # What happens for multi-config c = TestClient(default_server_user=True) c.run("new cmake_lib -d name=pkg -d version=0.1") conanfile = textwrap.dedent(""" import os, textwrap from conan import ConanFile from conan.tools.files import save, load from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps class pkgRecipe(ConanFile): name = "pkg" version = "0.1" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} # Sources are located in the same place as this recipe, copy them to the recipe exports_sources = "CMakeLists.txt", "src/*", "include/*" implements = ["auto_shared_fpic"] generators = "CMakeToolchain", "CMakeDeps" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() p = os.path.join(self.package_folder, "pkg", "cmake", "pkgConfig.cmake") new_config = textwrap.dedent(''' # Create imported target pkg::pkg add_library(pkg::pkg STATIC IMPORTED) if(NOT DEFINED CONAN_pkg_DIR_MULTI) get_filename_component(CONAN_pkg_DIR_MULTI "${CMAKE_CURRENT_LIST_FILE}" PATH) endif() foreach(_IMPORT_PREFIX ${CONAN_pkg_DIR_MULTI}) get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) set_target_properties(pkg::pkg PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" ) # Load information for each installed configuration. file(GLOB CONFIG_FILES "${_IMPORT_PREFIX}/pkg/cmake/pkgConfig-*.cmake") foreach(f ${CONFIG_FILES}) include(${f}) endforeach() endforeach() ''') save(self, p, new_config) def package_info(self): self.cpp_info.libs = ["pkg"] self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.builddirs = ["pkg/cmake"] """) cmake_export = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello CXX) add_library(pkg src/pkg.cpp) target_include_directories(pkg PUBLIC $ $ ) set_target_properties(pkg PROPERTIES PUBLIC_HEADER "include/pkg.h") install(TARGETS pkg EXPORT pkgConfig) export(TARGETS pkg NAMESPACE pkg:: FILE "${CMAKE_CURRENT_BINARY_DIR}/pkgConfig.cmake" ) install(EXPORT pkgConfig DESTINATION "pkg/cmake" NAMESPACE pkg:: ) """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake_export}) c.run("create .") c.run("create . -s build_type=Debug") c.run("upload * -r=default -c") c.run("remove * -c") c2 = TestClient(servers=c.servers) c2.run("new cmake_exe -d name=myapp -d version=0.1 -d requires=pkg/0.1") env = r".\build\generators\conanbuild.bat &&" if platform.system() == "Windows" else "" conf = ('-c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" ' '-c tools.cmake.cmakedeps:new=will_break_next') c2.run(f'install . {conf}') c2.run(f'install . {conf} -s build_type=Debug') c2.run_command(f"{env} cmake --preset conan-default") c2.run_command(f"{env} cmake --build --preset conan-release") c2.run_command(f"{env} cmake --build --preset conan-debug") ================================================ FILE: test/functional/toolchains/cmake/test_cmake_multi.py ================================================ import os import platform import textwrap from conan.test.utils.tools import TestClient def test_multi_cmake(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout import os class multiRecipe(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = ("cmake_one/CMakeLists.txt", "cmake_two/CMakeLists.txt", "src_one/*", "src_two/*") def layout(self): cmake_layout(self) def generate(self): tc = CMakeToolchain(self) tc.generate() def build_one(self): cmake = CMake(self) cmake.configure(build_script_folder="cmake_one", subfolder="one") cmake.build(subfolder="one") def build_two(self): cmake = CMake(self) cmake.configure(build_script_folder="cmake_two", subfolder="two") cmake.build(subfolder="two") def build(self): self.build_one() self.build_two() def package(self): cmake = CMake(self) cmake.install(subfolder="two") def package_info(self): self.cpp_info.libs = ["hello_two"] self.cpp_info.includedirs = ['two/include'] self.cpp_info.libdirs = ['two/lib'] """) hello_cpp = textwrap.dedent(""" #include #include "hello_{name}.h" void hello_{name}() {{ std::cout << "Hello, World {name}!" << std::endl; }} """) hello_h = textwrap.dedent(""" #pragma once void hello_{name}(); """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(hello_{name} LANGUAGES CXX) add_library(hello_{name} ../src_{name}/hello_{name}.cpp) target_include_directories(hello_{name} PUBLIC ../src_{name}) set_target_properties(hello_{name} PROPERTIES PUBLIC_HEADER "../src_{name}/hello_{name}.h") install(TARGETS hello_{name}) """) client = TestClient() client.save({"conanfile.py": conanfile, "cmake_one/CMakeLists.txt": cmakelist.format(name="one"), "cmake_two/CMakeLists.txt": cmakelist.format(name="two"), "src_one/hello_one.h": hello_h.format(name="one"), "src_one/hello_one.cpp": hello_cpp.format(name="one"), "src_two/hello_two.h": hello_h.format(name="two"), "src_two/hello_two.cpp": hello_cpp.format(name="two")}) client.run("create . --name=multi --version=0.1") ext = '.a' if platform.system() != "Windows" else '.lib' prefix = 'lib' if platform.system() != "Windows" else '' assert "multi/0.1: package(): Packaged 1 '.h' file: hello_two.h" in client.out assert f"multi/0.1: package(): Packaged 1 '{ext}' file: {prefix}hello_two{ext}" in client.out package_folder = client.created_layout().package() assert not os.path.exists(os.path.join(package_folder, "one", "include", "hello_one.h")) assert not os.path.exists(os.path.join(package_folder, "one", "lib", f"{prefix}hello_one{ext}")) assert os.path.exists(os.path.join(package_folder, "two", "include", "hello_two.h")) assert os.path.exists(os.path.join(package_folder, "two", "lib", f"{prefix}hello_two{ext}")) ================================================ FILE: test/functional/toolchains/cmake/test_cmake_toolchain.py ================================================ import json import os import platform import re import textwrap import pytest from conan.tools.microsoft.visual import vcvars_command from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save from test.conftest import tools_locations @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") @pytest.mark.parametrize("compiler, version, update, runtime", [("msvc", "192", None, "dynamic"), ("msvc", "192", "6", "static"), ("msvc", "192", "8", "static")]) def test_cmake_toolchain_win_toolset(compiler, version, update, runtime): client = TestClient(path_with_spaces=False) settings = {"compiler": compiler, "compiler.version": version, "compiler.update": update, "compiler.cppstd": "17", "compiler.runtime": runtime, "build_type": "Release", "arch": "x86_64"} # Build the profile according to the settings provided settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v) conanfile = GenConanfile().with_settings("os", "compiler", "build_type", "arch").\ with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile}) client.run("install . {}".format(settings)) toolchain = client.load("conan_toolchain.cmake") value = "v14{}".format(version[-1]) if update is not None: # Fullversion value += f",version=14.{version[-1]}{update}" assert 'set(CMAKE_GENERATOR_TOOLSET "{}" CACHE STRING "" FORCE)'.format(value) in toolchain def test_cmake_toolchain_user_toolchain(): client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "compiler", "build_type", "arch").\ with_generator("CMakeToolchain") client.save_home({"global.conf": "tools.cmake.cmaketoolchain:user_toolchain+=mytoolchain.cmake"}) client.save({"conanfile.py": conanfile}) client.run("install .") toolchain = client.load("conan_toolchain.cmake") assert 'include("mytoolchain.cmake")' in toolchain def test_cmake_toolchain_user_toolchain_from_dep(): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): exports_sources = "*" def package(self): copy(self, "*", self.build_folder, self.package_folder) def package_info(self): f = os.path.join(self.package_folder, "mytoolchain.cmake") self.conf_info.append("tools.cmake.cmaketoolchain:user_toolchain", f) """) client.save({"conanfile.py": conanfile, "mytoolchain.cmake": 'message(STATUS "mytoolchain.cmake !!!running!!!")'}) client.run("create . --name=toolchain --version=0.1") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" exports_sources = "CMakeLists.txt" build_requires = "toolchain/0.1" generators = "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": gen_cmakelists()}, clean_first=True) client.run("create . --name=pkg --version=0.1") assert "mytoolchain.cmake !!!running!!!" in client.out def test_cmake_toolchain_without_build_type(): # If "build_type" is not defined, toolchain will still be generated, it will not crash # Main effect is CMAKE_MSVC_RUNTIME_LIBRARY not being defined client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "compiler", "arch").\ with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile}) client.run("install .") toolchain = client.load("conan_toolchain.cmake") assert "CMAKE_MSVC_RUNTIME_LIBRARY" not in toolchain assert "CMAKE_BUILD_TYPE" not in toolchain @pytest.mark.skipif(platform.system() != "Windows", reason="Only on Windows with msvc") @pytest.mark.tool("cmake") def test_cmake_toolchain_cmake_vs_debugger_environment(): client = TestClient() client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_package_type("shared-library") .with_settings("build_type")}) client.run("create . -s build_type=Release") client.run("create . -s build_type=Debug") client.run("create . -s build_type=MinSizeRel") client.run("install --require=pkg/1.0 -s build_type=Debug -g CMakeToolchain --format=json") debug_graph = json.loads(client.stdout) debug_bindir = debug_graph['graph']['nodes']['1']['cpp_info']['root']['bindirs'][0] debug_bindir = debug_bindir.replace('\\', '/') toolchain = client.load("conan_toolchain.cmake") debugger_environment = f"PATH=$<$:{debug_bindir}>;%PATH%" assert debugger_environment in toolchain client.run("install --require=pkg/1.0 -s build_type=Release -g CMakeToolchain --format=json") release_graph = json.loads(client.stdout) release_bindir = release_graph['graph']['nodes']['1']['cpp_info']['root']['bindirs'][0] release_bindir = release_bindir.replace('\\', '/') toolchain = client.load("conan_toolchain.cmake") debugger_environment = f"PATH=$<$:{debug_bindir}>" \ f"$<$:{release_bindir}>;%PATH%" assert debugger_environment in toolchain client.run("install --require=pkg/1.0 -s build_type=MinSizeRel -g CMakeToolchain --format=json") minsizerel_graph = json.loads(client.stdout) minsizerel_bindir = minsizerel_graph['graph']['nodes']['1']['cpp_info']['root']['bindirs'][0] minsizerel_bindir = minsizerel_bindir.replace('\\', '/') toolchain = client.load("conan_toolchain.cmake") debugger_environment = f"PATH=$<$:{debug_bindir}>" \ f"$<$:{release_bindir}>" \ f"$<$:{minsizerel_bindir}>;%PATH%" assert debugger_environment in toolchain @pytest.mark.tool("cmake") def test_cmake_toolchain_cmake_vs_debugger_environment_not_needed(): client = TestClient() client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_package_type("shared-library") .with_settings("build_type")}) client.run("create . -s build_type=Release") cmake_generator = "" if platform.system() != "Windows" else "-c tools.cmake.cmaketoolchain:generator=Ninja" client.run(f"install --require=pkg/1.0 -s build_type=Release -g CMakeToolchain {cmake_generator}") toolchain = client.load("conan_toolchain.cmake") assert "CMAKE_VS_DEBUGGER_ENVIRONMENT" not in toolchain @pytest.mark.tool("cmake") def test_cmake_toolchain_multiple_user_toolchain(): """ A consumer consuming two packages that declare: self.conf_info["tools.cmake.cmaketoolchain:user_toolchain"] The consumer wants to use apply both toolchains in the CMakeToolchain. There are two ways to customize the CMakeToolchain (parametrized): 1. Altering the context of the block (with_context = True) 2. Using the t.blocks["user_toolchain"].user_toolchains = [] (with_context = False) """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): exports_sources = "*" def package(self): copy(self, "*", self.source_folder, self.package_folder) def package_info(self): f = os.path.join(self.package_folder, "mytoolchain.cmake") self.conf_info.append("tools.cmake.cmaketoolchain:user_toolchain", f) """) client.save({"conanfile.py": conanfile, "mytoolchain.cmake": 'message(STATUS "mytoolchain1.cmake !!!running!!!")'}) client.run("create . --name=toolchain1 --version=0.1") client.save({"conanfile.py": conanfile, "mytoolchain.cmake": 'message(STATUS "mytoolchain2.cmake !!!running!!!")'}) client.run("create . --name=toolchain2 --version=0.1") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" exports_sources = "CMakeLists.txt" tool_requires = "toolchain1/0.1", "toolchain2/0.1" generators = "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": gen_cmakelists()}, clean_first=True) client.run("create . --name=pkg --version=0.1") assert "mytoolchain1.cmake !!!running!!!" in client.out assert "mytoolchain2.cmake !!!running!!!" in client.out assert "CMake Warning" not in client.out @pytest.mark.tool("cmake") def test_cmaketoolchain_no_warnings(): """Make sure uninitialized variables do not cause any warnings, passing -Werror=dev and --warn-uninitialized, calling "cmake" with conan_toolchain.cmake used to fail """ # Issue https://github.com/conan-io/conan/issues/10288 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain", "CMakeDeps" requires = "dep/0.1" """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_package(dep CONFIG REQUIRED) """) client.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "conanfile.py": conanfile, "CMakeLists.txt": consumer}) client.run("create dep") client.run("install .") build_type = "-DCMAKE_BUILD_TYPE=Release" if platform.system() != "Windows" else "" client.run_command("cmake -Werror=dev --warn-uninitialized . {}" " -DCMAKE_TOOLCHAIN_FILE=./conan_toolchain.cmake".format(build_type)) assert "Using Conan toolchain" in client.out # The real test is that there are no errors, it returns successfully def test_install_output_directories(): """ If we change the libdirs of the cpp.package, as we are doing cmake.install, the output directory for the libraries is changed """ client = TestClient() client.run("new cmake_lib -d name=zlib -d version=1.2.11") # Edit the cpp.package.libdirs and check if the library is placed anywhere else cf = client.load("conanfile.py") cf = cf.replace("cmake_layout(self)", 'cmake_layout(self)\n self.cpp.package.libdirs = ["mylibs"]') client.save({"conanfile.py": cf}) client.run("create . -tf=") layout = client.created_layout() p_folder = layout.package() assert os.path.exists(os.path.join(p_folder, "mylibs")) assert not os.path.exists(os.path.join(p_folder, "lib")) b_folder = layout.build() if platform.system() != "Windows": gen_folder = os.path.join(b_folder, "build", "Release", "generators") else: gen_folder = os.path.join(b_folder, "build", "generators") toolchain = client.load(os.path.join(gen_folder, "conan_toolchain.cmake")) assert 'set(CMAKE_INSTALL_LIBDIR "mylibs")' in toolchain @pytest.mark.tool("cmake") def test_cmake_toolchain_definitions_complex_strings(): # https://github.com/conan-io/conan/issues/11043 client = TestClient(path_with_spaces=False) profile = textwrap.dedent(r''' include(default) [conf] tools.build:defines+=["escape=partially \"escaped\""] tools.build:defines+=["spaces=me you"] tools.build:defines+=["foobar=bazbuz"] tools.build:defines+=["answer=42"] ''') conanfile = textwrap.dedent(r''' from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout class Test(ConanFile): exports_sources = "CMakeLists.txt", "src/*" settings = "os", "compiler", "arch", "build_type" def generate(self): tc = CMakeToolchain(self) tc.preprocessor_definitions["escape2"] = "partially \"escaped\"" tc.preprocessor_definitions["spaces2"] = "me you" tc.preprocessor_definitions["foobar2"] = "bazbuz" tc.preprocessor_definitions["answer2"] = 42 tc.preprocessor_definitions["NOVALUE_DEF"] = None tc.preprocessor_definitions.release["escape_release"] = "release partially \"escaped\"" tc.preprocessor_definitions.release["spaces_release"] = "release me you" tc.preprocessor_definitions.release["foobar_release"] = "release bazbuz" tc.preprocessor_definitions.release["answer_release"] = 42 tc.preprocessor_definitions.release["NOVALUE_DEF_RELEASE"] = None tc.preprocessor_definitions.debug["escape_debug"] = "debug partially \"escaped\"" tc.preprocessor_definitions.debug["spaces_debug"] = "debug me you" tc.preprocessor_definitions.debug["foobar_debug"] = "debug bazbuz" tc.preprocessor_definitions.debug["answer_debug"] = 21 tc.generate() def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() ''') main = textwrap.dedent(""" #include #define STR(x) #x #define SHOW_DEFINE(x) printf("%s=%s", #x, STR(x)) int main(int argc, char *argv[]) { SHOW_DEFINE(escape); SHOW_DEFINE(spaces); SHOW_DEFINE(foobar); SHOW_DEFINE(answer); SHOW_DEFINE(escape2); SHOW_DEFINE(spaces2); SHOW_DEFINE(foobar2); SHOW_DEFINE(answer2); #ifdef NDEBUG SHOW_DEFINE(escape_release); SHOW_DEFINE(spaces_release); SHOW_DEFINE(foobar_release); SHOW_DEFINE(answer_release); #else SHOW_DEFINE(escape_debug); SHOW_DEFINE(spaces_debug); SHOW_DEFINE(foobar_debug); SHOW_DEFINE(answer_debug); #endif #ifdef NOVALUE_DEF printf("NO VALUE!!!!"); #endif #ifdef NOVALUE_DEF_RELEASE printf("NO VALUE RELEASE!!!!"); #endif return 0; } """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Test CXX) set(CMAKE_CXX_STANDARD 11) add_executable(example src/main.cpp) """) client.save({"conanfile.py": conanfile, "profile": profile, "src/main.cpp": main, "CMakeLists.txt": cmakelists}, clean_first=True) client.run("install . -pr=./profile") client.run("build . -pr=./profile") exe = "build/Release/example" if platform.system() != "Windows" else r"build\Release\example.exe" client.run_command(exe) assert 'escape=partially "escaped"' in client.out assert 'spaces=me you' in client.out assert 'foobar=bazbuz' in client.out assert 'answer=42' in client.out assert 'escape2=partially "escaped"' in client.out assert 'spaces2=me you' in client.out assert 'foobar2=bazbuz' in client.out assert 'answer2=42' in client.out assert 'escape_release=release partially "escaped"' in client.out assert 'spaces_release=release me you' in client.out assert 'foobar_release=release bazbuz' in client.out assert 'answer_release=42' in client.out assert "NO VALUE!!!!" in client.out assert "NO VALUE RELEASE!!!!" in client.out client.run("install . -pr=./profile -s build_type=Debug") client.run("build . -pr=./profile -s build_type=Debug") exe = "build/Debug/example" if platform.system() != "Windows" else r"build\Debug\example.exe" client.run_command(exe) assert 'escape_debug=debug partially "escaped"' in client.out assert 'spaces_debug=debug me you' in client.out assert 'foobar_debug=debug bazbuz' in client.out assert 'answer_debug=21' in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_cmake_toolchain_runtime_types(): # everything works with the default cmake_minimum_required version 3.15 in the template client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("install . -s compiler.runtime=static -s build_type=Debug") client.run("build . -s compiler.runtime=static -s build_type=Debug") vcvars = vcvars_command(version="15", architecture="x64") lib = os.path.join(client.current_folder, "build", "Debug", "hello.lib") dumpbind_cmd = '{} && dumpbin /directives "{}"'.format(vcvars, lib) client.run_command(dumpbind_cmd) assert "LIBCMTD" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_cmake_toolchain_runtime_types_cmake_older_than_3_15(): client = TestClient(path_with_spaces=False) # Setting an older cmake_minimum_required in the CMakeLists fails, will link # against the default debug runtime (MDd->MSVCRTD), not against MTd->LIBCMTD client.run("new cmake_lib -d name=hello -d version=0.1") cmake = client.load("CMakeLists.txt") cmake2 = cmake.replace('cmake_minimum_required(VERSION 3.15)', 'cmake_minimum_required(VERSION 3.1)') assert cmake != cmake2 client.save({"CMakeLists.txt": cmake2}) client.run("install . -s compiler.runtime=static -s build_type=Debug") client.run("build . -s compiler.runtime=static -s build_type=Debug") vcvars = vcvars_command(version="15", architecture="x64") lib = os.path.join(client.current_folder, "build", "Debug", "hello.lib") dumpbind_cmd = '{} && dumpbin /directives "{}"'.format(vcvars, lib) client.run_command(dumpbind_cmd) assert "LIBCMTD" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") class TestWinSDKVersion: @pytest.mark.tool("cmake", "3.23") @pytest.mark.tool("visual_studio", "17") def test_cmake_toolchain_winsdk_version(self): # This test passes also with CMake 3.28, as long as cmake_minimum_required(VERSION 3.27) # is not defined client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=0.1") cmake = client.load("CMakeLists.txt") cmake += 'message(STATUS "CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION = ' \ '${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")' client.save({"CMakeLists.txt": cmake}) client.run("create . -s arch=x86_64 -s compiler.version=194 " "-c tools.microsoft:winsdk_version=10.0") assert "CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION = 10.0" in client.out assert "Conan toolchain: CMAKE_GENERATOR_PLATFORM=x64" in client.out assert "Conan toolchain: CMAKE_GENERATOR_PLATFORM=x64,version" not in client.out @pytest.mark.tool("cmake", "3.27") @pytest.mark.tool("visual_studio", "17") def test_cmake_toolchain_winsdk_version2(self): # https://github.com/conan-io/conan/issues/15372 client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=0.1") cmake = client.load("CMakeLists.txt") cmake = cmake.replace("cmake_minimum_required(VERSION 3.15)", "cmake_minimum_required(VERSION 3.27)") cmake += 'message(STATUS "CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION = ' \ '${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")' client.save({"CMakeLists.txt": cmake}) client.run("create . -s arch=x86_64 -s compiler.version=194 " "-c tools.microsoft:winsdk_version=10.0 " '-c tools.cmake.cmaketoolchain:generator="Visual Studio 17"') assert "CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION = 10.0" in client.out assert "Conan toolchain: CMAKE_GENERATOR_PLATFORM=x64,version=10.0" in client.out @pytest.mark.tool("cmake", "3.23") def test_cmake_layout_missing_option(): client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=\'["options.missing"]\' ' \ '-c tools.cmake.cmaketoolchain:generator=Ninja' client.run("install . {}".format(settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "Release", "generators")) @pytest.mark.tool("cmake", "3.23") def test_cmake_layout_missing_setting(): client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.missing"]\' ' \ '-c tools.cmake.cmaketoolchain:generator=Ninja' client.run("install . {}".format(settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "Release", "generators")) @pytest.mark.tool("cmake") def test_cmaketoolchain_sysroot(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout class AppConan(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "CMakeLists.txt" def generate(self): tc = CMakeToolchain(self) {} tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(app NONE) message("sysroot: '${CMAKE_SYSROOT}'") message("osx_sysroot: '${CMAKE_OSX_SYSROOT}'") """) client.save({ "conanfile.py": conanfile.format(""), "CMakeLists.txt": cmakelist }) fake_sysroot = client.current_folder output_fake_sysroot = fake_sysroot.replace("\\", "/") if platform.system() == "Windows" else fake_sysroot client.run("create . --name=app --version=1.0 -c tools.build:sysroot='{}'".format(fake_sysroot)) assert "sysroot: '{}'".format(output_fake_sysroot) in client.out # set in a block instead of using conf set_sysroot_in_block = 'tc.blocks["generic_system"].values["cmake_sysroot"] = "{}"'.format(output_fake_sysroot) client.save({ "conanfile.py": conanfile.format(set_sysroot_in_block), }) client.run("create . --name=app --version=1.0") assert "sysroot: '{}'".format(output_fake_sysroot) in client.out def test_cmake_layout_not_forbidden_build_type(): client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=' \ '\'["options.missing", "settings.build_type"]\'' client.run("install . {}".format(settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build/release/generators/conan_toolchain.cmake")) def test_resdirs_cmake_install(): """If resdirs is declared, the CMAKE_INSTALL_DATAROOTDIR folder is set""" client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout class AppConan(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "CMakeLists.txt", "my_license" name = "foo" version = "1.0" def generate(self): tc = CMakeToolchain(self) tc.generate() def layout(self): self.cpp.package.resdirs = ["res"] def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() """) cmake = """ cmake_minimum_required(VERSION 3.15) project(foo NONE) if(NOT CMAKE_INSTALL_DATAROOTDIR) message(FATAL_ERROR "Cannot install stuff") endif() install(FILES my_license DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/licenses) """ client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "my_license": "MIT"}) client.run("create .") assert "/res/licenses/my_license" in client.out assert "Packaged 1 file: my_license" in client.out def test_resdirs_none_cmake_install(): """If no resdirs are declared, the CMAKE_INSTALL_DATAROOTDIR folder is not set""" client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout class AppConan(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "CMakeLists.txt", "my_license" name = "foo" version = "1.0" def generate(self): tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() """) cmake = """ cmake_minimum_required(VERSION 3.15) project(foo NONE) if(NOT CMAKE_INSTALL_DATAROOTDIR) message(FATAL_ERROR "Cannot install stuff") endif() """ client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "my_license": "MIT"}) client.run("create .", assert_error=True) assert "Cannot install stuff" in client.out @pytest.mark.tool("cmake") def test_cmake_toolchain_vars_when_option_declared(): t = TestClient() cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 2.8) # <---- set this to an old version for old policies cmake_policy(SET CMP0091 NEW) # <-- Needed on Windows project(mylib CXX) message("CMake version: ${CMAKE_VERSION}") # Set the options AFTER the call to project, that is, after toolchain is loaded option(BUILD_SHARED_LIBS "" ON) option(CMAKE_POSITION_INDEPENDENT_CODE "" OFF) add_library(mylib src/mylib.cpp) target_include_directories(mylib PUBLIC include) get_target_property(MYLIB_TARGET_TYPE mylib TYPE) get_target_property(MYLIB_PIC mylib POSITION_INDEPENDENT_CODE) message("mylib target type: ${MYLIB_TARGET_TYPE}") message("MYLIB_PIC value: ${MYLIB_PIC}") if(MYLIB_PIC) #Note: the value is "True"/"False" if set by cmake, # and "ON"/"OFF" if set by Conan message("mylib position independent code: ON") else() message("mylib position independent code: OFF") endif() set_target_properties(mylib PROPERTIES PUBLIC_HEADER "include/mylib.h") install(TARGETS mylib) """) t.run("new cmake_lib -d name=mylib -d version=1.0") t.save({"CMakeLists.txt": cmakelists}) # The generated toolchain should set `BUILD_SHARED_LIBS` to `OFF`, # and `CMAKE_POSITION_INDEPENDENT_CODE` to `ON` and the calls to # `option()` in the CMakeLists.txt should respect the existing values. # Note: on *WINDOWS* `fPIC` is not an option for this recipe, so it's invalid # to pass it to Conan, in which case the value in CMakeLists.txt # takes precedence. fpic_option = "-o mylib/*:fPIC=True" if platform.system() != "Windows" else "" fpic_cmake_value = "ON" if platform.system() != "Windows" else "OFF" t.run(f"create . -o mylib/*:shared=False {fpic_option} --test-folder=") assert "mylib target type: STATIC_LIBRARY" in t.out assert f"mylib position independent code: {fpic_cmake_value}" in t.out # When building manually, ensure the value passed by the toolchain overrides the ones in # the CMakeLists fpic_option = "-o mylib/*:fPIC=False" if platform.system() != "Windows" else "" t.run(f"install . -o mylib/*:shared=False {fpic_option}") folder = "build/generators" if platform.system() == "Windows" else "build/Release/generators" t.run_command(f"cmake -S . -B build/ -DCMAKE_TOOLCHAIN_FILE={folder}/conan_toolchain.cmake") assert "mylib target type: STATIC_LIBRARY" in t.out assert f"mylib position independent code: OFF" in t.out # Note: from this point forward, the CMakeCache is already initialised. # When explicitly overriding `CMAKE_POSITION_INDEPENDENT_CODE` via command line, ensure # this takes precedence to the value defined by the toolchain t.run_command("cmake -S . -B build/ -DCMAKE_POSITION_INDEPENDENT_CODE=ON") assert "mylib target type: STATIC_LIBRARY" in t.out assert "mylib position independent code: ON" in t.out t.run_command("cmake -S . -B build/ -DBUILD_SHARED_LIBS=ON") assert "mylib target type: SHARED_LIBRARY" in t.out assert "mylib position independent code: ON" in t.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("single_profile", [True, False]) def test_find_program_for_tool_requires(single_profile): """Test that the same reference can be both a tool_requires and a regular requires, and that find_program (executables) and find_package (libraries) find the correct ones when cross building. """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): name = "foobar" version = "1.0" settings = "os", "arch", "compiler", "build_type" exports_sources = "*" def layout(self): pass def package(self): copy(self, pattern="lib*", src=self.build_folder, dst=os.path.join(self.package_folder, "lib")) copy(self, pattern="*bin", src=self.build_folder, dst=os.path.join(self.package_folder, "bin")) """) host_profile = textwrap.dedent(""" [settings] os=Linux arch=armv8 compiler=gcc compiler.version=12 compiler.libcxx=libstdc++11 build_type=Release """) build_profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=12 compiler.libcxx=libstdc++11 build_type=Release """) client.save({"conanfile.py": conanfile, "libfoo.so": "", "foobin": "", "host_profile": host_profile, "build_profile": build_profile }) client.run("create . -pr:b build_profile -pr:h build_profile") build_context_package_folder = re.search(r"Package folder ([\w\W]+).conan2([\w\W]+)", str(client.out)).group(2).strip() build_context_package_folder = build_context_package_folder.replace("\\", "/") client.run("create . -pr:b build_profile -pr:h host_profile") host_context_package_folder = re.search(r"Package folder ([\w\W]+).conan2([\w\W]+)", str(client.out)).group(2).strip() host_context_package_folder = host_context_package_folder.replace("\\", "/") conanfile_consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class PkgConan(ConanFile): settings = "os", "arch", "compiler", "build_type" def layout(self): cmake_layout(self) def requirements(self): self.requires("foobar/1.0") def build_requirements(self): self.tool_requires("foobar/1.0") """) cmakelists_consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(Hello LANGUAGES NONE) find_package(foobar CONFIG REQUIRED) find_program(FOOBIN_EXECUTABLE foobin) message("foobin executable: ${FOOBIN_EXECUTABLE}") message("foobar include dir: ${foobar_INCLUDE_DIR}") if(NOT FOOBIN_EXECUTABLE) message(FATAL_ERROR "FOOBIN executable not found") endif() """) client.save({ "conanfile_consumer.py": conanfile_consumer, "CMakeLists.txt": cmakelists_consumer, "host_profile": host_profile, "build_profile": build_profile}, clean_first=True) client.run("install conanfile_consumer.py -g CMakeToolchain -g CMakeDeps -pr:b build_profile -pr:h host_profile") with client.chdir("build"): client.run_command("cmake .. -DCMAKE_TOOLCHAIN_FILE=Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release") # Verify binary executable is found from build context package, # and library comes from host context package assert f"{build_context_package_folder}/bin/foobin" in client.out assert f"{host_context_package_folder}/include" in client.out @pytest.mark.tool("pkg_config") def test_cmaketoolchain_and_pkg_config_path(): """ Lightweight test which is loading a dependency as a *.pc file through CMake thanks to pkg_check_modules macro. It's working because PKG_CONFIG_PATH env variable is being loaded automatically by the CMakeToolchain generator. """ client = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class DepConan(ConanFile): name = "dep" version = "1.0" def package_info(self): self.cpp_info.libs = ["dep"] """) pkg = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class HelloConan(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps", "CMakeToolchain" exports_sources = "CMakeLists.txt" def requirements(self): self.requires("dep/1.0") def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(pkg NONE) find_package(PkgConfig REQUIRED) # We should have PKG_CONFIG_PATH created in the current environment pkg_check_modules(DEP REQUIRED IMPORTED_TARGET dep) """) client.save({ "dep/conanfile.py": dep, "pkg/conanfile.py": pkg, "pkg/CMakeLists.txt": cmakelists }) client.run("create dep/conanfile.py") client.run("create pkg/conanfile.py") assert "Found dep, version 1.0" in client.out def test_cmaketoolchain_conf_from_tool_require(): # https://github.com/conan-io/conan/issues/13914 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): name = "toolchain" version = "1.0" def package_info(self): self.conf_info.define("tools.cmake.cmaketoolchain:system_name", "GENERIC-POTATO") self.conf_info.define("tools.cmake.cmaketoolchain:system_processor", "ARM-POTATO") self.conf_info.define("tools.build.cross_building:can_run", False) self.conf_info.define("tools.build:compiler_executables", { "c": "arm-none-eabi-gcc", "cpp": "arm-none-eabi-g++", "asm": "arm-none-eabi-as", }) """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_test("pass") .with_tool_requires("toolchain/1.0") .with_generator("CMakeToolchain")}) c.run("create .") toolchain = c.load("test_package/conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME GENERIC-POTATO)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR ARM-POTATO)" in toolchain def test_inject_user_toolchain(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class AppConan(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "CMakeLists.txt" name = "foo" version = "1.0" generators = "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = """ cmake_minimum_required(VERSION 3.15) project(foo LANGUAGES NONE) message(STATUS "MYVAR1 ${MY_USER_VAR1}!!") """ profile = textwrap.dedent(""" include(default) [conf] tools.cmake.cmaketoolchain:user_toolchain+={{profile_dir}}/myvars.cmake""") save(os.path.join(client.paths.profiles_path, "myprofile"), profile) save(os.path.join(client.paths.profiles_path, "myvars.cmake"), 'set(MY_USER_VAR1 "MYVALUE1")') client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake}) client.run("build . -pr=myprofile") assert "-- MYVAR1 MYVALUE1!!" in client.out # Now test with the global.conf global_conf = 'tools.cmake.cmaketoolchain:user_toolchain=' \ '["{{conan_home_folder}}/my.cmake"]' client.save_home({"global.conf": global_conf}) save(os.path.join(client.cache_folder, "my.cmake"), 'message(STATUS "IT WORKS!!!!")') client.run("build .") # The toolchain is found and can be used assert "IT WORKS!!!!" in client.out def test_no_build_type(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class AppConan(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "arch" exports_sources = "CMakeLists.txt" def layout(self): self.folders.build = "build" def generate(self): tc = CMakeToolchain(self) if not tc.is_multi_configuration: tc.cache_variables["CMAKE_BUILD_TYPE"] = "Release" tc.generate() def build(self): cmake = CMake(self) cmake.configure() build_type = "Release" if cmake.is_multi_configuration else None cmake.build(build_type=build_type) def package(self): cmake = CMake(self) build_type = "Release" if cmake.is_multi_configuration else None cmake.install(build_type=build_type) """) cmake = """ cmake_minimum_required(VERSION 3.15) project(pkg LANGUAGES NONE) """ client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake}) client.run("create .") assert "Don't specify 'build_type' at build time" not in client.out @pytest.mark.tool("cmake", "3.19") def test_redirect_stdout(): client = TestClient() conanfile = textwrap.dedent(""" import os from io import StringIO from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain from conan.tools.cmake import cmake_layout class Pkg(ConanFile): name = "foo" version = "1.0" settings = "os", "arch", "build_type", "compiler" generators = "CMakeToolchain" exports_sources = "CMakeLists.txt", "main.cpp" def build(self): cmake = CMake(self) config_stdout, config_stderr = StringIO(), StringIO() cmake.configure(stdout=config_stdout, stderr=config_stderr) self.output.info(f"Configure stdout: '{config_stdout.getvalue()}'") self.output.info(f"Configure stderr: '{config_stderr.getvalue()}'") build_stdout, build_stderr = StringIO(), StringIO() cmake.build(stdout=build_stdout, stderr=build_stderr) self.output.info(f"Build stdout: '{build_stdout.getvalue()}'") self.output.info(f"Build stderr: '{build_stderr.getvalue()}'") test_stdout, test_stderr = StringIO(), StringIO() cmake.test(stdout=test_stdout, stderr=test_stderr) self.output.info(f"Test stdout: '{test_stdout.getvalue()}'") self.output.info(f"Test stderr: '{test_stderr.getvalue()}'") def package(self): cmake = CMake(self) install_stdout, install_stderr = StringIO(), StringIO() cmake.install(stdout=install_stdout, stderr=install_stderr) self.output.info(f"Install stdout: '{install_stdout.getvalue()}'") self.output.info(f"Install stderr: '{install_stderr.getvalue()}'") """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": 'project(foo)\nadd_executable(mylib main.cpp)\ninclude(CTest)', "main.cpp": "int main() {return 0;}"}) client.run("create .") # Ensure the output is not unexpectedly empty assert re.search("Configure stdout: '[^']", client.out) assert re.search("Configure stderr: '[^']", client.out) assert re.search("Build stdout: '[^']", client.out) assert re.search("Build stderr: ''", client.out) assert re.search("Test stdout: '[^']", client.out) # Empty on Windows if platform.system() == "Windows": assert re.search("Test stderr: ''", client.out) else: assert re.search("Test stderr: '[^']", client.out) if platform.system() == "Windows": assert re.search("Install stdout: ''", client.out) else: assert re.search("Install stdout: '[^']", client.out) assert re.search("Install stderr: ''", client.out) @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Windows", reason="neeed multi-config") def test_cmake_toolchain_cxxflags_multi_config(): c = TestClient() profile_release = textwrap.dedent(r""" include(default) [conf] tools.build:defines=["conan_test_answer=42", "conan_test_other=24"] tools.build:cxxflags=["/Zc:__cplusplus"] """) profile_debug = textwrap.dedent(r""" include(default) [settings] build_type=Debug [conf] tools.build:defines=["conan_test_answer=123"] tools.build:cxxflags=["/W4"] """) conanfile = textwrap.dedent(r''' from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout class Test(ConanFile): exports_sources = "CMakeLists.txt", "src/*" settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() ''') main = textwrap.dedent(r""" #include #include #define STR(x) #x #define SHOW_DEFINE(x) printf("DEFINE %s=%s!\n", #x, STR(x)) int main() { SHOW_DEFINE(conan_test_answer); #ifdef conan_test_other SHOW_DEFINE(conan_test_other); #endif char a = 123L; // to trigger warnings #if __cplusplus std::cout << "CPLUSPLUS: __cplusplus" << __cplusplus<< "\n"; #endif } """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Test CXX) add_executable(example src/main.cpp) """) c.save({"conanfile.py": conanfile, "profile_release": profile_release, "profile_debug": profile_debug, "src/main.cpp": main, "CMakeLists.txt": cmakelists}, clean_first=True) c.run("install . -pr=./profile_release") c.run("install . -pr=./profile_debug") with c.chdir("build"): c.run_command("cmake .. -DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake") c.run_command("cmake --build . --config Release") assert "warning C4189" not in c.out c.run_command("cmake --build . --config Debug") assert "warning C4189" in c.out c.run_command(r"build\Release\example.exe") assert 'DEFINE conan_test_answer=42!' in c.out assert 'DEFINE conan_test_other=24!' in c.out assert "CPLUSPLUS: __cplusplus20" in c.out c.run_command(r"build\Debug\example.exe") assert 'DEFINE conan_test_answer=123' in c.out assert 'other=' not in c.out assert "CPLUSPLUS: __cplusplus19" in c.out @pytest.mark.tool("ninja") @pytest.mark.tool("cmake", "3.23") def test_cmake_toolchain_ninja_multi_config(): c = TestClient() profile_release = textwrap.dedent(r""" include(default) [conf] tools.cmake.cmaketoolchain:generator=Ninja Multi-Config tools.build:defines=["conan_test_answer=42", "conan_test_other=24"] """) profile_debug = textwrap.dedent(r""" include(default) [settings] build_type=Debug [conf] tools.cmake.cmaketoolchain:generator=Ninja Multi-Config tools.build:defines=["conan_test_answer=123"] """) profile_relwithdebinfo = textwrap.dedent(r""" include(default) [settings] build_type=RelWithDebInfo [conf] tools.cmake.cmaketoolchain:generator=Ninja Multi-Config tools.build:defines=["conan_test_answer=456", "conan_test_other=abc", 'conan_test_complex="1 2"'] """) conanfile = textwrap.dedent(r''' from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout class Test(ConanFile): exports_sources = "CMakeLists.txt", "src/*" settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() ''') main = textwrap.dedent(r""" #include #include #define STR(x) #x #define SHOW_DEFINE(x) printf("DEFINE %s=%s!\n", #x, STR(x)) int main() { SHOW_DEFINE(conan_test_answer); #ifdef conan_test_other SHOW_DEFINE(conan_test_other); #endif #ifdef conan_test_complex SHOW_DEFINE(conan_test_complex); #endif } """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Test CXX) add_executable(example src/main.cpp) """) c.save({"conanfile.py": conanfile, "profile_release": profile_release, "profile_debug": profile_debug, "profile_relwithdebinfo": profile_relwithdebinfo, "src/main.cpp": main, "CMakeLists.txt": cmakelists}) c.run("install . -pr=./profile_release") c.run("install . -pr=./profile_debug") c.run("install . -pr=./profile_relwithdebinfo") with c.chdir("build"): env = r".\generators\conanbuild.bat &&" if platform.system() == "Windows" else "" c.run_command(f"{env} cmake .. -G \"Ninja Multi-Config\" " "-DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake") c.run_command(f"{env} cmake --build . --config Release") c.run_command(f"{env} cmake --build . --config Debug") c.run_command(f"{env} cmake --build . --config RelWithDebInfo") c.run_command(os.sep.join([".", "build", "Release", "example"])) assert 'DEFINE conan_test_answer=42!' in c.out assert 'DEFINE conan_test_other=24!' in c.out assert 'complex=' not in c.out c.run_command(os.sep.join([".", "build", "Debug", "example"])) assert 'DEFINE conan_test_answer=123' in c.out assert 'other=' not in c.out assert 'complex=' not in c.out c.run_command(os.sep.join([".", "build", "RelWithDebInfo", "example"])) assert 'DEFINE conan_test_answer=456!' in c.out assert 'DEFINE conan_test_other=abc!' in c.out assert 'DEFINE conan_test_complex="1 2"!' in c.out @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only needs to run once, no need for extra platforms") def test_cxx_version_not_overriden_if_hardcoded(): """Any C++ standard set in the CMakeLists.txt will have priority even if the compiler.cppstd is set in the profile""" tc = TestClient() tc.run("new cmake_exe -dname=foo -dversion=1.0") cml_contents = tc.load("CMakeLists.txt") cml_contents = cml_contents.replace("project(foo CXX)\n", "project(foo CXX)\nset(CMAKE_CXX_STANDARD 17)\n") tc.save({"CMakeLists.txt": cml_contents}) # The compiler.cppstd will not override the CXX_STANDARD variable tc.run("create . -s=compiler.cppstd=11") assert "Conan toolchain: C++ Standard 11 with extensions OFF" in tc.out assert "Warning: Standard CMAKE_CXX_STANDARD value defined in conan_toolchain.cmake to 11 has been modified to 17" in tc.out # Even though Conan warns, the compiled code is C++17 assert "foo/1.0: __cplusplus2017" in tc.out tc.run("create . -s=compiler.cppstd=17") assert "Conan toolchain: C++ Standard 17 with extensions OFF" in tc.out assert "Warning: Standard CMAKE_CXX_STANDARD value defined in conan_toolchain.cmake to 17 has been modified to 17" not in tc.out @pytest.mark.tool("cmake", "3.23") # Android complains if <3.19 @pytest.mark.tool("android_ndk") @pytest.mark.skipif(platform.system() != "Darwin", reason="NDK only installed on MAC") def test_cmake_toolchain_crossbuild_set_cmake_compiler(): # To reproduce https://github.com/conan-io/conan/issues/16960 # the issue happens when you set CMAKE_CXX_COMPILER and CMAKE_C_COMPILER as cache variables # and then cross-build c = TestClient() ndk_path = tools_locations["android_ndk"]["system"]["path"][platform.system()] bin_path = ndk_path + ( "/toolchains/llvm/prebuilt/darwin-x86_64/bin" if platform.machine() == "x86_64" else "/toolchains/llvm/prebuilt/darwin-arm64/bin" ) android = textwrap.dedent(f""" [settings] os=Android os.api_level=23 arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=c++_shared build_type=Release [conf] tools.android:ndk_path={ndk_path} tools.build:compiler_executables = {{"c": "{bin_path}/x86_64-linux-android23-clang", "cpp": "{bin_path}/x86_64-linux-android23-clang++"}} """) conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout from conan.tools.build import check_min_cppstd class SDKConan(ConanFile): name = "sdk" version = "1.0" generators = "CMakeDeps", "VirtualBuildEnv" settings = "os", "compiler", "arch", "build_type" exports_sources = "CMakeLists.txt" def layout(self): cmake_layout(self) def generate(self): tc = CMakeToolchain(self) tc.cache_variables["SDK_VERSION"] = str(self.version) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmake = textwrap.dedent(f""" cmake_minimum_required(VERSION 3.23) project(sdk VERSION ${{SDK_VERSION}}.0 LANGUAGES NONE) message("sdk: ${{SDK_VERSION}}.0") """) c.save({"android": android, "conanfile.py": conanfile, "CMakeLists.txt": cmake}) # first run works ok c.run('build . --profile:host=android') assert 'sdk: 1.0.0' in c.out # in second run CMake says that you have changed variables that require your cache to be deleted. # and deletes the cache and fails c.run('build . --profile:host=android') assert 'VERSION ".0" format invalid.' not in c.out assert 'sdk: 1.0.0' in c.out @pytest.mark.tool("cmake") def test_cmake_toolchain_language_c(): client = TestClient() conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain" languages = "C" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(pkg NONE) """) client.save( { "conanfile.py": conanfile, "CMakeLists.txt": cmakelists, }, clean_first=True, ) if platform.system() == "Windows": # compiler.version=191 is already the default now client.run("build . -s compiler.cstd=11 -s compiler.version=191", assert_error=True) assert "The provided compiler.cstd=11 is not supported by msvc 191. Supported values are: []" \ in client.out else: client.run("build . -s compiler.cppstd=11") # It doesn't fail @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") @pytest.mark.tool("cmake") def test_cmaketoolchain_check_function_exists(): """ covers the problematic check_function_exists() CMake DISCOURAGED check, that can fail for Release configuration, due to CMake issues. This was broken when we tried to add the automatic CMAKE_TRY_COMPILE_CONFIGURATION in release cases, as CheckFunctionExists.c is optimized away or fails to compile in Release, but works in Debug. This test is here to capture this legacy and problematic use case NOT to recommended this as a good practice, rather then opposite, DONT DO IT """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class Conan(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = CMakeToolchain(self) # This gets back to older default behavior # tc.blocks["try_compile"].values["config"] = None tc.generate() def build(self): CMake(self).configure() """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello C) include(CheckFunctionExists) check_function_exists(pow HAVE_POW) if(NOT HAVE_POW) message(FATAL_ERROR "Not math!!!!!") endif() """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}) c.run("build . ") # Release breaks the check assert "Looking for pow - found" in c.out # And it no longer crashes @pytest.mark.skipif(platform.system() != "Linux", reason="Linker scripts in Linux only") def test_cmake_linker_scripts(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout class Test(ConanFile): exports_sources = "CMakeLists.txt", "src/*" settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) main = "int main() {}" cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(Test CXX) add_executable(example main.cpp) """) profile = textwrap.dedent(""" include(default) [conf] tools.build:linker_scripts=['{{profile_dir}}/mylinkscript.ld'] """) c = TestClient() c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists, "main.cpp": main, "mylinkscript.ld": "", "profile": profile}) c.run('build . -pr=profile', assert_error=True) # This error means the linker script was fonud and loaded, it failed because empty assert "PHDR segment not covered by LOAD segment" in c.out def test_cmake_toolchain_verbosity_propagation(): t = TestClient(light=True) t.save({"conanfile.py": GenConanfile("mylib", "1.0")}) t.run("create .") t.run("install --requires=mylib/1.0 -g CMakeToolchain") toolchain = t.load("conan_toolchain.cmake") assert 'set(CMAKE_VERBOSE_MAKEFILE' not in toolchain assert 'set(CMAKE_MESSAGE_LOG_LEVEL' not in toolchain t.run("install --requires=mylib/1.0 -g CMakeToolchain -c tools.build:verbosity=verbose " "-c tools.compilation:verbosity=verbose") toolchain = t.load("conan_toolchain.cmake") assert 'set(CMAKE_VERBOSE_MAKEFILE "ON"' in toolchain assert 'set(CMAKE_MESSAGE_LOG_LEVEL "VERBOSE"' in toolchain t.run("install --requires=mylib/1.0 -g CMakeToolchain -c tools.build:verbosity=quiet " "-c tools.compilation:verbosity=quiet") toolchain = t.load("conan_toolchain.cmake") assert 'set(CMAKE_MESSAGE_LOG_LEVEL "ERROR"' in toolchain # Extra variables have preference t.run("install --requires=mylib/1.0 -g CMakeToolchain -c tools.compilation:verbosity=quiet " "-c tools.cmake.cmaketoolchain:extra_variables=\"{'CMAKE_MESSAGE_LOG_LEVEL': 'WARNING'}\"") toolchain = t.load("conan_toolchain.cmake") # assert 'set(CMAKE_VERBOSE_MAKEFILE "OFF"' in toolchain assert 'set(CMAKE_MESSAGE_LOG_LEVEL "WARNING"' in toolchain ================================================ FILE: test/functional/toolchains/cmake/test_cmake_toolchain_m1.py ================================================ import textwrap import platform import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("op_system", ["Macos", "iOS"]) @pytest.mark.tool("cmake") def test_m1(op_system): os_version = "os.version=12.0" if op_system == "iOS" else "" os_sdk = "" if op_system == "Macos" else "os.sdk=iphoneos" profile = textwrap.dedent(""" include(default) [settings] os={} {} {} arch=armv8 """.format(op_system, os_sdk, os_version)) client = TestClient(path_with_spaces=False) client.save({"m1": profile}, clean_first=True) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("create . --profile:build=default --profile:host=m1 -tf=\"\"") main = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) custom_content = 'message("CMAKE_SYSTEM_NAME: ${CMAKE_SYSTEM_NAME}") \n' \ 'message("CMAKE_SYSTEM_PROCESSOR: ${CMAKE_SYSTEM_PROCESSOR}") \n' cmakelists = gen_cmakelists(find_package=["hello"], appname="main", appsources=["main.cpp"], custom_content=custom_content) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class TestConan(ConanFile): requires = "hello/0.1" settings = "os", "compiler", "arch", "build_type" exports_sources = "CMakeLists.txt", "main.cpp" generators = "CMakeDeps", "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmakelists, "main.cpp": main, "m1": profile}, clean_first=True) client.run("build . --profile:build=default --profile:host=m1") system_name = 'Darwin' if op_system == 'Macos' else 'iOS' assert "CMAKE_SYSTEM_NAME: {}".format(system_name) in client.out assert "CMAKE_SYSTEM_PROCESSOR: arm64" in client.out main_path = "./build/Release/main.app/main" if op_system == "iOS" \ else "./build/Release/main" client.run_command("lipo -info {}".format(main_path)) assert "Non-fat file" in client.out assert "is architecture: arm64" in client.out client.run_command(f"vtool -show-build {main_path}") if op_system == "Macos": assert "platform MACOS" elif op_system == "iOS": assert "platform IOS" assert "minos 12.0" ================================================ FILE: test/functional/toolchains/cmake/test_cmake_toolchain_presets.py ================================================ import json import os import platform import textwrap import pytest from conan.internal.util.files import load, rmdir from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.tools.cmake.presets import load_cmake_presets @pytest.mark.tool("cmake", "3.23") def test_cmake_presets_multiple_settings_single_config(): client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=' \ '\'["settings.compiler", "settings.compiler.version", ' \ ' "settings.compiler.cppstd"]\'' user_presets_path = os.path.join(client.current_folder, "CMakeUserPresets.json") # Check that all generated names are expected, both in the layout and in the Presets settings = "-s compiler=apple-clang -s compiler.libcxx=libc++ " \ "-s compiler.version=12.0 -s compiler.cppstd=gnu17" client.run("install . {} {}".format(settings, settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "apple-clang-12.0-gnu17", "Release", "generators")) assert os.path.exists(user_presets_path) user_presets = json.loads(load(user_presets_path)) assert len(user_presets["include"]) == 1 presets = json.loads(client.load(user_presets["include"][0])) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert len(presets["testPresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-release" assert presets["buildPresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-release" assert presets["buildPresets"][0]["configurePreset"] == "conan-apple-clang-12.0-gnu17-release" assert presets["testPresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-release" assert presets["testPresets"][0]["configurePreset"] == "conan-apple-clang-12.0-gnu17-release" # If we create the "Debug" one, it will be appended client.run("install . {} -s build_type=Debug {}".format(settings, settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "apple-clang-12.0-gnu17", "Release", "generators")) assert os.path.exists(user_presets_path) user_presets = json.loads(load(user_presets_path)) assert len(user_presets["include"]) == 2 presets = json.loads(client.load(user_presets["include"][0])) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert len(presets["testPresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-release" assert presets["buildPresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-release" assert presets["buildPresets"][0]["configurePreset"] == "conan-apple-clang-12.0-gnu17-release" assert presets["testPresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-release" assert presets["testPresets"][0]["configurePreset"] == "conan-apple-clang-12.0-gnu17-release" presets = json.loads(client.load(user_presets["include"][1])) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert len(presets["testPresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-debug" assert presets["buildPresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-debug" assert presets["buildPresets"][0]["configurePreset"] == "conan-apple-clang-12.0-gnu17-debug" assert presets["testPresets"][0]["name"] == "conan-apple-clang-12.0-gnu17-debug" assert presets["testPresets"][0]["configurePreset"] == "conan-apple-clang-12.0-gnu17-debug" # But If we change, for example, the cppstd and the compiler version, the toolchain # and presets will be different, but it will be appended to the UserPresets.json settings = "-s compiler=apple-clang -s compiler.libcxx=libc++ " \ "-s compiler.version=13 -s compiler.cppstd=gnu20" client.run("install . {} {}".format(settings, settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "apple-clang-13-gnu20", "Release", "generators")) assert os.path.exists(user_presets_path) user_presets = json.loads(load(user_presets_path)) # The [0] is the apple-clang 12 the [1] is the apple-clang 13 assert len(user_presets["include"]) == 3 presets = json.loads(client.load(user_presets["include"][2])) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert len(presets["testPresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-apple-clang-13-gnu20-release" assert presets["buildPresets"][0]["name"] == "conan-apple-clang-13-gnu20-release" assert presets["buildPresets"][0]["configurePreset"] == "conan-apple-clang-13-gnu20-release" assert presets["testPresets"][0]["name"] == "conan-apple-clang-13-gnu20-release" assert presets["testPresets"][0]["configurePreset"] == "conan-apple-clang-13-gnu20-release" # We can build with cmake manually if platform.system() == "Darwin": client.run_command("cmake . --preset conan-apple-clang-12.0-gnu17-release") client.run_command("cmake --build --preset conan-apple-clang-12.0-gnu17-release") client.run_command("ctest --preset conan-apple-clang-12.0-gnu17-release") client.run_command("./build/apple-clang-12.0-gnu17/Release/hello") assert "Hello World Release!" in client.out assert "__cplusplus2017" in client.out client.run_command("cmake . --preset conan-apple-clang-12.0-gnu17-debug") client.run_command("cmake --build --preset conan-apple-clang-12.0-gnu17-debug") client.run_command("ctest --preset conan-apple-clang-12.0-gnu17-debug") client.run_command("./build/apple-clang-12.0-gnu17/Debug/hello") assert "Hello World Debug!" in client.out assert "__cplusplus2017" in client.out client.run_command("cmake . --preset conan-apple-clang-13-gnu20-release") client.run_command("cmake --build --preset conan-apple-clang-13-gnu20-release") client.run_command("ctest --preset conan-apple-clang-13-gnu20-release") client.run_command("./build/apple-clang-13-gnu20/Release/hello") assert "Hello World Release!" in client.out assert "__cplusplus2020" in client.out @pytest.mark.parametrize("multiconfig", [True, False]) def test_cmake_presets_duplicated_install(multiconfig): # https://github.com/conan-io/conan/issues/11409 """Only failed when using a multiconfig generator""" client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") settings = '-s compiler=gcc -s compiler.version=5 -s compiler.libcxx=libstdc++11 ' \ '-c tools.cmake.cmake_layout:build_folder_vars=' \ '\'["settings.compiler", "settings.compiler.version"]\' ' if multiconfig: settings += '-c tools.cmake.cmaketoolchain:generator="Multi-Config"' client.run("install . {}".format(settings)) client.run("install . {}".format(settings)) if multiconfig: presets_path = os.path.join(client.current_folder, "build", "gcc-5", "generators", "CMakePresets.json") else: presets_path = os.path.join(client.current_folder, "build", "gcc-5", "Release", "generators", "CMakePresets.json") assert os.path.exists(presets_path) contents = json.loads(load(presets_path)) assert len(contents["buildPresets"]) == 1 assert len(contents["testPresets"]) == 1 def test_remove_missing_presets(): # https://github.com/conan-io/conan/issues/11413 client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") settings = '-s compiler=gcc -s compiler.version=5 -s compiler.libcxx=libstdc++11 ' \ '-c tools.cmake.cmake_layout:build_folder_vars=' \ '\'["settings.compiler", "settings.compiler.version"]\' ' client.run("install . {}".format(settings)) client.run("install . {} -s compiler.version=6".format(settings)) presets_path_5 = os.path.join(client.current_folder, "build", "gcc-5") assert os.path.exists(presets_path_5) presets_path_6 = os.path.join(client.current_folder, "build", "gcc-6") assert os.path.exists(presets_path_6) rmdir(presets_path_5) # If we generate another configuration, the missing one (removed) for gcc-5 is not included client.run("install . {} -s compiler.version=11".format(settings)) user_presets_path = os.path.join(client.current_folder, "CMakeUserPresets.json") assert os.path.exists(user_presets_path) contents = json.loads(load(user_presets_path)) assert len(contents["include"]) == 2 assert "gcc-6" in contents["include"][0] assert "gcc-11" in contents["include"][1] @pytest.mark.tool("cmake", "3.23") def test_cmake_presets_options_single_config(): client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=0.1") conf_layout = '-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.compiler",' \ '"settings.build_type", "options.shared"]\'' default_compiler = {"Darwin": "apple-clang", "Windows": "msvc", "Linux": "gcc"}.get(platform.system()) for shared in (True, False): client.run("install . {} -o shared={}".format(conf_layout, shared)) shared_str = "shared" if shared else "static" assert os.path.exists(os.path.join(client.current_folder, "build", f"{default_compiler}-release-{shared_str}", "generators")) client.run("install . {}".format(conf_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "{}-release-static".format(default_compiler), "generators")) user_presets_path = os.path.join(client.current_folder, "CMakeUserPresets.json") assert os.path.exists(user_presets_path) # We can build with cmake manually if platform.system() == "Darwin": for shared in (True, False): shared_str = "shared" if shared else "static" client.run_command(f"cmake . --preset conan-apple-clang-release-{shared_str}") client.run_command(f"cmake --build --preset conan-apple-clang-release-{shared_str}") client.run_command(f"ctest --preset conan-apple-clang-release-{shared_str}") the_lib = "libhello.a" if not shared else "libhello.dylib" path = os.path.join(client.current_folder, "build", "apple-clang-release-{}".format(shared_str), the_lib) assert os.path.exists(path) @pytest.mark.tool("cmake", "3.23") def test_cmake_presets_with_conanfile_txt(): c = TestClient() c.run("new cmake_exe -d name=foo -d version=1.0") os.unlink(os.path.join(c.current_folder, "conanfile.py")) c.save({"conanfile.txt": textwrap.dedent(""" [generators] CMakeToolchain [layout] cmake_layout """)}) c.run("install .") c.run("install . -s build_type=Debug") if platform.system() != "Windows": c.run_command("cmake --preset conan-debug") c.run_command("cmake --build --preset conan-debug") c.run_command("ctest --preset conan-debug") c.run_command("./build/Debug/foo") else: c.run_command("cmake --preset conan-default") # this second called used to fail # https://github.com/conan-io/conan/issues/13792, CMake ignoring toolchain architecture # and toolset c.run_command("cmake --preset conan-default") c.run_command("cmake --build --preset conan-debug") c.run_command("ctest --preset conan-debug") c.run_command("build\\Debug\\foo") assert "Hello World Debug!" in c.out if platform.system() != "Windows": c.run_command("cmake --preset conan-release") c.run_command("cmake --build --preset conan-release") c.run_command("ctest --preset conan-release") c.run_command("./build/Release/foo") else: c.run_command("cmake --build --preset conan-release") c.run_command("ctest --preset conan-release") c.run_command("build\\Release\\foo") assert "Hello World Release!" in c.out @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows") @pytest.mark.tool("ninja") @pytest.mark.tool("cmake", "3.23") def test_cmake_presets_with_conanfile_txt_ninja(): c = TestClient() c.run("new cmake_exe -d name=foo -d version=1.0") os.unlink(os.path.join(c.current_folder, "conanfile.py")) c.save({"conanfile.txt": textwrap.dedent(""" [generators] CMakeToolchain [layout] cmake_layout """)}) conf = "-c tools.cmake.cmaketoolchain:generator=Ninja" c.run(f"install . {conf}") c.run(f"install . -s build_type=Debug {conf}") c.run_command("build\\Release\\generators\\conanbuild.bat && cmake --preset conan-release") c.run_command("build\\Release\\generators\\conanbuild.bat && cmake --preset conan-release") c.run_command("build\\Release\\generators\\conanbuild.bat " "&& cmake --build --preset conan-release") c.run_command("build\\Release\\generators\\conanbuild.bat && ctest --preset conan-release") c.run_command("build\\Release\\foo") assert "Hello World Release!" in c.out def test_cmake_toolchain_custom_toolchain(): client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "compiler", "build_type", "arch").\ with_generator("CMakeToolchain") client.save_home({"global.conf": "tools.cmake.cmaketoolchain:toolchain_file=mytoolchain.cmake"}) client.save({"conanfile.py": conanfile}) client.run("install .") assert not os.path.exists(os.path.join(client.current_folder, "conan_toolchain.cmake")) presets = load_cmake_presets(client.current_folder) assert "mytoolchain.cmake" in presets["configurePresets"][0]["toolchainFile"] assert "binaryDir" in presets["configurePresets"][0] @pytest.mark.skipif(platform.system() != "Darwin", reason="Single config test, Linux CI still without 3.23") @pytest.mark.tool("cmake", "3.23") @pytest.mark.parametrize("existing_user_presets", [None, "user_provided", "conan_generated"]) def test_cmake_user_presets_load(existing_user_presets): """ Test if the CMakeUserPresets.cmake is generated and use CMake to use it to verify the right syntax of generated CMakeUserPresets.cmake and CMakePresets.cmake. If the user already provided a CMakeUserPresets.cmake, leave the file untouched, and only generate or modify the file if the `conan` object exists in the `vendor` field. """ t = TestClient() t.run("new -d name=mylib -d version=1.0 -f cmake_lib") t.run("create . -s:h build_type=Release") t.run("create . -s:h build_type=Debug") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Consumer(ConanFile): settings = "build_type", "os", "arch", "compiler" requires = "mylib/1.0" generators = "CMakeToolchain", "CMakeDeps" def layout(self): cmake_layout(self) """) cmakelist = textwrap.dedent(""" cmake_minimum_required(VERSION 3.1) project(PackageTest NONE) find_package(mylib REQUIRED CONFIG) """) user_presets = None if existing_user_presets == "user_provided": user_presets = "{}" elif existing_user_presets == "conan_generated": user_presets = '{ "vendor": {"conan": {} } }' files_to_save = {"conanfile.py": consumer, "CMakeLists.txt": cmakelist} if user_presets: files_to_save['CMakeUserPresets.json'] = user_presets t.save(files_to_save, clean_first=True) t.run("install . -s:h build_type=Debug -g CMakeToolchain") t.run("install . -s:h build_type=Release -g CMakeToolchain") user_presets_path = os.path.join(t.current_folder, "CMakeUserPresets.json") assert os.path.exists(user_presets_path) user_presets_data = json.loads(load(user_presets_path)) if existing_user_presets == "user_provided": assert not user_presets_data else: assert "include" in user_presets_data.keys() if existing_user_presets is None: t.run_command("cmake . --preset conan-release") assert 'CMAKE_BUILD_TYPE="Release"' in t.out t.run_command("cmake . --preset conan-debug") assert 'CMAKE_BUILD_TYPE="Debug"' in t.out @pytest.mark.tool("cmake", "3.23") @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows") def test_cmake_presets_multiple_settings_multi_config(): client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=' \ '\'["settings.compiler.runtime", "settings.compiler.cppstd"]\'' user_presets_path = os.path.join(client.current_folder, "CMakeUserPresets.json") # Check that all generated names are expected, both in the layout and in the Presets settings = "-s compiler=msvc -s compiler.version=191 -s compiler.runtime=dynamic " \ "-s compiler.cppstd=14" client.run("install . {} {}".format(settings, settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "dynamic-14", "generators")) assert os.path.exists(user_presets_path) user_presets = json.loads(load(user_presets_path)) assert len(user_presets["include"]) == 1 presets = json.loads(client.load(user_presets["include"][0])) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert len(presets["testPresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-dynamic-14" assert presets["buildPresets"][0]["name"] == "conan-dynamic-14-release" assert presets["buildPresets"][0]["configurePreset"] == "conan-dynamic-14" assert presets["testPresets"][0]["name"] == "conan-dynamic-14-release" assert presets["testPresets"][0]["configurePreset"] == "conan-dynamic-14" # If we create the "Debug" one, it has the same toolchain and preset file, that is # always multiconfig client.run("install . {} -s build_type=Debug {}".format(settings, settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "dynamic-14", "generators")) assert os.path.exists(user_presets_path) user_presets = json.loads(load(user_presets_path)) assert len(user_presets["include"]) == 1 presets = json.loads(client.load(user_presets["include"][0])) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 2 assert len(presets["testPresets"]) == 2 assert presets["configurePresets"][0]["name"] == "conan-dynamic-14" assert presets["buildPresets"][0]["name"] == "conan-dynamic-14-release" assert presets["buildPresets"][1]["name"] == "conan-dynamic-14-debug" assert presets["buildPresets"][0]["configurePreset"] == "conan-dynamic-14" assert presets["buildPresets"][1]["configurePreset"] == "conan-dynamic-14" assert presets["testPresets"][0]["name"] == "conan-dynamic-14-release" assert presets["testPresets"][1]["name"] == "conan-dynamic-14-debug" assert presets["testPresets"][0]["configurePreset"] == "conan-dynamic-14" assert presets["testPresets"][1]["configurePreset"] == "conan-dynamic-14" # But If we change, for example, the cppstd and the compiler version, the toolchain # and presets will be different, but it will be appended to the UserPresets.json settings = "-s compiler=msvc -s compiler.version=191 -s compiler.runtime=static " \ "-s compiler.cppstd=17" client.run("install . {} {}".format(settings, settings_layout)) assert os.path.exists(os.path.join(client.current_folder, "build", "static-17", "generators")) assert os.path.exists(user_presets_path) user_presets = json.loads(load(user_presets_path)) # The [0] is the msvc dynamic/14 the [1] is the static/17 assert len(user_presets["include"]) == 2 presets = json.loads(client.load(user_presets["include"][1])) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert len(presets["testPresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-static-17" assert presets["buildPresets"][0]["name"] == "conan-static-17-release" assert presets["buildPresets"][0]["configurePreset"] == "conan-static-17" assert presets["testPresets"][0]["name"] == "conan-static-17-release" assert presets["testPresets"][0]["configurePreset"] == "conan-static-17" # We can build with cmake manually client.run_command("cmake . --preset conan-dynamic-14") client.run_command("cmake --build --preset conan-dynamic-14-release") client.run_command("ctest --preset conan-dynamic-14-release") client.run_command("build\\dynamic-14\\Release\\hello") assert "Hello World Release!" in client.out assert "MSVC_LANG2014" in client.out client.run_command("cmake --build --preset conan-dynamic-14-debug") client.run_command("ctest --preset conan-dynamic-14-debug") client.run_command("build\\dynamic-14\\Debug\\hello") assert "Hello World Debug!" in client.out assert "MSVC_LANG2014" in client.out client.run_command("cmake . --preset conan-static-17") client.run_command("cmake --build --preset conan-static-17-release") client.run_command("ctest --preset conan-static-17-release") client.run_command("build\\static-17\\Release\\hello") assert "Hello World Release!" in client.out assert "MSVC_LANG2017" in client.out @pytest.mark.tool("cmake", "3.23") @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows") # Test both with a local folder and an absolute folder @pytest.mark.parametrize("build", ["mybuild", "temp"]) def test_cmake_presets_build_folder(build): client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=hello -d version=0.1") build = temp_folder() if build == "temp" else build settings_layout = f' -c tools.cmake.cmake_layout:build_folder="{build}" '\ '-c tools.cmake.cmake_layout:build_folder_vars=' \ '\'["settings.compiler.runtime", "settings.compiler.cppstd"]\'' # But If we change, for example, the cppstd and the compiler version, the toolchain # and presets will be different, but it will be appended to the UserPresets.json settings = "-s compiler=msvc -s compiler.version=191 -s compiler.runtime=static " \ "-s compiler.cppstd=17" client.run("install . {} {}".format(settings, settings_layout)) assert os.path.exists(os.path.join(client.current_folder, build, "static-17", "generators")) client.run_command("cmake . --preset conan-static-17") client.run_command("cmake --build --preset conan-static-17-release") client.run_command("ctest --preset conan-static-17-release") client.run_command(f"{build}\\static-17\\Release\\hello") assert "Hello World Release!" in client.out assert "MSVC_LANG2017" in client.out @pytest.mark.tool("cmake", "3.23") class TestEnvironmentInPresets: @pytest.fixture(scope="class") def _init_client(self): c = TestClient() tool = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import chdir, save class Tool(ConanFile): version = "0.1" settings = "os", "compiler", "arch", "build_type" def package(self): with chdir(self, self.package_folder): v = f"{{self.name}}/{{self.version}}" save(self, f"bin/{{self.name}}.bat", f"@echo off\necho running: {{v}}") save(self, f"bin/{{self.name}}.sh", f"echo running: {{v}}") os.chmod(f"bin/{{self.name}}.sh", 0o777) def package_info(self): self.buildenv_info.define("MY_BUILD_VAR", "MY_BUILDVAR_VALUE") {} """) consumer = textwrap.dedent(""" [tool_requires] mytool/0.1 [test_requires] mytesttool/0.1 [layout] cmake_layout """) test_env = textwrap.dedent(""" #include int main() { return std::getenv("MY_RUNVAR") ? 0 : 1; } """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyProject CXX) if(WIN32) set(MYTOOL_SCRIPT "mytool.bat") else() set(MYTOOL_SCRIPT "mytool.sh") endif() add_custom_target(run_mytool COMMAND ${MYTOOL_SCRIPT}) function(check_and_report_variable var_name) set(var_value $ENV{${var_name}}) if (var_value) message("${var_name}:${var_value}") else() message("${var_name} NOT FOUND") endif() endfunction() check_and_report_variable("MY_BUILD_VAR") check_and_report_variable("MY_RUNVAR") check_and_report_variable("MY_ENV_VAR") enable_testing() add_executable(test_env test_env.cpp) add_test(NAME TestRunEnv COMMAND test_env) """) c.save({"tool.py": tool.format(""), "test_tool.py": tool.format('self.runenv_info.define("MY_RUNVAR", "MY_RUNVAR_VALUE")'), "conanfile.txt": consumer, "CMakeLists.txt": cmakelists, "test_env.cpp": test_env}) c.run("create tool.py --name=mytool") c.run("create test_tool.py --name=mytesttool") c.run("create test_tool.py --name=mytesttool -s build_type=Debug") yield c def test_add_env_to_presets(self, _init_client): c = _init_client # do a first conan install with env disabled just to test that the conf works c.run("install . -g CMakeToolchain -g CMakeDeps " "-c tools.cmake.cmaketoolchain:presets_environment=disabled") presets_path = os.path.join("build", "Release", "generators", "CMakePresets.json") \ if platform.system() != "Windows" else os.path.join("build", "generators", "CMakePresets.json") presets = json.loads(c.load(presets_path)) assert presets["configurePresets"][0].get("env") is None c.run("install . -g CMakeToolchain -g CMakeDeps") c.run("install . -g CMakeToolchain -g CMakeDeps -s:h build_type=Debug") # test that the buildenv is correctly injected to configure and build steps # that the runenv is not injected to configure, but it is when running tests preset = "conan-default" if platform.system() == "Windows" else "conan-release" c.run_command(f"cmake --preset {preset}") assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE" in c.out assert "MY_RUNVAR NOT FOUND" in c.out c.run_command("cmake --build --preset conan-release --target run_mytool --target test_env") assert "running: mytool/0.1" in c.out c.run_command("ctest --preset conan-release") assert "tests passed" in c.out if platform.system() != "Windows": c.run_command("cmake --preset conan-debug") assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE" in c.out assert "MY_RUNVAR NOT FOUND" in c.out c.run_command("cmake --build --preset conan-debug --target run_mytool --target test_env") assert "running: mytool/0.1" in c.out c.run_command("ctest --preset conan-debug") assert "tests passed" in c.out def test_add_generate_env_to_presets(self, _init_client): c = _init_client consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout, CMakeToolchain, CMakeDeps from conan.tools.env import VirtualBuildEnv, VirtualRunEnv, Environment class mypkgRecipe(ConanFile): name = "mypkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" tool_requires = "mytool/0.1" test_requires = "mytesttool/0.1" def layout(self): cmake_layout(self) def generate(self): buildenv = VirtualBuildEnv(self) buildenv.environment().define("MY_BUILD_VAR", "MY_BUILDVAR_VALUE_OVERRIDEN") buildenv.generate() runenv = VirtualRunEnv(self) runenv.environment().define("MY_RUN_VAR", "MY_RUNVAR_SET_IN_GENERATE") runenv.generate() env = Environment() env.define("MY_ENV_VAR", "MY_ENV_VAR_VALUE") env = env.vars(self) env.save_script("other_env") deps = CMakeDeps(self) deps.generate() tc = CMakeToolchain(self) tc.presets_build_environment = buildenv.environment().compose_env(env) tc.presets_run_environment = runenv.environment() tc.generate() """) test_env = textwrap.dedent(""" #include int main() { return std::string(std::getenv("MY_RUNVAR"))=="MY_RUNVAR_SET_IN_GENERATE"; } """) c.save({"conanfile.py": consumer, "test_env.cpp": test_env}) c.run("install conanfile.py") preset = "conan-default" if platform.system() == "Windows" else "conan-release" c.run_command(f"cmake --preset {preset}") assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE_OVERRIDEN" in c.out assert "MY_ENV_VAR:MY_ENV_VAR_VALUE" in c.out c.run_command("cmake --build --preset conan-release --target run_mytool --target test_env") assert "running: mytool/0.1" in c.out c.run_command(f"ctest --preset conan-release") assert "tests passed" in c.out ================================================ FILE: test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py ================================================ import platform import re import tempfile import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp, gen_function_c from test.conftest import tools_locations from test.functional.utils import check_vs_runtime, check_exe_run from conan.test.utils.tools import TestClient @pytest.fixture def client(): # IMPORTANT: This cannot use the default tests location, if in Windows, it can be another unit # like F and Visual WONT FIND ClangCL t = tempfile.mkdtemp(suffix='conans') c = TestClient(cache_folder=t) clang_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 build_type=Release compiler=clang compiler.version=18 compiler.cppstd=14 [conf] tools.build:compiler_executables={"cpp": "clang++", "c": "clang", "rc": "clang"} """) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "*" generators = "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() cmd = os.path.join(self.cpp.build.bindirs[0], "my_app") self.run(cmd) def package(self): cmake = CMake(self) cmake.install() def package_info(self): cmd = os.path.join(self.package_folder, "bin", "my_app") self.output.info("MYCMD={}!".format(os.path.abspath(cmd))) """) clangpath = tools_locations["clang"]["20"]["path"]["Windows"] llvm_clang_path = textwrap.dedent(f""" [buildenv] PATH=+(path){clangpath} """) c.save({"conanfile.py": conanfile, "clang": clang_profile, "clang_path": llvm_clang_path, "CMakeLists.txt": gen_cmakelists(appname="my_app", appsources=["src/main.cpp"], install=True), "src/main.cpp": gen_function_cpp(name="main")}) return c @pytest.mark.tool("cmake") @pytest.mark.tool("clang", "20") @pytest.mark.skipif(platform.system() != "Windows", reason="requires Win") class TestLLVMClang: """ External LLVM/clang, with different CMake generators This links always with the VS runtime, it is built-in """ @pytest.mark.tool("mingw64") @pytest.mark.tool("visual_studio", "17") @pytest.mark.parametrize("runtime", ["static", "dynamic"]) def test_clang_mingw(self, client, runtime): """ compiling with an LLVM-clang installed, which uses by default the VS runtime """ client.run("create . --name=pkg --version=0.1 -pr=clang -pr=clang_path " "-s compiler.runtime_version=v144 " "-s compiler.runtime={}".format(runtime)) # clang compilations in Windows will use MinGW Makefiles by default assert 'cmake -G "MinGW Makefiles"' in client.out assert "GNU-like command-line" in client.out assert "main __clang_major__20" in client.out assert "main _MSC_VER194" in client.out assert "main _MSVC_LANG2014" in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out check_exe_run(client.out, "main", "clang", None, "Release", "x86_64", "14") cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" static_runtime = (runtime == "static") check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=static_runtime) @pytest.mark.tool("ninja") @pytest.mark.tool("visual_studio", "17") @pytest.mark.parametrize("generator", ["Ninja", "NMake Makefiles"]) def test_clang_cmake_ninja_nmake(self, client, generator): client.run("create . --name=pkg --version=0.1 -pr=clang -pr=clang_path " "-s compiler.runtime=dynamic -s compiler.runtime_version=v144 " '-c tools.cmake.cmaketoolchain:generator="{}"'.format(generator)) assert 'cmake -G "{}"'.format(generator) in client.out assert "GNU-like command-line" in client.out assert "main __clang_major__20" in client.out assert "main _MSC_VER194" in client.out assert "main _MSVC_LANG2014" in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=False) @pytest.mark.tool("ninja") @pytest.mark.tool("visual_studio", "17") def test_clang_cmake_ninja_clang_cl(self, client): pr = textwrap.dedent("""\ [conf] tools.build:compiler_executables={"cpp": "clang-cl", "c": "clang-cl", "rc": "clang"} """) client.save({"comp_exes": pr}) client.run("create . --name=pkg --version=0.1 -pr=clang -pr=clang_path -pr=comp_exes " "-s compiler.runtime=dynamic -s compiler.runtime_version=v144 " '-c tools.cmake.cmaketoolchain:generator="Ninja"') assert 'cmake -G "Ninja"' in client.out assert "MSVC-like command-line" in client.out assert "main __clang_major__20" in client.out assert "main _MSC_VER194" in client.out assert "main _MSVC_LANG2014" in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=False) @pytest.mark.tool("ninja") @pytest.mark.tool("visual_studio", "17") def test_clang_cmake_runtime_version(self, client): generator = "Ninja" # Make sure that normal CMakeLists with verify=False works client.save({"CMakeLists.txt": gen_cmakelists(verify=False, appname="my_app", appsources=["src/main.cpp"], install=True)}) client.run("create . --name=pkg --version=0.1 -pr=clang -pr=clang_path " "-s compiler.runtime=dynamic " "-s compiler.cppstd=17 -s compiler.runtime_version=v144 " '-c tools.cmake.cmaketoolchain:generator="{}"'.format(generator)) assert 'cmake -G "{}"'.format(generator) in client.out assert "GNU-like command-line" in client.out assert "main __clang_major__20" in client.out # Check this! Clang compiler in Windows is reporting MSC_VER and MSVC_LANG! assert "main _MSC_VER194" in client.out assert "main _MSVC_LANG2017" in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=False) @pytest.mark.skipif(platform.system() != "Windows", reason="requires Win") class TestVSClangCL: """ This is also LLVM/Clang, but distributed with the VS installation """ @pytest.mark.tool("cmake", "3.27") @pytest.mark.tool("visual_studio", "17") def test_clang_visual_studio_generator(self, client): """ This is using the embedded ClangCL compiler, not the external one""" generator = "Visual Studio 17" client.run("create . --name=pkg --version=0.1 -pr=clang -s compiler.runtime=dynamic " "-s compiler.cppstd=17 -s compiler.runtime_version=v144 " '-c tools.cmake.cmaketoolchain:generator="{}"'.format(generator)) assert 'cmake -G "{}"'.format(generator) in client.out assert "MSVC-like command-line" in client.out assert "main __clang_major__19" in client.out # The one inside VS is clang 19 # Check this! Clang compiler in Windows is reporting MSC_VER and MSVC_LANG! # CI forced the installation of 19.38, seems to prevail there assert "main _MSC_VER19" in client.out assert "main _MSVC_LANG2017" in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out assert "-m64" not in client.out cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=False) @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Windows", reason="requires Win") class TestMsysClang: @pytest.mark.tool("msys2_clang64") def test_msys2_clang(self, client): """ Using the msys2 clang64 subsystem We are not really injecting the msys2 root with make, so using MinGW Makefiles """ client.run('create . --name=pkg --version=0.1 -pr=clang -s os.subsystem=msys2 ' '-s compiler.libcxx=libc++ ' '-c tools.cmake.cmaketoolchain:generator="MinGW Makefiles"') # clang compilations in Windows will use MinGW Makefiles by default assert 'cmake -G "MinGW Makefiles"' in client.out # TODO: Version is still not controlled assert "main __clang_major__21" in client.out # Not using libstdc++ assert "_GLIBCXX_USE_CXX11_ABI" not in client.out assert "main __cplusplus2014" in client.out assert "main __GNUC__" in client.out assert "main __MINGW32__1" in client.out assert "main __MINGW64__1" in client.out assert "main _MSC_" not in client.out assert "main _MSVC_" not in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=False, subsystem="clang64") @pytest.mark.tool("msys2_mingw64_clang64") def test_msys2_clang_mingw(self, client): """ compiling with the clang INSIDE mingw, which uses the MinGW runtime, not the MSVC one For 32 bits, it doesn't seem possible to install the toolchain For 64 bits require "pacman -S mingw-w64-x86-clang++" """ # TODO: This should probably go to the ``os.subsystem=ming64" but lets do it in other PR client.run('create . --name=pkg --version=0.1 -pr=clang ' '-s compiler.libcxx=libstdc++') # clang compilations in Windows will use MinGW Makefiles by default assert 'cmake -G "MinGW Makefiles"' in client.out # TODO: Version is still not controlled assert "main __clang_major__21" in client.out assert "main _GLIBCXX_USE_CXX11_ABI 0" in client.out assert "main __cplusplus2014" in client.out assert "main __GNUC__" in client.out assert "main __MINGW32__1" in client.out assert "main __MINGW64__1" in client.out assert "main _MSC_" not in client.out assert "main _MSVC_" not in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=False, subsystem="mingw64") @pytest.mark.tool("msys2_clang64") def test_clang_pure_c(self, client): """ compiling with the clang INSIDE mingw, which uses the MinGW runtime, not the MSVC one For 32 bits, it doesn't seem possible to install the toolchain For 64 bits require "pacman -S mingw-w64-x86-clang++" """ client.save({"CMakeLists.txt": gen_cmakelists(verify=False, language="C", appname="my_app", appsources=["src/main.c"], install=True), "src/main.c": gen_function_c(name="main")}) client.run(f"create . --name=pkg --version=0.1 -pr=clang") # clang compilations in Windows will use MinGW Makefiles by default assert 'cmake -G "MinGW Makefiles"' in client.out assert "main __clang_major__21" in client.out assert "GLIBCXX" not in client.out assert "cplusplus" not in client.out assert "main __GNUC__" in client.out assert "main __MINGW32__1" in client.out assert "main __MINGW64__1" in client.out assert "main _MSC_" not in client.out assert "main _MSVC_" not in client.out assert "main _M_X64 defined" in client.out assert "main __x86_64__ defined" in client.out cmd = re.search(r"MYCMD=(.*)!", str(client.out)).group(1) cmd = cmd + ".exe" # static_runtime equivalent to C, for checking, no dep on libc++ check_vs_runtime(cmd, client, "17", build_type="Release", static_runtime=True, subsystem="clang64") @pytest.mark.tool("cmake") @pytest.mark.tool("ninja") @pytest.mark.tool("clang", "20") @pytest.mark.skipif(platform.system() != "Windows", reason="requires Win") def test_error_clang_cmake_ninja_custom_cxx(client): clang_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 build_type=Release compiler=clang compiler.version=20 [buildenv] CXX=/no/exist/clang++ """) client.save({"clang": clang_profile}) client.run("create . --name=pkg --version=0.1 -pr=clang " "-c tools.cmake.cmaketoolchain:generator=Ninja", assert_error=True) assert 'Could not find compiler' in client.out assert '/no/exist/clang++' in client.out ================================================ FILE: test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py ================================================ import textwrap import platform import os import pytest from conan.test.utils.tools import TestClient def _add_message_status_flags(client): cmakelists_path = os.path.join(client.current_folder, "CMakeLists.txt") with open(cmakelists_path, "a") as cmakelists_file: cmakelists_file.write('message(STATUS "CONAN_C_FLAGS: ${CONAN_C_FLAGS}")\n') cmakelists_file.write('message(STATUS "CONAN_CXX_FLAGS: ${CONAN_CXX_FLAGS}")\n') cmakelists_file.write('message(STATUS "CONAN_OBJC_FLAGS: ${CONAN_OBJC_FLAGS}")\n') cmakelists_file.write('message(STATUS "CONAN_OBJCXX_FLAGS: ${CONAN_OBJCXX_FLAGS}")\n') @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("op_system,os_version,sdk,arch", [ ("watchOS", "8.1", "watchos", "armv7k"), ("tvOS", "13.2", "appletvos", "armv8") ]) def test_cmake_apple_bitcode_arc_and_visibility_flags_enabled(op_system, os_version, sdk, arch): profile = textwrap.dedent(""" include(default) [settings] os={} os.version={} os.sdk={} arch={} [conf] tools.apple:enable_arc=True tools.apple:enable_visibility=True """.format(op_system, os_version, sdk, arch)) client = TestClient(path_with_spaces=False) client.save({"host": profile}, clean_first=True) client.run("new -d name=hello -d version=0.1 cmake_lib") _add_message_status_flags(client) client.run("install . --profile:build=default --profile:host=host") toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake")) # arc assert 'set(FOBJC_ARC "-fobjc-arc")' in toolchain assert 'set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "YES")' in toolchain # visibility assert 'set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "NO")' in toolchain assert 'set(VISIBILITY "-fvisibility=default")' in toolchain client.run("create . --profile:build=default --profile:host=host -tf=") # flags assert "-- CONAN_C_FLAGS: -fvisibility=default" in client.out assert "-- CONAN_CXX_FLAGS: -fvisibility=default" in client.out assert "-- CONAN_OBJC_FLAGS: -fvisibility=default -fobjc-arc" in client.out assert "-- CONAN_OBJCXX_FLAGS: -fvisibility=default -fobjc-arc" in client.out assert "[100%] Built target hello" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.tool("cmake", "3.19") @pytest.mark.parametrize("op_system,os_version,sdk,arch", [ ("watchOS", "8.1", "watchos", "armv7k"), ("tvOS", "13.2", "appletvos", "armv8") ]) def test_cmake_apple_bitcode_arc_and_visibility_flags_enabled_and_xcode_generator(op_system, os_version, sdk, arch): """ Testing when all the Bitcode, ARC and Visibility are enabled, and Xcode as generator. Note: When using CMake and Xcode as generator, the C/CXX flags do not need to be appended. """ profile = textwrap.dedent(""" include(default) [settings] os={} os.version={} os.sdk={} arch={} [conf] tools.apple:enable_bitcode=True tools.apple:enable_arc=True tools.apple:enable_visibility=True """.format(op_system, os_version, sdk, arch)) client = TestClient(path_with_spaces=False) client.save({"host": profile}, clean_first=True) client.run("new -d name=hello -d version=0.1 cmake_lib") _add_message_status_flags(client) client.run("create . --profile:build=default --profile:host=host -tf=\"\" " "-c tools.cmake.cmaketoolchain:generator=Xcode") assert "** BUILD SUCCEEDED **" in client.out # flags are not appended when Xcode generator is used for line in str(client.out).splitlines(): if "CONAN_C_FLAGS:" in line: assert "-- CONAN_C_FLAGS:" == line.strip() if "CONAN_CXX_FLAGS:" in line: assert "-- CONAN_CXX_FLAGS: -stdlib=libc++" == line.strip() break @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("op_system,os_version,sdk,arch", [ ("watchOS", "8.1", "watchos", "armv7k"), ("tvOS", "13.2", "appletvos", "armv8") ]) def test_cmake_apple_bitcode_arc_and_visibility_flags_disabled(op_system, os_version, sdk, arch): profile = textwrap.dedent(""" include(default) [settings] os={} os.version={} os.sdk={} arch={} [conf] tools.apple:enable_bitcode=False tools.apple:enable_arc=False tools.apple:enable_visibility=False """.format(op_system, os_version, sdk, arch)) client = TestClient(path_with_spaces=False) client.save({"host": profile}, clean_first=True) client.run("new -d name=hello -d version=0.1 cmake_lib") _add_message_status_flags(client) client.run("install . --profile:build=default --profile:host=host") toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake")) # bitcode assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO")' in toolchain assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' not in toolchain assert 'set(BITCODE "-fembed-bitcode")' not in toolchain # arc assert 'set(FOBJC_ARC "-fno-objc-arc")' in toolchain assert 'set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "NO")' in toolchain # visibility assert 'set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "YES")' in toolchain assert 'set(VISIBILITY "-fvisibility=hidden -fvisibility-inlines-hidden")' in toolchain client.run("create . --profile:build=default --profile:host=host -tf=\"\"") # flags assert "-- CONAN_C_FLAGS: -fvisibility=hidden -fvisibility-inlines-hidden" in client.out assert "-- CONAN_CXX_FLAGS: -fvisibility=hidden -fvisibility-inlines-hidden" in client.out assert "-- CONAN_OBJC_FLAGS: -fvisibility=hidden -fvisibility-inlines-hidden -fno-objc-arc" in client.out assert "-- CONAN_OBJCXX_FLAGS: -fvisibility=hidden -fvisibility-inlines-hidden -fno-objc-arc" in client.out assert "[100%] Built target hello" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("op_system,os_version,sdk,arch", [ ("watchOS", "8.1", "watchos", "armv7k"), ("tvOS", "13.2", "appletvos", "armv8") ]) def test_cmake_apple_bitcode_arc_and_visibility_flags_are_none(op_system, os_version, sdk, arch): """ Testing what happens when any of the Bitcode, ARC or Visibility configurations are not defined. """ profile = textwrap.dedent(""" include(default) [settings] os={} os.version={} os.sdk={} arch={} """.format(op_system, os_version, sdk, arch)) client = TestClient(path_with_spaces=False) client.save({"host": profile}, clean_first=True) client.run("new -d name=hello -d version=0.1 cmake_lib") _add_message_status_flags(client) client.run("install . --profile:build=default --profile:host=host") toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake")) # bitcode assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO")' not in toolchain assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' not in toolchain assert 'set(BITCODE "-fembed-bitcode")' not in toolchain # arc assert 'set(FOBJC_ARC "-' not in toolchain assert 'set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC' not in toolchain # visibility assert 'set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN' not in toolchain assert 'set(VISIBILITY "-' not in toolchain client.run("create . --profile:build=default --profile:host=host -tf=\"\"") # flags are not appended for flag in ["-fembed-bitcode", "-fno-objc-arc", "-fobjc-arc", "-fvisibility"]: assert flag not in client.out assert "[100%] Built target hello" in client.out ================================================ FILE: test/functional/toolchains/cmake/test_cmake_transitive_rpath.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Linux", reason="Linux/gcc required for -rpath/-rpath-link testing") @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("use_cmake_config_deps", [True, False]) def test_cmake_sysroot_transitive_rpath(use_cmake_config_deps): c = TestClient() extra_profile = textwrap.dedent(""" [conf] tools.build:sysroot=/path/to/nowhere """) # Avoid using any C or C++ standard functionality, so that we can "redirect" the sysroot # to an empty or non-existing directory foo_h = textwrap.dedent(""" #pragma once int foo(int x, int y); """) foo_cpp = textwrap.dedent(""" #include "foo.h" int foo(int x, int y) { return x + y; } """) foo_test = textwrap.dedent(""" #include "foo.h" int main() { return foo(2, 3) == 5 ? 0 : 1; } """) bar_h = textwrap.dedent(""" #pragma once int bar(int x, int y); """) bar_cpp = textwrap.dedent(""" #include "bar.h" #include "foo.h" int bar(int x, int y) { return foo(x, y) * 2; } """) bar_test = textwrap.dedent(""" #include "bar.h" int main() { return bar(2, 3) == 10 ? 0 : 1; } """) c.save({"extra_profile": extra_profile}) extra_conf = "-c tools.cmake.cmakedeps:new=will_break_next" if use_cmake_config_deps else "" if not use_cmake_config_deps: # CMakeConfigDeps does not fail, so nothing extra is needed # this is only needed to cover the case of CMakeDeps extra_conf += " -c tools.build:add_rpath_link=True" with c.chdir("foo"): c.run("new cmake_lib -d name=foo -d version=0.1") c.save({"include/foo.h": foo_h, "src/foo.cpp": foo_cpp, "test_package/src/example.cpp": foo_test}) c.run(f"create . -o '*:shared=True' -pr=default -pr=../extra_profile {extra_conf}") with c.chdir("bar"): c.run("new cmake_lib -d name=bar -d version=0.1 -d requires=foo/0.1") c.save({"include/bar.h": bar_h, "src/bar.cpp": bar_cpp, "test_package/src/example.cpp": bar_test}) # skip test package, which fails with CMakeToolchain+CMakeDeps c.run(f"create . -o '*:shared=True' -tf= -pr=default -pr=../extra_profile {extra_conf}") with c.chdir("app"): c.run("new cmake_exe -d name=app -d version=0.1 -d requires=bar/0.1") c.save({"src/main.cpp": bar_test, "src/app.cpp": ""}) c.run(f"create . -o '*:shared=True' -pr=default -pr=../extra_profile {extra_conf}") @pytest.mark.skipif(platform.system() != "Linux", reason="Linux/gcc required for -rpath/-rpath-link testing") @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("use_cmake_config_deps", [True, False]) def test_cmake_transitive_rpath_private_internal(use_cmake_config_deps): c = TestClient() foo_h = textwrap.dedent(""" #pragma once int foo(int x, int y); """) foo_cpp = textwrap.dedent(""" #include "foo.h" int foo(int x, int y) { return x + y; } """) bar_h = textwrap.dedent(""" #pragma once int bar(int x, int y); """) bar_cpp = textwrap.dedent(""" #include "bar.h" #include "foo.h" int bar(int x, int y) { return foo(x, y) * 2; } """) foobar_cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(foobar CXX) add_library(foo src/foo.cpp) target_include_directories(foo PUBLIC include) set_target_properties(foo PROPERTIES PUBLIC_HEADER "include/foo.h") add_library(bar src/bar.cpp) target_include_directories(bar PUBLIC include) set_target_properties(bar PROPERTIES PUBLIC_HEADER "include/bar.h") target_link_libraries(bar PRIVATE foo) install(TARGETS foo bar) """) cmake_deps_gen = "CMakeConfigDeps" if use_cmake_config_deps else "CMakeDeps" foobar_conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class foobarRecipe(ConanFile): name = "foobar" version = "1.0" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {{"shared": [True, False]}} default_options = {{"shared": True}} exports_sources = "CMakeLists.txt", "src/*", "include/*" generators = "{cmake_deps_gen}", "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.components["foo"].libs = ["foo"] self.cpp_info.components["bar"].libs = ["bar"] self.cpp_info.components["bar"].requires = ["foo"] """) consumer_conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class consumerRecipe(ConanFile): name = "consumer" version = "1.0" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {{"shared": [True, False]}} default_options = {{"shared": True}} generators = "{cmake_deps_gen}", "CMakeToolchain" exports_sources = "CMakeLists.txt", "src/*", "include/*" def layout(self): cmake_layout(self) def requirements(self): self.requires("foobar/1.0") def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["consumer"] """) consumer_cmakelists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(consumer CXX) find_package(foobar CONFIG REQUIRED) add_library(consumer src/consumer.cpp) target_include_directories(consumer PUBLIC include) target_link_libraries(consumer PRIVATE ${foobar_LIBRARIES}) # foobar_LIBRARIES is foobar::foobar set_target_properties(consumer PROPERTIES PUBLIC_HEADER "include/consumer.h") install(TARGETS consumer) add_executable(my_app src/my_app.cpp) target_link_libraries(my_app PRIVATE consumer) """) consumer_cpp = textwrap.dedent(""" #include "consumer.h" #include "bar.h" int consumer(int x, int y) {return bar(x, y) * 2;} """) consumer_h = textwrap.dedent(""" #pragma once int consumer(int x, int y); """) my_app_cpp = textwrap.dedent(""" #include "consumer.h" int main() { return consumer(2, 3) == 20 ? 0 : 1; } """) extra_conf = "-c tools.build:add_rpath_link=True" # removing this should break the test with c.chdir("foobar"): c.save({"include/foo.h": foo_h, "include/bar.h": bar_h, "src/foo.cpp": foo_cpp, "src/bar.cpp": bar_cpp, "CMakeLists.txt": foobar_cmakelists, "conanfile.py": foobar_conanfile}) c.run(f"create . {extra_conf} ") with c.chdir("consumer"): c.save({"src/consumer.cpp": consumer_cpp, "include/consumer.h": consumer_h, "src/my_app.cpp": my_app_cpp, "CMakeLists.txt": consumer_cmakelists, "conanfile.py": consumer_conanfile}) c.run(f"create . {extra_conf}") ================================================ FILE: test/functional/toolchains/cmake/test_cmaketoolchain_paths.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient ios10_armv8_settings = "-s os=iOS -s os.sdk=iphoneos -s os.version=10.0 -s arch=armv8" class _FindRootPathModes(object): def __init__(self, package=None, library=None, framework=None, include=None, program=None): self.package = package self.library = library self.framework = framework self.include = include self.program = program find_root_path_modes_default = _FindRootPathModes() find_root_path_modes_cross_build = _FindRootPathModes( package="ONLY", library="ONLY", framework="ONLY", include="ONLY", program="NEVER", ) def _cmake_command_toolchain(find_root_path_modes): build_type = "-DCMAKE_BUILD_TYPE=Release" if platform.system() != "Windows" else "" cmake_command = "cmake .. -DCMAKE_TOOLCHAIN_FILE=../conan_toolchain.cmake {}".format(build_type) if find_root_path_modes.package: cmake_command += " -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE={}".format(find_root_path_modes.package) if find_root_path_modes.library: cmake_command += " -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY={}".format(find_root_path_modes.library) if find_root_path_modes.framework: cmake_command += " -DCMAKE_FIND_ROOT_PATH_MODE_FRAMEWORK={}".format(find_root_path_modes.framework) if find_root_path_modes.include: cmake_command += " -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE={}".format(find_root_path_modes.include) if find_root_path_modes.program: cmake_command += " -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM={}".format(find_root_path_modes.program) return cmake_command @pytest.mark.tool("cmake") @pytest.mark.parametrize("package", ["hello", "zlib"]) @pytest.mark.parametrize("find_package", ["module", "config"]) @pytest.mark.parametrize( "settings", [ "", pytest.param( ios10_armv8_settings, marks=pytest.mark.skipif(platform.system() != "Darwin", reason="OSX only"), ), ], ) @pytest.mark.parametrize( "find_root_path_modes", [find_root_path_modes_default, find_root_path_modes_cross_build], ) def test_cmaketoolchain_path_find_package(package, find_package, settings, find_root_path_modes): """Test with user "Hello" and also ZLIB one, to check that package ZLIB has priority over the CMake system one """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): exports_sources = "*" def layout(self): pass def package(self): copy(self, "*", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.builddirs.append("cmake") """) find = textwrap.dedent(""" SET({package}_FOUND 1) MESSAGE("HELLO FROM THE {package} FIND PACKAGE!") """).format(package=package) filename = "{}Config.cmake" if find_package == "config" else "Find{}.cmake" filename = filename.format(package) client.save({"conanfile.py": conanfile, "cmake/{}".format(filename): find}) client.run("create . --name={} --version=0.1 {}".format(package, settings)) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_package({package} REQUIRED) """).format(package=package) client.save({"CMakeLists.txt": consumer}, clean_first=True) client.run("install --requires={}/0.1 -g CMakeToolchain {}".format(package, settings)) with client.chdir("build"): client.run_command(_cmake_command_toolchain(find_root_path_modes)) assert "Conan: Target declared" not in client.out assert "HELLO FROM THE {package} FIND PACKAGE!".format(package=package) in client.out # If using the CMakeDeps generator, the in-package .cmake will be ignored # But it is still possible to include(owncmake) client.run("install --requires={}/0.1 -g CMakeToolchain -g CMakeDeps {}".format(package, settings)) with client.chdir("build2"): # A clean folder, not the previous one, CMake cache doesnt affect client.run_command(_cmake_command_toolchain(find_root_path_modes)) assert "Conan: Target declared '{package}::{package}'".format(package=package) in client.out assert "HELLO FROM THE {package} FIND PACKAGE!".format(package=package) not in client.out @pytest.mark.tool("cmake") def test_cmaketoolchain_path_find_package_editable(): """ make sure a package in editable mode that contains a xxxConfig.cmake file can find that file in the user folder """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class TestConan(ConanFile): settings = "os", "compiler", "arch", "build_type" exports_sources = "*" def layout(self): cmake_layout(self) self.cpp.source.builddirs = ["cmake"] def package(self): self.copy(pattern="*") def package_info(self): self.cpp_info.builddirs.append("cmake") """) find = textwrap.dedent(""" SET(hello_FOUND 1) MESSAGE("HELLO FROM THE hello FIND PACKAGE!") """) consumer = textwrap.dedent("""\ cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_package(hello REQUIRED) """) client.save({"dep/conanfile.py": conanfile, "dep/cmake/helloConfig.cmake": find, "consumer/conanfile.txt": "[requires]\nhello/0.1\n[generators]\nCMakeToolchain", "consumer/CMakeLists.txt": consumer}) with client.chdir("dep"): client.run("install .") client.run("editable add . --name=hello --version=0.1") with client.chdir("consumer"): client.run("install .") with client.chdir("build"): build_type = "-DCMAKE_BUILD_TYPE=Release" if platform.system() != "Windows" else "" cmake_command = "cmake .. -DCMAKE_TOOLCHAIN_FILE=../conan_toolchain.cmake {}".format( build_type) client.run_command(cmake_command) assert "Conan: Target declared" not in client.out assert "HELLO FROM THE hello FIND PACKAGE!" in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize( "settings", [ "", pytest.param( ios10_armv8_settings, marks=pytest.mark.skipif(platform.system() != "Darwin", reason="OSX only"), ), ], ) @pytest.mark.parametrize( "find_root_path_modes", [find_root_path_modes_default, find_root_path_modes_cross_build], ) @pytest.mark.parametrize( "builddir", ["os.path.join('hello', 'cmake')", "self.package_folder", '"."'], ) def test_cmaketoolchain_path_find_package_real_config(settings, find_root_path_modes, builddir): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake import os class TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "*" generators = "CMakeToolchain" def layout(self): pass def build(self): cmake = CMake(self) cmake.configure() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.builddirs.append({}) """.format(builddir)) cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) add_library(hello INTERFACE) install(TARGETS hello EXPORT helloConfig) export(TARGETS hello NAMESPACE hello:: FILE "${CMAKE_CURRENT_BINARY_DIR}/helloConfig.cmake" ) install(EXPORT helloConfig DESTINATION "hello/cmake" NAMESPACE hello:: ) """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake}) client.run("create . --name=hello --version=0.1 {}".format(settings)) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_package(hello REQUIRED) """) client.save({"CMakeLists.txt": consumer}, clean_first=True) client.run("install --requires=hello/0.1 -g CMakeToolchain {}".format(settings)) with client.chdir("build"): client.run_command(_cmake_command_toolchain(find_root_path_modes)) # If it didn't fail, it found the helloConfig.cmake assert "Conan: Target declared" not in client.out # If using the CMakeDeps generator, the in-package .cmake will be ignored # But it is still possible to include(owncmake) client.run("install --requires=hello/0.1 -g CMakeToolchain -g CMakeDeps {}".format(settings)) with client.chdir("build2"): # A clean folder, not the previous one, CMake cache doesnt affect client.run_command(_cmake_command_toolchain(find_root_path_modes)) assert "Conan: Target declared 'hello::hello'" in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize("require_type", ["requires", "tool_requires"]) @pytest.mark.parametrize( "settings", [ "", pytest.param( ios10_armv8_settings, marks=pytest.mark.skipif(platform.system() != "Darwin", reason="OSX only"), ), ], ) @pytest.mark.parametrize( "find_root_path_modes", [find_root_path_modes_default, find_root_path_modes_cross_build], ) def test_cmaketoolchain_path_include_cmake_modules(require_type, settings, find_root_path_modes): """Test that cmake module files in builddirs of requires and tool_requires are accessible with include() in consumer CMakeLists """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): settings = "os", "compiler", "arch", "build_type" exports_sources = "*" def layout(self): pass def package(self): copy(self, "*", self.source_folder, self.package_folder) def package_info(self): self.cpp_info.builddirs.append("cmake") """) myowncmake = 'MESSAGE("MYOWNCMAKE FROM hello!")' client.save({"conanfile.py": conanfile, "cmake/myowncmake.cmake": myowncmake}) br_flag = "--build-require" if require_type != "requires" else "" client.run("create . --name=hello --version=0.1 {} {}".format(settings, br_flag)) conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConan(ConanFile): settings = "os", "compiler", "arch", "build_type" {require_type} = "hello/0.1" """.format(require_type=require_type)) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) include(myowncmake) """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) client.run("install . --name=pkg --version=0.1 -g CMakeToolchain {}".format(settings)) with client.chdir("build"): client.run_command(_cmake_command_toolchain(find_root_path_modes)) assert "MYOWNCMAKE FROM hello!" in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize( "settings", [ "", pytest.param( ios10_armv8_settings, marks=pytest.mark.skipif(platform.system() != "Darwin", reason="OSX only"), ), ], ) @pytest.mark.parametrize( "find_root_path_modes", [find_root_path_modes_default, find_root_path_modes_cross_build], ) def test_cmaketoolchain_path_find_file_find_path(settings, find_root_path_modes): """Test that headers in includedirs of requires can be found with find_file() and find_path() in consumer CMakeLists """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): settings = "os", "arch", "compiler", "build_type" exports_sources = "*" def layout(self): pass def package(self): copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include")) """) client.save({"conanfile.py": conanfile, "hello.h": ""}) client.run("create . --name=hello --version=0.1 {}".format(settings)) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_file(HELLOFILE hello.h) if(HELLOFILE) message("Found file hello.h") endif() find_path(HELLODIR hello.h) if(HELLODIR) message("Found path of hello.h") endif() """) client.save({"CMakeLists.txt": consumer}, clean_first=True) client.run("install --requires hello/0.1 -g CMakeToolchain {}".format(settings)) with client.chdir("build"): client.run_command(_cmake_command_toolchain(find_root_path_modes)) assert "Found file hello.h" in client.out assert "Found path of hello.h" in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize( "settings", [ "", pytest.param( ios10_armv8_settings, marks=pytest.mark.skipif(platform.system() != "Darwin", reason="OSX only"), ), ], ) @pytest.mark.parametrize( "find_root_path_modes", [find_root_path_modes_default, find_root_path_modes_cross_build], ) def test_cmaketoolchain_path_find_library(settings, find_root_path_modes): """Test that libraries in libdirs of requires can be found with find_library() in consumer CMakeLists """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): settings = "os", "arch", "compiler", "build_type" exports_sources = "*" def layout(self): pass def package(self): copy(self, "*", self.source_folder, dst=os.path.join(self.package_folder, "lib")) """) client.save({"conanfile.py": conanfile, "libhello.a": "", "hello.lib": ""}) client.run("create . --name=hello_host --version=0.1 {}".format(settings)) host_folder = client.created_layout().base_folder host_folder_hash = host_folder.replace("\\", "/").split("/")[-1] client.run("create . --name=hello_build --version=0.1 --build-require") build_folder = client.created_layout().base_folder build_folder_hash = build_folder.replace("\\", "/").split("/")[-1] conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConan(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello_host/0.1" tool_requires = "hello_build/0.1" """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_library(HELLOLIB hello) if(HELLOLIB) message("Found hello lib: ${HELLOLIB}") endif() """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) client.run("install . --name=pkg --version=0.1 -g CMakeToolchain {}".format(settings)) with client.chdir("build"): client.run_command(_cmake_command_toolchain(find_root_path_modes)) assert "Found hello lib" in client.out # The hash of the cache folder assert build_folder_hash != host_folder_hash assert host_folder_hash in client.out assert build_folder_hash not in client.out @pytest.mark.tool("cmake") @pytest.mark.parametrize( "settings", [ "", pytest.param( ios10_armv8_settings, marks=pytest.mark.skipif(platform.system() != "Darwin", reason="OSX only"), ), ], ) @pytest.mark.parametrize( "find_root_path_modes", [find_root_path_modes_default, find_root_path_modes_cross_build], ) def test_cmaketoolchain_path_find_program(settings, find_root_path_modes): """Test that executables in bindirs of tool_requires can be found with find_program() in consumer CMakeLists. """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): settings = "os", "arch", "compiler", "build_type" exports_sources = "*" def layout(self): pass def package(self): copy(self, "*", self.source_folder, os.path.join(self.package_folder, "bin")) """) client.save({"conanfile.py": conanfile, "hello": "", "hello.exe": ""}) client.run("create . --name=hello_host --version=0.1 {}".format(settings)) host_folder = client.created_layout().base_folder host_folder_hash = host_folder.replace("\\", "/").split("/")[-1] client.run("create . --name=hello_build --version=0.1 --build-require") build_folder = client.created_layout().base_folder build_folder_hash = build_folder.replace("\\", "/").split("/")[-1] conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConan(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello_host/0.1" tool_requires = "hello_build/0.1" """) consumer = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(MyHello NONE) find_program(HELLOPROG hello) if(HELLOPROG) message("Found hello prog: ${HELLOPROG}") endif() """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) client.run("install . --name=pkg --version=0.1 -g CMakeToolchain {}".format(settings)) with client.chdir("build"): client.run_command(_cmake_command_toolchain(find_root_path_modes)) assert "Found hello prog" in client.out assert host_folder_hash not in client.out assert build_folder_hash in client.out ================================================ FILE: test/functional/toolchains/cmake/test_cps.py ================================================ import os import platform import shutil import textwrap import pytest from conan.internal.api.new.cmake_lib import test_conanfile_v2 from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake", "4.2") @pytest.mark.parametrize("shared", [False, True]) def test_cps(shared): c = TestClient() c.run("new cmake_lib") conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout from conan.cps import CPS import glob class mypkgRecipe(ConanFile): name = "mypkg" version = "0.1" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "CMakeLists.txt", "src/*", "include/*" implements = ["auto_shared_fpic"] generators = "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): file_loc = glob.glob("**/mypkg.cps", recursive=True) self.cpp_info = CPS.load(file_loc[0]).to_conan() """) cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 4.2) project(mypkg CXX) set(CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO "b80be207-778e-46ba-8080-b23bba22639e") add_library(mypkg src/mypkg.cpp) target_include_directories(mypkg PUBLIC $ $) target_compile_definitions(mypkg PUBLIC FOO BAR=42) set_target_properties(mypkg PROPERTIES PUBLIC_HEADER "include/mypkg.h") install(TARGETS mypkg EXPORT mypkg) install(PACKAGE_INFO mypkg EXPORT mypkg) """) # First, try with the standard mypkg-config.cmake consumption c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake}) shared_arg = "-o &:shared=True" if shared else "" c.run(f"create {shared_arg}") assert "mypkg/0.1: Hello World Release!" in c.out # Lets consume directly with CPS test_cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 4.2) project(PackageTest CXX) set(CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES e82e467b-f997-4464-8ace-b00808fff261) find_package(mypkg CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example mypkg::mypkg) """) test_conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout, CMakeToolchain, CMakeConfigDeps from conan.tools.build import can_run class TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def generate(self): self.output.info(f"Dep defines: {self.dependencies[self.tested_reference_str].cpp_info.defines}") deps = CMakeConfigDeps(self) deps.set_property("mypkg", "cmake_find_mode", "none") deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """) shutil.rmtree(os.path.join(c.current_folder, "test_package", "build")) example_cpp = c.load(os.path.join("test_package", "src", "example.cpp")) example_cpp = example_cpp.replace("#include ", '#include \n#include ') example_cpp = example_cpp.replace("mypkg();", 'mypkg();\nstd::cout << "BAR: " << BAR << std::endl;') c.save({"test_package/conanfile.py": test_conanfile, "test_package/CMakeLists.txt": test_cmake, "test_package/src/example.cpp": example_cpp}) c.run(f"create {shared_arg} --build=never") assert "mypkg/0.1: Hello World Release!" in c.out assert "Dep defines: ['BAR=42', 'FOO']" in c.out assert "BAR: 42" in c.out @pytest.mark.tool("cmake", "4.2") @pytest.mark.parametrize("shared", [False, True]) def test_cps_components(shared): c = TestClient() c.run("new cmake_lib") conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout from conan.cps import CPS import glob class mypkgRecipe(ConanFile): name = "mypkg" version = "0.1" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "CMakeLists.txt", "src/*", "include/*" implements = ["auto_shared_fpic"] generators = "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): file_loc = glob.glob("**/mypkg.cps", recursive=True) cps_data = CPS.load(file_loc[0]) # Convert CPS to cpp_info with components self.cpp_info = cps_data.to_conan() """) cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 4.2) project(mypkg CXX) set(CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO "b80be207-778e-46ba-8080-b23bba22639e") # First library: core add_library(mypkg_core src/mypkg_core.cpp) target_include_directories(mypkg_core PUBLIC $ $) set_target_properties(mypkg_core PROPERTIES PUBLIC_HEADER "include/mypkg_core.h") # Second library: utils (independent from core) add_library(mypkg_utils src/mypkg_utils.cpp) target_include_directories(mypkg_utils PUBLIC $ $) set_target_properties(mypkg_utils PROPERTIES PUBLIC_HEADER "include/mypkg_utils.h") install(TARGETS mypkg_core mypkg_utils EXPORT mypkg) install(PACKAGE_INFO mypkg EXPORT mypkg) """) # Create source files for both libraries core_cpp = gen_function_cpp(name="mypkg_core") core_h = gen_function_h(name="mypkg_core") utils_cpp = gen_function_cpp(name="mypkg_utils") utils_h = gen_function_h(name="mypkg_utils") # Create test_package files for the two components test_package_cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) set(CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES e82e467b-f997-4464-8ace-b00808fff261) find_package(mypkg CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example mypkg::mypkg_core mypkg::mypkg_utils) """) test_package_example = textwrap.dedent("""\ #include "mypkg_core.h" #include "mypkg_utils.h" int main() { mypkg_core(); mypkg_utils(); return 0; } """) # First, try with the standard mypkg-config.cmake consumption c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "src/mypkg_core.cpp": core_cpp, "include/mypkg_core.h": core_h, "src/mypkg_utils.cpp": utils_cpp, "include/mypkg_utils.h": utils_h, "test_package/CMakeLists.txt": test_package_cmake, "test_package/src/example.cpp": test_package_example}) shared_arg = "-o &:shared=True" if shared else "" c.run(f"create {shared_arg}") assert "mypkg_core: Release!" in c.out assert "mypkg_utils: Release!" in c.out test_conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout, CMakeToolchain, CMakeConfigDeps from conan.tools.build import can_run class TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def generate(self): deps = CMakeConfigDeps(self) deps.set_property("mypkg", "cmake_find_mode", "none") deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """) shutil.rmtree(os.path.join(c.current_folder, "test_package", "build")) c.save({"test_package/conanfile.py": test_conanfile}) c.run(f"create {shared_arg} --build=never") assert "mypkg_core: Release!" in c.out assert "mypkg_utils: Release!" in c.out @pytest.mark.tool("cmake", "4.2") @pytest.mark.parametrize("kind", ["static_public", "static_private", "shared_private"]) def test_cps_components_requires(kind): if kind == "shared_private" and platform.system() == "Linux": pytest.skip("CPS still doesn't support this case") c = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout from conan.cps import CPS import glob class Recipe(ConanFile): name = "{name}" version = "0.1" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {{"shared": [True, False], "fPIC": [True, False]}} default_options = {{"shared": False, "fPIC": True}} exports_sources = "CMakeLists.txt", "src/*", "include/*" implements = ["auto_shared_fpic"] generators = "CMakeToolchain", "CMakeConfigDeps" def requirements(self): {requires} pass def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): file_loc = glob.glob("**/{name}.cps", recursive=True) cps_data = CPS.load(file_loc[0]) # Convert CPS to cpp_info with components self.cpp_info = cps_data.to_conan() """) lib_type = "PUBLIC" if "public" in kind else "PRIVATE" cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 4.2) project({name} CXX) set(CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO "b80be207-778e-46ba-8080-b23bba22639e") {find} # First library: core add_library({name}_core src/{name}_core.cpp) target_include_directories({name}_core PUBLIC $ $) set_target_properties({name}_core PROPERTIES PUBLIC_HEADER "include/{name}_core.h") {deps} # Second library: utils (independent from core) add_library({name}_utils src/{name}_utils.cpp) target_include_directories({name}_utils PUBLIC $ $) set_target_properties({name}_utils PROPERTIES PUBLIC_HEADER "include/{name}_utils.h") target_link_libraries({name}_utils {lib_type} {name}_core) install(TARGETS {name}_core EXPORT {name} PUBLIC_HEADER DESTINATION ${{CMAKE_INSTALL_INCLUDEDIR}}/core) install(TARGETS {name}_utils EXPORT {name} PUBLIC_HEADER DESTINATION ${{CMAKE_INSTALL_INCLUDEDIR}}/utils) install(PACKAGE_INFO {name} EXPORT {name}) """) # Create source files for both libraries core_cpp = gen_function_cpp(name="liba_core") core_h = gen_function_h(name="liba_core") utils_cpp = gen_function_cpp(name="liba_utils", includes=["liba_core"], calls=["liba_core"]) if "public" in kind: utils_h = gen_function_h(name="liba_utils", includes=["liba_core"]) else: utils_h = gen_function_h(name="liba_utils") # Create test_package files for the two components test_package_cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) set(CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES e82e467b-f997-4464-8ace-b00808fff261) find_package({name} CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example {name}::{name}_utils) """) test_package_example = textwrap.dedent("""\ #include "{name}_utils.h" int main() {{ {name}_utils(); return 0; }} """) test_conanfile = test_conanfile_v2.replace("{{package_name}}", "") test_conanfile = test_conanfile.replace("CMakeDeps", "CMakeConfigDeps") # First, try with the standard liba-config.cmake consumption c.save({"conanfile.py": conanfile.format(name="liba", requires=""), "CMakeLists.txt": cmake.format(name="liba", lib_type=lib_type, deps="", find=""), "src/liba_core.cpp": core_cpp, "include/liba_core.h": core_h, "src/liba_utils.cpp": utils_cpp, "include/liba_utils.h": utils_h, "test_package/conanfile.py": test_conanfile, "test_package/CMakeLists.txt": test_package_cmake.format(name="liba"), "test_package/src/example.cpp": test_package_example.format(name="liba")}) shared_arg = "-o *:shared=True" if "shared" in kind else "" c.run(f"create {shared_arg}") assert "liba_core: Release!" in c.out assert "liba_utils: Release!" in c.out test_conanfile_cps = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout, CMakeToolchain, CMakeConfigDeps from conan.tools.build import can_run class TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def generate(self): deps = CMakeConfigDeps(self) deps.set_property("liba", "cmake_find_mode", "none") deps.set_property("libb", "cmake_find_mode", "none") deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """) shutil.rmtree(os.path.join(c.current_folder, "test_package", "build")) c.save({"test_package/conanfile.py": test_conanfile_cps}) c.run(f"create {shared_arg} --build=never") assert "liba_core: Release!" in c.out assert "liba_utils: Release!" in c.out # Now a second level core_cpp = gen_function_cpp(name="libb_core", includes=["liba_utils"], calls=["liba_utils"]) if "public" in kind: core_h = gen_function_h(name="libb_core", includes=["liba_utils"]) else: core_h = gen_function_h(name="libb_core") utils_cpp = gen_function_cpp(name="libb_utils", includes=["libb_core"], calls=["libb_core"]) if "public" in kind: utils_h = gen_function_h(name="libb_utils", includes=["libb_core"]) else: utils_h = gen_function_h(name="libb_utils") deps = f'target_link_libraries(libb_core {lib_type} liba::liba_utils)' find = f'find_package(liba CONFIG REQUIRED)' transitive_headers = "public" in kind requires = f'self.requires("liba/0.1", transitive_headers={transitive_headers})' c.save({"conanfile.py": conanfile.format(name="libb", requires=requires), "CMakeLists.txt": cmake.format(name="libb", lib_type=lib_type, deps=deps, find=find), "src/libb_core.cpp": core_cpp, "include/libb_core.h": core_h, "src/libb_utils.cpp": utils_cpp, "include/libb_utils.h": utils_h, "test_package/conanfile.py": test_conanfile, "test_package/CMakeLists.txt": test_package_cmake.format(name="libb"), "test_package/src/example.cpp": test_package_example.format(name="libb")}, clean_first=True) c.run(f"create {shared_arg}") assert "libb_core: Release!" in c.out assert "libb_utils: Release!" in c.out assert "liba_core: Release!" in c.out assert "liba_utils: Release!" in c.out c.save({"test_package/conanfile.py": test_conanfile_cps}) c.run(f"create {shared_arg} --build=never") assert "libb_core: Release!" in c.out assert "libb_utils: Release!" in c.out assert "liba_core: Release!" in c.out assert "liba_utils: Release!" in c.out @pytest.mark.skip(reason="Just to report to CMake upstream, and CPS feature request") @pytest.mark.tool("cmake", "4.2") def test_pure_cmake_shared(): c = TestClient() cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 4.2) project(myproj CXX) set(CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO "b80be207-778e-46ba-8080-b23bba22639e") # First library: core add_library(mypkg_core src/mypkg_core.cpp) target_include_directories(mypkg_core PUBLIC $ $) set_target_properties(mypkg_core PROPERTIES PUBLIC_HEADER "include/mypkg_core.h") # Second library: utils add_library(mypkg_utils src/mypkg_utils.cpp) target_include_directories(mypkg_utils PUBLIC $ $) set_target_properties(mypkg_utils PROPERTIES PUBLIC_HEADER "include/mypkg_utils.h") target_link_libraries(mypkg_utils PRIVATE mypkg_core) install(TARGETS mypkg_core EXPORT mypkg PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/core) install(TARGETS mypkg_utils EXPORT mypkg PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/utils) install(PACKAGE_INFO mypkg EXPORT mypkg) """) # Create source files for both libraries core_cpp = gen_function_cpp(name="mypkg_core") core_h = gen_function_h(name="mypkg_core") utils_cpp = gen_function_cpp(name="mypkg_utils", includes=["mypkg_core"], calls=["mypkg_core"]) utils_h = gen_function_h(name="mypkg_utils") # First, try with the standard mypkg-config.cmake consumption c.save({"CMakeLists.txt": cmake, "src/mypkg_core.cpp": core_cpp, "include/mypkg_core.h": core_h, "src/mypkg_utils.cpp": utils_cpp, "include/mypkg_utils.h": utils_h}) c.run_command(f"cmake . -DBUILD_SHARED_LIBS=ON") print(c.out) c.run_command("cmake --build . --config Release") print(c.out) c.run_command("cmake --install . --config Release --prefix=mypkginstall") print(c.out) cps = c.load("mypkginstall/cps/mypkg.cps") print(cps) cps_release = c.load("mypkginstall/cps/mypkg@release.cps") print(cps_release) @pytest.mark.tool("cmake", "4.2") def test_cps_name_mapping(): c = TestClient() c.run("new cmake_lib") conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout from conan.cps import CPS import glob class mypkgRecipe(ConanFile): name = "mypkg" version = "0.1" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "CMakeLists.txt", "src/*", "include/*" implements = ["auto_shared_fpic"] generators = "CMakeToolchain" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): file_loc = glob.glob("**/potato.cps", recursive=True) self.cpp_info = CPS.load(file_loc[0]).to_conan() """) cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 4.2) project(mypkg CXX) set(CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO "b80be207-778e-46ba-8080-b23bba22639e") add_library(mypkg src/mypkg.cpp) target_include_directories(mypkg PUBLIC $ $) target_compile_definitions(mypkg PUBLIC FOO BAR=42) set_target_properties(mypkg PROPERTIES PUBLIC_HEADER "include/mypkg.h") install(TARGETS mypkg EXPORT mypkg) install(PACKAGE_INFO potato EXPORT mypkg) """) # First, try with the standard mypkg-config.cmake consumption test_package_cmakelists = c.load("test_package/CMakeLists.txt") # The target and the file name use the CPS name, not the package name test_package_cmakelists = test_package_cmakelists.replace("find_package(mypkg", "find_package(potato") test_package_cmakelists = test_package_cmakelists.replace("mypkg::mypkg", "potato::mypkg") test_package_cmakelists = test_package_cmakelists.replace("CMakeDeps", "CMakeConfigDeps") c.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "test_package/CMakeLists.txt": test_package_cmakelists}) c.run(f"create") assert "mypkg/0.1: Hello World Release!" in c.out # Lets consume directly with CPS test_cmake = textwrap.dedent("""\ cmake_minimum_required(VERSION 4.2) project(PackageTest CXX) set(CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES e82e467b-f997-4464-8ace-b00808fff261) find_package(potato CONFIG REQUIRED) add_executable(example src/example.cpp) target_link_libraries(example potato::mypkg) """) test_conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout, CMakeToolchain, CMakeConfigDeps from conan.tools.build import can_run class TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def generate(self): self.output.info(f"Dep defines: {self.dependencies[self.tested_reference_str].cpp_info.defines}") deps = CMakeConfigDeps(self) deps.set_property("mypkg", "cmake_find_mode", "none") deps.generate() tc = CMakeToolchain(self) tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): if can_run(self): cmd = os.path.join(self.cpp.build.bindir, "example") self.run(cmd, env="conanrun") """) shutil.rmtree(os.path.join(c.current_folder, "test_package", "build")) c.save({"test_package/conanfile.py": test_conanfile, "test_package/CMakeLists.txt": test_cmake}) c.run(f"create --build=never") assert "mypkg/0.1: Hello World Release!" in c.out ================================================ FILE: test/functional/toolchains/cmake/test_ninja.py ================================================ import os import textwrap import platform import pytest from conan.tools.cmake import CMakeToolchain from conan.tools.cmake.presets import load_cmake_presets from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_h, gen_function_cpp from test.functional.utils import check_vs_runtime, check_exe_run from conan.test.utils.tools import TestClient @pytest.fixture def client(): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain class Library(ConanFile): name = "hello" version = "1.0" settings = 'os', 'arch', 'compiler', 'build_type' exports_sources = 'hello.h', '*.cpp', 'CMakeLists.txt' options = {'shared': [True, False]} default_options = {'shared': False} def generate(self): tc = CMakeToolchain(self, generator="Ninja") tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.sep.join([".", "myapp"])) def package(self): cmake = CMake(self) cmake.install() """) test_client = TestClient(path_with_spaces=False) test_client.save({'conanfile.py': conanfile, "CMakeLists.txt": gen_cmakelists(libsources=["hello.cpp"], appsources=["main.cpp"], install=True), "hello.h": gen_function_h(name="hello"), "hello.cpp": gen_function_cpp(name="hello", includes=["hello"]), "main.cpp": gen_function_cpp(name="main", includes=["hello"], calls=["hello"])}) return test_client @pytest.mark.skipif(platform.system() != "Linux", reason="Only Linux") @pytest.mark.parametrize("build_type,shared", [("Release", False), ("Debug", True)]) @pytest.mark.tool("ninja") def test_locally_build_linux(build_type, shared, client): settings = f"-s os=Linux -s arch=x86_64 -s build_type={build_type} -o hello/*:shared={shared}" client.run("install . {}".format(settings)) client.run_command('cmake . -G "Ninja" -DCMAKE_TOOLCHAIN_FILE={} -DCMAKE_BUILD_TYPE={}' .format(CMakeToolchain.filename, build_type)) client.run_command('ninja') if shared: assert "Linking CXX shared library libmylibrary.so" in client.out else: assert "Linking CXX static library libmylibrary.a" in client.out client.run_command("./myapp") check_exe_run(client.out, ["main", "hello"], "gcc", None, build_type, "x86_64", cppstd=None) # create should also work client.run("create . --name=hello --version=1.0 {}".format(settings)) assert 'cmake -G "Ninja"' in client.out assert "main: {}!".format(build_type) in client.out client.run(f"install --requires=hello/1.0@ --deployer=full_deploy -of=mydeploy {settings}") deploy_path = os.path.join(client.current_folder, "mydeploy", "full_deploy", "host", "hello", "1.0", build_type, "x86_64") client.run_command(f"LD_LIBRARY_PATH='{deploy_path}/lib' {deploy_path}/bin/myapp") check_exe_run(client.out, ["main", "hello"], "gcc", None, build_type, "x86_64", cppstd=None) @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") @pytest.mark.parametrize("build_type,shared", [("Release", False), ("Debug", True)]) @pytest.mark.tool("visual_studio", "15") @pytest.mark.tool("ninja") def test_locally_build_msvc(build_type, shared, client): msvc_version = "15" settings = "-s build_type={} -o hello/*:shared={}".format(build_type, shared) client.run("install . {}".format(settings)) client.run_command('conanvcvars.bat && cmake . -G "Ninja" ' '-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake ' '-DCMAKE_BUILD_TYPE={}'.format(build_type)) client.run_command("conanvcvars.bat && ninja") libname = "mylibrary.dll" if shared else "mylibrary.lib" assert libname in client.out client.run_command("myapp.exe") # TODO: Need full msvc version check check_exe_run(client.out, ["main", "hello"], "msvc", "19", build_type, "x86_64", cppstd="14") check_vs_runtime("myapp.exe", client, msvc_version, build_type, architecture="amd64") check_vs_runtime(libname, client, msvc_version, build_type, architecture="amd64") # create should also work client.run("create . --name=hello --version=1.0 {}".format(settings)) assert 'cmake -G "Ninja"' in client.out assert "main: {}!".format(build_type) in client.out client.run(f"install --requires=hello/1.0@ --deployer=full_deploy -of=mydeploy {settings}") client.run_command(fr"mydeploy\full_deploy\host\hello\1.0\{build_type}\x86_64\bin\myapp.exe") check_exe_run(client.out, ["main", "hello"], "msvc", "19", build_type, "x86_64", cppstd="14") @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") @pytest.mark.tool("ninja") def test_locally_build_msvc_toolset(client): msvc_version = "15" profile = textwrap.dedent(""" [settings] os=Windows compiler=msvc compiler.version=191 compiler.runtime=dynamic compiler.cppstd=14 build_type=Release arch=x86_64 [conf] tools.cmake.cmaketoolchain:generator=Ninja tools.microsoft.msbuild:vs_version = 15 """) client.save({"profile": profile}) client.run("install . -pr=profile") client.run_command('conanvcvars.bat && cmake . -G "Ninja" ' '-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake ' '-DCMAKE_BUILD_TYPE=Release') client.run_command("conanvcvars.bat && ninja") client.run_command("myapp.exe") # Checking that compiler is indeed version 19.0, not 19.1-default of VS15 check_exe_run(client.out, ["main", "hello"], "msvc", "191", "Release", "x86_64", cppstd="14") check_vs_runtime("myapp.exe", client, msvc_version, "Release", architecture="amd64") check_vs_runtime("mylibrary.lib", client, msvc_version, "Release", architecture="amd64") @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") @pytest.mark.parametrize("build_type,shared", [("Release", False), ("Debug", True)]) @pytest.mark.tool("mingw64") @pytest.mark.tool("ninja") def test_locally_build_gcc(build_type, shared, client): # FIXME: Note the gcc version is still incorrect gcc = ("-s os=Windows -s compiler=gcc -s compiler.version=4.9 -s compiler.libcxx=libstdc++ " "-s arch=x86_64 -s build_type={}".format(build_type)) client.run("install . {} -o hello/*:shared={}".format(gcc, shared)) client.run_command('cmake . -G "Ninja" ' '-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake ' '-DCMAKE_BUILD_TYPE={}'.format(build_type)) libname = "mylibrary.dll" if shared else "libmylibrary.a" client.run_command("ninja") assert libname in client.out client.run_command("myapp.exe") # TODO: Need full gcc version check check_exe_run(client.out, ["main", "hello"], "gcc", None, build_type, "x86_64", cppstd=None, subsystem="mingw64") @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires apple-clang") @pytest.mark.parametrize("build_type,shared", [("Release", False), ("Debug", True)]) @pytest.mark.tool("ninja") def test_locally_build_macos(build_type, shared, client): client.run('install . -s os=Macos -s arch=x86_64 -s build_type={} -o hello/*:shared={}' .format(build_type, shared)) client.run_command('cmake . -G"Ninja" -DCMAKE_TOOLCHAIN_FILE={} -DCMAKE_BUILD_TYPE={}' .format(CMakeToolchain.filename, build_type)) client.run_command('ninja') if shared: assert "Linking CXX shared library libmylibrary.dylib" in client.out else: assert "Linking CXX static library libmylibrary.a" in client.out command_str = 'DYLD_LIBRARY_PATH="%s" ./myapp' % client.current_folder client.run_command(command_str) check_exe_run(client.out, ["main", "hello"], "apple-clang", None, build_type, "x86_64", cppstd=None) @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") @pytest.mark.tool("visual_studio") def test_ninja_conf(): conanfile = GenConanfile().with_generator("CMakeToolchain").with_settings("os", "compiler", "build_type", "arch") profile = textwrap.dedent(""" [settings] os=Windows compiler=msvc compiler.version=191 compiler.runtime=dynamic compiler.cppstd=14 build_type=Release arch=x86_64 [conf] tools.cmake.cmaketoolchain:generator=Ninja """) client = TestClient() client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . -pr=profile") presets = load_cmake_presets(client.current_folder) generator = presets["configurePresets"][0]["generator"] assert generator == "Ninja" vcvars = client.load("conanvcvars.bat") assert "2017" in vcvars # toolchain cannot define the CMAKE_GENERATOR_TOOLSET for Ninja cmake = client.load("conan_toolchain.cmake") assert "CMAKE_GENERATOR_TOOLSET" not in cmake ================================================ FILE: test/functional/toolchains/cmake/test_presets_inherit.py ================================================ import json import os import platform import textwrap from shutil import rmtree import pytest from conan.test.utils.tools import TestClient # Shared CMakePresets.json for tests that use user_presets_path + ConanPresets.json _CMAKE_PRESETS_FILE = textwrap.dedent(""" { "version": 4, "include": ["./ConanPresets.json"], "configurePresets": [ {"name": "default", "displayName": "multi config", "inherits": "conan-default"}, {"name": "release", "displayName": "release single config", "inherits": "conan-release"}, {"name": "debug", "displayName": "debug single config", "inherits": "conan-debug"} ], "buildPresets": [ {"name": "multi-release", "configurePreset": "default", "configuration": "Release", "inherits": "conan-release"}, {"name": "multi-debug", "configurePreset": "default", "configuration": "Debug", "inherits": "conan-debug"}, {"name": "release", "configurePreset": "release", "configuration": "Release", "inherits": "conan-release"}, {"name": "debug", "configurePreset": "debug", "configuration": "Debug", "inherits": "conan-debug"} ] } """) def _client_with_user_presets(): """TestClient with cmake_exe, user_presets_path and standard CMakePresets.json.""" c = TestClient() c.run("new cmake_exe -d name=foo -d version=1.0") conanfile = c.load("conanfile.py") conanfile = conanfile.replace( "tc = CMakeToolchain(self)", "tc = CMakeToolchain(self)\n tc.user_presets_path = 'ConanPresets.json'", ) c.save({"conanfile.py": conanfile, "CMakePresets.json": _CMAKE_PRESETS_FILE}) return c @pytest.mark.tool("cmake", "3.23") def test_cmake_presets_with_user_presets_file(): """ Test the integration of the generated one with a user root CMakePresets.json """ c = TestClient() c.run("new cmake_exe -d name=foo -d version=1.0") conanfile = c.load("conanfile.py") conanfile = conanfile.replace("tc = CMakeToolchain(self)", "tc = CMakeToolchain(self)\n" " tc.user_presets_path = 'ConanPresets.json'\n" " tc.presets_prefix = 'conan'\n") c.save({"conanfile.py": conanfile, "CMakePresets.json": _CMAKE_PRESETS_FILE}) c.run(f"install . ") c.run(f"install . -s build_type=Debug") if platform.system() != "Windows": c.run_command("cmake --preset debug") c.run_command("cmake --build --preset debug") c.run_command("./build/Debug/foo") else: c.run_command("cmake --preset default") c.run_command("cmake --build --preset multi-debug") c.run_command("build\\Debug\\foo") assert "Hello World Debug!" in c.out if platform.system() != "Windows": c.run_command("cmake --preset release") c.run_command("cmake --build --preset release") c.run_command("./build/Release/foo") else: c.run_command("cmake --build --preset multi-release") c.run_command("build\\Release\\foo") assert "Hello World Release!" in c.out @pytest.mark.tool("cmake", "3.23") def test_cmake_presets_build_preset_stub_needs_configure_preset(): """Reproduce issue #19180: buildPresets stubs in ConanPresets.json must include 'configurePreset' field for cmake --list-presets to succeed (single-config generators). """ c = _client_with_user_presets() c.run("install .") conan_presets = json.loads(c.load("ConanPresets.json")) for stub in conan_presets.get("buildPresets", []): assert "configurePreset" in stub c.run_command("cmake --list-presets") assert "Invalid preset" not in c.out, f"cmake --list-presets failed: {c.out}" @pytest.mark.tool("cmake", "3.23") def test_cmake_presets_stubs_restored_after_build_folder_deleted(): """Reproduce issue #19173: after deleting build/ and reinstalling one config, ConanPresets.json must still contain stubs for presets inherited by user (e.g. conan-release) so cmake --list-presets does not fail. """ c = _client_with_user_presets() c.run("install . -s build_type=Debug") c.run("install . -s build_type=Release") rmtree(os.path.join(c.current_folder, "build")) c.run("install . -s build_type=Debug") conan_presets = json.loads(c.load("ConanPresets.json")) stub_names = {s["name"] for s in conan_presets.get("configurePresets", [])} assert "conan-release" in stub_names c.run_command("cmake --list-presets") assert "Invalid preset" not in c.out, f"cmake --list-presets failed: {c.out}" ================================================ FILE: test/functional/toolchains/cmake/test_shared_cmake.py ================================================ import os.path import platform import textwrap import pytest from conan.test.utils.mocks import ConanFileMock from conan.tools.env.environment import environment_wrap_command from conan.test.utils.tools import TestClient from conan.internal.util.files import rmdir @pytest.fixture(scope="module") def transitive_shared_client(): # TODO: Reuse fixtures client = TestClient(default_server_user=True) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("create . -o hello/*:shared=True -tf=") client.save({}, clean_first=True) client.run("new cmake_lib -d name=chat -d version=0.1 -d requires=hello/0.1") client.run("create . -o chat/*:shared=True -o hello/*:shared=True -tf=") client.save({}, clean_first=True) client.run("new cmake_exe -d name=app -d version=0.1 -d requires=chat/0.1") client.run("create . -o chat/*:shared=True -o hello/*:shared=True -tf=") client.run("upload * -c -r default") client.run("remove * -c") return client @pytest.mark.tool("cmake") def test_other_client_can_execute(transitive_shared_client): _check_install_run(transitive_shared_client) def _check_install_run(client): client = TestClient(servers=client.servers) client.run("install --requires=app/0.1@ -o chat*:shared=True -o hello/*:shared=True " "-g VirtualRunEnv") # This only finds "app" executable because the "app/0.1" is declaring package_type="application" # otherwise, run=None and nothing can tell us if the conanrunenv should have the PATH. command = environment_wrap_command(ConanFileMock(), "conanrun", client.current_folder, "app") client.run_command(command) assert "app/0.1: Hello World Release!" in client.out assert "chat/0.1: Hello World Release!" in client.out assert "hello/0.1: Hello World Release!" in client.out @pytest.mark.tool("cmake") def test_other_client_can_link_cmake(transitive_shared_client): client = transitive_shared_client # https://github.com/conan-io/conan/issues/13000 # This failed, because of rpath link in Linux client = TestClient(servers=client.servers, inputs=["admin", "password"]) client.run("new cmake_exe -d name=app -d version=0.1 -d requires=chat/0.1") client.run("create . -o chat/*:shared=True -o hello/*:shared=True -tf=") # check exe also keep running client.run("upload * -c -r default") client.run("remove * -c") _check_install_run(transitive_shared_client) # FIXME: Move to the correct Meson space @pytest.mark.tool("meson") @pytest.mark.tool("ninja") @pytest.mark.tool("pkg_config") def test_other_client_can_link_meson(transitive_shared_client): client = transitive_shared_client # https://github.com/conan-io/conan/issues/13000 # This failed, because of rpath link in Linux client = TestClient(servers=client.servers, inputs=["admin", "password"], path_with_spaces=False) client.run("new meson_exe -d name=app -d version=0.1 -d requires=chat/0.1") client.run("create . -o chat/*:shared=True -o hello/*:shared=True") # TODO Check that static builds too # client.run("create . --build=missing") # FIXME: Move to the correct Meson space @pytest.mark.tool("autotools") @pytest.mark.skipif(platform.system() == "Windows", reason="Autotools needed") def test_other_client_can_link_autotools(transitive_shared_client): client = transitive_shared_client # https://github.com/conan-io/conan/issues/13000 # This failed, because of rpath link in Linux client = TestClient(servers=client.servers, inputs=["admin", "password"], path_with_spaces=False) client.run("new autotools_exe -d name=app -d version=0.1 -d requires=chat/0.1") client.run("create . -o chat/*:shared=True -o hello/*:shared=True") # TODO Check that static builds too # client.run("create . --build=missing") @pytest.mark.tool("cmake") def test_shared_cmake_toolchain_components(): """ the same as above, but with components. """ client = TestClient(default_server_user=True) client.run("new cmake_lib -d name=hello -d version=0.1") conanfile = client.load("conanfile.py") conanfile2 = conanfile.replace('self.cpp_info.libs = ["hello"]', 'self.cpp_info.components["hi"].libs = ["hello"]') assert conanfile != conanfile2 client.save({"conanfile.py": conanfile2}) client.run("create . -o hello/*:shared=True -tf=") # Chat client.save({}, clean_first=True) client.run("new cmake_lib -d name=chat -d version=0.1 -d requires=hello/0.1") conanfile = client.load("conanfile.py") conanfile2 = conanfile.replace('self.cpp_info.libs = ["chat"]', 'self.cpp_info.components["talk"].libs = ["chat"]\n' ' self.cpp_info.components["talk"].requires=["hello::hi"]') assert conanfile != conanfile2 client.save({"conanfile.py": conanfile2}) client.run("create . -o chat/*:shared=True -o hello/*:shared=True -tf=") # App client.save({}, clean_first=True) client.run("new cmake_exe -d name=app -d version=0.1 -d requires=chat/0.1") cmakelist = client.load("CMakeLists.txt") cmakelist2 = cmakelist.replace('target_link_libraries(app PRIVATE chat::chat)', 'target_link_libraries(app PRIVATE chat::talk)') assert cmakelist != cmakelist2 client.save({"CMakeLists.txt": cmakelist2}) client.run("create . -o chat/*:shared=True -o hello/*:shared=True -tf=") client.run("upload * -c -r default") client.run("remove * -c") client = TestClient(servers=client.servers) client.run("install --requires=app/0.1@ -o chat*:shared=True -o hello/*:shared=True") # This only finds "app" executable because the "app/0.1" is declaring package_type="application" # otherwise, run=None and nothing can tell us if the conanrunenv should have the PATH. command = environment_wrap_command(ConanFileMock(), "conanrun", client.current_folder, "app") client.run_command(command) assert "app/0.1 test_package" in client.out assert "app/0.1: Hello World Release!" in client.out assert "chat/0.1: Hello World Release!" in client.out assert "hello/0.1: Hello World Release!" in client.out # https://github.com/conan-io/conan/issues/13000 # This failed, because of rpath link in Linux client = TestClient(servers=client.servers, inputs=["admin", "password"]) client.run("new cmake_exe -d name=app -d version=0.1 -d requires=chat/0.1") client.run("create . -o chat/*:shared=True -o hello/*:shared=True") client.run("upload * -c -r default") client.run("remove * -c") client = TestClient(servers=client.servers) client.run("install --requires=app/0.1@ -o chat*:shared=True -o hello/*:shared=True") # This only finds "app" executable because the "app/0.1" is declaring package_type="application" # otherwise, run=None and nothing can tell us if the conanrunenv should have the PATH. command = environment_wrap_command(ConanFileMock(), "conanrun", client.current_folder, "app") client.run_command(command) assert "app/0.1: Hello World Release!" in client.out assert "chat/0.1: Hello World Release!" in client.out assert "hello/0.1: Hello World Release!" in client.out @pytest.mark.tool("cmake") def test_shared_cmake_toolchain_test_package(): # TODO: This is already tested in other places client = TestClient() client.run("new cmake_lib -d name=hello -d version=0.1") client.run("create . -o hello/*:shared=True") assert "hello/0.1: Hello World Release!" in client.out assert "hello/0.1 test_package" in client.out @pytest.fixture() def test_client_shared(): client = TestClient() client.run("new -d name=hello -d version=0.1 cmake_lib") test_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.files import copy class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain", "CMakeDeps" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def generate(self): for dep in self.dependencies.values(): copy(self, "*.dylib", dep.cpp_info.libdirs[0], self.build_folder) copy(self, "*.dll", dep.cpp_info.libdirs[0], self.build_folder) def test(self): cmd = os.path.join(self.cpp.build.bindirs[0], "example") # This is working without runenv because CMake is puting an internal rpath # to the executable pointing to the dylib of hello, internally is doing something # like: install_name_tool -add_rpath /path/to/hello/lib/libhello.dylib test self.run(cmd) """) files = {"test_package/conanfile.py": test_conanfile} client.save(files) client.run("create . -o hello*:shared=True") assert "Hello World Release!" in client.out # We can run the exe from the test package directory also, without environment # because there is an internal RPATH in the exe with an abs path to the "hello" build_folder = client.created_test_build_folder("hello/0.1") exe_folder = os.path.join("test_package", build_folder) client.test_exe_folder = exe_folder assert os.path.exists(os.path.join(client.current_folder, exe_folder, "example")) client.run_command(os.path.join(exe_folder, "example")) # We try to remove the hello package and run again the executable from the test package, # this time it should fail, it doesn't find the shared library client.run("remove '*' -c") client.run_command(os.path.join(exe_folder, "example"), assert_error=True) return client @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_shared_same_dir_using_tool(test_client_shared): """ If we build an executable in Mac and we want it to locate the shared libraries in the same directory, we have different alternatives, here we use the "install_name_tool" """ exe_folder = test_client_shared.test_exe_folder # Alternative 1, add the "." to the rpaths so the @rpath from the exe can be replaced with "." test_client_shared.current_folder = os.path.join(test_client_shared.current_folder, exe_folder) test_client_shared.run_command("install_name_tool -add_rpath '.' example") test_client_shared.run_command("./{}".format("example")) @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_shared_same_dir_using_cmake(test_client_shared): """ If we build an executable in Mac and we want it to locate the shared libraries in the same directory, we have different alternatives, here we use CMake to adjust CMAKE_INSTALL_RPATH to @executable_path so the exe knows that can replace @rpath with the current dir """ # Alternative 2, set the rpath in cmake # Only viable when installing with cmake cmake = """ set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(project CXX) set(CMAKE_INSTALL_RPATH "@executable_path") find_package(hello) add_executable(test src/example.cpp ) target_link_libraries(test hello::hello) # Hardcoded installation path to keep the exe in the same place in the tests install(TARGETS test DESTINATION "bin") """ # Same test conanfile but calling cmake.install() cf = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy from conan.tools.cmake import CMake, cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "CMakeToolchain", "CMakeDeps" def generate(self): # The exe is installed by cmake at test_package/bin dest = os.path.join(self.recipe_folder, "bin") for dep in self.dependencies.values(): copy(self, "*.dylib", dep.cpp_info.libdirs[0], dest) def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() cmake.install() def test(self): cmd = os.path.join(self.cpp.build.bindirs[0], "test") # This is working without runenv because CMake is puting an internal rpath # to the executable pointing to the dylib of hello, internally is doing something # like: install_name_tool -add_rpath /path/to/hello/lib/libhello.dylib test self.run(cmd) """) test_client_shared.save({"test_package/CMakeLists.txt": cmake, "test_package/conanfile.py": cf}) test_client_shared.run("create . -o hello*:shared=True") test_client_shared.run("remove '*' -c") exe_folder = os.path.join("test_package", "bin") test_client_shared.run_command(os.path.join(exe_folder, "test")) @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_shared_same_dir_using_env_var_current_dir(test_client_shared): """ If we build an executable in Mac and we want it to locate the shared libraries in the same directory, we have different alternatives, here we set DYLD_LIBRARY_PATH before calling the executable but running in current dir """ # Alternative 3, FAILING IN CI, set DYLD_LIBRARY_PATH in the current dir exe_folder = test_client_shared.test_exe_folder rmdir(os.path.join(test_client_shared.current_folder, exe_folder)) test_client_shared.run("create . -o hello*:shared=True") test_client_shared.run("remove '*' -c") test_client_shared.current_folder = os.path.join(test_client_shared.current_folder, exe_folder) test_client_shared.run_command("DYLD_LIBRARY_PATH=$(pwd) ./example") test_client_shared.run_command("DYLD_LIBRARY_PATH=. ./example") # This assert is not working in CI, only locally # test_client_shared.run_command("DYLD_LIBRARY_PATH=@executable_path ./test") ================================================ FILE: test/functional/toolchains/cmake/test_transitive_build_scripts.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") def test_transitive_build_scripts(): c = TestClient() scriptsa = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class ScriptsA(ConanFile): name = "scriptsa" version = "0.1" package_type = "build-scripts" exports_sources = "*.cmake" def package(self): copy(self, "*.cmake", src=self.source_folder, dst=self.package_folder) def package_info(self): self.cpp_info.builddirs = ["."] """) scriptsa_cmake = textwrap.dedent(""" function(myfunctionA) message(STATUS "MYFUNCTION CMAKE A: Hello world A!!!") endfunction() """) scriptsb = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class ScriptsA(ConanFile): name = "scriptsb" version = "0.1" package_type = "build-scripts" exports_sources = "*.cmake" def requirements(self): self.requires("scriptsa/0.1", run=True, visible=True) def package(self): copy(self, "*.cmake", src=self.source_folder, dst=self.package_folder) def package_info(self): self.cpp_info.builddirs = ["."] """) scriptsb_cmake = textwrap.dedent(""" find_package(scriptsa) function(myfunctionB) message(STATUS "MYFUNCTION CMAKE B: Hello world B!!!") myfunctionA() endfunction() """) app = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class App(ConanFile): package_type = "application" generators = "CMakeToolchain" settings = "os", "compiler", "arch", "build_type" def build_requirements(self): self.tool_requires("scriptsb/0.1") def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) app_cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(App LANGUAGES NONE) find_package(scriptsb) myfunctionB() """) c.save({"scriptsa/conanfile.py": scriptsa, "scriptsa/Findscriptsa.cmake": scriptsa_cmake, "scriptsb/conanfile.py": scriptsb, "scriptsb/Findscriptsb.cmake": scriptsb_cmake, "app/conanfile.py": app, "app/CMakeLists.txt": app_cmake}) c.run("create scriptsa") c.run("create scriptsb") c.run("build app") assert "MYFUNCTION CMAKE B: Hello world B!!!" in c.out assert "MYFUNCTION CMAKE A: Hello world A!!!" in c.out @pytest.mark.tool("cmake") def test_reuse_macro_from_dep(): """ A simple cmake script can be added from a regular requires, without even a ``find_package()`` https://github.com/conan-io/conan/issues/14013 """ c = TestClient() pkg = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): name = "pkg" version = "0.1" package_type = "static-library" exports_sources = "*.cmake" def package(self): copy(self, "*.cmake", src=self.source_folder, dst=self.package_folder) def package_info(self): self.cpp_info.builddirs = ["."] """) pkg_macros = textwrap.dedent(""" function(pkg_macro) message(STATUS "PKG MACRO WORKING!!!") endfunction() """) app = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class App(ConanFile): package_type = "application" generators = "CMakeToolchain" settings = "os", "compiler", "arch", "build_type" def requirements(self): self.requires("pkg/0.1") def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) app_cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.15) project(App LANGUAGES NONE) include(Macros) pkg_macro() """) c.save({"pkg/conanfile.py": pkg, "pkg/Macros.cmake": pkg_macros, "app/conanfile.py": app, "app/CMakeLists.txt": app_cmake}) c.run("create pkg") c.run("build app") assert "PKG MACRO WORKING!!!" in c.out ================================================ FILE: test/functional/toolchains/cmake/test_universal_binaries.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.tools import TestClient from conan.internal.util.files import rmdir @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.tool("cmake", "3.23") def test_create_universal_binary(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class mylibraryRecipe(ConanFile): package_type = "library" generators = "CMakeToolchain" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "CMakeLists.txt", "src/*", "include/*" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run("lipo -info libmylibrary.a") def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.libs = ["mylibrary"] """) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.build import can_run class mylibraryTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def layout(self): cmake_layout(self) def test(self): exe = os.path.join(self.cpp.build.bindir, "example") self.run(f"lipo {exe} -info", env="conanrun") """) client.run("new cmake_lib -d name=mylibrary -d version=1.0") client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) client.run('create . --name=mylibrary --version=1.0 ' '-s="arch=armv8|armv8.3|x86_64" --build=missing -tf=""') assert "libmylibrary.a are: x86_64 arm64 arm64e" in client.out client.run('test test_package mylibrary/1.0 -s="arch=armv8|armv8.3|x86_64"') assert "example are: x86_64 arm64 arm64e" in client.out client.run('new cmake_exe -d name=foo -d version=1.0 -d requires=mylibrary/1.0 --force') client.run('install . -s="arch=armv8|armv8.3|x86_64"') client.run_command("cmake --preset conan-release") client.run_command("cmake --build --preset conan-release") client.run_command("lipo -info ./build/Release/foo") assert "foo are: x86_64 arm64 arm64e" in client.out rmdir(os.path.join(client.current_folder, "build")) client.run('install . -s="arch=armv8|armv8.3|x86_64" ' '-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.arch"]\'') client.run_command("cmake --preset \"conan-armv8_armv8.3_x86_64-release\" ") client.run_command("cmake --build --preset \"conan-armv8_armv8.3_x86_64-release\" ") client.run_command("lipo -info './build/armv8_armv8.3_x86_64/Release/foo'") assert "foo are: x86_64 arm64 arm64e" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.tool("cmake", "3.23") def test_create_universal_binary_ninja(): client = TestClient() client.run("new cmake_lib -d name=mylibrary -d version=1.0") client.run('export .') conanfile = textwrap.dedent(""" [requires] mylibrary/1.0 [generators] CMakeToolchain CMakeDeps VirtualBuildEnv VirtualRunEnv """) cmake = textwrap.dedent(""" cmake_minimum_required(VERSION 3.23) project(ninjatest NONE) find_package(mylibrary CONFIG REQUIRED) """) client.save({"conanfile.txt": conanfile, "CMakeLists.txt": cmake}, clean_first=True) client.run('install . -s=arch="armv8|x86_64" --build=missing -of=build') with client.chdir("build"): client.run_command("cmake .. -GNinja " "-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake " "-DCMAKE_POLICY_DEFAULT_CMP0091=NEW " "-DCMAKE_BUILD_TYPE=Release") assert "expected newline, got '|'" not in client.out assert "Build files have been written to:" in client.out # test that there are no files with the "|" character in the build folder assert not any("|" in f for f in os.listdir(os.path.join(client.current_folder, "build"))) ================================================ FILE: test/functional/toolchains/cmake/test_v2_cmake_template.py ================================================ import os import pytest from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") def test_cmake_lib_template(): client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=0.1") # Local flow works client.run("build .") client.run("export-pkg .") package_folder = client.created_layout().package() assert os.path.exists(os.path.join(package_folder, "include", "hello.h")) @pytest.mark.tool("cmake") def test_cmake_lib_template_create(matrix_client_shared_debug): client = matrix_client_shared_debug client.run("new cmake_lib -d name=matrix -d version=1.0") # Create works client.run("create . --build=never") assert "matrix/1.0: Hello World Release!" in client.out client.run("create . -s build_type=Debug --build=never") assert "matrix/1.0: Hello World Debug!" in client.out # Create + shared works client.run("create . -o hello/*:shared=True --build=never") assert "matrix/1.0: Hello World Release!" in client.out @pytest.mark.tool("cmake") def test_cmake_exe_template(): client = TestClient(path_with_spaces=False) client.run("new cmake_exe -d name=greet -d version=0.1") # Local flow works client.run("build .") # Create works client.run("create .") assert "greet/0.1: Hello World Release!" in client.out client.run("create . -s build_type=Debug") assert "greet/0.1: Hello World Debug!" in client.out ================================================ FILE: test/functional/toolchains/emscripten/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/emscripten/test_emcc.py ================================================ # Test suite to check conan capabilities for cross compiling to web assembly and asmjs import textwrap import os import platform from shutil import rmtree import pytest import sys from conan.test.utils.tools import TestClient EMCC_MIN_PYTHON_VERSION = (3, 8) base_emscripten_profile = textwrap.dedent( """ [settings] build_type=Release compiler=emcc compiler.cppstd=17 compiler.libcxx=libc++ compiler.version=4.0.10 os=Emscripten [conf] tools.build:exelinkflags=['-sALLOW_MEMORY_GROWTH=1'] tools.build:sharedlinkflags=['-sALLOW_MEMORY_GROWTH=1'] # Define the emcc executable paths tools.build:compiler_executables={'c':'emcc', 'cpp':'em++'} # Set Ninja as default generator as it is faster and will sove issues on Windows tools.cmake.cmaketoolchain:generator=Ninja # Verbosity to see emcc invocations tools.compilation:verbosity=verbose # Distinguish between architectures tools.cmake.cmake_layout:build_folder_vars=['settings.build_type', 'settings.arch'] [buildenv] AR=emar NM=emnm RANLIB=emranlib STRIP=emstrip """ ) wasm32_profile = textwrap.dedent( """ include(base_emscripten_profile) [settings] arch=wasm [conf] tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=4GB', '-sINITIAL_MEMORY=64MB'] tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=4GB', '-sINITIAL_MEMORY=64MB'] """ ) wasm_64_profile = textwrap.dedent( """ include(base_emscripten_profile) [settings] arch=wasm64 [conf] tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=16GB', '-sINITIAL_MEMORY=16GB'] tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=16GB', '-sINITIAL_MEMORY=16GB'] """ ) asmjs_profile = textwrap.dedent( """ include(base_emscripten_profile) [settings] arch=asm.js [conf] tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=2GB', '-sINITIAL_MEMORY=64MB'] tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=2GB', '-sINITIAL_MEMORY=64MB'] """ ) @pytest.mark.slow @pytest.mark.tool("cmake") @pytest.mark.tool("emcc") @pytest.mark.tool("node") @pytest.mark.skipif(sys.version_info < EMCC_MIN_PYTHON_VERSION, reason = "emcc requires Python 3.8 or higher") @pytest.mark.skipif(platform.system() == "Windows", reason = "Emscripten not installed in Windows") def test_cmake_emscripten(): client = TestClient() client.run("new cmake_exe -d name=hello -d version=0.1") client.save({"wasm32": wasm32_profile, "asmjs": asmjs_profile, "base_emscripten_profile": base_emscripten_profile,}) client.run("build . -pr:h=wasm32") assert "Conan toolchain: Defining libcxx as C++ flags: -stdlib=libc++" in client.out assert os.path.exists(os.path.join(client.current_folder, "build/release-wasm" , "hello.wasm")) # Run JavaScript generated code which uses .wasm file client.run_command("node ./build/release-wasm/hello") assert "Hello World Release!" in client.out client.run("build . -pr:h=asmjs") assert "WASM=0" in client.out # No wasm should have been generated for asm.js architecture assert not os.path.exists(os.path.join(client.current_folder, "build/release-asm.js" , "hello.wasm")) client.run_command("node ./build/release-asm.js/hello") assert "Hello World Release!" in client.out @pytest.mark.slow @pytest.mark.tool("meson") @pytest.mark.tool("emcc") @pytest.mark.tool("node") @pytest.mark.skipif(sys.version_info < EMCC_MIN_PYTHON_VERSION, reason = "emcc requires Python 3.8 or higher") @pytest.mark.skipif(platform.system() == "Windows", reason = "Emscripten not installed in Windows") def test_meson_emscripten(): client = TestClient() client.run("new meson_exe -d name=hello -d version=0.1") client.save({"wasm32": wasm32_profile, "wasm64": wasm_64_profile, "asmjs": asmjs_profile, "base_emscripten_profile": base_emscripten_profile,}) client.run("build . -pr:h=wasm64") assert "C++ compiler for the host machine: em++" in client.out assert "C++ linker for the host machine: em++ ld.wasm" in client.out assert "Host machine cpu family: wasm64" in client.out assert os.path.exists(os.path.join(client.current_folder, "build", "hello.wasm")) # wasm64 only supported since node v23 so only run in MacOS where it is available if platform.system() == "Darwin": client.run_command("node ./build/hello") assert "Hello World Release!" in client.out rmtree(os.path.join(client.current_folder, "build")) client.run("build . -pr:h=asmjs") assert "C++ compiler for the host machine: em++" in client.out assert "C++ linker for the host machine: em++ ld.wasm" in client.out assert "Host machine cpu family: asm.js" in client.out assert "WASM=0" in client.out assert not os.path.exists(os.path.join(client.current_folder, "build", "hello.wasm")) client.run_command("node ./build/hello") assert "Hello World Release!" in client.out @pytest.mark.slow @pytest.mark.tool("autotools") @pytest.mark.tool("emcc") @pytest.mark.tool("node") @pytest.mark.skipif(sys.version_info < EMCC_MIN_PYTHON_VERSION, reason = "emcc requires Python 3.8 or higher") @pytest.mark.skipif(platform.system() == "Windows", reason = "Emscripten not installed in Windows") def test_autotools_emscripten(): client = TestClient(path_with_spaces=False) client.run("new autotools_exe -d name=hello -d version=0.1") client.save({"wasm32": wasm32_profile, "asmjs": asmjs_profile, "base_emscripten_profile": base_emscripten_profile,}) client.run("build . -pr:h=wasm32") assert "checking for wasm32-local-emscripten-ranlib... emranlib" in client.out assert "checking for wasm32-local-emscripten-gcc... emcc" in client.out assert "checking for wasm32-local-emscripten-ar... emar" in client.out assert "checking the archiver (emar) interface... ar" in client.out assert "checking for wasm32-local-emscripten-strip... emstrip" in client.out assert os.path.exists(os.path.join(client.current_folder, "build-release", "src", "hello.wasm")) # Run JavaScript generated code which uses .wasm file client.run_command("node ./build-release/src/hello") assert "Hello World Release!" in client.out rmtree(os.path.join(client.current_folder, "build-release")) client.run("build . -pr:h=asmjs") assert "WASM=0" in client.out # No wasm should have been generated for asm.js architecture assert not os.path.exists(os.path.join(client.current_folder, "build-release", "hello.wasm")) client.run_command("node ./build-release/src/hello") assert "Hello World Release!" in client.out @pytest.mark.slow @pytest.mark.tool("premake") @pytest.mark.tool("emcc") @pytest.mark.tool("node") @pytest.mark.skipif(sys.version_info < EMCC_MIN_PYTHON_VERSION, reason = "emcc requires Python 3.8 or higher") @pytest.mark.skipif(platform.system() != "Linux", reason = "Premake only installed in linux") def test_premake_emscripten(): client = TestClient() client.run("new premake_exe -d name=hello -d version=0.1") client.save({"wasm32": wasm32_profile, "asmjs": asmjs_profile, "base_emscripten_profile": base_emscripten_profile,}) client.run("build . -pr:h=wasm32") assert "gmake --arch=wasm32" in client.out assert os.path.exists(os.path.join(client.current_folder, "build-release", "bin", "hello.wasm")) # Run JavaScript generated code which uses .wasm file client.run_command("node ./build-release/bin/hello") assert "Hello World Release!" in client.out rmtree(os.path.join(client.current_folder, "build-release")) client.run("build . -pr:h=asmjs") assert "WASM=0" in client.out # No wasm should have been generated for asm.js architecture assert not os.path.exists(os.path.join(client.current_folder, "build-release", "bin", "hello.wasm")) client.run_command("node ./build-release/bin/hello") assert "Hello World Release!" in client.out # TODO: test_bazel_emscripten(): need WIP new bazel toolchain # TODO: test_msbuild_emscripten(): give support to msbuild ================================================ FILE: test/functional/toolchains/env/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/env/test_complete.py ================================================ import textwrap import pytest from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") def test_cmake_virtualenv(matrix_client): client = matrix_client cmakewrapper = textwrap.dedent(r""" from conan import ConanFile import os from conan.tools.files import save, chdir class Pkg(ConanFile): def package(self): with chdir(self, self.package_folder): save(self, "cmake.bat", "@echo off\necho MYCMAKE WRAPPER!!\ncmake.exe %*") save(self, "cmake.sh", 'echo MYCMAKE WRAPPER!!\ncmake "$@"') os.chmod("cmake.sh", 0o777) def package_info(self): # Custom buildenv not defined by cpp_info self.buildenv_info.prepend_path("PATH", self.package_folder) """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class App(ConanFile): settings = "os", "arch", "compiler", "build_type" exports_sources = "CMakeLists.txt", "main.cpp" requires = "matrix/1.0" build_requires = "cmakewrapper/0.1" generators = "CMakeDeps", "CMakeToolchain", "VirtualBuildEnv" def build(self): cmake = CMake(self) if self.settings.os != "Windows": cmake._cmake_program = "cmake.sh" # VERY DIRTY HACK cmake.configure() cmake.build() """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) find_package(matrix) add_executable(app main.cpp) target_link_libraries(app matrix::matrix) """) client.save({"cmakewrapper/conanfile.py": cmakewrapper, "consumer/conanfile.py": consumer, "consumer/main.cpp": gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"]), "consumer/CMakeLists.txt": cmakelists}, clean_first=True) client.run("create cmakewrapper --name=cmakewrapper --version=0.1") client.run("create consumer --name=consumer --version=0.1") assert "MYCMAKE WRAPPER!!" in client.out assert "consumer/0.1: Created package" in client.out @pytest.mark.tool("cmake") def test_complete(): client = TestClient() client.run("new cmake_lib -d name=myopenssl -d version=1.0") client.run("create . -o myopenssl/*:shared=True") client.run("create . -o myopenssl/*:shared=True -s build_type=Debug") mycmake_main = gen_function_cpp(name="main", msg="mycmake", includes=["myopenssl"], calls=["myopenssl"]) mycmake_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class App(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "myopenssl/1.0" default_options = {"myopenssl:shared": True} generators = "CMakeDeps", "CMakeToolchain", "VirtualBuildEnv" exports_sources = "*" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): src = str(self.settings.build_type) if self.settings.os == "Windows" else "" copy(self, "mycmake*", os.path.join(self.source_folder, src), os.path.join(self.package_folder, "bin")) def package_info(self): self.cpp_info.bindirs = ["bin"] """) mycmake_cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyCmake CXX) find_package(myopenssl REQUIRED) add_executable(mycmake main.cpp) target_link_libraries(mycmake PRIVATE myopenssl::myopenssl) """) client.save({"conanfile.py": mycmake_conanfile, "CMakeLists.txt": mycmake_cmakelists, "main.cpp": mycmake_main}, clean_first=True) client.run("create . --name=mycmake --version=1.0", assert_error=True) assert "The usage of package names `myopenssl:shared` in options is deprecated, " \ "use a pattern like `myopenssl/*:shared` instead" in client.out client.run("create . --name=mycmake --version=1.0 -o=:shared=True", assert_error=True) assert "Invalid empty package" in client.out # Fix the default options and repeat the create fixed_cf = mycmake_conanfile.replace('default_options = {"myopenssl:shared": True}', 'default_options = {"myopenssl*:shared": True}') client.save({"conanfile.py": fixed_cf}) client.run("create . --name=mycmake --version=1.0") mylib = textwrap.dedent(r""" from conan import ConanFile import os from conan.tools.cmake import CMake class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" build_requires = "mycmake/1.0" requires = "myopenssl/1.0" default_options = {"myopenssl/*:shared": True} exports_sources = "CMakeLists.txt", "main.cpp" generators = "CMakeDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run("mycmake") self.output.info("RUNNING MYAPP") if self.settings.os == "Windows": self.run(os.sep.join([".", str(self.settings.build_type), "myapp"]), env="conanrun") else: self.run(os.sep.join([".", "myapp"]), env=["conanrun"]) """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyApp CXX) find_package(myopenssl) add_executable(myapp main.cpp) target_link_libraries(myapp myopenssl::myopenssl) """) client.save({"conanfile.py": mylib, "main.cpp": gen_function_cpp(name="main", msg="myapp", includes=["myopenssl"], calls=["myopenssl"]), "CMakeLists.txt": cmakelists}, clean_first=True) client.run("create . --name=myapp --version=0.1 -s:b build_type=Release -s:h build_type=Debug") first, last = str(client.out).split("RUNNING MYAPP") assert "mycmake: Release!" in first assert "myopenssl/1.0: Hello World Release!" in first assert "myapp: Debug!" in last assert "myopenssl/1.0: Hello World Debug!" in last ================================================ FILE: test/functional/toolchains/env/test_virtualenv_powershell.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.mocks import ConanFileMock from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.tools.env.environment import environment_wrap_command @pytest.fixture def client(): # We use special characters and spaces, to check everything works # https://github.com/conan-io/conan/issues/12648 # FIXME: This path still fails the creation of the deactivation script cache_folder = os.path.join(temp_folder(), "[sub] folder") client = TestClient(cache_folder) conanfile = str(GenConanfile("pkg", "0.1")) conanfile += """ def package_info(self): self.buildenv_info.define_path("MYPATH1", "c:/path/to/ar") self.runenv_info.define("MYVAR1", 'some nice content\" with quotes') """ client.save({"conanfile.py": conanfile}) client.run("create .") client.save_home({"global.conf": "tools.env.virtualenv:powershell=powershell.exe\n"}) return client @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows powershell") def test_virtualenv(client): conanfile = textwrap.dedent(""" import os from conan import ConanFile class ConanFileToolsTest(ConanFile): name = "app" version = "0.1" requires = "pkg/0.1" def build(self): self.output.info("----------BUILD----------------") self.run("set") self.output.info("----------RUN----------------") self.run("set", env="conanrun") """) client.save({"conanfile.py": conanfile}) client.run("install . -s:b os=Windows -s:h os=Windows") assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.sh")) assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.bat")) assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.sh")) assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.bat")) with open(os.path.join(client.current_folder, "conanbuildenv.ps1"), "r", encoding="utf-16") as f: buildenv = f.read() assert '$env:MYPATH1="c:/path/to/ar"' in buildenv build = client.load("conanbuild.ps1") assert "conanbuildenv.ps1" in build with open(os.path.join(client.current_folder, "conanrunenv.ps1"), "r", encoding="utf-16") as f: run_contents = f.read() assert '$env:MYVAR1="some nice content`" with quotes"' in run_contents client.run("create .") assert "MYPATH1=c:/path/to/ar" in client.out assert 'MYVAR1=some nice content" with quotes' in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows powershell") @pytest.mark.parametrize("powershell", [True, "powershell.exe", "pwsh"]) def test_virtualenv_test_package(powershell): """ The test_package could crash if not cleaning correctly the test_package output folder. This will still crassh if the layout is not creating different build folders https://github.com/conan-io/conan/issues/12764 """ client = TestClient() test_package = textwrap.dedent(r""" from conan import ConanFile from conan.tools.files import save class Test(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def generate(self): # Emulates vcvars.bat behavior save(self, "myenv.bat", "echo MYENV!!!\nset MYVC_CUSTOMVAR1=PATATA1") self.env_scripts.setdefault("build", []).append("myenv.bat") save(self, "myps.ps1", "echo MYPS1!!!!\n$env:MYVC_CUSTOMVAR2=\"PATATA2\"") self.env_scripts.setdefault("build", []).append("myps.ps1") def layout(self): self.folders.build = "mybuild" self.folders.generators = "mybuild" def test(self): self.run('mkdir "hello world"') self.run("dir") self.run('cd "hello world"') self.run("set MYVC_CUSTOMVAR1") self.run("set MYVC_CUSTOMVAR2") """) client.save({"conanfile.py": GenConanfile("pkg", "1.0"), "test_package/conanfile.py": test_package}) client.run("create .") assert "hello world" in client.out assert "MYENV!!!" in client.out assert "MYPS1!!!!" in client.out assert "MYVC_CUSTOMVAR1=PATATA1" in client.out assert "MYVC_CUSTOMVAR2=PATATA2" in client.out # This was crashing because the .ps1 of test_package was not being cleaned client.run(f"create . -c tools.env.virtualenv:powershell={powershell}") assert "hello world" in client.out assert "MYENV!!!" in client.out assert "MYPS1!!!!" in client.out assert "MYVC_CUSTOMVAR1=PATATA1" in client.out assert "MYVC_CUSTOMVAR2=PATATA2" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows powershell") @pytest.mark.parametrize("powershell", [True, "powershell.exe", "pwsh"]) def test_vcvars(powershell): client = TestClient() conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout, CMakeToolchain, CMakeDeps from conan.tools.env import VirtualBuildEnv class Conan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = 'CMakeDeps', 'CMakeToolchain' def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() """) hello_cpp = gen_function_cpp(name="main") cmakelists = gen_cmakelists(appname="hello", appsources=["hello.cpp"]) client.save({"conanfile.py": conanfile, "hello.cpp": hello_cpp, "CMakeLists.txt": cmakelists}) powershell_exe = "powershell.exe" if powershell == "powershell" else "pwsh" client.run(f"build . -c tools.env.virtualenv:powershell={powershell} -c tools.cmake.cmaketoolchain:generator=Ninja") client.run_command(rf'{powershell_exe} -Command ".\build\Release\generators\conanbuild.ps1; dir env:"') #check the conanbuid.ps1 activation message assert "conanvcvars.ps1: Activated environment" in client.out #check that the new env variables are set assert "VSCMD_ARG_VCVARS_VER" in client.out client.run_command(rf'{powershell_exe} -Command ".\build\Release\generators\conanvcvars.ps1"') assert client.out.strip() == "conanvcvars.ps1: Activated environment" conanbuild = client.load(r".\build\Release\generators\conanbuild.ps1") vcvars_ps1 = client.load(r".\build\Release\generators\conanvcvars.ps1") #check that the conanvcvars.ps1 is being added to the conanbuild.ps1 assert "conanvcvars.ps1" in conanbuild #check that the conanvcvars.ps1 is setting the environment assert "conanvcvars.bat&set" in vcvars_ps1 @pytest.mark.skipif(platform.system() != "Windows", reason="Test for powershell") @pytest.mark.parametrize("powershell", [True, "powershell.exe", "pwsh", "powershell.exe -NoProfile", "pwsh -NoProfile"]) def test_concatenate_build_and_run_env(powershell): # this tests that if we have both build and run env, they are concatenated correctly when using # powershell client = TestClient(path_with_spaces=True) compiler_bat = "@echo off\necho MYTOOL {}!!\n" conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): exports_sources = "*" package_type = "application" def package(self): copy(self, "*", self.build_folder, os.path.join(self.package_folder, "bin")) """) num_deps = 2 for i in range(num_deps): client.save({"conanfile.py": conanfile, "mycompiler{}.bat".format(i): compiler_bat.format(i)}) client.run("create . --name=pkg{} --version=0.1".format(i)) conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): settings = "os" tool_requires = "pkg0/0.1" requires = "pkg1/0.1" generators = "VirtualBuildEnv", "VirtualRunEnv" """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run(f'install . -c tools.env.virtualenv:powershell="{powershell}"') conanfile = ConanFileMock() conanfile.conf.define("tools.env.virtualenv:powershell", powershell) cmd = environment_wrap_command(conanfile,["conanrunenv", "conanbuildenv"], client.current_folder,"mycompiler0.bat") client.run_command(cmd) assert "MYTOOL 0!!" in client.out cmd = environment_wrap_command(conanfile,["conanrunenv", "conanbuildenv"], client.current_folder,"mycompiler1.bat") client.run_command(cmd) assert "MYTOOL 1!!" in client.out @pytest.mark.parametrize("powershell", [None, True, False]) def test_powershell_deprecated_message(powershell): client = TestClient(light=True) conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): settings = "os" name = "pkg" version = "0.1" """) client.save({"conanfile.py": conanfile}) powershell_arg = f'-c tools.env.virtualenv:powershell={powershell}' if powershell is not None else "" client.run(f'install . {powershell_arg}') # only show message if the value is set to False or True if not set do not show message if powershell is not None: assert "Boolean values for 'tools.env.virtualenv:powershell' are deprecated" in client.out else: assert "Boolean values for 'tools.env.virtualenv:powershell' are deprecated" not in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Test for powershell") @pytest.mark.parametrize("powershell", [True, "pwsh", "powershell.exe"]) def test_powershell_quoting(powershell): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): settings = "os" name = "pkg" version = "0.1" def build(self): self.run('python -c "print(\\'Hello World\\')"', scope="build") """) client.save({"conanfile.py": conanfile}) client.run(f'create . -c tools.env.virtualenv:powershell={powershell}') assert "Hello World" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_verbosity_flag(): tc = TestClient() tc.run("new cmake_lib -d name=pkg -d version=1.0") tc.run('create . -tf="" -c tools.build:verbosity=verbose ' '-c tools.env.virtualenv:powershell=powershell.exe') assert "/verbosity:Detailed" in tc.out assert "-verbosity:Detailed" not in tc.out ================================================ FILE: test/functional/toolchains/gnu/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/gnu/autotools/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/gnu/autotools/test_android.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.tools import TestClient from test.conftest import tools_locations @pytest.mark.parametrize("arch, expected_arch", [('armv8', 'aarch64'), ('armv7', 'arm'), ('x86', 'i386'), ('x86_64', 'x86_64') ]) @pytest.mark.tool("android_ndk") @pytest.mark.tool("autotools") @pytest.mark.skipif(platform.system() != "Darwin", reason="NDK only installed on MAC") def test_android_autotools_toolchain_cross_compiling(arch, expected_arch): profile_host = textwrap.dedent(""" include(default) [settings] os = Android os.api_level = 21 arch = {arch} [conf] tools.android:ndk_path={ndk_path} """) ndk_path = tools_locations["android_ndk"]["system"]["path"][platform.system()] profile_host = profile_host.format( arch=arch, ndk_path=ndk_path ) client = TestClient(path_with_spaces=False) client.run("new autotools_lib -d name=hello -d version=1.0") client.save({"profile_host": profile_host}) client.run("build . --profile:build=default --profile:host=profile_host") libhello = os.path.join("build-release", "src", ".libs", "libhello.a") # Check binaries architecture client.run_command('objdump -f "%s"' % libhello) assert "architecture: %s" % expected_arch in client.out ================================================ FILE: test/functional/toolchains/gnu/autotools/test_apple_toolchain.py ================================================ import os import platform import textwrap import pytest from conan.tools.apple.apple import _to_apple_arch from conan.test.assets.autotools import gen_makefile from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.tools import TestClient makefile = gen_makefile(apps=["app"], libs=["hello"]) conanfile_py = textwrap.dedent(""" from conan import ConanFile, tools from conan.tools.gnu import Autotools class App(ConanFile): settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} generators = "AutotoolsToolchain" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def build(self): env_build = Autotools(self) env_build.make() """) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("config", [("x86_64", "Macos", "10.14", None), ("armv8", "iOS", "10.0", "iphoneos"), ("x86_64", "iOS", "10.0", "iphonesimulator"), ("armv8", "Macos", "10.14", None) # M1 ]) def test_makefile_arch(config): arch, os_, os_version, os_sdk = config profile = textwrap.dedent(""" include(default) [settings] os = {os} {os_sdk} os.version = {os_version} arch = {arch} """).format(os=os_, arch=arch, os_version=os_version, os_sdk="os.sdk = " + os_sdk if os_sdk else "") t = TestClient() hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello") main_cpp = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) t.save({"Makefile": makefile, "hello.h": hello_h, "hello.cpp": hello_cpp, "app.cpp": main_cpp, "conanfile.py": conanfile_py, "profile": profile}) t.run("install . --profile:host=profile --profile:build=default") t.run("build . --profile:host=profile --profile:build=default") libhello = os.path.join(t.current_folder, "libhello.a") app = os.path.join(t.current_folder, "app") assert os.path.isfile(libhello) assert os.path.isfile(app) expected_arch = _to_apple_arch(arch) t.run_command('lipo -info "%s"' % libhello) assert "architecture: %s" % expected_arch in t.out t.run_command('lipo -info "%s"' % app) assert "architecture: %s" % expected_arch in t.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("arch", ["x86_64", "armv8"]) def test_catalyst(arch): profile = textwrap.dedent(""" include(default) [settings] os = Macos os.version = 14.6 os.subsystem = catalyst os.subsystem.ios_version = 16.1 arch = {arch} [buildenv] DEVELOPER_DIR=/Applications/Xcode_16.0.app/Contents/Developer """).format(arch=arch) t = TestClient() hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello") main_cpp = textwrap.dedent(""" #include "hello.h" #include #include int main() { #if TARGET_OS_MACCATALYST std::cout << "running catalyst " << __IPHONE_OS_VERSION_MIN_REQUIRED << std::endl; #else #error "not building for Apple Catalyst" #endif } """) t.save({"Makefile": makefile, "hello.h": hello_h, "hello.cpp": hello_cpp, "app.cpp": main_cpp, "conanfile.py": conanfile_py, "profile": profile}) t.run("install . --profile:host=profile --profile:build=default") t.run("build . --profile:host=profile --profile:build=default") libhello = os.path.join(t.current_folder, "libhello.a") app = os.path.join(t.current_folder, "app") assert os.path.isfile(libhello) assert os.path.isfile(app) expected_arch = _to_apple_arch(arch) t.run_command('lipo -info "%s"' % libhello) assert "architecture: %s" % expected_arch in t.out t.run_command('lipo -info "%s"' % app) assert "architecture: %s" % expected_arch in t.out #FIXME: recover when ci is fixed for M2 #t.run_command('"%s"' % app) #assert "running catalyst 160100" in t.out ================================================ FILE: test/functional/toolchains/gnu/autotools/test_basic.py ================================================ import os import platform import textwrap import time import re import pytest from conan.test.utils.mocks import ConanFileMock from conan.tools.env.environment import environment_wrap_command from conan.test.assets.autotools import gen_makefile_am, gen_configure_ac, gen_makefile from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp from test.functional.utils import check_exe_run, check_vs_runtime from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_autotools(matrix_client_nospace): client = matrix_client_nospace main = gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"]) makefile_am = gen_makefile_am(main="main", main_srcs="main.cpp") configure_ac = gen_configure_ac() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools class TestConan(ConanFile): requires = "matrix/1.0" settings = "os", "compiler", "arch", "build_type" exports_sources = "configure.ac", "Makefile.am", "main.cpp" generators = "AutotoolsDeps", "AutotoolsToolchain" def build(self): self.run("aclocal") self.run("autoconf") self.run("automake --add-missing --foreign") autotools = Autotools(self) autotools.configure() autotools.make() """) client.save({"conanfile.py": conanfile, "configure.ac": configure_ac, "Makefile.am": makefile_am, "main.cpp": main}, clean_first=True) client.run("build .") client.run_command("./main") cxx11_abi = 1 if platform.system() == "Linux" else None compiler = "gcc" if platform.system() == "Linux" else "apple-clang" host_arch = client.get_default_host_profile().settings['arch'] check_exe_run(client.out, "main", compiler, None, "Release", host_arch, None, cxx11_abi=cxx11_abi) assert "matrix/1.0: Hello World Release!" in client.out def build_windows_subsystem(profile, make_program, subsystem): """ The AutotoolsDeps can be used also in pure Makefiles, if the makefiles follow the Autotools conventions This doesn't run in bash at all, not win_bash, pure Windows terminal """ # FIXME: cygwin in CI (my local machine works) seems broken for path with spaces client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=0.1") # TODO: Test Windows subsystems in CMake, at least msys is broken os.rename(os.path.join(client.current_folder, "test_package"), os.path.join(client.current_folder, "test_package2")) client.save({"profile": profile}) client.run("create . --profile=profile") main = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) makefile = gen_makefile(apps=["app"]) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools, AutotoolsDeps class TestConan(ConanFile): requires = "hello/0.1" settings = "os", "compiler", "arch", "build_type" exports_sources = "Makefile" generators = "AutotoolsDeps", "AutotoolsToolchain" def build(self): autotools = Autotools(self) autotools.make() """) client.save({"app.cpp": main, "Makefile": makefile, "conanfile.py": conanfile, "profile": profile}, clean_first=True) client.run("install . --profile=profile") cmd = environment_wrap_command(ConanFileMock(), ["conanbuildenv", "conanautotoolstoolchain", "conanautotoolsdeps"], client.current_folder, make_program) client.run_command(cmd) client.run_command("app") # TODO: fill compiler version when ready check_exe_run(client.out, "main", "gcc", None, "Release", "x86_64", None, subsystem=subsystem) assert "hello/0.1: Hello World Release!" in client.out check_vs_runtime("app.exe", client, vs_version="15", build_type="Release", architecture="amd64", static_runtime=False, subsystem=subsystem) client.save({"app.cpp": gen_function_cpp(name="main", msg="main2", includes=["hello"], calls=["hello"])}) # Make sure it is newer t = time.time() + 1 os.utime(os.path.join(client.current_folder, "app.cpp"), (t, t)) client.run("build . --profile=profile") client.run_command("app") # TODO: fill compiler version when ready check_exe_run(client.out, "main2", "gcc", None, "Release", "x86_64", None, cxx11_abi=0, subsystem=subsystem) assert "hello/0.1: Hello World Release!" in client.out return client.out @pytest.mark.tool("cygwin") @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows") def test_autotoolsdeps_cygwin(): # TODO: This test seems broken locally, need to really verify is passing in CI gcc = textwrap.dedent(""" [settings] os=Windows os.subsystem=cygwin compiler=gcc compiler.version=4.9 compiler.libcxx=libstdc++ arch=x86_64 build_type=Release """) build_windows_subsystem(gcc, make_program="make", subsystem="cygwin") @pytest.mark.tool("mingw64") @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows") def test_autotoolsdeps_mingw_msys(): # FIXME: Missing subsystem to model mingw libstdc++ gcc = textwrap.dedent(""" [settings] os=Windows compiler=gcc compiler.version=4.9 compiler.libcxx=libstdc++ arch=x86_64 build_type=Release """) build_windows_subsystem(gcc, make_program="mingw32-make", subsystem="mingw64") @pytest.mark.tool("msys2") @pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows") # If we use the cmake inside msys2, it fails, so better force our own cmake @pytest.mark.tool("cmake", "3.19") def test_autotoolsdeps_msys(): gcc = textwrap.dedent(""" [settings] os=Windows os.subsystem=msys2 compiler=gcc compiler.version=4.9 compiler.libcxx=libstdc++ arch=x86_64 build_type=Release """) build_windows_subsystem(gcc, make_program="make", subsystem="msys2") @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_install_output_directories(matrix_client_nospace): """ If we change the libdirs of the cpp.package, as we are doing cmake.install, the output directory for the libraries is changed """ client = matrix_client_nospace consumer_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools class TestConan(ConanFile): requires = "matrix/1.0" settings = "os", "compiler", "arch", "build_type" exports_sources = "configure.ac", "Makefile.am", "main.cpp", "consumer.h" generators = "AutotoolsDeps", "AutotoolsToolchain" def layout(self): self.folders.build = "." self.cpp.package.bindirs = ["mybin"] def build(self): self.run("aclocal") self.run("autoconf") self.run("automake --add-missing --foreign") autotools = Autotools(self) autotools.configure() autotools.make() autotools.install() """) main = gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"]) makefile_am = gen_makefile_am(main="main", main_srcs="main.cpp") configure_ac = gen_configure_ac() client.save({"conanfile.py": consumer_conanfile, "configure.ac": configure_ac, "Makefile.am": makefile_am, "main.cpp": main}, clean_first=True) client.run("create . --name=zlib --version=1.2.11") p_folder = client.created_layout().package() assert os.path.exists(os.path.join(p_folder, "mybin", "main")) assert not os.path.exists(os.path.join(p_folder, "bin")) @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_autotools_with_pkgconfigdeps(): client = TestClient(path_with_spaces=False) client.run("new cmake_lib -d name=hello -d version=1.0") client.run("create .") consumer_conanfile = textwrap.dedent(""" [requires] hello/1.0 [generators] AutotoolsToolchain PkgConfigDeps """) client.save({"conanfile.txt": consumer_conanfile}, clean_first=True) client.run("install .") client.run_command(". ./conanautotoolstoolchain.sh && " "pkg-config --cflags hello && " "pkg-config --libs-only-l hello && " "pkg-config --libs-only-L --libs-only-other hello") assert re.search("I.*/p/include", str(client.out)) assert "-lhello" in client.out assert re.search("L.*/p/lib", str(client.out)) @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_autotools_option_checking(): # https://github.com/conan-io/conan/issues/11265 client = TestClient(path_with_spaces=False) client.run("new autotools_lib -d name=mylib -d version=1.0") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools from conan.tools.layout import basic_layout from conan.tools.build import cross_building class MylibTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "AutotoolsDeps" def requirements(self): self.requires(self.tested_reference_str) def generate(self): at_toolchain = AutotoolsToolchain(self) # we override the default shared/static flags here at_toolchain.configure_args = ['--enable-option-checking=fatal'] at_toolchain.generate() def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def layout(self): basic_layout(self) def test(self): if not cross_building(self): cmd = os.path.join(self.cpp.build.bindirs[0], "main") self.run(cmd, env="conanrun") """) client.save({"test_package/conanfile.py": conanfile}) client.run("create . -tf=\"\"") # check that the shared flags are not added to the exe's configure, making it fail client.run("test test_package mylib/1.0@") assert "configure: error: unrecognized options: --disable-shared, --enable-static, --with-pic" \ not in client.out @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_autotools_arguments_override(): client = TestClient(path_with_spaces=False) client.run("new autotools_lib -d name=mylib -d version=1.0") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools from conan.tools.layout import basic_layout class MyLibConan(ConanFile): name = "mylib" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" exports_sources = "configure.ac", "Makefile.am", "src/*" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def layout(self): basic_layout(self) def generate(self): at_toolchain = AutotoolsToolchain(self) at_toolchain.configure_args = ['--disable-shared'] at_toolchain.make_args = ['--warn-undefined-variables'] at_toolchain.autoreconf_args = ['--verbose'] at_toolchain.generate() def build(self): autotools = Autotools(self) autotools.autoreconf(args=['--install']) autotools.configure(args=['--prefix=/', '--libdir=${prefix}/customlibfolder', '--includedir=${prefix}/customincludefolder', '--pdfdir=${prefix}/res']) autotools.make(args=['--keep-going']) def package(self): autotools = Autotools(self) autotools.install(args=['DESTDIR={}/somefolder'.format(self.package_folder)]) def package_info(self): self.cpp_info.libs = ["mylib"] self.cpp_info.libdirs = ["somefolder/customlibfolder"] self.cpp_info.includedirs = ["somefolder/customincludefolder"] """) client.save({"conanfile.py": conanfile}) client.run("create . -tf=\"\"") # autoreconf args --force that is default should not be there assert "--force" not in client.out assert "--install" in client.out # we override the default DESTDIR in the install assert re.search("^.*make install .*DESTDIR=(.*)/somefolder.*$", str(client.out), re.MULTILINE) # we did override the default install args for arg in ['--bindir=${prefix}/bin', '--sbindir=${prefix}/bin', '--libdir=${prefix}/lib', '--includedir=${prefix}/include', '--oldincludedir=${prefix}/include', '--datarootdir=${prefix}/res']: assert arg not in client.out # and use our custom arguments for arg in ['--prefix=/', '--libdir=${prefix}/customlibfolder', '--includedir=${prefix}/customincludefolder', '--pdfdir=${prefix}/res']: assert arg in client.out # check the other arguments we set are there assert "--disable-shared" in client.out assert "--warn-undefined-variables" in client.out assert "--verbose" in client.out assert "--keep-going" in client.out client.run("test test_package mylib/1.0@") assert "mylib/1.0: Hello World Release!" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only MSVC") def test_msvc_extra_flag(): profile = textwrap.dedent(""" [settings] os=Windows compiler=msvc compiler.version=193 compiler.runtime=dynamic arch=x86_64 build_type=Release """) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("AutotoolsToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load("conanautotoolstoolchain{}".format('.bat')) assert 'set "CXXFLAGS=%CXXFLAGS% -MD -O2 -Ob2 -FS"' in toolchain assert 'set "CFLAGS=%CFLAGS% -MD -O2 -Ob2 -FS"' in toolchain # now verify not duplicated conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): tc = AutotoolsToolchain(self) tc.extra_cxxflags.append("-FS") tc.extra_cflags.append("-FS") tc.generate() """) client.save({"conanfile.py": conanfile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load("conanautotoolstoolchain{}".format('.bat')) assert 'set "CXXFLAGS=%CXXFLAGS% -MD -O2 -Ob2 -FS"' in toolchain assert 'set "CFLAGS=%CFLAGS% -MD -O2 -Ob2 -FS"' in toolchain ================================================ FILE: test/functional/toolchains/gnu/autotools/test_crossbuild_triplet.py ================================================ import platform import pytest import textwrap from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() not in ["Darwin", "Linux"], reason="Autotools on Linux or macOS") def test_crossbuild_triplet_from_conf(): settings_yml = textwrap.dedent(""" os: Linux: Windows: arch: [x86_64, hexagon] compiler: gcc: version: ["10", "11"] libcxx: [libstdc++11] build_type: [None, Debug, Release] """) host_profile = textwrap.dedent(""" [settings] os=Linux arch=hexagon compiler=gcc compiler.version=10 compiler.libcxx=libstdc++11 build_type=Release [conf] tools.gnu:host_triplet=hexagon-acme-linux-gnu """) build_profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=11 compiler.libcxx=libstdc++11 build_type=Release """) client = TestClient(path_with_spaces=False) client.save({client.paths.settings_path: settings_yml}) client.save({"host_profile": host_profile}) client.save({"build_profile": build_profile}) client.run("new autotools_lib -d name=hello -d version=0.1") client.run("create . --profile:build=build_profile --profile:host=host_profile -tf=\"\"") assert "--host=hexagon-acme-linux-gnu" in client.out assert "checking host system type... hexagon-acme-linux-gnu" in client.out ================================================ FILE: test/functional/toolchains/gnu/autotools/test_ios.py ================================================ import platform import textwrap import pytest from conan.tools.build import load_toolchain_args from conan.test.assets.autotools import gen_makefile_am, gen_configure_ac from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires Xcode") @pytest.mark.tool("cmake") @pytest.mark.tool("autotools") def test_ios(): profile = textwrap.dedent(""" include(default) [settings] os=iOS os.sdk=iphoneos os.version=12.0 arch=armv8 """) client = TestClient(path_with_spaces=False) client.save({"ios-armv8": profile}, clean_first=True) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("create . --profile:build=default --profile:host=ios-armv8 -tf=\"\"") main = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) makefile_am = gen_makefile_am(main="main", main_srcs="main.cpp") configure_ac = gen_configure_ac() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools class TestConan(ConanFile): requires = "hello/0.1" settings = "os", "compiler", "arch", "build_type" exports_sources = "configure.ac", "Makefile.am", "main.cpp" generators = "AutotoolsToolchain", "AutotoolsDeps" def layout(self): self.cpp.package.resdirs = ["res"] def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() """) client.save({"conanfile.py": conanfile, "configure.ac": configure_ac, "Makefile.am": makefile_am, "main.cpp": main, "ios-armv8": profile}, clean_first=True) client.run("build . --profile:build=default --profile:host=ios-armv8") client.run_command("lipo -info main") assert "Non-fat file: main is architecture: arm64" in client.out client.run_command("vtool -show-build main") assert "platform IOS" in client.out assert "minos 12.0" in client.out conanbuild = load_toolchain_args(client.current_folder) configure_args = conanbuild["configure_args"] make_args = conanbuild["make_args"] autoreconf_args = conanbuild["autoreconf_args"] build_arch = client.api.profiles.get_profile([client.api.profiles.get_default_build()]).settings['arch'] build_arch = "aarch64" if build_arch == "armv8" else build_arch assert configure_args == "--prefix=/ '--bindir=${prefix}/bin' '--sbindir=${prefix}/bin' " \ "'--libdir=${prefix}/lib' '--includedir=${prefix}/include' " \ "'--oldincludedir=${prefix}/include' '--datarootdir=${prefix}/res' " \ f"--host=aarch64-apple-ios --build={build_arch}-apple-darwin" assert make_args == "" assert autoreconf_args == "--force --install" ================================================ FILE: test/functional/toolchains/gnu/autotools/test_win_bash.py ================================================ import platform import textwrap import os import pytest from conan.test.assets.autotools import gen_makefile_am, gen_configure_ac, gen_makefile from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp from test.conftest import tools_locations from test.functional.utils import check_exe_run, check_vs_runtime from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") @pytest.mark.tool("msys2") def test_autotools_bash_complete(): client = TestClient(path_with_spaces=False) profile_win = textwrap.dedent(f""" include(default) [conf] tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path=bash """) main = gen_function_cpp(name="main") # The autotools support for "cl" compiler (VS) is very limited, linking with deps doesn't # work but building a simple app do makefile_am = gen_makefile_am(main="main", main_srcs="main.cpp") configure_ac = gen_configure_ac() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools class TestConan(ConanFile): settings = "os", "compiler", "arch", "build_type" exports_sources = "configure.ac", "Makefile.am", "main.cpp" generators = "AutotoolsToolchain" win_bash = True def build(self): # These commands will run in bash activating first the vcvars and # then inside the bash activating the self.run("aclocal") self.run("autoconf") self.run("automake --add-missing --foreign") autotools = Autotools(self) autotools.configure() autotools.make() autotools.install() """) client.save({"conanfile.py": conanfile, "configure.ac": configure_ac, "Makefile.am": makefile_am, "main.cpp": main, "profile_win": profile_win}) client.run("build . -pr=profile_win") client.run_command("main.exe") check_exe_run(client.out, "main", "msvc", None, "Release", "x86_64", None) bat_contents = client.load("conanbuild.bat") assert "conanvcvars.bat" in bat_contents @pytest.mark.slow @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") @pytest.mark.tool("msys2") @pytest.mark.tool("clang", "20") @pytest.mark.parametrize("frontend", ("clang", "clang-cl")) @pytest.mark.parametrize("runtime", ("static", "dynamic")) @pytest.mark.parametrize("build_type", ("Debug", "Release")) def test_autotools_bash_complete_clang(frontend, runtime, build_type): client = TestClient(path_with_spaces=False) # Problem is that msys2 also has clang in the path, so we need to make it explicit clangpath = tools_locations["clang"]["20"]["path"]["Windows"] # compilers c, cpp = ("clang", "clang++") if frontend == "clang" else ("clang-cl", "clang-cl") comps = f'{{"cpp":"{cpp}", "c":"{c}", "rc":"{c}"}}' profile_win = textwrap.dedent(f""" [settings] os=Windows arch=x86_64 build_type={build_type} compiler=clang compiler.version=20 compiler.cppstd=14 compiler.runtime_version=v144 compiler.runtime={runtime} [conf] tools.build:compiler_executables={comps} tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path=bash tools.compilation:verbosity=verbose [buildenv] PATH=+(path){clangpath} """) main = gen_function_cpp(name="main") # The autotools support for "cl" compiler (VS) is very limited, linking with deps doesn't # work but building a simple app do makefile_am = gen_makefile_am(main="main", main_srcs="main.cpp") configure_ac = gen_configure_ac() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools class TestConan(ConanFile): settings = "os", "compiler", "arch", "build_type" exports_sources = "configure.ac", "Makefile.am", "main.cpp" generators = "AutotoolsToolchain" win_bash = True def build(self): # These commands will run in bash activating first the vcvars and # then inside the bash activating the self.run("aclocal") self.run("autoconf") self.run("automake --add-missing --foreign") autotools = Autotools(self) autotools.configure() autotools.make() autotools.install() """) client.save({"conanfile.py": conanfile, "configure.ac": configure_ac, "Makefile.am": makefile_am, "main.cpp": main, "profile_win": profile_win}) client.run("build . -pr=profile_win") client.run_command("main.exe") assert "__GNUC__" not in client.out assert "main __clang_major__20" in client.out check_exe_run(client.out, "main", "clang", None, build_type, "x86_64", None) bat_contents = client.load("conanbuild.bat") assert "conanvcvars.bat" in bat_contents static_runtime = runtime == "static" check_vs_runtime("main.exe", client, "17", build_type=build_type, static_runtime=static_runtime) @pytest.mark.parametrize("scope", ["build", "run"]) @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_add_msys2_path_automatically(scope): """ Check that commands like ar, autoconf, etc, that are in the /usr/bin folder together with the bash.exe, can be automaticallly used when running in windows bash, without user extra addition to [buildenv] of that msys64/usr/bin path # https://github.com/conan-io/conan/issues/12110 """ client = TestClient(path_with_spaces=False) bash_path = None try: bash_path = tools_locations["msys2"]["system"]["path"]["Windows"] + "/bash.exe" except KeyError: pytest.skip("msys2 path not defined") client.save_home({"global.conf": textwrap.dedent(""" tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path={} """.format(bash_path))}) conanfile = textwrap.dedent(f""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" def configure(self): if "{scope}" == "build": self.win_bash = True else: self.win_bash_run = True def build(self): self.run("ar -h", scope="{scope}") """) client.save({"conanfile.py": conanfile}) client.run("build .") assert "ar.exe" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_conf_inherited_in_test_package(): client = TestClient() bash_path = None try: bash_path = tools_locations["msys2"]["system"]["path"]["Windows"] + "/bash.exe" except KeyError: pytest.skip("msys2 path not defined") conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): name="msys2" version="1.0" def package_info(self): self.conf_info.define("tools.microsoft.bash:subsystem", "msys2") self.conf_info.define("tools.microsoft.bash:path", r"{}") """.format(bash_path)) client.save({"conanfile.py": conanfile}) client.run("create .") conanfile = GenConanfile("consumer", "1.0") test_package = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): name="test" version="1.0" win_bash = True def build_requirements(self): self.tool_requires(self.tested_reference_str) self.tool_requires("msys2/1.0") def build(self): self.output.warning(self.conf.get("tools.microsoft.bash:subsystem")) self.run("aclocal --version") def test(self): pass """) client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_package}) client.run("create . -s:b os=Windows -s:h os=Windows") assert "are needed to run commands in a Windows subsystem" not in client.out assert "aclocal (GNU automake)" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") @pytest.mark.tool("msys2") def test_msys2_and_msbuild(): """ Check that msbuild can be executed in msys2 environment # https://github.com/conan-io/conan/issues/15627 """ client = TestClient(path_with_spaces=False) profile_win = textwrap.dedent(f""" include(default) [conf] tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path=bash """) main = gen_function_cpp(name="main") # The autotools support for "cl" compiler (VS) is very limited, linking with deps doesn't # work but building a simple app do makefile_am = gen_makefile_am(main="main", main_srcs="main.cpp") configure_ac = gen_configure_ac() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools from conan.tools.microsoft import MSBuild class TestConan(ConanFile): settings = "os", "compiler", "arch", "build_type" exports_sources = "configure.ac", "Makefile.am", "main.cpp", "MyProject.vcxproj" generators = "AutotoolsToolchain" win_bash = True def build(self): # These commands will run in bash activating first the vcvars and # then inside the bash activating the self.run("aclocal") self.run("autoconf") self.run("automake --add-missing --foreign") autotools = Autotools(self) autotools.configure() autotools.make() autotools.install() msbuild = MSBuild(self) msbuild.build("MyProject.vcxproj") """) # A minimal project is sufficient - here just copy the application file to another directory my_vcxproj = r""" Debug Win32 Release Win32 Debug x64 Release x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840} Win32Proj Application v141 $(ProjectDir)msbuild_out PreserveNewest """ client.save({"conanfile.py": conanfile, "configure.ac": configure_ac, "Makefile.am": makefile_am, "main.cpp": main, "profile_win": profile_win, "MyProject.vcxproj": my_vcxproj}) client.run("build . -pr=profile_win") # Run application in msbuild output directory client.run_command(os.path.join("msbuild_out", "main.exe")) check_exe_run(client.out, "main", "msvc", None, "Release", "x86_64", None) bat_contents = client.load("conanbuild.bat") assert "conanvcvars.bat" in bat_contents @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_autotools_support_custom_make(): """ Check that the conf setting `tools.gnu:make_program` works when set with windows native paths. For example, when set programatically by a package """ client = TestClient(path_with_spaces=False) bash_path = None make_path = None try: bash_path = tools_locations["msys2"]["system"]["path"]["Windows"] + "/bash.exe" make_path = tools_locations["msys2"]["system"]["path"]["Windows"] + "/make.exe" except KeyError: pytest.skip("msys2 path not defined") if not os.path.exists(make_path): pytest.skip("msys2 make not installed") make_path = make_path.replace("/", "\\") assert os.path.exists(make_path) profile = textwrap.dedent(f""" include(default) [conf] tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path={bash_path} tools.gnu:make_program={make_path} tools.build:compiler_executables={{"c": "cl", "cpp": "cl"}} """) # The autotools support for "cl" compiler (VS) is very limited, linking with deps doesn't # work but building a simple app do makefile = gen_makefile() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools class TestConan(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "AutotoolsToolchain" win_bash = True def build(self): # These commands will run in bash activating first the vcvars and # then inside the bash activating the autotools = Autotools(self) autotools.make() """) client.save({"conanfile.py": conanfile, "Makefile": makefile, "profile": profile}) client.run("build . -pr=profile") # This used to crash, because ``make_program`` was not unix_path assert "conanfile.py: Calling build()" in client.out ================================================ FILE: test/functional/toolchains/gnu/test_gnutoolchain_android.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.mocks import ConanFileMock from conan.test.utils.tools import TestClient from conan.tools.files import replace_in_file from test.conftest import tools_locations @pytest.mark.parametrize("arch, expected_arch", [('armv8', 'aarch64'), ('armv7', 'arm'), ('x86', 'i386'), ('x86_64', 'x86_64') ]) @pytest.mark.tool("android_ndk") @pytest.mark.tool("autotools") @pytest.mark.skipif(platform.system() != "Darwin", reason="NDK only installed on MAC") def test_android_gnutoolchain_cross_compiling(arch, expected_arch): profile_host = textwrap.dedent(""" include(default) [settings] os = Android os.api_level = 21 arch = {arch} [conf] tools.android:ndk_path={ndk_path} """) ndk_path = tools_locations["android_ndk"]["system"]["path"][platform.system()] profile_host = profile_host.format( arch=arch, ndk_path=ndk_path ) client = TestClient(path_with_spaces=False) # FIXME: Change this when we have gnu_lib as template in the new command client.run("new autotools_lib -d name=hello -d version=1.0") replace_in_file(ConanFileMock(), os.path.join(client.current_folder, "conanfile.py"), "AutotoolsToolchain", "GnuToolchain") client.save({"profile_host": profile_host}) client.run("build . --profile:build=default --profile:host=profile_host") libhello = os.path.join("build-release", "src", ".libs", "libhello.a") # Check binaries architecture client.run_command('objdump -f "%s"' % libhello) assert "architecture: %s" % expected_arch in client.out ================================================ FILE: test/functional/toolchains/gnu/test_gnutoolchain_apple.py ================================================ import os import platform import textwrap import pytest from conan.tools.apple.apple import _to_apple_arch from conan.test.assets.autotools import gen_makefile from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("config", [("x86_64", "Macos", "10.14", None), ("armv8", "iOS", "10.0", "iphoneos"), ("x86_64", "iOS", "10.0", "iphonesimulator"), ("armv8", "Macos", "10.14", None) # M1 ]) def test_makefile_arch(config): makefile = gen_makefile(apps=["app"], libs=["hello"]) conanfile_py = textwrap.dedent(""" from conan import ConanFile, tools from conan.tools.gnu import Autotools class App(ConanFile): settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} generators = "GnuToolchain" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def build(self): env_build = Autotools(self) env_build.make() """) arch, os_, os_version, os_sdk = config profile = textwrap.dedent(""" include(default) [settings] os = {os} {os_sdk} os.version = {os_version} arch = {arch} """).format(os=os_, arch=arch, os_version=os_version, os_sdk="os.sdk = " + os_sdk if os_sdk else "") t = TestClient() hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello") main_cpp = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) t.save({"Makefile": makefile, "hello.h": hello_h, "hello.cpp": hello_cpp, "app.cpp": main_cpp, "conanfile.py": conanfile_py, "profile": profile}) t.run("install . --profile:host=profile --profile:build=default") t.run("build . --profile:host=profile --profile:build=default") libhello = os.path.join(t.current_folder, "libhello.a") app = os.path.join(t.current_folder, "app") assert os.path.isfile(libhello) assert os.path.isfile(app) expected_arch = _to_apple_arch(arch) t.run_command('lipo -info "%s"' % libhello) assert "architecture: %s" % expected_arch in t.out t.run_command('lipo -info "%s"' % app) assert "architecture: %s" % expected_arch in t.out ================================================ FILE: test/functional/toolchains/gnu/test_makedeps.py ================================================ import textwrap import platform import pytest from conan.test.assets.autotools import gen_makefile from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.tools import TestClient from conan.tools.gnu.makedeps import CONAN_MAKEFILE_FILENAME @pytest.mark.tool("make" if platform.system() != "Windows" else "msys2") def test_make_deps_definitions_escape(): """ MakeDeps has to escape the definitions properly. """ client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(r''' from conan import ConanFile class HelloLib(ConanFile): def package_info(self): self.cpp_info.defines.append("USER_CONFIG=\"user_config.h\"") self.cpp_info.defines.append('OTHER="other.h"') self.cpp_info.cflags.append("flag1=\"my flag1\"") self.cpp_info.cxxflags.append('flag2="my flag2"') ''') client.save({"conanfile.py": conanfile}) client.run("export . --name=hello --version=0.1.0") client.run("install --requires=hello/0.1.0 --build=missing -g MakeDeps") client.run_command(f"make --print-data-base -f {CONAN_MAKEFILE_FILENAME}", assert_error=True) assert r'CONAN_CXXFLAGS_HELLO = flag2=\"my flag2\"' in client.out assert r'CONAN_CFLAGS_HELLO = flag1=\"my flag1\"' in client.out assert r'CONAN_DEFINES_HELLO = $(CONAN_DEFINE_FLAG)USER_CONFIG="user_config.h" $(CONAN_DEFINE_FLAG)OTHER="other.h"' in client.out def test_makedeps_with_tool_requires(): """ MakeDeps has to create any test requires to be declared on the recipe. """ conanfile = textwrap.dedent(r''' from conan import ConanFile class HelloLib(ConanFile): def package_info(self): self.cpp_info.libs = [self.name] ''') client = TestClient(path_with_spaces=False) client.save({"conanfile.py": conanfile}) client.run("create . --name=hello --version=0.1.0") client.run("create . --name=test --version=0.1.0") client.run("create . --name=tool --version=0.1.0") # Create library having build and test requires conanfile = textwrap.dedent(r''' from conan import ConanFile class HelloLib(ConanFile): def build_requirements(self): self.test_requires('hello/0.1.0') self.test_requires('test/0.1.0') self.tool_requires('tool/0.1.0') ''') client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -g MakeDeps") content = client.load(CONAN_MAKEFILE_FILENAME) assert "CONAN_NAME_TEST" in content assert "CONAN_NAME_HELLO" in content assert "CONAN_NAME_TOOL" not in content @pytest.mark.tool("make" if platform.system() != "Windows" else "msys2") def test_makedeps_with_makefile_build(): """ Build a small application using MakeDeps generator and with components """ client = TestClient(path_with_spaces=False) with client.chdir("lib"): client.save({"Makefile": gen_makefile(libs=["hello"]), "hello.cpp": gen_function_cpp(name="hello", includes=["hello"], calls=["hello"]), "hello.h": gen_function_h(name="hello"), "conanfile.py": textwrap.dedent(r''' from conan import ConanFile from conan.tools.gnu import Autotools from conan.tools.files import copy import os class PackageConan(ConanFile): exports_sources = ("Makefile", "hello.cpp", "hello.h") settings = "os", "arch", "compiler" def configure(self): self.win_bash = self.settings.os == "Windows" def build(self): self.run("make -f Makefile") def package(self): copy(self, "libhello.a", src=self.build_folder, dst=os.path.join(self.package_folder, "lib")) copy(self, "hello.h", src=self.source_folder, dst=os.path.join(self.package_folder, "include")) def package_info(self): self.cpp_info.components["qux"].includedirs = ["include"] self.cpp_info.components["baz"].libs = ["hello"] self.cpp_info.components["baz"].defines = ["FOOBAR=1"] ''') }) client.run('create . --name=hello --version=0.1.0 -c tools.microsoft.bash:subsystem=msys2 -c tools.microsoft.bash:path=bash') with client.chdir("global"): # Consume from global variables client.run("install --requires=hello/0.1.0 -pr:b=default -pr:h=default -g MakeDeps -of build") client.save({"Makefile": textwrap.dedent(''' include build/conandeps.mk CXXFLAGS += $(CONAN_CXXFLAGS) CPPFLAGS += $(addprefix -I, $(CONAN_INCLUDE_DIRS)) CPPFLAGS += $(addprefix -D, $(CONAN_DEFINES)) LDFLAGS += $(addprefix -L, $(CONAN_LIB_DIRS)) LDLIBS += $(addprefix -l, $(CONAN_LIBS)) EXELINKFLAGS += $(CONAN_EXELINKFLAGS) all: \t$(CXX) main.cpp $(CPPFLAGS) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) $(EXELINKFLAGS) -o main '''), "main.cpp": gen_function_cpp(name="main", includes=["hello"], calls=["hello"]), }) client.run_command("make -f Makefile") with client.chdir("components"): # Consume from components client.run("install --requires=hello/0.1.0 -pr:b=default -pr:h=default -g MakeDeps -of build") client.save({"Makefile": textwrap.dedent(''' include build/conandeps.mk CXXFLAGS += $(CONAN_CXXFLAGS) CPPFLAGS += $(addprefix -I, $(CONAN_INCLUDE_DIRS_HELLO_QUX)) CPPFLAGS += $(addprefix -D, $(CONAN_DEFINES) $(CONAN_DEFINES_HELLO_BAZ)) LDFLAGS += $(addprefix -L, $(CONAN_LIB_DIRS_HELLO_BAZ)) LDLIBS += $(addprefix -l, $(CONAN_LIBS_HELLO_BAZ)) EXELINKFLAGS += $(CONAN_EXELINKFLAGS) all: \t$(CXX) main.cpp $(CPPFLAGS) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) $(EXELINKFLAGS) -o main '''), "main.cpp": gen_function_cpp(name="main", includes=["hello"], calls=["hello"]), }) client.run_command("make -f Makefile") ================================================ FILE: test/functional/toolchains/gnu/test_pkg_config.py ================================================ import platform import textwrap import pytest from test.conftest import tools_locations from conan.test.utils.tools import TestClient @pytest.mark.tool("pkg_config") class TestPkgConfig: """ This test uses the pkg_config in the system """ def test_negative(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import PkgConfig class Pkg(ConanFile): def generate(self): pkg_config = PkgConfig(self, "something_that_not_exist") pkg_config.libs """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "PkgConfig failed. Command: pkg-config --libs-only-l something_that_not_exist " \ "--print-errors" in c.out assert "Package something_that_not_exist was not found" in c.out def test_pc(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import PkgConfig from conan.tools import CppInfo class Pkg(ConanFile): def generate(self): pkg_config = PkgConfig(self, "libastral", pkg_config_path=".") self.output.info(f"PROVIDES: {pkg_config.provides}") self.output.info(f"VERSION: {pkg_config.version}") self.output.info(f"VARIABLES: {pkg_config.variables['prefix']}") cpp_info = CppInfo(self) pkg_config.fill_cpp_info(cpp_info, is_system=False, system_libs=["m"]) assert cpp_info.includedirs == ['/usr/local/include/libastral'] assert cpp_info.defines == ['_USE_LIBASTRAL'] assert cpp_info.libs == ['astral'] assert cpp_info.system_libs == ['m'] assert cpp_info.libdirs == ['/usr/local/lib/libastral'] assert cpp_info.sharedlinkflags == ['-Wl,--whole-archive'] """) libastral_pc = textwrap.dedent("""\ PC FILE EXAMPLE: prefix=/usr/local exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include Name: libastral Description: Interface library for Astral data flows Version: 6.6.6 Libs: -L${libdir}/libastral -lastral -lm -Wl,--whole-archive Cflags: -I${includedir}/libastral -D_USE_LIBASTRAL """) c.save({"conanfile.py": conanfile, "libastral.pc": libastral_pc}) c.run("install .") assert "conanfile.py: PROVIDES: libastral = 6.6.6" in c.out assert "conanfile.py: VERSION: 6.6.6" in c.out assert "conanfile.py: VARIABLES: /usr/local" in c.out def test_pkg_config_round_tripe_cpp_info(): """ test that serialize and deserialize CppInfo works """ try: version = tools_locations["pkg_config"]["default"] exe = tools_locations["pkg_config"]["exe"] os_ = platform.system() pkg_config_path = tools_locations["pkg_config"][version]["path"][os_] + "/" + exe except KeyError: pytest.skip("pkg-config path not defined") return c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import PkgConfig from conan.tools import CppInfo class Pkg(ConanFile): name = "pkg" version = "0.1" exports_sources = "*.pc" def package(self): pkg_config = PkgConfig(self, "libastral", pkg_config_path=".") cpp_info = CppInfo(self) pkg_config.fill_cpp_info(cpp_info, is_system=False, system_libs=["m"]) cpp_info.save(os.path.join(self.package_folder, "cpp_info.json")) def package_info(self): self.cpp_info = CppInfo(self).load("cpp_info.json") """) prefix = "C:" if platform.system() == "Windows" else "" libastral_pc = textwrap.dedent("""\ PC FILE EXAMPLE: prefix=%s/usr/local exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include Name: libastral Description: Interface library for Astral data flows Version: 6.6.6 Libs: -L${libdir}/libastral -lastral -lm -Wl,--whole-archive Cflags: -I${includedir}/libastral -D_USE_LIBASTRAL """ % prefix) c.save({"conanfile.py": conanfile, "libastral.pc": libastral_pc, "profile": f"[conf]\ntools.gnu:pkg_config={pkg_config_path}"}) c.run("export .") c.run("install --requires=pkg/0.1 -pr=profile -g CMakeDeps --build=missing") pkg_data = c.load("pkg-none-data.cmake") assert 'set(pkg_DEFINITIONS_NONE "-D_USE_LIBASTRAL")' in pkg_data assert 'set(pkg_SHARED_LINK_FLAGS_NONE "-Wl,--whole-archive")' in pkg_data assert 'set(pkg_COMPILE_DEFINITIONS_NONE "_USE_LIBASTRAL")' in pkg_data assert 'set(pkg_LIBS_NONE astral)' in pkg_data assert 'set(pkg_SYSTEM_LIBS_NONE m)' in pkg_data # paths assert f'set(pkg_INCLUDE_DIRS_NONE "{prefix}/usr/local/include/libastral")' in pkg_data assert f'set(pkg_LIB_DIRS_NONE "{prefix}/usr/local/lib/libastral")' in pkg_data ================================================ FILE: test/functional/toolchains/gnu/test_pkgconfigdeps.py ================================================ import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() == "Windows", reason="Needs pkg-config") @pytest.mark.tool("pkg_config") def test_pkgconfigdeps_definitions_escape(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(r''' from conan import ConanFile class HelloLib(ConanFile): def package_info(self): self.cpp_info.defines.append("USER_CONFIG=\"user_config.h\"") self.cpp_info.defines.append('OTHER="other.h"') self.cpp_info.cflags.append("flag1=\"my flag1\"") self.cpp_info.cxxflags.append('flag2="my flag2"') ''') client.save({"conanfile.py": conanfile}) client.run("export . --name=hello --version=1.0") client.save({"conanfile.txt": "[requires]\nhello/1.0\n"}, clean_first=True) client.run("install . --build=missing -g PkgConfigDeps") client.run_command("PKG_CONFIG_PATH=$(pwd) pkg-config --cflags hello") assert r'flag2=\"my flag2\" flag1=\"my flag1\" ' \ r'-DUSER_CONFIG=\"user_config.h\" -DOTHER=\"other.h\"' in client.out @pytest.mark.tool("cmake") def test_pkgconfigdeps_with_test_requires(): """ PkgConfigDeps has to create any test requires declared on the recipe. Related issue: https://github.com/conan-io/conan/issues/11376 """ client = TestClient() client.save({"app/conanfile.py": GenConanfile("app", "1.0"), "test/conanfile.py": GenConanfile("test", "1.0")}) client.run("create app") client.run("create test") # Create library having build and test requires conanfile = textwrap.dedent(r''' from conan import ConanFile class HelloLib(ConanFile): def build_requirements(self): self.test_requires('app/1.0') self.test_requires('test/1.0') ''') client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -g PkgConfigDeps") assert "Description: Conan package: test" in client.load("test.pc") assert "Description: Conan package: app" in client.load("app.pc") @pytest.mark.skipif(platform.system() != "Windows", reason="It makes sense only for Windows") @pytest.mark.tool("meson") # https://github.com/mesonbuild/meson/pull/11649 is part of Meson 1.1.0 @pytest.mark.tool("pkg_config") def test_pkgconfigdeps_bindir_and_meson(): """ This test checks that the field bindir introduced by PkgConfigDeps is useful for Windows OS and shared=True where all the DLL's files are located by default there. Basically, Meson (version >= 1.1.0) reads from the *.pc files the bindir variable if exists, and uses that variable to link with if SHARED libraries. Issue: https://github.com/conan-io/conan/issues/13532 """ client = TestClient() client.run("new meson_lib -d name=hello -d version=1.0") client.run("create . -tf \"\" -o *:shared=True") test_meson_build = textwrap.dedent(""" project('Testhello', 'cpp') hello = dependency('hello', version : '>=1.0') example = executable('example', 'src/example.cpp', dependencies: hello) test('./src/example', example) """) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.build import can_run from conan.tools.meson import Meson from conan.tools.layout import basic_layout class helloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps", "MesonToolchain" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": True, "fPIC": True} def requirements(self): self.requires("hello/1.0") def build(self): meson = Meson(self) meson.configure() meson.build() def layout(self): basic_layout(self) """) client.save({ "test_package/conanfile.py": test_conanfile, "test_package/meson.build": test_meson_build }) client.run("build test_package/conanfile.py -o *:shared=True") # Important: Only Meson >= 1.1.0 brings this capability # Executing directly "meson test" fails if the bindir field does not exist client.run_command("meson test -C test_package/build-release") assert "1/1 ./src/example OK" def test_pkgconfigdeps_component_matches_package_name(): client = TestClient(path_with_spaces=False) # Create library having build and test requires conanfile = textwrap.dedent(r''' from conan import ConanFile class MyLib(ConanFile): name = "hello" version = "0.1" def package_info(self): self.cpp_info.components["mycomponent"].set_property("pkg_config_name", "hello") ''') client.save({"conanfile.py": conanfile}, clean_first=True) client.run("export-pkg .") client.run("install --requires=hello/0.1 -g PkgConfigDeps") content = client.load("hello.pc") assert "Conan component: hello" in content ================================================ FILE: test/functional/toolchains/gnu/test_pkgconfigdeps_autotools.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.tool("pkg_config") @pytest.mark.tool("autotools") @pytest.mark.skipif(platform.system() == "Windows", reason="Needs pkg-config") def test_pkgconfigdeps_and_autotools(): """ This test aims to show how to use PkgConfigDeps and AutotoolsToolchain. In this case, the test_package is using PkgConfigDeps and AutotoolsToolchain to use the created pkg/1.0 package. It's important to make a full compilation to ensure that the test main.cpp is using correctly the flags passed via pkg.pc file. Issue related: https://github.com/conan-io/conan/issues/11867 """ client = TestClient(path_with_spaces=False) conanfile_pkg = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools from conan.tools.layout import basic_layout from conan.tools.apple import fix_apple_shared_install_name class PkgConan(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "configure.ac", "Makefile.am", "src/*" generators = "AutotoolsToolchain" def layout(self): basic_layout(self) def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def package(self): autotools = Autotools(self) autotools.install() fix_apple_shared_install_name(self) def package_info(self): self.cpp_info.libs = ["pkg"] # Add non-existing frameworkdirs to check that it's not failing because of this self.cpp_info.frameworkdirs = ["/my/framework/file1", "/my/framework/file2"] """) conanfile_test = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import Autotools from conan.tools.layout import basic_layout from conan.tools.build import cross_building class PkgTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps", "AutotoolsToolchain" def requirements(self): self.requires(self.tested_reference_str) def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def layout(self): basic_layout(self) def test(self): if not cross_building(self): cmd = os.path.join(self.cpp.build.bindirs[0], "main") self.run(cmd, env="conanrun") """) configure_test = textwrap.dedent(""" AC_INIT([main], [1.0], []) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_PROG_CXX PKG_PROG_PKG_CONFIG PKG_CHECK_MODULES([pkg], [pkg >= 1.0]) AC_CONFIG_FILES([Makefile]) AC_OUTPUT """) makefile_test = textwrap.dedent(""" bin_PROGRAMS = main main_SOURCES = main.cpp AM_CXXFLAGS = $(pkg_CFLAGS) main_LDADD = $(pkg_LIBS) """) client.run("new autotools_lib -d name=pkg -d version=1.0") client.save({"conanfile.py": conanfile_pkg, "test_package/conanfile.py": conanfile_test, "test_package/configure.ac": configure_test, "test_package/Makefile.am": makefile_test, }) # client.run("new autotools_lib -d name=pkg -d version=1.0") client.run("create .") if platform.system() == "Darwin": # Checking that frameworkdirs appear all together instead of "-F /whatever/f1" # Issue: https://github.com/conan-io/conan/issues/11867 assert '-F/my/framework/file1' in client.out assert '-F/my/framework/file2' in client.out ================================================ FILE: test/functional/toolchains/gnu/test_universal_binaries.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_gnutoolchain_universal_binary(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import GnuToolchain, Autotools from conan.tools.layout import basic_layout class mylibraryRecipe(ConanFile): name = "mylibrary" version = "1.0" package_type = "library" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "configure.ac", "Makefile.am", "src/*" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): basic_layout(self) def generate(self): gnu_toolchain = GnuToolchain(self) gnu_toolchain.generate() def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def package(self): autotools = Autotools(self) autotools.install() self.run(f"lipo -info {os.path.join(self.package_folder, 'lib', 'libmylibrary.a')}") def package_info(self): self.cpp_info.libs = ["mylibrary"] """) client.run('new autotools_lib -d name=mylibrary -d version=0.1') client.save({"conanfile.py": conanfile}) client.run('create . -s="arch=armv8|x86_64" -tf=""') assert "libmylibrary.a are: x86_64 arm64" in client.out ================================================ FILE: test/functional/toolchains/gnu/test_v2_autotools_template.py ================================================ import platform import os import shutil import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_autotools_lib_template(): client = TestClient(path_with_spaces=False) client.run("new autotools_lib -d name=hello -d version=0.1") # Local flow works client.run("install .") client.run("build .") client.run("export-pkg .") pkg_layout = client.created_layout() package_folder = pkg_layout.package() assert os.path.exists(os.path.join(package_folder, "include", "hello.h")) assert os.path.exists(os.path.join(package_folder, "lib", "libhello.a")) # Local flow for shared library works client.save({}, clean_first=True) client.run("new autotools_lib -d name=hello -d version=0.1") client.run("install . -o hello/*:shared=True") client.run("build . -o hello/*:shared=True") client.run("export-pkg . -o hello/*:shared=True") pkg_layout = client.created_layout() package_folder = pkg_layout.package() if platform.system() == "Darwin": # Ensure that install name of dylib is patched client.run_command(f"otool -L {package_folder}/lib/libhello.0.dylib") assert "@rpath/libhello.0.dylib" in client.out elif platform.system() == "Linux": assert os.path.exists(os.path.join(package_folder, "lib", "libhello.so.0")) # Create works client.run("create .") assert "hello/0.1: Hello World Release!" in client.out client.run("create . -s build_type=Debug") assert "hello/0.1: Hello World Debug!" in client.out # Create + shared works client.save({}, clean_first=True) client.run("new autotools_lib -d name=hello -d version=0.1") client.run("create . -o hello/*:shared=True") build_folder = client.created_test_build_folder("hello/0.1") assert "hello/0.1: Hello World Release!" in client.out if platform.system() == "Darwin": client.run_command(f"otool -l test_package/{build_folder}/main") assert "@rpath/libhello.0.dylib" in client.out else: client.run_command(f"ldd test_package/{build_folder}/main") assert "libhello.so.0" in client.out @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_autotools_exe_template(): client = TestClient(path_with_spaces=False) client.run("new autotools_exe -d name=greet -d version=0.1") # Local flow works client.run("install .") client.run("build .") # Create works client.run("create .") # check that for exe's we don't add any static/shared flag for flag in ["--enable-static", "--disable-static", "--disable-shared", "--with-pic"]: assert flag not in client.out assert "greet/0.1: Hello World Release!" in client.out client.run("create . -s build_type=Debug") for flag in ["--enable-static", "--disable-static", "--disable-shared", "--with-pic"]: assert flag not in client.out assert "greet/0.1: Hello World Debug!" in client.out @pytest.mark.skipif(platform.system() not in ["Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") def test_autotools_relocatable_libs_darwin(): client = TestClient(path_with_spaces=False) client.run("new autotools_lib -d name=hello -d version=0.1") client.run("create . -o hello/*:shared=True") build_folder = client.created_test_build_folder("hello/0.1") pkg_layout = client.created_layout() package_folder = pkg_layout.package() dylib = os.path.join(package_folder, "lib", "libhello.0.dylib") if platform.system() == "Darwin": client.run_command("otool -l {}".format(dylib)) assert "@rpath/libhello.0.dylib" in client.out client.run_command("otool -l {}".format(f"test_package/{build_folder}/main")) assert package_folder in client.out # will work because rpath set client.run_command(f"test_package/{build_folder}/main") assert "hello/0.1: Hello World Release!" in client.out # move to another location so that the path set in the rpath does not exist # then the execution should fail shutil.move(os.path.join(package_folder, "lib"), os.path.join(client.current_folder, "tempfolder")) # will fail because rpath does not exist client.run_command(f"test_package/{build_folder}/main", assert_error=True) assert "Library not loaded: @rpath/libhello.0.dylib" in str(client.out).replace("'", "") # Use DYLD_LIBRARY_PATH and should run client.run_command("DYLD_LIBRARY_PATH={} test_package/{}/main".format(os.path.join(client.current_folder, "tempfolder"), build_folder)) assert "hello/0.1: Hello World Release!" in client.out # FIXME: investigate this test @pytest.mark.skipif(platform.system() not in ["Darwin"], reason="Requires Autotools") @pytest.mark.tool("autotools") @pytest.mark.xfail(reason="This test is failing for newer MacOS versions, but used to pass in the ci") def test_autotools_relocatable_libs_darwin_downloaded(): client = TestClient(default_server_user=True, path_with_spaces=False) client2 = TestClient(servers=client.servers, path_with_spaces=False) assert client2.cache_folder != client.cache_folder client.run("new autotools_lib -d name=hello -d version=0.1") client.run("create . -o hello/*:shared=True -tf=\"\"") client.run("upload hello/0.1 -c -r default") client.run("remove * -c") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import Autotools from conan.tools.layout import basic_layout class GreetConan(ConanFile): name = "greet" version = "1.0" settings = "os", "compiler", "build_type", "arch" exports_sources = "configure.ac", "Makefile.am", "main.cpp" generators = "AutotoolsDeps", "AutotoolsToolchain", "VirtualRunEnv" def requirements(self): self.requires("hello/0.1") def layout(self): basic_layout(self) def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() """) main = textwrap.dedent(""" #include "hello.h" int main() { hello(); } """) makefileam = textwrap.dedent(""" bin_PROGRAMS = greet greet_SOURCES = main.cpp """) configureac = textwrap.dedent(""" AC_INIT([greet], [1.0], []) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_PROG_CXX AM_PROG_AR LT_INIT AC_CONFIG_FILES([Makefile]) AC_OUTPUT """) client2.save({"conanfile.py": conanfile, "main.cpp": main, "makefile.am": makefileam, "configure.ac": configureac}) client2.run("install . -o hello/*:shared=True -r default") client2.run("build . -o hello/*:shared=True -r default") # for some reason this is failing for newer macos # although -Wl,-rpath -Wl, if passed to set the rpath when building # it fails with LC_RPATH's not found client2.run_command("build-release/greet") # activating the environment should not be needed as the rpath is set when linking greet #client2.run_command(". build-release/conan/conanrun.sh && build-release/greet") assert "Hello World Release!" in client2.out @pytest.mark.skipif(platform.system() not in ["Darwin"], reason="Only affects apple platforms") @pytest.mark.tool("autotools") def test_autotools_fix_shared_libs(): """ From comments in: https://github.com/conan-io/conan/pull/11365 Case 1: libopencv_core.3.4.17.dylib libopencv_core.3.4.dylib (symlink) -> libopencv_core.3.4.17.dylib libopencv_core.dylib (symlink) -> libopencv_core.3.4.dylib Install name in libopencv_core.3.4.17.dylib is libopencv_core.3.4.dylib NOT the dylib name So we have to add the rpath to that. Case 2: libopencv_core.dylib libopencv_imgproc.dylib libopencv_imgproc.dylib depends on libopencv_core.dylib and declares that dependency not using the @rpath, we have to make sure that we patch the dependencies in the dylibs using install_name_tool -change Let's create a Conan package with two libraries: bye and hello (bye depends on hello) and recreate this whole situation to check that we are correctly fixing the dylibs """ client = TestClient(path_with_spaces=False) client.run("new autotools_lib -d name=hello -d version=0.1") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save from conan.tools.gnu import AutotoolsToolchain, Autotools from conan.tools.layout import basic_layout from conan.tools.apple import fix_apple_shared_install_name class HelloConan(ConanFile): name = "hello" version = "0.1" settings = "os", "compiler", "build_type", "arch" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} exports_sources = "configure.ac", "Makefile.am", "src/*" def layout(self): basic_layout(self) def generate(self): at_toolchain = AutotoolsToolchain(self) at_toolchain.generate() def build(self): autotools = Autotools(self) autotools.autoreconf() autotools.configure() autotools.make() def package(self): autotools = Autotools(self) autotools.install() # before fixing the names we try to reproduce the two cases explained # in the test that dylib name and install name are not the same self.run("install_name_tool {} -id /lib/libbye.dylib".format(os.path.join(self.package_folder, "lib", "libbye.0.dylib"))) # also change that in the libbye dependencies self.run("install_name_tool {} -change /lib/libhello.0.dylib /lib/libhello.dylib".format(os.path.join(self.package_folder, "lib", "libbye.0.dylib"))) self.run("install_name_tool {} -id /lib/libhello.dylib".format(os.path.join(self.package_folder, "lib","libhello.0.dylib"))) # https://github.com/conan-io/conan/issues/12727 save(self, os.path.join(self.package_folder, "bin", "subfolder", "testfile"), "foobar") save(self, os.path.join(self.package_folder, "lib", "subfolder", "randomfile"), "foobar") fix_apple_shared_install_name(self) def package_info(self): self.cpp_info.libs = ["hello", "bye"] """) bye_cpp = textwrap.dedent(""" #include #include "hello.h" #include "bye.h" void bye(){ hello(); std::cout << "Bye, bye!" << std::endl; } """) bye_h = textwrap.dedent(""" #pragma once void bye(); """) makefile_am = textwrap.dedent(""" lib_LTLIBRARIES = libhello.la libbye.la libhello_la_SOURCES = hello.cpp hello.h libhello_la_HEADERS = hello.h libhello_ladir = $(includedir) libbye_la_SOURCES = bye.cpp bye.h libbye_la_HEADERS = bye.h libbye_ladir = $(includedir) libbye_la_LIBADD = libhello.la bin_PROGRAMS = main main_SOURCES = main.cpp main_LDADD = libhello.la libbye.la """) test_src = textwrap.dedent(""" #include "bye.h" int main() { bye(); } """) client.save({ "src/makefile.am": makefile_am, "src/bye.cpp": bye_cpp, "src/bye.h": bye_h, "src/main.cpp": test_src, "test_package/main.cpp": test_src, "conanfile.py": conanfile, }) client.run("create . -o hello/*:shared=True -tf=\"\"") package_folder = client.created_layout().package() # install name fixed client.run_command("otool -D {}".format(os.path.join(package_folder, "lib", "libhello.0.dylib"))) assert "@rpath/libhello.dylib" in client.out client.run_command("otool -D {}".format(os.path.join(package_folder, "lib", "libbye.0.dylib"))) assert "@rpath/libbye.dylib" in client.out # dependencies fixed client.run_command("otool -L {}".format(os.path.join(package_folder, "lib", "libbye.0.dylib"))) assert "/lib/libhello.dylib (compatibility version 1.0.0, current version 1.0.0)" not in client.out assert "/lib/libbye.dylib (compatibility version 1.0.0, current version 1.0.0)" not in client.out assert "@rpath/libhello.dylib (compatibility version 1.0.0, current version 1.0.0)" in client.out assert "@rpath/libbye.dylib (compatibility version 1.0.0, current version 1.0.0)" in client.out # app rpath fixed in executable exe_path = os.path.join(package_folder, "bin", "main") client.run_command("otool -L {}".format(exe_path)) assert "@rpath/libhello.dylib" in client.out client.run_command(exe_path) assert "Bye, bye!" in client.out # Running the test-package also works client.run("test test_package hello/0.1@ -o hello/*:shared=True") assert "Bye, bye!" in client.out @pytest.mark.slow @pytest.mark.skipif(platform.system() != "Windows", reason="Using msys2") @pytest.mark.tool("msys2") class TestAutotoolsTemplateWindows: def test_msys2_autotools_windows(self): c = TestClient(path_with_spaces=False) c.run("new autotools_lib -d name=hello -d version=1.0") # TODO: Can we reduce the configuration? maybe path=bash can be defaulted? msys2 = textwrap.dedent(""" include(default) [conf] tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path=bash """) c.save({"msys2": msys2}) # FIXME: Need to deactivate test_package because AutotoolsDeps doesn't work in Win c.run("create . -pr=msys2 -tf=") # This will not crash assert "conanvcvars.bat: Activating environment" in c.out assert "hello/1.0: package(): Packaged 1 '.lib' file: hello.lib" in c.out def test_msys2_autotools_exe_windows(self): c = TestClient(path_with_spaces=False) c.run("new autotools_exe -d name=hello -d version=1.0") msys2 = textwrap.dedent(""" include(default) [conf] tools.microsoft.bash:subsystem=msys2 tools.microsoft.bash:path=bash """) c.save({"msys2": msys2}) c.run("create . -pr=msys2") assert "hello/1.0: _MSC_VER19" in c.out ================================================ FILE: test/functional/toolchains/google/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/google/test_bazel.py ================================================ import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def bazel_output_root_dir(): return temp_folder(path_with_spaces=False).replace("\\", "/") @pytest.fixture(scope="module") def bazelrc(): return textwrap.dedent(""" build:Debug -c dbg --copt=-g build:Release -c opt build:RelWithDebInfo -c opt --copt=-O3 --copt=-DNDEBUG build:MinSizeRel -c opt --copt=-Os --copt=-DNDEBUG build --color=yes build:withTimeStamps --show_timestamps """) @pytest.fixture(scope="module") def base_profile(): return textwrap.dedent(""" include(default) [settings] build_type={build_type} [conf] tools.google.bazel:bazelrc_path=["{curdir}/mybazelrc"] tools.google.bazel:configs=["{build_type}", "withTimeStamps"] """) @pytest.mark.slow @pytest.mark.parametrize("build_type", ["Debug", "Release", "RelWithDebInfo", "MinSizeRel"]) @pytest.mark.tool("bazel", "6.x") def test_basic_exe_6x(bazelrc, build_type, base_profile, bazel_output_root_dir): client = TestClient(path_with_spaces=False) client.run(f"new bazel_exe -d name=myapp -d version=1.0 -d output_root_dir={bazel_output_root_dir}") # The build: define several configurations that can be activated by passing # the bazel config with tools.google.bazel:configs client.save({"mybazelrc": bazelrc}) profile = base_profile.format(build_type=build_type, curdir=client.current_folder.replace("\\", "/")) client.save({"my_profile": profile}) client.run("create . --profile=./my_profile") if build_type != "Debug": assert "myapp/1.0: Hello World Release!" in client.out else: assert "myapp/1.0: Hello World Debug!" in client.out @pytest.mark.slow @pytest.mark.parametrize("build_type", ["Debug", "Release", "RelWithDebInfo", "MinSizeRel"]) @pytest.mark.tool("bazel", "7.x") def test_basic_exe(bazelrc, build_type, base_profile, bazel_output_root_dir): client = TestClient(path_with_spaces=False) client.run(f"new bazel_7_exe -d name=myapp -d version=1.0 -d output_root_dir={bazel_output_root_dir}") # The build: define several configurations that can be activated by passing # the bazel config with tools.google.bazel:configs client.save({"mybazelrc": bazelrc}) profile = base_profile.format(build_type=build_type, curdir=client.current_folder.replace("\\", "/")) client.save({"my_profile": profile}) client.run("create . --profile=./my_profile") if build_type != "Debug": assert "myapp/1.0: Hello World Release!" in client.out else: assert "myapp/1.0: Hello World Debug!" in client.out @pytest.mark.slow @pytest.mark.tool("bazel", "8.x") def test_basic_lib(bazelrc, base_profile, bazel_output_root_dir): """ Issue related: https://github.com/conan-io/conan/issues/17438 """ client = TestClient(path_with_spaces=False) client.run(f"new bazel_7_lib -d name=mylib -d version=1.0 -d output_root_dir={bazel_output_root_dir}") client.run("create .") assert "mylib/1.0: Hello World Release!" in client.out @pytest.mark.slow @pytest.mark.parametrize("shared", [False, True]) @pytest.mark.tool("bazel", "6.x") def test_transitive_libs_consuming_6x(shared, bazel_output_root_dir): """ Testing the next dependencies structure for shared/static libs /. |- myfirstlib/ |- conanfile.py |- WORKSPACE |- main/ |- BUILD |- myfirstlib.cpp |- myfirstlib.h |- mysecondlib/ (requires myfirstlib) |- conanfile.py |- WORKSPACE |- main/ |- BUILD |- mysecondlib.cpp |- mysecondlib.h |- test_package/ |- WORKSPACE |- conanfile.py |- main |- example.cpp |- BUILD """ client = TestClient(path_with_spaces=False) # A regular library made with Bazel with client.chdir("myfirstlib"): client.run(f"new bazel_lib -d name=myfirstlib -d version=1.2.11 -d output_root_dir={bazel_output_root_dir}") conanfile = client.load("conanfile.py") conanfile += """ self.cpp_info.defines.append("MY_DEFINE=\\"MY_VALUE\\"") self.cpp_info.defines.append("MY_OTHER_DEFINE=2") if self.settings.os != "Windows": self.cpp_info.system_libs.append("m") else: self.cpp_info.system_libs.append("ws2_32") """ client.save({"conanfile.py": conanfile}) client.run(f"create . -o '*:shared={shared}' -tf ''") # skipping tests with client.chdir("mysecondlib"): # We prepare a consumer with Bazel (library mysecondlib using myfirstlib) # and a test_package with an example executable os_ = platform.system() client.run(f"new bazel_lib -d name=mysecondlib -d version=1.0 -d output_root_dir={bazel_output_root_dir}") conanfile = client.load("conanfile.py") conanfile = conanfile.replace('generators = "BazelToolchain"', 'generators = "BazelToolchain", "BazelDeps"\n' ' requires = "myfirstlib/1.2.11"') workspace = textwrap.dedent(""" load("@//conan:dependencies.bzl", "load_conan_dependencies") load_conan_dependencies() """) bazel_build_linux = textwrap.dedent("""\ cc_library( name = "mysecondlib", srcs = ["mysecondlib.cpp"], hdrs = ["mysecondlib.h"], deps = [ "@myfirstlib//:myfirstlib" ] ) """) bazel_build = textwrap.dedent("""\ cc_library( name = "mysecondlib", srcs = ["mysecondlib.cpp"], hdrs = ["mysecondlib.h"], deps = [ "@myfirstlib//:myfirstlib" ] ) cc_shared_library( name = "mysecondlib_shared", shared_lib_name = "libmysecondlib_shared.{}", deps = [":mysecondlib"], ) """.format("dylib" if os_ == "Darwin" else "dll")) mysecondlib_cpp = textwrap.dedent(""" #include #include "mysecondlib.h" #include "myfirstlib.h" #include void mysecondlib(){ std::cout << "mysecondlib() First define " << MY_DEFINE << " and other define " << MY_OTHER_DEFINE << std::endl; myfirstlib(); // This comes from the systemlibs declared in the myfirstlib sqrt(25); } void mysecondlib_print_vector(const std::vector &strings) { for(std::vector::const_iterator it = strings.begin(); it != strings.end(); ++it) { std::cout << "mysecondlib/1.0 " << *it << std::endl; } } """) mysecondlib_cpp_win = textwrap.dedent(""" #include #include "mysecondlib.h" #include "myfirstlib.h" #include void mysecondlib(){ SOCKET foo; // From the system library std::cout << "mysecondlib() First define " << MY_DEFINE << " and other define " << MY_OTHER_DEFINE << std::endl; myfirstlib(); } void mysecondlib_print_vector(const std::vector &strings) { for(std::vector::const_iterator it = strings.begin(); it != strings.end(); ++it) { std::cout << "mysecondlib/1.0 " << *it << std::endl; } } """) # Overwriting files client.save({"conanfile.py": conanfile, "WORKSPACE": workspace, "main/BUILD": bazel_build_linux if os_ == "Linux" else bazel_build, "main/mysecondlib.cpp": mysecondlib_cpp if os_ != "Windows" else mysecondlib_cpp_win, }) client.run(f"create . -o '*:shared={shared}'") assert "mysecondlib() First define MY_VALUE and other define 2" in client.out assert "myfirstlib/1.2.11: Hello World Release!" @pytest.mark.slow @pytest.mark.parametrize("shared", [False, True]) @pytest.mark.tool("bazel", "7.x") @pytest.mark.skipif(platform.system() == "Linux", reason="Conan CI fails (likely related to parallel " "tests running??). Skipping it for now!") def test_transitive_libs_consuming_7x(shared, bazel_output_root_dir): """ Testing the next dependencies structure for shared/static libs /. |- myfirstlib/ |- conanfile.py |- MODULE.bazel |- main/ |- BUILD |- myfirstlib.cpp |- myfirstlib.h |- mysecondlib/ (requires myfirstlib) |- conanfile.py |- MODULE.bazel |- main/ |- BUILD |- mysecondlib.cpp |- mysecondlib.h |- test_package/ |- MODULE.bazel |- conanfile.py |- main |- example.cpp |- BUILD """ client = TestClient(path_with_spaces=False) # A regular library made with Bazel with client.chdir("myfirstlib"): client.run(f"new bazel_7_lib -d name=myfirstlib -d version=1.2.11 -d output_root_dir={bazel_output_root_dir}") conanfile = client.load("conanfile.py") conanfile += """ self.cpp_info.defines.append("MY_DEFINE=\\"MY_VALUE\\"") self.cpp_info.defines.append("MY_OTHER_DEFINE=2") if self.settings.os != "Windows": self.cpp_info.system_libs.append("m") else: self.cpp_info.system_libs.append("ws2_32") # Issue: https://github.com/conan-io/conan/issues/18748 from conan.tools.apple.apple import is_apple_os if is_apple_os(self): self.cpp_info.frameworks = ["CoreFoundation"] """ client.save({"conanfile.py": conanfile}) client.run(f"create . -o '*:shared={shared}' -tf ''") # skipping tests with client.chdir("mysecondlib"): # We prepare a consumer with Bazel (library mysecondlib using myfirstlib) # and a test_package with an example executable os_ = platform.system() client.run(f"new bazel_7_lib -d name=mysecondlib -d version=1.0 -d output_root_dir={bazel_output_root_dir}") conanfile = client.load("conanfile.py") conanfile = conanfile.replace('generators = "BazelToolchain"', 'generators = "BazelToolchain", "BazelDeps"\n' ' requires = "myfirstlib/1.2.11"') workspace = textwrap.dedent(""" load_conan_dependencies = use_extension("//conan:conan_deps_module_extension.bzl", "conan_extension") use_repo(load_conan_dependencies, "myfirstlib") """) bazel_build_linux = textwrap.dedent("""\ cc_library( name = "mysecondlib", srcs = ["mysecondlib.cpp"], hdrs = ["mysecondlib.h"], deps = [ "@myfirstlib//:myfirstlib" ] ) """) bazel_build = textwrap.dedent("""\ cc_library( name = "mysecondlib", srcs = ["mysecondlib.cpp"], hdrs = ["mysecondlib.h"], deps = [ "@myfirstlib//:myfirstlib" ] ) cc_shared_library( name = "mysecondlib_shared", shared_lib_name = "libmysecondlib_shared.{}", deps = [":mysecondlib"], ) """.format("dylib" if os_ == "Darwin" else "dll")) mysecondlib_cpp = textwrap.dedent(""" #include #include "mysecondlib.h" #include "myfirstlib.h" #include void mysecondlib(){ std::cout << "mysecondlib() First define " << MY_DEFINE << " and other define " << MY_OTHER_DEFINE << std::endl; myfirstlib(); // This comes from the systemlibs declared in the myfirstlib sqrt(25); } void mysecondlib_print_vector(const std::vector &strings) { for(std::vector::const_iterator it = strings.begin(); it != strings.end(); ++it) { std::cout << "mysecondlib/1.0 " << *it << std::endl; } } """) mysecondlib_cpp_win = textwrap.dedent(""" #include #include "mysecondlib.h" #include "myfirstlib.h" #include void mysecondlib(){ SOCKET foo; // From the system library std::cout << "mysecondlib() First define " << MY_DEFINE << " and other define " << MY_OTHER_DEFINE << std::endl; myfirstlib(); } void mysecondlib_print_vector(const std::vector &strings) { for(std::vector::const_iterator it = strings.begin(); it != strings.end(); ++it) { std::cout << "mysecondlib/1.0 " << *it << std::endl; } } """) # Overwriting files client.save({"conanfile.py": conanfile, "MODULE.bazel": workspace, "main/BUILD": bazel_build_linux if os_ == "Linux" else bazel_build, "main/mysecondlib.cpp": mysecondlib_cpp if os_ != "Windows" else mysecondlib_cpp_win, }) client.run(f"create . -o '*:shared={shared}'") assert "mysecondlib() First define MY_VALUE and other define 2" in client.out assert "myfirstlib/1.2.11: Hello World Release!" @pytest.mark.slow @pytest.mark.tool("bazel", "8.x") def test_empty_bazel_query(): """ Test that following a simple steps using the BazelDeps and running a global `bazel query //...` runs OK (bazel >= 8.0) Issue related: https://github.com/conan-io/conan/issues/18743 """ zlib = GenConanfile("zlib", "0.1") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.google import BazelDeps, bazel_layout class App(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "zlib/0.1" def layout(self): bazel_layout(self) def generate(self): bz = BazelDeps(self) bz.generate() """) module = textwrap.dedent("""\ load_conan_dependencies = use_extension("//conan:conan_deps_module_extension.bzl", "conan_extension") use_repo(load_conan_dependencies, "zlib") """) client = TestClient() client.save({ "zlib/conanfile.py": zlib, "consumer/conanfile.py": consumer, "consumer/MODULE.bazel": module, }) client.run("create zlib") client.run("install consumer") with client.chdir("consumer"): client.run_command("bazel query //...") assert "//conan/zlib:zlib" in client.out assert "//conan/zlib:zlib_binaries" in client.out ================================================ FILE: test/functional/toolchains/google/test_bazeltoolchain_cross_compilation.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient @pytest.mark.slow @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for Darwin") @pytest.mark.tool("bazel", "6.x") # not working for Bazel 7.x def test_bazel_simple_cross_compilation(): profile = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=13.0 os=Macos """) profile_host = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=13.0 os=Macos """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.google import Bazel, bazel_layout class MyappConan(ConanFile): name = "myapp" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "BazelToolchain" def config_options(self): if self.settings.os == "Windows": del self.options.fPIC def layout(self): bazel_layout(self) def build(self): bazel = Bazel(self) bazel.build() """) BUILD = textwrap.dedent(""" cc_library( name = "myapp", srcs = ["myapp.cpp"], hdrs = ["myapp.h"], ) """) client = TestClient(path_with_spaces=False) bazel_root_dir = temp_folder(path_with_spaces=False).replace("\\", "/") client.save({ "profile": profile, "profile_host": profile_host, "conanfile.py": conanfile, "WORKSPACE": "", ".bazelrc": f"startup --output_user_root={bazel_root_dir}", "main/BUILD": BUILD, "main/myapp.cpp": gen_function_cpp(name="myapp"), "main/myapp.h": gen_function_h(name="myapp"), }) client.run("build . -pr:h profile_host -pr:b profile") libmyapp = os.path.join(client.current_folder, "bazel-bin", "main", "libmyapp.a") client.run_command(f'otool -hv {libmyapp}') assert "ARM64" in client.out ================================================ FILE: test/functional/toolchains/intel/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/intel/test_intel_cc.py ================================================ import pytest import platform import textwrap from conan.test.utils.tools import TestClient @pytest.mark.tool("cmake") @pytest.mark.tool("intel_oneapi") @pytest.mark.xfail(reason="Intel oneAPI Toolkit is not installed on CI yet") @pytest.mark.skipif(platform.system() != "Linux", reason="Only for Linux") class TestIntelCC: """Tests for Intel oneAPI C++/DPC++ compilers""" def test_intel_oneapi_and_dpcpp(self): client = TestClient() # Let's create a default hello/0.1 example client.run("new cmake_lib -d name=hello -d version=0.1") intel_profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=intel-cc compiler.mode=dpcpp compiler.version=2021.3 compiler.libcxx=libstdc++ build_type=Release [env] CC=dpcpp CXX=dpcpp """) client.save({"intel_profile": intel_profile}) # Build in the cache client.run('create . --profile:build=intel_profile --profile:host=intel_profile') assert ":: initializing oneAPI environment ..." in client.out assert ":: oneAPI environment initialized ::" in client.out assert "Check for working CXX compiler: /opt/intel/oneapi/compiler/2021.3.0" \ "/linux/bin/dpcpp -- works" in client.out assert "hello/0.1: Package " \ "'5d42bcd2e9be3378ed0c2f2928fe6dc9ea1b0922' created" in client.out # TODO: # self.t.run_command(exe) # self.assertIn("main __INTEL_COMPILER1910", self.t.out) ================================================ FILE: test/functional/toolchains/intel/test_using_msbuild.py ================================================ import os import platform import pytest import textwrap from conan.tools.microsoft.visual import vcvars_command from conan.test.assets.sources import gen_function_cpp from ..microsoft.test_msbuild import sln_file, myapp_vcxproj conanfile_py = textwrap.dedent(""" from conan import ConanFile, MSBuild, MSBuildToolchain class App(ConanFile): settings = 'os', 'arch', 'compiler', 'build_type' exports_sources = "MyProject.sln", "MyApp/MyApp.vcxproj", "MyApp/MyApp.cpp" requires = "hello/0.1" def generate(self): tc = MSBuildToolchain(self) tc.generate() def build(self): msbuild = MSBuild(self) msbuild.build("MyProject.sln") """) @pytest.mark.tool("cmake") @pytest.mark.tool("msbuild") @pytest.mark.tool("icc") @pytest.mark.xfail(reason="Intel compiler not installed yet on CI") @pytest.mark.skipif(platform.system() != "Windows", reason="msbuild requires Windows") class MSBuildIntelTestCase: def test_use_msbuild_toolchain(self): self.t.save({'profile': self.profile}) self.t.run("new hello/0.1 -s") self.t.run("create . --name=hello --version=0.1 -pr:h=profile") app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) # Prepare the actual consumer package self.t.save({"conanfile.py": conanfile_py, "MyProject.sln": sln_file, "MyApp/MyApp.vcxproj": myapp_vcxproj, "MyApp/MyApp.cpp": app, 'profile': self.profile}, clean_first=True) # Build in the cache self.t.run("install . -pr:h=profile -of=conan") self.assertIn("conanfile.py: MSBuildToolchain created conan_toolchain_release_x64.props", self.t.out) self.t.run("build . -bf=conan") self.assertIn("Visual Studio 2017", self.t.out) self.assertIn("[vcvarsall.bat] Environment initialized for: 'x64'", self.t.out) exe = "x64\\Release\\MyApp.exe" self.t.run_command(exe) self.assertIn("main __INTEL_COMPILER1910", self.t.out) vcvars = vcvars_command(version="15", architecture="x64") dumpbind_cmd = '%s && dumpbin /dependents "%s"' % (vcvars, exe) self.t.run_command(dumpbind_cmd) self.assertIn("KERNEL32.dll", self.t.out) # Build locally os.unlink(os.path.join(self.t.current_folder, exe)) cmd = vcvars + ' && msbuild "MyProject.sln" /p:Configuration=Release ' \ '/p:Platform=x64 /p:PlatformToolset="Intel C++ Compiler 19.1"' self.t.run_command(cmd) self.assertIn("Visual Studio 2017", self.t.out) self.assertIn("[vcvarsall.bat] Environment initialized for: 'x64'", self.t.out) self.t.run_command(exe) self.assertIn("main __INTEL_COMPILER1910", self.t.out) self.t.run_command(dumpbind_cmd) self.assertIn("KERNEL32.dll", self.t.out) ================================================ FILE: test/functional/toolchains/ios/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/ios/_utils.py ================================================ import textwrap lib_h = textwrap.dedent(""" #pragma once #include class HelloLib { public: void hello(const std::string& name); }; """) lib_cpp = textwrap.dedent(""" #include "hello.h" #include using namespace std; void HelloLib::hello(const std::string& name) { #ifdef DEBUG std::cout << "Hello " << name << " Debug!" < HELLO_MSG = Hi everyone, which is incorrect assert "WARN: deprecated: Please, do not use a Conan option value directly. " \ "Convert 'options.shared' into" in t.out assert "WARN: deprecated: Please, do not use a Conan option value directly. " \ "Convert 'options.msg' into" in t.out assert "Malformed value" in t.out # Let's transform the Conan option into other allowed data type to solve the issue t.save({"conanfile.py": conanfile_py.format(msg="str(self.options.msg)")}) t.run("build .") content = t.load(os.path.join("build", "gen_folder", "conan_meson_native.ini")) assert "[project options]" in content assert "STRING_DEFINITION = 'Text'" in content assert "TRUE_DEFINITION = true" in content assert "FALSE_DEFINITION = false" in content assert "DYNAMIC = False" in content # Meson transforms correctly this value into bool assert "HELLO_MSG = 'Hi everyone!'" in content assert "INT_DEFINITION = 42" in content assert "ARRAY_DEFINITION = ['Text1', 'Text2']" in content assert "[built-in options]" in content assert "buildtype = 'release'" in content t.run_command(os.path.join("build", "demo")) assert "hello: Release!" in t.out assert "STRING_DEFINITION: Text" in t.out assert "[properties]" in content assert "needs_exe_wrapper" not in content check_binary(t) def test_meson_default_dirs(self): t = TestClient() t.run("new meson_exe -d name=hello -d version=1.0") # t.run("new meson_exe -d name=hello -d version=1.0 -m meson_exe") meson_build = textwrap.dedent(""" project('tutorial', 'cpp') # Creating specific library hello = library('hello', 'src/hello.cpp', install: true) # Creating specific executable executable('demo', 'src/main.cpp', link_with: hello, install: true) # Creating specific data in src/ (they're going to be exported) install_data(['src/file1.txt', 'src/file2.txt']) """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson class HelloConan(ConanFile): name = "hello" version = "1.0" settings = "os", "compiler", "build_type", "arch" exports_sources = "meson.build", "src/*" generators = "MesonToolchain" package_type = "application" def layout(self): self.folders.build = "build" # Only adding "res" to resdirs self.cpp.package.resdirs = ["res"] def build(self): meson = Meson(self) meson.configure() meson.build() def package(self): meson = Meson(self) meson.install() """) test_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import cross_building class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def test(self): if not cross_building(self): self.run("demo", env="conanrun") """) # Replace meson.build, conanfile.py and test_package/conanfile.py t.save({"meson.build": meson_build, "conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile, "src/file1.txt": "", "src/file2.txt": ""}) t.run("create . -c tools.build:verbosity=quiet -c tools.compilation:verbosity=verbose") # Check verbosity control assert "unrecognized arguments" not in t.out assert re.search("meson compile .*? --verbose", t.out) assert re.search("meson install .*? --quiet", t.out) # Check if all the files are in the final directories ref = RecipeReference.loads("hello/1.0") pref = t.get_latest_package_reference(ref) package_folder = t.get_latest_pkg_layout(pref).package() if platform.system() == "Windows": assert os.path.exists(os.path.join(package_folder, "lib", "hello.lib")) assert os.path.exists(os.path.join(package_folder, "bin", "hello.dll")) assert os.path.exists(os.path.join(package_folder, "bin", "demo.exe")) else: ext = "dylib" if platform.system() == "Darwin" else "so" assert os.path.exists(os.path.join(package_folder, "bin", "demo")) assert os.path.exists(os.path.join(package_folder, "lib", "libhello." + ext)) # res/tutorial -> tutorial is being added automatically by Meson assert os.path.exists(os.path.join(package_folder, "res", "tutorial", "file1.txt")) assert os.path.exists(os.path.join(package_folder, "res", "tutorial", "file2.txt")) @pytest.mark.tool("meson") @pytest.mark.skipif(sys.version_info.minor < 8, reason="Latest Meson versions needs Python >= 3.8") def test_meson_and_additional_machine_files_composition(): """ Testing when users wants to append their own meson machine files and override/complement some sections from Conan file ones. See more information in Meson web page: https://mesonbuild.com/Machine-files.html In this test, we're overriding only the Meson section ``[binaries]`` for instance. """ profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++11 build_type=Release [conf] tools.meson.mesontoolchain:extra_machine_files=["myfilename.ini"] """) myfilename = textwrap.dedent(""" [project options] my_option = 'fake-option' """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "MesonToolchain" def build(self): meson = Meson(self) meson.configure() meson.build() """) client = TestClient() client.save({"conanfile.py": conanfile, "build/myfilename.ini": myfilename, "meson.build": "project('tutorial', 'cpp')", # dummy one "profile": profile}) client.run("install . -pr:h=profile -pr:b=profile") client.run("build . -pr:h=profile -pr:b=profile", assert_error=True) # Checking the order of the appended user file (the order matters) match = re.search(r"meson setup --native-file .* --native-file \"myfilename\.ini\"", client.out) assert match @pytest.mark.tool("ninja") @pytest.mark.tool("meson") @pytest.mark.skipif(platform.system() != "Windows", reason="Only for Windows") @pytest.mark.skipif(sys.version_info.minor < 8, reason="Latest Meson versions needs Python >= 3.8") def test_meson_using_prefix_path_in_application(): """ Issue related https://github.com/conan-io/conan/issues/14213 """ meson_build = textwrap.dedent(""" project('myhello ', 'c') executable('myhello', 'src/main.c', install: true) prefix_dir = get_option('prefix') cfg_var = configuration_data() cfg_var.set_quoted('MYHELLO_PREFIX', prefix_dir) config_file = configure_file( configuration: cfg_var, output: 'config.h' ) """) main_c = textwrap.dedent(""" #include int main(void) { return 0; } char *issue_func() { return (MYHELLO_PREFIX); } """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson from conan.tools.layout import basic_layout class myhelloConan(ConanFile): name = "demo" version = "0.1" settings = "os", "compiler", "build_type", "arch" exports_sources = "meson.build", "*.c" package_type = "application" generators = "MesonToolchain" def layout(self): basic_layout(self) def build(self): meson = Meson(self) meson.configure() meson.build() """) client = TestClient() client.save({"conanfile.py": conanfile, "src/main.c": main_c, "meson.build": meson_build}) client.run("build .") assert "unrecognized character escape sequence" not in str(client.out) # if Visual assert "unknown escape sequence" not in str(client.out) # if mingw ================================================ FILE: test/functional/toolchains/meson/test_meson_and_gnu_deps_flags.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.tools import TestClient class TestMesonToolchainAndGnuFlags: @pytest.mark.tool("ninja") @pytest.mark.tool("meson") @pytest.mark.tool("pkg_config") def test_mesondeps_flags_are_being_appended_and_not_replacing_toolchain_ones(self): """ Test PkgConfigDeps and MesonToolchain are keeping all the flags/definitions defined from both generators and nothing is being messed up. """ client = TestClient(path_with_spaces=False) if platform.system() == "Windows": deps_flags = '"/GA", "/analyze:quiet"' flags = '"/Wall", "/W4"' else: deps_flags = '"-Wpedantic", "-Werror"' flags = '"-Wall", "-finline-functions"' # Dependency - hello/0.1 conanfile_py = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" def package_info(self): self.cpp_info.cxxflags = [{}] self.cpp_info.defines = ['DEF1=one_string', 'DEF2=other_string'] """.format(deps_flags)) client.save({"conanfile.py": conanfile_py}) client.run("create .") # Dependency - other/0.1 conanfile_py = textwrap.dedent(""" from conan import ConanFile class OtherConan(ConanFile): name = "other" version = "0.1" def package_info(self): self.cpp_info.defines = ['DEF3=simple_string'] """) client.save({"conanfile.py": conanfile_py}, clean_first=True) client.run("create .") # Consumer using PkgConfigDeps and MesonToolchain conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson, MesonToolchain from conan.tools.gnu import PkgConfigDeps class App(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello/0.1", "other/0.1" def layout(self): self.folders.build = "build" def generate(self): deps = PkgConfigDeps(self) deps.generate() tc = MesonToolchain(self) tc.preprocessor_definitions["VAR"] = "VALUE" tc.preprocessor_definitions["VAR2"] = "VALUE2" tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() """) meson_build = textwrap.dedent(""" project('tutorial', 'cpp') cxx = meson.get_compiler('cpp') hello = dependency('hello', version : '>=0.1') other = dependency('other', version : '>=0.1') # It's not needed to declare "hello/0.1" as a dependency, only interested in flags executable('demo', 'main.cpp', dependencies: [hello, other]) """) main = textwrap.dedent(""" #include #define STR(x) #x #define SHOW_DEFINE(x) printf("%s=%s", #x, STR(x)) int main(int argc, char *argv[]) { SHOW_DEFINE(VAR); SHOW_DEFINE(VAR2); SHOW_DEFINE(DEF1); SHOW_DEFINE(DEF2); SHOW_DEFINE(DEF3); return 0; } """) client.save({"conanfile.py": conanfile_py, "meson.build": meson_build, "main.cpp": main}, clean_first=True) client.run("build . -c 'tools.build:cxxflags=[%s]'" % flags) assert "WARN: deprecated: Use 'extra_defines' attribute for compiler preprocessor " \ "definitions instead of 'preprocessor_definitions'" in client.out app_name = "demo.exe" if platform.system() == "Windows" else "demo" client.run_command(os.path.join("build", app_name)) assert 'VAR="VALUE' in client.out assert 'VAR2="VALUE2"' in client.out assert 'DEF1=one_string' in client.out assert 'DEF2=other_string' in client.out assert 'DEF3=simple_string' in client.out ================================================ FILE: test/functional/toolchains/meson/test_meson_and_objc.py ================================================ import os import platform import textwrap import pytest from conan.tools.apple.apple import _to_apple_arch, XCRun from conan.test.utils.mocks import ConanFileMock from conan.test.utils.tools import TestClient from conan.internal.util.runners import conan_run _conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson, MesonToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def layout(self): self.folders.build = "build" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def generate(self): tc = MesonToolchain(self) tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() """) _meson_build_objc = textwrap.dedent(""" project('tutorial', 'objc') executable('demo', 'main.m', link_args: ['-framework', 'Foundation']) """) @pytest.mark.tool("meson") @pytest.mark.skipif(platform.system() != "Darwin", reason="requires Xcode") def test_apple_meson_toolchain_native_compilation_objective_c(): t = TestClient() arch = t.get_default_host_profile().settings['arch'] profile = textwrap.dedent(f""" [settings] os = Macos arch = {arch} compiler = apple-clang compiler.version = 12.0 compiler.libcxx = libc++ """) app = textwrap.dedent(""" #import int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... NSLog(@"Hello, World!"); } return 0; } """) t.save({"conanfile.py": _conanfile_py, "meson.build": _meson_build_objc, "main.m": app, "macos_pr": profile}) t.run("build . -pr macos_pr") t.run_command("./demo", cwd=os.path.join(t.current_folder, "build")) assert "Hello, World!" in t.out @pytest.mark.parametrize("arch, os_, os_version, sdk", [ ('armv8', 'iOS', '10.0', 'iphoneos'), ('x86_64', 'iOS', '10.0', 'iphonesimulator'), ('armv8', 'Macos', '11.0', None) # Apple Silicon ]) @pytest.mark.tool("meson") @pytest.mark.skipif(platform.system() != "Darwin", reason="requires Xcode") def test_apple_meson_toolchain_cross_compiling_and_objective_c(arch, os_, os_version, sdk): profile = textwrap.dedent(""" include(default) [settings] os = {os} os.version = {os_version} {os_sdk} arch = {arch} compiler = apple-clang compiler.version = 12.0 compiler.libcxx = libc++ """) app = textwrap.dedent(""" #import int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... NSLog(@"Hello, World!"); } return 0; } """) profile = profile.format( os=os_, os_version=os_version, os_sdk=f'os.sdk = {sdk}' if sdk else '', arch=arch) t = TestClient() t.save({"conanfile.py": _conanfile_py, "meson.build": _meson_build_objc, "main.m": app, "profile_host": profile}) t.run("build . --profile:build=default --profile:host=profile_host") assert "Objective-C compiler for the host machine: clang" in t.out demo = os.path.join(t.current_folder, "build", "demo") assert os.path.isfile(demo) is True conanfile = ConanFileMock({}, runner=conan_run) xcrun = XCRun(conanfile, sdk) lipo = xcrun.find('lipo') t.run_command('"%s" -info "%s"' % (lipo, demo)) assert "architecture: %s" % _to_apple_arch(arch) in t.out ================================================ FILE: test/functional/toolchains/meson/test_meson_native_attribute.py ================================================ import os import platform import textwrap import pytest from conan.tools.apple.apple import _to_apple_arch, XCRun from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.mocks import ConanFileMock from conan.test.utils.tools import TestClient from conan.internal.util.runners import conan_run @pytest.mark.tool("meson") @pytest.mark.skipif(platform.system() != "Darwin", reason="requires OSX") def test_apple_meson_toolchain_cross_compiling(): arch_host = 'armv8' if platform.machine() == "x86_64" else "x86_64" arch_build = 'armv8' if platform.machine() != "x86_64" else "x86_64" profile = textwrap.dedent(f""" [settings] os = Macos arch = {arch_host} compiler = apple-clang compiler.version = 13.0 compiler.libcxx = libc++ """) profile_build = textwrap.dedent(f""" [settings] os = Macos arch = {arch_build} compiler = apple-clang compiler.version = 13.0 compiler.libcxx = libc++ """) conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson, MesonToolchain from conan.tools.build import cross_building class App(ConanFile): settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def layout(self): self.folders.build = "build" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def generate(self): tc = MesonToolchain(self) tc.generate() # Forcing to create the native context too if cross_building(self): tc = MesonToolchain(self, native=True) tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() """) meson_build = textwrap.dedent(""" project('tutorial', 'cpp') hello = library('hello', 'hello.cpp') # Even cross-building the library, we want to create an executable using only the native context executable('mygen', 'mygen.cpp', native: true) """) my_gen_cpp = gen_function_cpp(name="main") hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello") client = TestClient() client.save({"conanfile.py": conanfile_py, "meson.build": meson_build, "hello.h": hello_h, "hello.cpp": hello_cpp, "mygen.cpp": my_gen_cpp, "profile_host": profile, "profile_build": profile_build}) client.run("build . --profile:build=profile_build --profile:host=profile_host") libhello = os.path.join(client.current_folder, "build", "libhello.a") assert os.path.isfile(libhello) is True # Now, ensuring that we can run the mygen executable mygen = os.path.join(client.current_folder, "build", "mygen") client.run_command(f"'{mygen}'") assert "Release!" in client.out # Extra check for lib arch conanfile = ConanFileMock({}, runner=conan_run) xcrun = XCRun(conanfile) lipo = xcrun.find('lipo') client.run_command('"%s" -info "%s"' % (lipo, libhello)) assert "architecture: %s" % _to_apple_arch(arch_host) in client.out ================================================ FILE: test/functional/toolchains/meson/test_meson_transitive_rpath_sysroot.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.tool("ninja") @pytest.mark.tool("meson") @pytest.mark.tool("pkg_config") @pytest.mark.skipif(platform.system() != "Linux", reason="Linux/gcc required for -rpath/-rpath-link testing") def test_meson_sysroot_transitive_rpath(): c = TestClient() extra_profile = textwrap.dedent(""" [conf] tools.build:sysroot=/path/to/nowhere tools.build:add_rpath_link=True """) foobar_h = textwrap.dedent(""" #pragma once int foobar(int x, int y); """) foobar_cpp = textwrap.dedent(""" #include "foobar.h" int foobar(int x, int y) { return x + y; } """) test_package_cpp = textwrap.dedent(""" #include "foobar.h" int main() { return foobar(2, 3) == 5 ? 0 : 1; } """) consumer_meson_build = textwrap.dedent(""" project('consumer ', 'cpp') cxx = meson.get_compiler('cpp') #add_project_link_arguments('--sysroot=/path/to/nowhere', language: 'cpp') foobar = dependency('foobar', required: true) consumer_lib = library('consumer', 'src/consumer.cpp', install: true, dependencies: foobar) executable('consumer_app', 'src/main.cpp', install: true, link_with: consumer_lib) """) consumer_consumer_h = textwrap.dedent(""" #pragma once int consumer(int x, int y); """) consumer_consumer_cpp = textwrap.dedent(""" #include "consumer.h" #include "foobar.h" int consumer(int x, int y) { return foobar(x, y) * 2; } """) consumer_main_cpp = textwrap.dedent(""" #include "consumer.h" int main() { return consumer(2, 3) == 10 ? 0 : 1; } """) c.save({"extra_profile": extra_profile}) with c.chdir("foobar"): c.run("new cmake_lib -d name=foobar -d version=1.0") c.save({"include/foobar.h": foobar_h, "src/foobar.cpp": foobar_cpp, "test_package/src/example.cpp": test_package_cpp,}) c.run(f'create . -o "*:shared=True" -pr=default -pr=../extra_profile') with c.chdir("consumer"): c.run(f'new meson_lib -d name=consumer -d version=1.0 -d requires=foobar/1.0') c.save({"src/consumer.cpp": consumer_consumer_cpp, "src/main.cpp": consumer_main_cpp, "src/consumer.h": consumer_consumer_h, "meson.build": consumer_meson_build}) c.run(f'create . -o "*:shared=True" -tf= -pr=default -pr=../extra_profile') ================================================ FILE: test/functional/toolchains/meson/test_pkg_config_reuse.py ================================================ import os import pytest import textwrap from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient from test.functional.toolchains.meson._base import check_binary @pytest.mark.tool("ninja") @pytest.mark.tool("meson") @pytest.mark.tool("pkg_config") class TestMesonPkgConfig: _conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson, MesonToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "PkgConfigDeps" requires = "hello/0.1" def layout(self): self.folders.build = "build" def generate(self): tc = MesonToolchain(self) tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() """) _meson_build = textwrap.dedent(""" project('tutorial', 'cpp') hello = dependency('hello', version : '>=0.1') executable('demo', 'main.cpp', dependencies: hello) """) def test_reuse(self): t = TestClient() t.run("new cmake_lib -d name=hello -d version=0.1") t.run("create . -tf=\"\"") app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) # Prepare the actual consumer package t.save({"conanfile.py": self._conanfile_py, "meson.build": self._meson_build, "main.cpp": app}, clean_first=True) # Build in the cache t.run("build .") t.run_command(os.path.join("build", "demo")) assert "Hello World Release!" in t.out check_binary(t) ================================================ FILE: test/functional/toolchains/meson/test_preprocessor_definitions.py ================================================ import os import textwrap import pytest from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.utils.tools import TestClient from test.functional.toolchains.meson._base import check_binary class TestMesonPreprocessorDefinitionsTest: _conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import Meson, MesonToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def config_options(self): if self.settings.os == "Windows": del self.options.fPIC def layout(self): self.folders.build = "build" def generate(self): tc = MesonToolchain(self) tc.preprocessor_definitions["TEST_DEFINITION1"] = "TestPpdValue1" tc.preprocessor_definitions["TEST_DEFINITION2"] = "TestPpdValue2" tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() """) _meson_build = textwrap.dedent(""" project('tutorial', 'cpp') hello = library('hello', 'hello.cpp') executable('demo', 'main.cpp', link_with: hello) """) @pytest.mark.tool("ninja") @pytest.mark.tool("meson") def test_build(self): hello_h = gen_function_h(name="hello") hello_cpp = gen_function_cpp(name="hello", preprocessor=["TEST_DEFINITION1", "TEST_DEFINITION2"]) app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) t = TestClient() t.save({"conanfile.py": self._conanfile_py, "meson.build": self._meson_build, "hello.h": hello_h, "hello.cpp": hello_cpp, "main.cpp": app}) t.run("install .") content = t.load("conan_meson_native.ini") assert "[built-in options]" in content assert "buildtype = 'release'" in content t.run("build .") assert "WARN: deprecated: Use 'extra_defines' attribute for compiler preprocessor " \ "definitions instead of 'preprocessor_definitions'" in t.out t.run_command(os.path.join("build", "demo")) assert "hello: Release!" in t.out assert "TEST_DEFINITION1: TestPpdValue1" in t.out assert "TEST_DEFINITION2: TestPpdValue2" in t.out check_binary(t) ================================================ FILE: test/functional/toolchains/meson/test_subproject.py ================================================ import textwrap import pytest from conan.test.assets.sources import gen_function_c from conan.test.utils.tools import TestClient _conanfile_py = textwrap.dedent(""" import os import shutil from conan import ConanFile from conan.tools.meson import Meson, MesonToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False], "fPIC": [True, False], "use_french": ['true', 'false']} default_options = {"shared": False, "fPIC": True, "use_french": 'false'} exports_sources = "**" def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def configure(self): if self.options.shared: self.options.rm_safe("fPIC") def layout(self): self.folders.build = "build" def generate(self): tc = MesonToolchain(self) tc.subproject_options["hello"] = [{'french': str(self.options.use_french)}] tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() def package(self): meson = Meson(self) meson.install() # https://mesonbuild.com/FAQ.html#why-does-building-my-project-with-msvc-output-static-libraries-called-libfooa if self.settings.compiler == 'msvc' and not self.options.shared: shutil.move(os.path.join(self.package_folder, "lib", "libhello.a"), os.path.join(self.package_folder, "lib", "hello.lib")) shutil.move(os.path.join(self.package_folder, "lib", "libgreeter.a"), os.path.join(self.package_folder, "lib", "greeter.lib")) def package_info(self): self.cpp_info.components["hello"].libs = ['hello'] self.cpp_info.components["greeter"].libs = ['greeter'] self.cpp_info.components["greeter"].requires = ['hello'] """) _meson_build = textwrap.dedent(""" project('greeter', 'c') hello_proj = subproject('hello') hello_dep = hello_proj.get_variable('hello_dep') inc = include_directories('include') greeter = static_library('greeter', 'greeter.c', include_directories : inc, dependencies : hello_dep, install : true) install_headers('greeter.h') """) _meson_subproject_build = textwrap.dedent(""" project('hello', 'c') hello_c_args = [] if get_option('french') hello_c_args = ['-DFRENCH'] endif inc = include_directories('include') hello = static_library('hello', 'hello.c', include_directories : inc, c_args: hello_c_args, install : true) hello_dep = declare_dependency(include_directories : inc, link_with : hello) """) _meson_subproject_options = textwrap.dedent(""" option('french', type : 'boolean', value : false) """) _hello_c = textwrap.dedent(""" #include void hello(void) { #ifdef FRENCH printf("Le sous-projet vous salut\\n"); #else printf("Hello from subproject\\n"); #endif } """) _greeter_c = textwrap.dedent(""" #include void greeter(void) { hello(); } """) _hello_h = textwrap.dedent(""" #pragma once void hello(void); """) _greeter_h = textwrap.dedent(""" #pragma once void greeter(void); """) _test_package_conanfile_py = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout from conan.tools.build import cross_building class TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain", "CMakeDeps" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def test(self): if not cross_building(self): cmd = os.path.join(self.cpp.build.bindirs[0], "test_package") self.run(cmd, env=["conanrunenv"]) """) _test_package_cmake_lists = textwrap.dedent(""" cmake_minimum_required(VERSION 3.1) project(test_package C) add_executable(${PROJECT_NAME} src/test_package.c) find_package(greeter CONFIG REQUIRED) target_link_libraries(${PROJECT_NAME} greeter::greeter) """) @pytest.mark.tool("ninja") @pytest.mark.tool("cmake") @pytest.mark.tool("meson") def test_subproject(): client = TestClient() test_package_c = gen_function_c(name="main", includes=["greeter"], calls=["greeter"]) client.save({"conanfile.py": _conanfile_py, "meson.build": _meson_build, "greeter.c": _greeter_c, "greeter.h": _greeter_h, "include/greeter.h": _greeter_h, "subprojects/hello/include/hello.h": _hello_h, "subprojects/hello/hello.c": _hello_c, "subprojects/hello/meson.build": _meson_subproject_build, "subprojects/hello/meson_options.txt": _meson_subproject_options, "test_package/conanfile.py": _test_package_conanfile_py, "test_package/CMakeLists.txt": _test_package_cmake_lists, "test_package/src/test_package.c": test_package_c}) client.run("create . --name=greeter --version=0.1") assert "Hello from subproject" in client.out assert "Le sous-projet vous salut" not in client.out # Using subproject options client.run("create . --name=greeter --version=0.1 -o use_french='true'") assert "Hello from subproject" not in client.out assert "Le sous-projet vous salut" in client.out ================================================ FILE: test/functional/toolchains/meson/test_test.py ================================================ import pytest import textwrap from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient from test.functional.toolchains.meson._base import check_binary @pytest.mark.tool("ninja") @pytest.mark.tool("meson") @pytest.mark.tool("pkg_config") class TestMeson: _test_package_meson_build = textwrap.dedent(""" project('test_package', 'cpp') hello = dependency('hello', version : '>=0.1') test_package = executable('test_package', 'test_package.cpp', dependencies: hello) test('test package', test_package) """) _test_package_conanfile_py = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.meson import Meson, MesonToolchain class TestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps" def requirements(self): self.requires(self.tested_reference_str) def layout(self): self.folders.build = "build" def generate(self): tc = MesonToolchain(self) tc.generate() def build(self): meson = Meson(self) meson.configure() meson.build() def test(self): meson = Meson(self) meson.configure() meson.test() """) def test_reuse(self): t = TestClient() t.run("new cmake_lib -d name=hello -d version=0.1") test_package_cpp = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) t.save({"test_package/conanfile.py": self._test_package_conanfile_py, "test_package/meson.build": self._test_package_meson_build, "test_package/test_package.cpp": test_package_cpp}) t.run("create . --name=hello --version=0.1") check_binary(t) ================================================ FILE: test/functional/toolchains/meson/test_v2_meson_template.py ================================================ import os import pytest from conan.test.utils.tools import TestClient @pytest.mark.tool("ninja") @pytest.mark.tool("meson") @pytest.mark.tool("pkg_config") def test_meson_lib_template(): # Identical to def test_cmake_lib_template(), but for Meson client = TestClient(path_with_spaces=False) client.run("new meson_lib -d name=hello -d version=0.1") # Local flow works client.run("install .") client.run("build .") client.run("export-pkg . --name=hello --version=0.1") package_folder = client.created_layout().package() assert os.path.exists(os.path.join(package_folder, "include", "hello.h")) # Create works client.run("create .") assert "hello/0.1: Hello World Release!" in client.out client.run("create . -s build_type=Debug") assert "hello/0.1: Hello World Debug!" in client.out # Create + shared works client.run("create . -o hello/*:shared=True") assert "hello/0.1: Hello World Release!" in client.out @pytest.mark.tool("ninja") @pytest.mark.tool("meson") def test_meson_exe_template(): client = TestClient(path_with_spaces=False) client.run("new meson_exe -d name=greet -d version=0.1") # Local flow works client.run("install .") client.run("build .") # Create works client.run("create .") assert "greet/0.1: Hello World Release!" in client.out client.run("create . -s build_type=Debug") assert "greet/0.1: Hello World Debug!" in client.out ================================================ FILE: test/functional/toolchains/microsoft/__init__.py ================================================ ================================================ FILE: test/functional/toolchains/microsoft/test_msbuild.py ================================================ import os import platform import textwrap import pytest from conan.tools.microsoft.visual import vcvars_command from conan.internal.api.detect.detect_vs import vs_installation_path from conan.test.assets.sources import gen_function_cpp from test.functional.utils import check_vs_runtime, check_exe_run from conan.test.utils.tools import TestClient from conan.internal.util.files import rmdir sln_file = r""" Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.757 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MyApp", "MyApp\MyApp.vcxproj", "{B58316C0-C78A-4E9B-AE8F-5D6368CE3840}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|x64 = Release|x64 Release|x86 = Release|x86 Release - Shared|x64 = Release - Shared|x64 Release - Shared|x86 = Release - Shared|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x64.ActiveCfg = Debug|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x64.Build.0 = Debug|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x86.ActiveCfg = Debug|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x86.Build.0 = Debug|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x64.ActiveCfg = Release|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x64.Build.0 = Release|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x86.ActiveCfg = Release|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x86.Build.0 = Release|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release - Shared|x64.ActiveCfg = Release - Shared|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release - Shared|x64.Build.0 = Release - Shared|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release - Shared|x86.ActiveCfg = Release - Shared|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release - Shared|x86.Build.0 = Release - Shared|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DE6E462F-E299-4F9C-951A-F9404EB51521} EndGlobalSection EndGlobal """ myapp_vcxproj = r""" Debug Win32 Release - Shared Win32 Release - Shared x64 Release Win32 Debug x64 Release x64 15.0 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840} Win32Proj MyApp Application false v141 true Unicode Application false v141 true Unicode Application true v141 Unicode Application false v141 true Unicode Application true v141 Unicode Application false v141 true Unicode true true false false false false NotUsing Level3 Disabled true WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true NotUsing Level3 MaxSpeed true true true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true NotUsing Level3 MaxSpeed true true true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true NotUsing Level3 Disabled true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true NotUsing Level3 MaxSpeed true true true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true NotUsing Level3 MaxSpeed true true true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true """ @pytest.mark.tool("visual_studio", "15") @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_msvc_runtime_flag_vs2017(): check_msvc_runtime_flag("191") @pytest.mark.tool("visual_studio", "17") @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_msvc_runtime_flag_vs2022(): check_msvc_runtime_flag("193") def check_msvc_runtime_flag(msvc_version): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import msvc_runtime_flag class App(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): self.output.info("MSVC FLAG={}!!".format(msvc_runtime_flag(self))) """) client.save({"conanfile.py": conanfile}) client.run('install . -s compiler=msvc -s compiler.version={vs_version} ' '-s compiler.runtime=dynamic'.format(vs_version=msvc_version)) assert "MSVC FLAG=MD!!" in client.out client.run('install . -s compiler=msvc -s compiler.version={msvc_version} ' '-s compiler.runtime=static ' '-s compiler.runtime_type=Debug ' '-s compiler.cppstd=14'.format(msvc_version=msvc_version)) assert "MSVC FLAG=MTd!!" in client.out client.run('install . -s compiler=msvc -s compiler.version={msvc_version} ' '-s compiler.runtime=dynamic ' '-s compiler.cppstd=14'.format(msvc_version=msvc_version)) assert "MSVC FLAG=MD!!" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") @pytest.mark.tool("visual_studio") class TestWin: # FIXME: This test needs to be parameterized correctly for different VS versions conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.microsoft import MSBuildToolchain, MSBuild, MSBuildDeps from conan.tools.files import copy class App(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello/0.1" options = {"shared": [True, False]} default_options = {"shared": False} def layout(self): self.folders.generators = "conan" self.folders.build = "." def generate(self): tc = MSBuildToolchain(self) gen = MSBuildDeps(self) shared_option = self.dependencies["hello"].options.get_safe("shared") if shared_option and self.settings.build_type == "Release": tc.configuration = "Release - Shared" gen.configuration = "Release - Shared" tc.preprocessor_definitions["DEFINITIONS_BOTH"] = '"True"' tc.preprocessor_definitions["DEFINITIONS_BOTH2"] = 'DEFINITIONS_BOTH' tc.preprocessor_definitions["DEFINITIONS_BOTH_INT"] = 123 if self.settings.build_type == "Debug": tc.preprocessor_definitions["DEFINITIONS_CONFIG"] = '"Debug"' tc.preprocessor_definitions["DEFINITIONS_CONFIG_INT"] = 234 else: tc.preprocessor_definitions["DEFINITIONS_CONFIG"] = '"Release"' tc.preprocessor_definitions["DEFINITIONS_CONFIG_INT"] = 456 tc.preprocessor_definitions["DEFINITIONS_CONFIG2"] = 'DEFINITIONS_CONFIG' tc.generate() gen.generate() shared_option = self.dependencies["hello"].options.get_safe("shared") if shared_option and self.settings.build_type == "Release": configuration = "Release - Shared" if self.settings.arch == "x86_64": dst = "x64/%s" % configuration else: dst = configuration else: configuration = self.settings.build_type dst = "%s/%s" % (self.settings.arch, configuration) src = os.path.join(self.dependencies["hello"].package_folder, "bin") dst = os.path.join(self.build_folder, dst) copy(self, "*.dll", src, dst, keep_path=False) def build(self): msbuild = MSBuild(self) msbuild.build("MyProject.sln") """) app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"], preprocessor=["DEFINITIONS_BOTH", "DEFINITIONS_BOTH2", "DEFINITIONS_BOTH_INT", "DEFINITIONS_CONFIG", "DEFINITIONS_CONFIG2", "DEFINITIONS_CONFIG_INT"]) @staticmethod def _run_app(client, arch, build_type, shared=None): if build_type == "Release" and shared: configuration = "Release - Shared" else: configuration = build_type if arch == "x86": command_str = "%s\\MyApp.exe" % configuration else: command_str = "x64\\%s\\MyApp.exe" % configuration client.run_command(command_str) @pytest.mark.tool("cmake") @pytest.mark.tool("visual_studio", "15") @pytest.mark.parametrize("compiler,version,runtime,cppstd", [("msvc", "191", "static", "17"), # ("msvc", "190", "static", "14") ]) def test_toolchain_win_vs2017(self, compiler, version, runtime, cppstd): self.check_toolchain_win(compiler, version, runtime, cppstd, ide_version=15) @pytest.mark.tool("cmake", "3.23") @pytest.mark.tool("visual_studio", "17") @pytest.mark.parametrize("compiler,version,runtime,cppstd", [("msvc", "193", "static", "17")]) def test_toolchain_win_vs2022(self, compiler, version, runtime, cppstd): self.check_toolchain_win(compiler, version, runtime, cppstd, ide_version=17) def check_toolchain_win(self, compiler, version, runtime, cppstd, ide_version): client = TestClient(path_with_spaces=False) settings = [("compiler", compiler), ("compiler.version", version), ("compiler.cppstd", cppstd), ("compiler.runtime", runtime), ("build_type", "Release"), ("arch", "x86")] profile = textwrap.dedent(""" [settings] os=Windows [conf] tools.microsoft.msbuild:vs_version={vs_version} """.format(vs_version=ide_version)) client.save({"myprofile": profile}) # Build the profile according to the settings provided settings_h = " ".join('-s:h %s="%s"' % (k, v) for k, v in settings if v) settings_b = " ".join('-s:b %s="%s"' % (k, v) for k, v in settings if v) client.run("new cmake_lib -d name=hello -d version=0.1") client.run(f"create . {settings_h} -c tools.microsoft.msbuild:vs_version={ide_version} -c tools.build:verbosity=verbose -c tools.compilation:verbosity=verbose") assert "MSBUILD : error MSB1001: Unknown switch" not in client.out assert "-verbosity:Detailed" in client.out # Prepare the actual consumer package client.save({"conanfile.py": self.conanfile, "MyProject.sln": sln_file, "MyApp/MyApp.vcxproj": myapp_vcxproj, "MyApp/MyApp.cpp": self.app, "myprofile": profile}, clean_first=True) # Run the configure corresponding to this test case client.run("build . %s %s -pr:h=myprofile " % (settings_h, settings_b)) assert "conanfile.py: MSBuildToolchain created conantoolchain_release_win32.props" in client.out assert f"conanvcvars.bat: Activating environment Visual Studio {ide_version}" in client.out assert "[vcvarsall.bat] Environment initialized for: 'x86'" in client.out self._run_app(client, "x86", "Release") assert "Hello World Release" in client.out check_exe_run(client.out, "main", "msvc", version, "Release", "x86", cppstd, {"DEFINITIONS_BOTH": 'True', "DEFINITIONS_BOTH2": "True", "DEFINITIONS_BOTH_INT": "123", "DEFINITIONS_CONFIG": 'Release', "DEFINITIONS_CONFIG2": 'Release', "DEFINITIONS_CONFIG_INT": "456"}) static_runtime = True if runtime == "static" or "MT" in runtime else False check_vs_runtime("Release/MyApp.exe", client, ide_version, build_type="Release", static_runtime=static_runtime) @pytest.mark.tool("cmake", "3.23") @pytest.mark.tool("visual_studio", "17") def test_toolchain_win_debug(self): client = TestClient(path_with_spaces=False) settings = [("compiler", "msvc"), ("compiler.version", "193"), ("compiler.runtime", "dynamic"), ("build_type", "Debug"), ("arch", "x86_64")] # Build the profile according to the settings provided settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings if v) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("create . %s -tf=\"\"" % (settings,)) # Prepare the actual consumer package client.save({"conanfile.py": self.conanfile, "MyProject.sln": sln_file, "MyApp/MyApp.vcxproj": myapp_vcxproj, "MyApp/MyApp.cpp": self.app}, clean_first=True) # Run the configure corresponding to this test case client.run("build . %s" % (settings, )) assert "conanfile.py: MSBuildToolchain created conantoolchain_debug_x64.props" in client.out assert f"conanvcvars.bat: Activating environment Visual Studio 17" in client.out assert "[vcvarsall.bat] Environment initialized for: 'x64'" in client.out self._run_app(client, "x64", "Debug") assert "Hello World Debug" in client.out check_exe_run(client.out, "main", "msvc", "19", "Debug", "x86_64", "14", {"DEFINITIONS_BOTH": 'True', "DEFINITIONS_BOTH2": "True", "DEFINITIONS_BOTH_INT": "123", "DEFINITIONS_CONFIG": 'Debug', "DEFINITIONS_CONFIG2": 'Debug', "DEFINITIONS_CONFIG_INT": "234"}) check_vs_runtime("x64/Debug/MyApp.exe", client, "17", build_type="Debug") @pytest.mark.tool("cmake", "3.23") @pytest.mark.tool("visual_studio", "17") def test_toolchain_win_multi(self): ide_version = "17" client = TestClient(path_with_spaces=False) settings = [("compiler", "msvc"), ("compiler.version", "193"), ("compiler.cppstd", "17"), ("compiler.runtime", "static")] settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings if v) client.run("new cmake_lib -d name=hello -d version=0.1") configs = [("Release", "x86", True), ("Release", "x86_64", True), ("Debug", "x86", False), ("Debug", "x86_64", False)] for build_type, arch, shared in configs: # Build the profile according to the settings provided # TODO: It is a bit ugly to remove manually build_test_folder = os.path.join(client.current_folder, "test_package", "build") rmdir(build_test_folder) runtime = "static" client.run("create . --name=hello --version=0.1 %s -s build_type=%s -s arch=%s -s compiler.runtime=%s " " -o hello/*:shared=%s" % (settings, build_type, arch, runtime, shared)) # Prepare the actual consumer package client.save({"conanfile.py": self.conanfile, "MyProject.sln": sln_file, "MyApp/MyApp.vcxproj": myapp_vcxproj, "MyApp/MyApp.cpp": self.app}, clean_first=True) # Run the configure corresponding to this test case for build_type, arch, shared in configs: runtime = "static" client.run("install . %s -s build_type=%s -s arch=%s -s compiler.runtime=%s" " -o hello/*:shared=%s" % (settings, build_type, arch, runtime, shared)) vs_path = vs_installation_path(ide_version) vcvars_path = os.path.join(vs_path, "VC/Auxiliary/Build/vcvarsall.bat") for build_type, arch, shared in configs: platform_arch = "x86" if arch == "x86" else "x64" if build_type == "Release" and shared: configuration = "Release - Shared" else: configuration = build_type # The "conan build" command is not good enough, cannot do the switch between configs cmd = ('set "VSCMD_START_DIR=%%CD%%" && ' '"%s" x64 && msbuild "MyProject.sln" /p:Configuration="%s" ' '/p:Platform=%s ' % (vcvars_path, configuration, platform_arch)) client.run_command(cmd) assert "[vcvarsall.bat] Environment initialized for: 'x64'" in client.out self._run_app(client, arch, build_type, shared) check_exe_run(client.out, "main", "msvc", "19", build_type, arch, "17", {"DEFINITIONS_BOTH": "True", "DEFINITIONS_CONFIG": build_type}) if arch == "x86": command_str = "%s\\MyApp.exe" % configuration else: command_str = "x64\\%s\\MyApp.exe" % configuration vcvars = vcvars_command(version=ide_version, architecture="amd64") cmd = ('%s && dumpbin /dependents "%s"' % (vcvars, command_str)) client.run_command(cmd) if shared: assert "hello.dll" in client.out else: assert "hello.dll" not in client.out assert "KERNEL32.dll" in client.out ================================================ FILE: test/functional/toolchains/microsoft/test_msbuilddeps.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.assets.sources import gen_function_cpp, gen_function_h from conan.test.assets.visual_project_files import get_vs_project_files from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID sln_file = r""" Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.757 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MyProject", "MyProject\MyProject.vcxproj", "{6F392A05-B151-490C-9505-B2A49720C4D9}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MyApp", "MyApp\MyApp.vcxproj", "{B58316C0-C78A-4E9B-AE8F-5D6368CE3840}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x64.ActiveCfg = Debug|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x64.Build.0 = Debug|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x86.ActiveCfg = Debug|Win32 {6F392A05-B151-490C-9505-B2A49720C4D9}.Debug|x86.Build.0 = Debug|Win32 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x64.ActiveCfg = Release|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x64.Build.0 = Release|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x86.ActiveCfg = Release|Win32 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x86.Build.0 = Release|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x64.ActiveCfg = Debug|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x64.Build.0 = Debug|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x86.ActiveCfg = Debug|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Debug|x86.Build.0 = Debug|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x64.ActiveCfg = Release|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x64.Build.0 = Release|x64 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x86.ActiveCfg = Release|Win32 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DE6E462F-E299-4F9C-951A-F9404EB51521} EndGlobalSection EndGlobal """ myproject_vcxproj = r""" Debug Win32 Release Win32 Debug x64 Release x64 15.0 {6F392A05-B151-490C-9505-B2A49720C4D9} Win32Proj MyProject Application true v141 Unicode Application false v141 true Unicode Application true v141 Unicode Application false v141 true Unicode true true false false NotUsing Level3 Disabled true WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true NotUsing Level3 Disabled true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true NotUsing Level3 MaxSpeed true true true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true NotUsing Level3 MaxSpeed true true true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true """ myapp_vcxproj = r""" Debug Win32 Release Win32 Debug x64 Release x64 15.0 {B58316C0-C78A-4E9B-AE8F-5D6368CE3840} Win32Proj MyApp Application true v141 Unicode Application false v141 true Unicode Application true v141 Unicode Application false v141 true Unicode true true false false NotUsing Level3 Disabled true WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true NotUsing Level3 Disabled true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true NotUsing Level3 MaxSpeed true true true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true NotUsing Level3 MaxSpeed true true true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true """ @pytest.mark.tool("cmake") @pytest.mark.tool("visual_studio") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_msbuild_generator(): client = TestClient() client.run("new cmake_lib -d name=hello0 -d version=1.0") client.run("create . -tf=") client.save({}, clean_first=True) client.run("new cmake_lib -d name=hello3 -d version=1.0") client.run("create . -tf=") client.save({}, clean_first=True) client.run("new cmake_lib -d name=hello1 -d version=1.0 -d requires=hello0/1.0") client.run("create . -tf=") client.save({}, clean_first=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuild class HelloConan(ConanFile): settings = "os", "build_type", "compiler", "arch" requires = "hello1/1.0", "hello3/1.0" generators = "MSBuildDeps", "MSBuildToolchain" def build(self): msbuild = MSBuild(self) msbuild.build("MyProject.sln") """) myapp_cpp = gen_function_cpp(name="main", msg="MyApp", includes=["hello1"], calls=["hello1"]) myproject_cpp = gen_function_cpp(name="main", msg="MyProject", includes=["hello3"], calls=["hello3"]) files = {"MyProject.sln": sln_file, "MyProject/MyProject.vcxproj": myproject_vcxproj, "MyProject/MyProject.cpp": myproject_cpp, "MyApp/MyApp.vcxproj": myapp_vcxproj, "MyApp/MyApp.cpp": myapp_cpp, "conanfile.py": conanfile} client.save(files, clean_first=True) client.run("install .") client.run("build .") assert "warning MSB4011" not in client.out client.run_command(r"x64\Release\MyProject.exe") assert "MyProject: Release!" in client.out assert "hello3/1.0: Hello World Release!" in client.out client.run_command(r"x64\Release\MyApp.exe") assert "MyApp: Release!" in client.out assert "hello0/1.0: Hello World Release!" in client.out assert "hello1/1.0: Hello World Release" in client.out @pytest.mark.tool("visual_studio") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_install_reference_gcc(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "MSBuildDeps" requires = "pkg/1.0" """) client.save({"conanfile.py": conanfile}) client.run('install . -s os=Windows -s compiler=msvc ' '-s compiler.version=194 -s compiler.runtime=dynamic') assert "conanfile.py: Generator 'MSBuildDeps' calling 'generate()'" in client.out props = client.load("conan_pkg_release_x64.props") assert '' in props # This will overwrite the existing one, cause configuration and arch is the same client.run("install . -s os=Linux -s compiler=gcc -s compiler.version=5.2 '" "'-s compiler.libcxx=libstdc++") assert "conanfile.py: Generator 'MSBuildDeps' calling 'generate()'" in client.out pkg_props = client.load("conan_pkg.props") assert 'Project="conan_pkg_release_x64.props"' in pkg_props @pytest.mark.tool("visual_studio") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_custom_configuration(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuildDeps class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "pkg/1.0" def generate(self): ms = MSBuildDeps(self) ms.configuration = "My"+str(self.settings.build_type) ms.platform = "My"+str(self.settings.arch) ms.generate() """) client.save({"conanfile.py": conanfile}) client.run('install . -s os=Windows -s compiler=msvc ' '-s compiler.version=194 -s compiler.runtime=dynamic') props = client.load("conan_pkg_myrelease_myx86_64.props") assert '' in props client.run('install . -s os=Windows -s compiler=msvc ' '-s compiler.version=194 -s compiler.runtime=dynamic -s arch=x86 ' '-s build_type=Debug') props = client.load("conan_pkg_mydebug_myx86.props") assert '' in props props = client.load("conan_pkg.props") assert "conan_pkg_myrelease_myx86_64.props" in props assert "conan_pkg_mydebug_myx86.props" in props def test_custom_configuration_errors(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuildDeps class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "pkg/1.0" def generate(self): ms = MSBuildDeps(self) ms.configuration = None ms.generate() """) client.save({"conanfile.py": conanfile}) client.run('install . -s os=Windows -s compiler=msvc' ' -s compiler.version=194 -s compiler.runtime=dynamic', assert_error=True) assert "MSBuildDeps.configuration is None, it should have a value" in client.out client.save({"conanfile.py": conanfile.replace("configuration", "platform")}) client.run('install . -s os=Windows -s compiler=msvc' ' -s compiler.version=194 -s compiler.runtime=dynamic', assert_error=True) assert "MSBuildDeps.platform is None, it should have a value" in client.out @pytest.mark.tool("visual_studio") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_install_transitive(): # https://github.com/conan-io/conan/issues/8065 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkga --version=1.0") client.save({"conanfile.py": GenConanfile().with_requires("pkga/1.0")}) client.run("create . --name=pkgb --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuild class HelloConan(ConanFile): settings = "os", "build_type", "compiler", "arch" requires = "pkgb/1.0@", "pkga/1.0" generators = "MSBuildDeps", "MSBuildToolchain" def build(self): msbuild = MSBuild(self) msbuild.build("MyProject.sln") """) myapp_cpp = gen_function_cpp(name="main", msg="MyApp") myproject_cpp = gen_function_cpp(name="main", msg="MyProject") files = {"MyProject.sln": sln_file, "MyProject/MyProject.vcxproj": myproject_vcxproj.replace("conan_hello3.props", "conandeps.props"), "MyProject/MyProject.cpp": myproject_cpp, "MyApp/MyApp.vcxproj": myapp_vcxproj.replace("conan_hello1.props", "conandeps.props"), "MyApp/MyApp.cpp": myapp_cpp, "conanfile.py": conanfile} client.save(files, clean_first=True) client.run("build .") assert "warning MSB4011" not in client.out def test_no_build_type_error(): client = TestClient() client.save({"conanfile.py": GenConanfile(), "profile": "[settings]\nos=Windows\ncompiler=msvc\narch=x86_64"}) client.run("create . --name=mypkg --version=0.1") client.run("install --requires=mypkg/0.1@ -g MSBuildDeps -pr=profile", assert_error=True) assert "The 'msbuild' generator requires a 'build_type' setting value" in client.out def test_install_build_requires(): # https://github.com/conan-io/conan/issues/8170 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=tool --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class HelloConan(ConanFile): settings = "os", "build_type", "compiler", "arch" build_requires = "tool/1.0" generators = "MSBuildDeps" def build(self): deps = load(self, "conandeps.props") assert "conan_tool.props" not in deps self.output.info("Conan_tools.props not in deps") """) client.save({"conanfile.py": conanfile}) client.run("install .") deps = client.load("conandeps.props") assert "conan_tool.props" not in deps client.run("create . --name=pkg --version=0.1") assert "Conan_tools.props not in deps" in client.out @pytest.mark.tool("visual_studio") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_install_transitive_build_requires(): # https://github.com/conan-io/conan/issues/8170 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=dep --version=1.0") client.run("create . --name=tool_build --version=1.0") client.run("create . --name=tool_test --version=1.0") conanfile = GenConanfile().with_requires("dep/1.0").\ with_tool_requires("tool_build/1.0").\ with_test_requires("tool_test/1.0") client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=1.0") client.save({"conanfile.py": GenConanfile(). with_settings("os", "compiler", "arch", "build_type"). with_requires("pkg/1.0")}, clean_first=True) client.run("install . -g MSBuildDeps -pr:b=default -pr:h=default") pkg = client.load("conan_pkg_release_x64.props") assert "conan_dep.props" in pkg assert "tool_test" not in pkg # test requires are not there assert "tool_build" not in pkg @pytest.mark.tool("visual_studio") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_install_reference(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=mypkg --version=0.1") client.run("install --requires=mypkg/0.1@ -g MSBuildDeps") assert "Generator 'MSBuildDeps' calling 'generate()'" in client.out # https://github.com/conan-io/conan/issues/8163 props = client.load("conan_mypkg_vars_release_x64.props") # default Release/x64 folder = props[props.find("")+len("") :props.find("")] assert os.path.isfile(os.path.join(folder, "conaninfo.txt")) @pytest.mark.parametrize("pattern,exclude_a,exclude_b", [("['*']", True, True), ("['pkga']", True, False), ("['pkgb']", False, True), ("['pkg*']", True, True), ("['pkga', 'pkgb']", True, True), ("['*a', '*b']", True, True), ("['nonexist']", False, False), ]) def test_exclude_code_analysis(pattern, exclude_a, exclude_b): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkga --version=1.0") client.run("create . --name=pkgb --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuild class HelloConan(ConanFile): settings = "os", "build_type", "compiler", "arch" requires = "pkgb/1.0@", "pkga/1.0" generators = "MSBuildDeps" def build(self): msbuild = MSBuild(self) msbuild.build("MyProject.sln") """) profile = textwrap.dedent(""" include(default) [settings] build_type=Release arch=x86_64 [conf] tools.microsoft.msbuilddeps:exclude_code_analysis = %s """ % pattern) client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile profile") depa = client.load("conan_pkga_release_x64.props") depb = client.load("conan_pkgb_release_x64.props") if exclude_a: inc = "$(ConanpkgaIncludeDirectories)" ca_exclude = "%s;$(CAExcludePath)" % inc assert ca_exclude in depa else: assert "CAExcludePath" not in depa if exclude_b: inc = "$(ConanpkgbIncludeDirectories)" ca_exclude = "%s;$(CAExcludePath)" % inc assert ca_exclude in depb else: assert "CAExcludePath" not in depb @pytest.mark.tool("visual_studio", "15") @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_build_vs_project_with_a_vs2017(): check_build_vs_project_with_a("191") @pytest.mark.tool("visual_studio", "17") @pytest.mark.tool("cmake", "3.23") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_build_vs_project_with_a_vs2022(): check_build_vs_project_with_a("193") def check_build_vs_project_with_a(vs_version): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=updep.pkg.team --version=0.1") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy class HelloConan(ConanFile): settings = "os", "build_type", "compiler", "arch" exports_sources = '*' requires = "updep.pkg.team/0.1@" generators = "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include")) copy(self, "*.a", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) def package_info(self): self.cpp_info.libs = ["hello.a"] """) hello_cpp = gen_function_cpp(name="hello") hello_h = gen_function_h(name="hello") cmake = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyLib CXX) set(CMAKE_STATIC_LIBRARY_SUFFIX ".a") add_library(hello hello.cpp) """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "hello.cpp": hello_cpp, "hello.h": hello_h}) client.run('create . --name=mydep.pkg.team --version=0.1 -s compiler=msvc' ' -s compiler.version={vs_version}'.format(vs_version=vs_version)) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuild class HelloConan(ConanFile): settings = "os", "build_type", "compiler", "arch" requires = "mydep.pkg.team/0.1@" generators = "MSBuildDeps", "MSBuildToolchain" def build(self): msbuild = MSBuild(self) msbuild.build("MyProject.sln") """) files = get_vs_project_files() main_cpp = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) files["MyProject/main.cpp"] = main_cpp files["conanfile.py"] = consumer # INJECT PROPS FILES props = os.path.join(client.current_folder, "conandeps.props") old = r'' new = old + f'' files["MyProject/MyProject.vcxproj"] = files["MyProject/MyProject.vcxproj"].replace(old, new) old = r'' toolchain = os.path.join(client.current_folder, "conantoolchain.props") new = f'' + old files["MyProject/MyProject.vcxproj"] = files["MyProject/MyProject.vcxproj"].replace(old, new) client.save(files, clean_first=True) client.run('build . -s compiler=msvc' ' -s compiler.version={vs_version}'.format(vs_version=vs_version)) client.run_command(r"x64\Release\MyProject.exe") assert "hello: Release!" in client.out # TODO: This doesnt' work because get_vs_project_files() don't define NDEBUG correctly # assert "main: Release!" in client.out @pytest.mark.tool("visual_studio", "15") @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_build_vs_project_with_test_requires_vs2017(): check_build_vs_project_with_test_requires("191") @pytest.mark.tool("visual_studio", "17") @pytest.mark.tool("cmake", "3.23") @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_build_vs_project_with_test_requires_vs2022(): check_build_vs_project_with_test_requires("193") def check_build_vs_project_with_test_requires(vs_version): client = TestClient() client.run("new cmake_lib -d name=updep.pkg.team -d version=0.1") client.run("create . -s compiler.version={vs_version} -tf=".format(vs_version=vs_version)) client.save({}, clean_first=True) client.run("new cmake_lib -d name=mydep.pkg.team -d version=0.1 -d requires=updep.pkg.team/0.1") client.run("create . -s compiler.version={vs_version} -tf=".format(vs_version=vs_version)) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuild class HelloConan(ConanFile): settings = "os", "build_type", "compiler", "arch" generators = "MSBuildDeps", "MSBuildToolchain" def build_requirements(self): self.test_requires("mydep.pkg.team/0.1") def build(self): msbuild = MSBuild(self) msbuild.build("MyProject.sln") """) files = get_vs_project_files() main_cpp = gen_function_cpp(name="main", includes=["mydep.pkg.team"], calls=["mydep_pkg_team"]) files["MyProject/main.cpp"] = main_cpp files["conanfile.py"] = consumer # INJECT PROPS FILES props = os.path.join(client.current_folder, "conandeps.props") old = r'' new = old + f'' files["MyProject/MyProject.vcxproj"] = files["MyProject/MyProject.vcxproj"].replace(old, new) old = r'' toolchain = os.path.join(client.current_folder, "conantoolchain.props") new = f'' + old files["MyProject/MyProject.vcxproj"] = files["MyProject/MyProject.vcxproj"].replace(old, new) client.save(files, clean_first=True) client.run('build . -s compiler.version={vs_version}'.format(vs_version=vs_version)) client.run_command(r"x64\Release\MyProject.exe") assert "mydep.pkg.team/0.1: Hello World Release!" in client.out assert "updep.pkg.team/0.1: Hello World Release!" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_private_transitive(): # https://github.com/conan-io/conan/issues/9514 client = TestClient() client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requirement("dep/0.1", visible=False), "consumer/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_settings("os", "build_type", "arch")}) client.run("create dep --name=dep --version=0.1") client.run("create pkg --name=pkg --version=0.1") client.run("install consumer -g MSBuildDeps -s arch=x86_64 -s build_type=Release -v") client.assert_listed_binary({"dep/0.1": (NO_SETTINGS_PACKAGE_ID, "Skip")}) deps_props = client.load("consumer/conandeps.props") assert "conan_pkg.props" in deps_props assert "dep" not in deps_props pkg_data_props = client.load("consumer/conan_pkg_release_x64.props") assert "conan_dep.props" not in pkg_data_props @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_build_requires(): # https://github.com/conan-io/conan/issues/9545 client = TestClient() package = "copy(self, '*', os.path.join(self.build_folder, str(self.settings.arch))," \ " os.path.join(self.package_folder, 'bin'))" dep = GenConanfile().with_exports_sources("*").with_settings("arch").with_package(package)\ .with_import("import os").with_import("from conan.tools.files import copy") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuild class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" build_requires = "dep/0.1" generators = "MSBuildDeps", "MSBuildToolchain" def build(self): msbuild = MSBuild(self) msbuild.build("hello.sln") """) hello_vcxproj = textwrap.dedent(r""" Release Win32 Release x64 15.0 {6F392A05-B151-490C-9505-B2A49720C4D9} Win32Proj MyProject Application false v141 true Unicode Application false v141 true Unicode Document data.proto.h dep1tool """) hello_sln = textwrap.dedent(r""" Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.757 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MyProject", "MyProject\MyProject.vcxproj", "{6F392A05-B151-490C-9505-B2A49720C4D9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x64.ActiveCfg = Release|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x64.Build.0 = Release|x64 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x86.ActiveCfg = Release|Win32 {6F392A05-B151-490C-9505-B2A49720C4D9}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DE6E462F-E299-4F9C-951A-F9404EB51521} EndGlobalSection EndGlobal """) client.save({"dep/conanfile.py": dep, "dep/x86/dep1tool.bat": "@echo Invoking 32bit dep_1 build tool", "dep/x86_64/dep1tool.bat": "@echo Invoking 64bit dep_1 build tool", "consumer/conanfile.py": consumer, "consumer/hello.sln": hello_sln, "consumer/MyProject/MyProject.vcxproj": hello_vcxproj, "consumer/MyProject/data.proto": "dataproto"}) client.run("create dep --name=dep --version=0.1 -s arch=x86") client.run("create dep --name=dep --version=0.1 -s arch=x86_64") with client.chdir("consumer"): client.run('build . -s compiler=msvc -s compiler.version=191 ' " -s arch=x86_64 -s build_type=Release") client.assert_listed_binary({"dep/0.1": ("62e589af96a19807968167026d906e63ed4de1f5", "Cache")}, build=True) deps_props = client.load("conandeps.props") assert "conan_dep_build.props" in deps_props assert "Invoking 64bit dep_1 build tool" in client.out client.run('build . -s compiler=msvc -s compiler.version=191 ' " -s:b arch=x86 -s build_type=Release") assert "Invoking 32bit dep_1 build tool" in client.out # Make sure it works with 2 profiles too client.run('install . -s compiler=msvc -s compiler.version=191 ' " -s arch=x86_64 -s build_type=Release -s:b os=Windows -s:h os=Windows") client.run("build .") assert "Invoking 64bit dep_1 build tool" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_build_requires_transitives(): """ The tool-requires should not bring transitive dependencies, they will conflict and are useless for linking """ # https://github.com/conan-io/conan/issues/10222 c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_package_type("shared-library"), "tool/conanfile.py": GenConanfile("tool", "0.1").with_requires("dep/0.1"), "consumer/conanfile.py": GenConanfile().with_settings("os", "compiler", "build_type", "arch") .with_build_requires("tool/0.1")}) c.run("create dep") c.run("create tool") c.run("install consumer -g MSBuildDeps -of=.") tool = c.load("conan_tool_build_release_x64.props") assert "conan_dep_build.props" in tool assert "conan_dep.props" not in tool tool_vars = c.load("conan_tool_build_vars_release_x64.props") assert "" in tool_vars ================================================ FILE: test/functional/toolchains/microsoft/test_msbuilddeps_components.py ================================================ import platform import textwrap import pytest from conan.test.assets.sources import gen_function_cpp from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_msbuild_deps_components(): # TODO: Duplicated from xcodedeps_components client = TestClient() client.run("new cmake_lib -d name=tcp -d version=1.0") client.run("create . -tf=\"\"") header = textwrap.dedent(""" #pragma once void {name}(); """) source = textwrap.dedent(""" #include #include "{name}.h" {include} void {name}(){{ {call} #ifdef NDEBUG std::cout << "{name}/1.0: Hello World Release!" << std::endl; #else std::cout << "{name}/1.0: Hello World Debug!" << std::endl; #endif }} """) cmakelists = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(myproject CXX) find_package(tcp REQUIRED CONFIG) add_library(core src/core.cpp include/core.h) add_library(client src/client.cpp include/client.h) add_library(server src/server.cpp include/server.h) target_include_directories(core PUBLIC include) target_include_directories(client PUBLIC include) target_include_directories(server PUBLIC include) set_target_properties(core PROPERTIES PUBLIC_HEADER "include/core.h") set_target_properties(client PROPERTIES PUBLIC_HEADER "include/client.h") set_target_properties(server PROPERTIES PUBLIC_HEADER "include/server.h") target_link_libraries(client core tcp::tcp) target_link_libraries(server core tcp::tcp) install(TARGETS core client server) """) conanfile_py = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" exports_sources = "CMakeLists.txt", "src/*", "include/*" {requires} generators = "CMakeToolchain", "CMakeDeps" def layout(self): cmake_layout(self) def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() {package_info} """) network_pi = """ def package_info(self): self.cpp_info.components["core"].libs = ["core"] self.cpp_info.components["core"].includedirs.append("include") self.cpp_info.components["core"].libdirs.append("lib") self.cpp_info.components["client"].libs = ["client"] self.cpp_info.components["client"].includedirs.append("include") self.cpp_info.components["client"].libdirs.append("lib") self.cpp_info.components["client"].requires.extend(["core", "tcp::tcp"]) self.cpp_info.components["server"].libs = ["server"] self.cpp_info.components["server"].includedirs.append("include") self.cpp_info.components["server"].libdirs.append("lib") self.cpp_info.components["server"].requires.extend(["core", "tcp::tcp"]) """ client.save({ "include/core.h": header.format(name="core"), "include/server.h": header.format(name="server"), "include/client.h": header.format(name="client"), "src/core.cpp": source.format(name="core", include="", call=""), "src/server.cpp": source.format(name="server", include='#include "core.h"\n#include "tcp.h"', call="core(); tcp();"), "src/client.cpp": source.format(name="client", include='#include "core.h"\n#include "tcp.h"', call="core(); tcp();"), "conanfile.py": conanfile_py.format(requires='requires= "tcp/1.0"', package_info=network_pi), "CMakeLists.txt": cmakelists, }, clean_first=True) client.run("create . --name=network --version=1.0") chat_pi = """ def package_info(self): self.cpp_info.libs = ["chat"] self.cpp_info.includedirs.append("include") self.cpp_info.libdirs.append("lib") self.cpp_info.requires.append("network::client") """ cmakelists_chat = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(chat CXX) find_package(network REQUIRED CONFIG) add_library(chat src/chat.cpp include/chat.h) target_include_directories(chat PUBLIC include) set_target_properties(chat PROPERTIES PUBLIC_HEADER "include/chat.h") target_link_libraries(chat network::client) install(TARGETS chat) """) client.save({ "include/chat.h": header.format(name="chat"), "src/chat.cpp": source.format(name="chat", include='#include "client.h"', call="client();"), "conanfile.py": conanfile_py.format(requires='requires= "network/1.0"', package_info=chat_pi), "CMakeLists.txt": cmakelists_chat, }, clean_first=True) client.run("create . --name=chat --version=1.0") client.run("new msbuild_exe -d name=greet -d version=0.1 -f") conanfile = client.load("conanfile.py").replace("settings = ", 'requires="chat/1.0"\n' ' generators = "MSBuildDeps"\n' ' settings = ') vcproj = client.load("greet.vcxproj") vcproj2 = vcproj.replace(r'', r""" """) assert vcproj2 != vcproj client.save({"conanfile.py": conanfile, "src/greet.cpp": gen_function_cpp(name="main", includes=["chat"], calls=["chat"]), "greet.vcxproj": vcproj2}) client.run("create .") assert "main: Release!" in client.out assert "core/1.0: Hello World Release!" in client.out assert "tcp/1.0: Hello World Release!" in client.out assert "client/1.0: Hello World Release!" in client.out assert "chat/1.0: Hello World Release!" in client.out assert "server/1.0" not in client.out ================================================ FILE: test/functional/toolchains/microsoft/test_msbuilddeps_traits.py ================================================ import os import platform import pytest from conan.test.assets.genconanfile import GenConanfile @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_transitive_headers_not_public(transitive_libraries): c = transitive_libraries c.save({"conanfile.py": GenConanfile().with_settings("build_type", "arch") .with_generator("MSBuildDeps").with_requires("engine/1.0")}, clean_first=True) c.run("install .") matrix_data = c.load("conan_matrix_vars_release_x64.props") assert "" in matrix_data assert "$(ConanmatrixRootFolder)/lib;" \ "" in matrix_data assert "matrix.lib;" in matrix_data @pytest.mark.skipif(platform.system() != "Windows", reason="Requires MSBuild") def test_shared_requires_static(transitive_libraries): c = transitive_libraries c.save({"conanfile.py": GenConanfile().with_settings("build_type", "arch") .with_generator("MSBuildDeps").with_requires("engine/1.0")}, clean_first=True) c.run("install . -o engine*:shared=True") assert not os.path.exists(os.path.join(c.current_folder, "conan_matrix_vars_release_x64.props")) engine_data = c.load("conan_engine_release_x64.props") # No dependency to matrix, it has been skipped as unnecessary assert "conan_matrix.props" not in engine_data ================================================ FILE: test/functional/toolchains/microsoft/test_msbuildtoolchain.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_msbuildtoolchain_props_with_extra_flags(): """ Real test which is injecting some compiler/linker options and other dummy defines and checking that they are being processed somehow. Expected result: everything was built successfully. """ profile = textwrap.dedent("""\ include(default) [conf] tools.build:cxxflags=["/analyze:quiet"] tools.build:cflags+=["/doc"] tools.build:sharedlinkflags+=["/VERBOSE:UNUSEDLIBS"] tools.build:exelinkflags+=["/PDB:mypdbfile"] tools.build:defines+=["DEF1", "DEF2"] """) client = TestClient(path_with_spaces=False) client.run("new msbuild_exe -d name=hello -d version=0.1") client.save({"myprofile": profile}) # conantoolchain.props is already imported in the msbuild_exe tempalte client.run("create . -pr myprofile -tf=") assert "/analyze:quiet /doc src/hello.cpp" in client.out assert r"/VERBOSE:UNUSEDLIBS /PDB:mypdbfile x64\Release\hello.obj" in client.out assert "/D DEF1 /D DEF2" in client.out assert "Build succeeded." in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_msbuildtoolchain_winsdk_version(): """ Configure sdk_version """ client = TestClient(path_with_spaces=False) client.run("new msbuild_lib -d name=hello -d version=0.1") # conantoolchain.props is already imported in the msbuild_exe tempalte client.run("create . -s arch=x86_64 -s compiler.version=193 " "-c tools.microsoft:winsdk_version=10.0") # I have verified also opening VS IDE that the setting is correctly configured # because the test always run over vcvars that already activates it assert "amd64 - winsdk_version=10.0 - vcvars_ver=14.3" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_msbuildtoolchain_compiler_update(): # It only works for update=8, because 19.38 is the compiler in Github actions! client = TestClient(path_with_spaces=False) client.run("new msbuild_lib -d name=hello -d version=0.1") # conantoolchain.props is already imported in the msbuild_exe tempalte client.run("create . -s arch=x86_64 -s compiler.version=193 -s compiler.update=8") # I have verified also opening VS IDE that the setting is correctly configured # because the test always run over vcvars that already activates it assert "amd64 - winsdk_version=None - vcvars_ver=14.38" in client.out assert "hello/0.1: _MSC_VER1938" in client.out @pytest.mark.tool("visual_studio") @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") class TestTxtCommandLineMSBuild: def test_declarative(self): conanfile = textwrap.dedent(""" [generators] MSBuildToolchain """) client = TestClient() client.save({"conanfile.txt": conanfile}) client.run("install .") self._check(client) @staticmethod def _check(client): assert "conanfile.txt: Generator 'MSBuildToolchain' calling 'generate()'" in client.out toolchain = client.load("conantoolchain.props") assert " #include "hello.h" void hello(){ #ifdef NDEBUG std::cout << "Hello World Release!" <Pkg" in conan_toolchain_props assert "0.1" in conan_toolchain_props ================================================ FILE: test/functional/toolchains/test_nmake_toolchain.py ================================================ import platform import textwrap import pytest from conan.test.assets.sources import gen_function_cpp from test.functional.utils import check_exe_run from conan.test.utils.tools import TestClient @pytest.mark.slow @pytest.mark.parametrize( "compiler, version, runtime, cppstd, build_type, defines, cflags, cxxflags, sharedlinkflags, exelinkflags", [ ("msvc", "191", "dynamic", "14", "Release", [], [], [], [], []), ("msvc", "191", "dynamic", "14", "Release", ["TEST_DEFINITION1", "TEST_DEFINITION2=0", "TEST_DEFINITION3=", "TEST_DEFINITION4=TestPpdValue4", "TEST_DEFINITION5=__declspec(dllexport)", "TEST_DEFINITION6=foo bar"], ["/GL"], ["/GL"], ["/LTCG"], ["/LTCG"]), ("msvc", "191", "static", "17", "Debug", [], [], [], [], []), ], ) @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_toolchain_nmake(compiler, version, runtime, cppstd, build_type, defines, cflags, cxxflags, sharedlinkflags, exelinkflags): client = TestClient(path_with_spaces=False) settings = {"compiler": compiler, "compiler.version": version, "compiler.cppstd": cppstd, "compiler.runtime": runtime, "build_type": build_type, "arch": "x86_64"} serialize_array = lambda arr: "[{}]".format(",".join([f"'{v}'" for v in arr])) conf = { "tools.build:defines": serialize_array(defines) if defines else "", "tools.build:cflags": serialize_array(cflags) if cflags else "", "tools.build:cxxflags": serialize_array(cxxflags) if cxxflags else "", "tools.build:sharedlinkflags": serialize_array(sharedlinkflags) if sharedlinkflags else "", "tools.build:exelinkflags": serialize_array(exelinkflags) if exelinkflags else "", } # Build the profile according to the settings provided settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v) client.run("new cmake_lib -d name=dep -d version=1.0") conf = " ".join(f'-c {k}="{v}"' for k, v in conf.items() if v) client.run(f'create . -tf=\"\" {settings} {conf}' f' -c tools.cmake.cmaketoolchain:generator="Visual Studio 15"') # Rearrange defines to macro / value dict conf_preprocessors = {} for define in defines: if "=" in define: key, value = define.split("=", 1) # gen_function_cpp doesn't properly handle empty macros if value: conf_preprocessors[key] = value else: conf_preprocessors[define] = "1" conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "dep/1.0" generators = "NMakeToolchain", "NMakeDeps" def build(self): self.run(f"nmake /f makefile") """) makefile = textwrap.dedent("""\ all: simple.exe .cpp.obj: $(CPP) $*.cpp simple.exe: simple.obj $(CPP) simple.obj """) client.save({"conanfile.py": conanfile, "makefile": makefile, "simple.cpp": gen_function_cpp(name="main", includes=["dep"], calls=["dep"], preprocessor=conf_preprocessors.keys())}, clean_first=True) client.run(f"build . {settings} {conf}") client.run_command("simple.exe") assert "dep/1.0" in client.out check_exe_run(client.out, "main", "msvc", version, build_type, "x86_64", cppstd, conf_preprocessors) @pytest.mark.slow @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") @pytest.mark.tool("cmake", "3.23") # This test uses clang inside Visual Studio, not managed by mark.tool @pytest.mark.tool("visual_studio") def test_toolchain_nmake_clang(): cppstd = "14" build_type = "Debug" defines = ["TEST_DEFINITION1", "TEST_DEFINITION2=0", "TEST_DEFINITION3=", "TEST_DEFINITION4=TestPpdValue4", "TEST_DEFINITION5=__declspec(dllexport)", "TEST_DEFINITION6=foo bar"] client = TestClient(path_with_spaces=False) settings = {"compiler": "clang", "compiler.version": "20", "compiler.cppstd": "14", "compiler.runtime": "dynamic", "build_type": build_type, "compiler.runtime_version": "v144", "arch": "x86_64"} conf = { "tools.build:defines": "[{}]".format(",".join([f"'{v}'" for v in defines])), "tools.build:compiler_executables": r'{\"c\": \"clang-cl\", \"cpp\": \"clang-cl\"}', "tools.cmake.cmaketoolchain:generator": "Visual Studio 17", } # Build the profile according to the settings provided settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v) client.run("new cmake_lib -d name=dep -d version=1.0") conf = " ".join(f'-c {k}="{v}"' for k, v in conf.items() if v) client.run(f'create . -tf=\"\" {settings} {conf}') # Rearrange defines to macro / value dict conf_preprocessors = {} for define in defines: if "=" in define: key, value = define.split("=", 1) # gen_function_cpp doesn't properly handle empty macros if value: conf_preprocessors[key] = value else: conf_preprocessors[define] = "1" conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "dep/1.0" generators = "NMakeToolchain", "NMakeDeps" def build(self): self.run(f"nmake /f makefile") """) makefile = textwrap.dedent("""\ all: simple.exe simple.exe: simple.cpp $(CPP) simple.cpp -o simple.exe """) client.save({"conanfile.py": conanfile, "makefile": makefile, "simple.cpp": gen_function_cpp(name="main", includes=["dep"], calls=["dep"], preprocessor=conf_preprocessors.keys())}, clean_first=True) client.run(f"build . {settings} {conf}") client.run_command("simple.exe") assert "dep/1.0" in client.out check_exe_run(client.out, "main", "clang", "19.1", build_type, "x86_64", cppstd, conf_preprocessors) ================================================ FILE: test/functional/toolchains/test_premake.py ================================================ import os import platform import textwrap from conan.test.assets.premake import gen_premake5 from conan.test.utils.mocks import ConanFileMock from conan.tools.files.files import replace_in_file, rmdir import pytest from conan.test.utils.tools import TestClient from conan.test.assets.sources import gen_function_cpp @pytest.mark.slow @pytest.mark.skipif(platform.machine() not in ("x86_64", "AMD64"), reason="Premake Legacy generator only supports x86_64 machines") @pytest.mark.tool("premake") def test_premake_legacy(matrix_client): c = matrix_client conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.premake import Premake from conan.tools.microsoft import MSBuild class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "matrix/1.0" generators = "PremakeDeps", "VCVars" def build(self): p = Premake(self) p.configure() build_type = str(self.settings.build_type) if self.settings.os == "Windows": msbuild = MSBuild(self) msbuild.build("HelloWorld.sln") else: self.run(f"make config={build_type.lower()}_x86_64") p = os.path.join(self.build_folder, "bin", build_type, "HelloWorld") self.run(f'"{p}"') """) premake = textwrap.dedent(""" -- premake5.lua include('conandeps.premake5.lua') workspace "HelloWorld" conan_setup() configurations { "Debug", "Release" } platforms { "x86_64" } project "HelloWorld" kind "ConsoleApp" language "C++" targetdir "bin/%{cfg.buildcfg}" files { "**.h", "**.cpp" } filter "configurations:Debug" defines { "DEBUG" } symbols "On" filter "configurations:Release" defines { "NDEBUG" } optimize "On" filter "platforms:x86_64" architecture "x86_64" """) c.save({"conanfile.py": conanfile, "premake5.lua": premake, "main.cpp": gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"])}) c.run("build .") assert "main: Release!" in c.out assert "matrix/1.0: Hello World Release!" in c.out assert "main _M_X64 defined" in c.out or "main __x86_64__ defined" in c.out c.run("build . -s build_type=Debug --build=missing") assert "main: Debug!" in c.out assert "matrix/1.0: Hello World Debug!" in c.out @pytest.mark.slow @pytest.mark.tool("premake") def test_premake_new_generator(): c = TestClient() c.run("new premake_lib -d name=lib -d version=0.1 -o lib") c.run("create lib") c.run("new premake_exe -d name=example -d requires=lib/0.1 -d version=1.0 -o exe") c.run("create exe") assert "example/1.0 (test package): RUN: example" in c.out assert "lib/0.1: Hello World Release!" in c.out assert "example/1.0: Hello World Release!" in c.out @pytest.mark.slow @pytest.mark.tool("premake") def test_premake_shared_lib(): c = TestClient() c.run("new premake_lib -d name=lib -d version=0.1 -o lib") c.run("create lib -o '&:shared=True'") assert "lib/0.1: package(): Packaged 1 '.so' file: liblib.so" in c.out assert "lib/0.1: package(): Packaged 1 '.a' file: liblib.a" not in c.out @pytest.mark.slow @pytest.mark.tool("premake") @pytest.mark.parametrize("transitive_libs", [True, False]) def test_premake_components(transitive_libs): c = TestClient() c.run("new premake_lib -d name=liba -d version=1.0 -o liba") libb_premake = gen_premake5( workspace="Libb", includedirs=[".", "libb1", "libb2"], projects=[ {"name": "libb1", "files": ["libb1/*.h", "libb1/*.cpp"], "kind": "StaticLib"}, {"name": "libb2", "files": ["libb2/*.h", "libb2/*.cpp"], "kind": "StaticLib", "links": ["libb1"]} ] ) libb_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy from conan.tools.layout import basic_layout from conan.tools.premake import Premake class libbRecipe(ConanFile): name = "libb" version = "1.0" package_type = "static-library" settings = "os", "compiler", "build_type", "arch" exports_sources = "premake5.lua", "libb1/*", "libb2/*" generators= "PremakeDeps", "PremakeToolchain" def layout(self): basic_layout(self) def requirements(self): self.requires("liba/1.0", transitive_libs=%s) def build(self): premake = Premake(self) premake.configure() premake.build(workspace="Libb") def package(self): for lib in ("libb1", "libb2"): copy(self, "*.h", os.path.join(self.source_folder, lib), os.path.join(self.package_folder, "include", lib)) for pattern in ("*.lib", "*.a", "*.so*", "*.dylib"): copy(self, pattern, os.path.join(self.build_folder, "bin"), os.path.join(self.package_folder, "lib")) copy(self, "*.dll", os.path.join(self.build_folder, "bin"), os.path.join(self.package_folder, "bin")) def package_info(self): self.cpp_info.components["libb1"].libs = ["libb1"] self.cpp_info.components["libb1"].requires = ["liba::liba"] self.cpp_info.components["libb2"].libs = ["libb2"] self.cpp_info.components["libb2"].requires = ["libb1"] """) libb_h = textwrap.dedent(""" #pragma once #include void libb%s(); """) libb_cpp = textwrap.dedent(""" #include #include "libb%s.h" #include "liba.h" void libb%s(){ liba(); std::cout << "libb%s/1.0" << std::endl; } """) c.save( { "libb/premake5.lua": libb_premake, "libb/conanfile.py": libb_conanfile % transitive_libs, "libb/libb1/libb1.h": libb_h % "1", "libb/libb1/libb1.cpp": libb_cpp % ("1", "1", "1"), "libb/libb2/libb2.h": libb_h % "2", "libb/libb2/libb2.cpp": libb_cpp % ("2", "2", "2"), } ) # Create a consumer application which depends on libb c.run("new premake_exe -d name=consumer -d version=1.0 -o consumer -d requires=libb/1.0") # Adapt includes and usage of libb in the consumer application replace_in_file(ConanFileMock(), os.path.join(c.current_folder, "consumer", "src", "consumer.cpp"), '#include "libb.h"', '#include "libb1/libb1.h"\n#include "libb2/libb2.h"') replace_in_file(ConanFileMock(), os.path.join(c.current_folder, "consumer", "src", "consumer.cpp"), 'libb()', 'libb1();libb2()') c.run("create liba") c.run("create libb") # If transitive_libs is false, consumer will not compile because it will not find liba c.run("build consumer", assert_error=not transitive_libs) @pytest.mark.slow @pytest.mark.tool("premake") def test_transitive_headers_not_public(transitive_libraries): c = transitive_libraries main = gen_function_cpp(name="main", includes=["engine"], calls=["engine"]) premake5 = gen_premake5( workspace="Consumer", projects=[ {"name": "consumer", "files": ["src/main.cpp"], "kind": "ConsoleApp"} ], ) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.layout import basic_layout from conan.tools.premake import Premake class ConsumerRecipe(ConanFile): name = "consumer" version = "1.0" package_type = "application" settings = "os", "compiler", "build_type", "arch" exports_sources = "*" generators= "PremakeDeps", "PremakeToolchain" def layout(self): basic_layout(self) def requirements(self): self.requires("engine/1.0") def build(self): premake = Premake(self) premake.configure() premake.build(workspace="Consumer") """) c.save({"src/main.cpp": main, "premake5.lua": premake5, "conanfile.py": conanfile }) # Test it builds successfully c.run("build .") # Test including a transitive header: it should fail as engine does not require matrix with # transitive_headers enabled main = gen_function_cpp(name="main", includes=["engine", "matrix"], calls=["engine"]) c.save({"src/main.cpp": main}) rmdir(ConanFileMock(), os.path.join(c.current_folder, "build-release")) c.run("build .", assert_error=True) # Error should be about not finding matrix @pytest.mark.slow @pytest.mark.tool("premake") def test_premake_custom_configuration(transitive_libraries): c = transitive_libraries conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.layout import basic_layout from conan.tools.premake import Premake, PremakeDeps, PremakeToolchain class ConsumerRecipe(ConanFile): name = "consumer" version = "1.0" package_type = "application" settings = "os", "compiler", "build_type", "arch" options = {"sanitizer": [True, False]} default_options = {"sanitizer": False} exports_sources = "*" def layout(self): basic_layout(self) @property def _premake_configuration(self): return str(self.settings.build_type) + "Sanitizer" if self.options.sanitizer else "" def generate(self): deps = PremakeDeps(self) deps.configuration = self._premake_configuration deps.generate() tc = PremakeToolchain(self) tc.generate() def build(self): premake = Premake(self) premake.configure() # premake.build(workspace="Consumer") premake.build(workspace="Consumer", configuration=self._premake_configuration) """) c.save({"src/main.cpp": gen_function_cpp(name="main"), "premake5.lua": gen_premake5( workspace="Consumer", projects=[ { "name": "consumer", "files": ["src/main.cpp"], "kind": "ConsoleApp", } ], configurations=["Debug", "Release", "DebugSanitizer", "ReleaseSanitizer"], ), "conanfile.py": conanfile }) # Test it builds successfully with the custom configuration c.run("build . -o sanitizer=True") ================================================ FILE: test/functional/tools/__init__.py ================================================ ================================================ FILE: test/functional/tools/scm/__init__.py ================================================ ================================================ FILE: test/functional/tools/scm/test_git.py ================================================ import os import platform import re import textwrap import pytest from conan.test.assets.cmake import gen_cmakelists from conan.test.assets.sources import gen_function_cpp from conan.test.utils.scm import create_local_git_repo, git_add_changes_commit, git_create_bare_repo from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import rmdir, save_files, save @pytest.mark.tool("git") class TestGitBasicCapture: """ base Git capture operations. They do not raise (unless errors) """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self, self.recipe_folder, excluded=["myfile.txt", "mynew.txt", "file with spaces.txt"]) commit = git.get_commit() repo_commit = git.get_commit(repository=True) url = git.get_remote_url() self.output.info("URL: {}".format(url)) self.output.info("COMMIT: {}".format(commit)) self.output.info("REPO_COMMIT: {}".format(repo_commit)) in_remote = git.commit_in_remote(commit) self.output.info("COMMIT IN REMOTE: {}".format(in_remote)) self.output.info("DIRTY: {}".format(git.is_dirty())) """) def test_capture_commit_local(self): """ A local repo, without remote, will have commit, but no URL """ c = TestClient() c.save({"conanfile.py": self.conanfile}) commit = c.init_git_repo() c.run("export .") assert "pkg/0.1: COMMIT: {}".format(commit) in c.out assert "pkg/0.1: URL: None" in c.out assert "pkg/0.1: COMMIT IN REMOTE: False" in c.out assert "pkg/0.1: DIRTY: False" in c.out def test_capture_remote_url(self): """ a cloned repo, will have a default "origin" remote and will manage to get URL """ folder = temp_folder() url, commit = create_local_git_repo(files={"conanfile.py": self.conanfile}, folder=folder) c = TestClient() c.run_command('git clone "{}" myclone'.format(folder)) with c.chdir("myclone"): c.run("export .") assert "pkg/0.1: COMMIT: {}".format(commit) in c.out assert "pkg/0.1: REPO_COMMIT: {}".format(commit) in c.out assert "pkg/0.1: URL: {}".format(url) in c.out assert "pkg/0.1: COMMIT IN REMOTE: True" in c.out assert "pkg/0.1: DIRTY: False" in c.out def test_capture_remote_pushed_commit(self): """ a cloned repo, after doing some new commit, no longer commit in remote, until push """ url = git_create_bare_repo() c = TestClient() c.run_command('git clone "{}" myclone'.format(url)) with c.chdir("myclone"): c.save({"conanfile.py": self.conanfile + "\n# some coment!"}) new_commit = git_add_changes_commit(c.current_folder) c.run("export .") assert "pkg/0.1: COMMIT: {}".format(new_commit) in c.out assert "pkg/0.1: URL: {}".format(url) in c.out assert "pkg/0.1: COMMIT IN REMOTE: False" in c.out assert "pkg/0.1: DIRTY: False" in c.out c.run_command("git push") c.run("export .") assert "pkg/0.1: COMMIT: {}".format(new_commit) in c.out assert "pkg/0.1: URL: {}".format(url) in c.out assert "pkg/0.1: COMMIT IN REMOTE: True" in c.out assert "pkg/0.1: DIRTY: False" in c.out def test_capture_commit_local_subfolder(self): """ A local repo, without remote, will have commit, but no URL, and sibling folders can be dirty, no prob """ c = TestClient() c.save({"subfolder/conanfile.py": self.conanfile, "other/myfile.txt": "content"}) commit = c.init_git_repo() c.save({"other/myfile.txt": "change content"}) c.run("export subfolder") assert "pkg/0.1: COMMIT: {}".format(commit) in c.out assert "pkg/0.1: REPO_COMMIT: {}".format(commit) in c.out assert "pkg/0.1: URL: None" in c.out assert "pkg/0.1: COMMIT IN REMOTE: False" in c.out assert "pkg/0.1: DIRTY: False" in c.out commit2 = git_add_changes_commit(c.current_folder, msg="fix") c.run("export subfolder") assert "pkg/0.1: COMMIT: {}".format(commit) in c.out assert "pkg/0.1: REPO_COMMIT: {}".format(commit2) in c.out assert "pkg/0.1: URL: None" in c.out assert "pkg/0.1: COMMIT IN REMOTE: False" in c.out assert "pkg/0.1: DIRTY: False" in c.out def test_git_excluded(self): """ A local repo, without remote, will have commit, but no URL """ c = TestClient() c.save({"conanfile.py": self.conanfile, "myfile.txt": ""}) c.init_git_repo() c.run("export . -vvv") assert "pkg/0.1: DIRTY: False" in c.out c.save({"myfile.txt": "changed", "mynew.txt": "new", "file with spaces.txt": "hello"}) c.run("export .") assert "pkg/0.1: DIRTY: False" in c.out c.save({"other.txt": "new"}) c.run("export .") assert "pkg/0.1: DIRTY: True" in c.out conf_excluded = f'core.scm:excluded+=["other.txt"]' save(c.paths.global_conf_path, conf_excluded) c.run("export .") assert "pkg/0.1: DIRTY: False" in c.out @pytest.mark.tool("git") class TestGitCaptureSCM: """ test the get_url_and_commit() high level method intended for SCM capturing into conandata.yaml """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self, self.recipe_folder) scm_url, scm_commit = git.get_url_and_commit() self.output.info("SCM URL: {}".format(scm_url)) self.output.info("SCM COMMIT: {}".format(scm_commit)) """) def test_capture_commit_local(self): """ A local repo, without remote, will provide its own URL to the export(), and if it has local changes, it will be marked as dirty, and raise an error """ c = TestClient() c.save({"conanfile.py": self.conanfile}) commit = c.init_git_repo() c.run("export .") assert "This revision will not be buildable in other computer" in c.out assert "pkg/0.1: SCM COMMIT: {}".format(commit) in c.out assert "pkg/0.1: SCM URL: {}".format(c.current_folder.replace("\\", "/")) in c.out c.save({"conanfile.py": self.conanfile + "\n# something...."}) c.run("export .", assert_error=True) assert "Repo is dirty, cannot capture url and commit" in c.out def test_capture_commit_local_repository(self): """ same as above, but with ``get_url_and_commit(repository=True)`` """ c = TestClient() c.save({"pkg/conanfile.py": self.conanfile.replace("get_url_and_commit()", "get_url_and_commit(repository=True)"), "somefile.txt": ""}) commit = c.init_git_repo() c.run("export pkg") assert "This revision will not be buildable in other computer" in c.out assert "pkg/0.1: SCM COMMIT: {}".format(commit) in c.out assert "pkg/0.1: SCM URL: {}".format(c.current_folder.replace("\\", "/")) in c.out c.save({"somefile.txt": "something"}) c.run("export pkg", assert_error=True) assert "Repo is dirty, cannot capture url and commit" in c.out def test_capture_remote_url(self): """ a cloned repo that is expored, will report the URL of the remote """ folder = temp_folder() url, commit = create_local_git_repo(files={"conanfile.py": self.conanfile}, folder=folder) c = TestClient() c.run_command('git clone "{}" myclone'.format(folder)) with c.chdir("myclone"): c.run("export .") assert "pkg/0.1: SCM COMMIT: {}".format(commit) in c.out assert "pkg/0.1: SCM URL: {}".format(url) in c.out def test_capture_remote_pushed_commit(self): """ a cloned repo, after doing some new commit, no longer commit in remote, until push """ url = git_create_bare_repo() c = TestClient() c.run_command('git clone "{}" myclone'.format(url)) with c.chdir("myclone"): c.save({"conanfile.py": self.conanfile + "\n# some coment!"}) new_commit = git_add_changes_commit(c.current_folder) c.run("export .") assert "This revision will not be buildable in other computer" in c.out assert "pkg/0.1: SCM COMMIT: {}".format(new_commit) in c.out # NOTE: commit not pushed yet, so locally is the current folder assert "pkg/0.1: SCM URL: {}".format(c.current_folder.replace("\\", "/")) in c.out c.run_command("git push") c.run("export .") assert "pkg/0.1: SCM COMMIT: {}".format(new_commit) in c.out assert "pkg/0.1: SCM URL: {}".format(url) in c.out def test_capture_commit_modified_config(self): """ A clean repo with the status.branch git config set to on Expected to not raise an error an return the commit and url """ folder = temp_folder() url, commit = create_local_git_repo(files={"conanfile.py": self.conanfile}, folder=folder) c = TestClient() with c.chdir(folder): c.run_command("git config --local status.branch true") c.run("export .") assert "pkg/0.1: SCM COMMIT: {}".format(commit) in c.out assert "pkg/0.1: SCM URL: {}".format(url) in c.out def test_capture_commit_modified_config_untracked(self): """ A dirty repo with the showUntrackedFiles git config set to off. Expected to throw an exception """ folder = temp_folder() create_local_git_repo(files={"conanfile.py": self.conanfile}, folder=folder) c = TestClient() with c.chdir(folder): c.save(files={"some_header.h": "now the repo is dirty"}) c.run_command("git config --local status.showUntrackedFiles no") c.run("export .", assert_error=True) assert "Repo is dirty, cannot capture url and commit" in c.out @pytest.mark.tool("git") class TestGitBasicClone: """ base Git cloning operations """ conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def source(self): git = Git(self) git.clone(url="{url}", target=".") git.checkout(commit="{commit}") self.output.info("MYCMAKE: {{}}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {{}}".format(load(self, "src/myfile.h"))) """) def test_clone_checkout(self): folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder) # This second commit will NOT be used, as I will use the above commit in the conanfile save_files(path=folder, files={"src/myfile.h": "my2header2!"}) git_add_changes_commit(folder=folder) c = TestClient() c.save({"conanfile.py": self.conanfile.format(url=url, commit=commit)}) c.run("create . -v") # Clone is not printed, it might contain tokens assert 'pkg/0.1: RUN: git clone "" "."' in c.out assert "pkg/0.1: RUN: git checkout" in c.out assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out # It also works in local flow c.run("source .") assert "conanfile.py (pkg/0.1): MYCMAKE: mycmake" in c.out assert "conanfile.py (pkg/0.1): MYFILE: myheader!" in c.out assert c.load("source/src/myfile.h") == "myheader!" assert c.load("source/CMakeLists.txt") == "mycmake" def test_clone_url_not_hidden(self): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def source(self): git = Git(self) git.clone(url="{url}", target=".", hide_url=False) """) folder = os.path.join(temp_folder(), "myrepo") url, _ = create_local_git_repo(files={"CMakeLists.txt": "mycmake"}, folder=folder) c = TestClient(light=True) c.save({"conanfile.py": conanfile.format(url=url)}) c.run("create . -v") # Clone URL is explicitly printed assert f'pkg/0.1: RUN: git clone "{url}" "."' in c.out # It also works in local flow c.run("source .") assert f'conanfile.py (pkg/0.1): RUN: git clone "{url}" "."' in c.out def test_clone_target(self): # Clone to a different target folder # https://github.com/conan-io/conan/issues/14058 conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def source(self): # Alternative, first defining the folder # git = Git(self, "target") # git.clone(url="{url}", target=".") # git.checkout(commit="{commit}") git = Git(self) git.clone(url="{url}", target="tar get") # git clone url target git.folder = "tar get" # cd target git.checkout(commit="{commit}") # git checkout commit self.output.info("MYCMAKE: {{}}".format(load(self, "tar get/CMakeLists.txt"))) self.output.info("MYFILE: {{}}".format(load(self, "tar get/src/myfile.h"))) """) folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder) # This second commit will NOT be used, as I will use the above commit in the conanfile save_files(path=folder, files={"src/myfile.h": "my2header2!"}) git_add_changes_commit(folder=folder) c = TestClient() c.save({"conanfile.py": conanfile.format(url=url, commit=commit)}) c.run("create .") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out @pytest.mark.tool("msys2") def test_clone_msys2_win_bash(self): # To avoid regression in https://github.com/conan-io/conan/issues/14754 folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder) c = TestClient() conanfile_win_bash = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" win_bash = True def layout(self): self.folders.source = "source" def source(self): git = Git(self) git.clone(url="{url}", target=".") git.checkout(commit="{commit}") self.output.info("MYCMAKE: {{}}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {{}}".format(load(self, "src/myfile.h"))) """) c.save({"conanfile.py": conanfile_win_bash.format(url=url, commit=commit)}) conf = "-c tools.microsoft.bash:subsystem=msys2 -c tools.microsoft.bash:path=bash.exe" c.run(f"create . {conf}") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out # It also works in local flow, not running in msys2 at all c.run(f"source .") assert "conanfile.py (pkg/0.1): MYCMAKE: mycmake" in c.out assert "conanfile.py (pkg/0.1): MYFILE: myheader!" in c.out assert c.load("source/src/myfile.h") == "myheader!" assert c.load("source/CMakeLists.txt") == "mycmake" @pytest.mark.tool("git") class TestGitShallowClone: """ base Git cloning operations """ conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def source(self): git = Git(self) git.fetch_commit(url="{url}", commit="{commit}") self.output.info("MYCMAKE: {{}}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {{}}".format(load(self, "src/myfile.h"))) """) @pytest.mark.skipif(platform.system() == "Linux", reason="Git version in Linux not support it") def test_clone_checkout(self): folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder) # This second commit will NOT be used, as I will use the above commit in the conanfile save_files(path=folder, files={"src/myfile.h": "my2header2!"}) git_add_changes_commit(folder=folder) c = TestClient() c.save({"conanfile.py": self.conanfile.format(url=url, commit=commit)}) c.run("create . -v") assert 'pkg/0.1: RUN: git remote add origin ""' in c.out assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out # It also works in local flow c.run("source .") assert "conanfile.py (pkg/0.1): MYCMAKE: mycmake" in c.out assert "conanfile.py (pkg/0.1): MYFILE: myheader!" in c.out assert c.load("source/src/myfile.h") == "myheader!" assert c.load("source/CMakeLists.txt") == "mycmake" def test_clone_url_not_hidden(self): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def source(self): git = Git(self) git.fetch_commit(url="{url}", commit="{commit}", hide_url=False) """) folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"CMakeLists.txt": "mycmake"}, folder=folder) c = TestClient(light=True) c.save({"conanfile.py": conanfile.format(url=url, commit=commit)}) c.run("create . -v") # Clone URL is explicitly printed assert f'pkg/0.1: RUN: git remote add origin "{url}"' in c.out # It also works in local flow c.run("source .") assert f'conanfile.py (pkg/0.1): RUN: git remote add origin "{url}"' in c.out @pytest.mark.skipif(platform.system() == "Linux", reason="Git version in Linux not support it") def test_clone_to_subfolder(self): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def source(self): git = Git(self, folder="folder") git.fetch_commit(url="{url}", commit="{commit}") self.output.info("MYCMAKE: {{}}".format(load(self, "folder/CMakeLists.txt"))) self.output.info("MYFILE: {{}}".format(load(self, "folder/src/myfile.h"))) """) folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder) # This second commit will NOT be used, as I will use the above commit in the conanfile save_files(path=folder, files={"src/myfile.h": "my2header2!"}) git_add_changes_commit(folder=folder) c = TestClient() c.save({"conanfile.py": conanfile.format(url=url, commit=commit)}) c.run("create . -v") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out # It also works in local flow c.run("source .") assert "conanfile.py (pkg/0.1): MYCMAKE: mycmake" in c.out assert "conanfile.py (pkg/0.1): MYFILE: myheader!" in c.out assert c.load("source/folder/CMakeLists.txt") == "mycmake" assert c.load("source/folder/src/myfile.h") == "myheader!" class TestGitCloneWithArgs: """ Git cloning passing additional arguments """ conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def source(self): git = Git(self) git.clone(url="{url}", target=".", args={args}) self.output.info("MYCMAKE: {{}}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {{}}".format(load(self, "src/myfile.h"))) """) def test_clone_specify_branch_or_tag(self): folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder, commits=3, branch="main", tags=["v1.2.3"]) c = TestClient() git_args = ['--branch', 'main'] c.save({"conanfile.py": self.conanfile.format(url=url, commit=commit, args=str(git_args))}) c.run("create .") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out git_args = ['--branch', 'v1.2.3'] c.save({"conanfile.py": self.conanfile.format(url=url, commit=commit, args=str(git_args))}) c.run("create .") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out def test_clone_invalid_branch_argument(self): folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder, commits=3, branch="main", tags=["v1.2.3"]) c = TestClient() git_args = ['--branch', 'foobar'] c.save({"conanfile.py": self.conanfile.format(url=url, commit=commit, args=str(git_args))}) c.run("create .", assert_error=True) assert "Remote branch foobar not found" in c.out @pytest.mark.tool("git") class TestGitBasicSCMFlow: """ Build the full new SCM approach: - export() captures the URL and commit with get_url_and_commit( - export() stores it in conandata.yml - source() recovers the info from conandata.yml and clones it """ conanfile_full = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load, update_conandata class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self, self.recipe_folder) scm_url, scm_commit = git.get_url_and_commit() update_conandata(self, {"sources": {"commit": scm_commit, "url": scm_url}}) def layout(self): self.folders.source = "." def source(self): git = Git(self) sources = self.conan_data["sources"] git.clone(url=sources["url"], target=".") git.checkout(commit=sources["commit"]) self.output.info("MYCMAKE: {}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {}".format(load(self, "src/myfile.h"))) def build(self): cmake = os.path.join(self.source_folder, "CMakeLists.txt") file_h = os.path.join(self.source_folder, "src/myfile.h") self.output.info("MYCMAKE-BUILD: {}".format(load(self, cmake))) self.output.info("MYFILE-BUILD: {}".format(load(self, file_h))) """) conanfile_scm = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load, trim_conandata class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): Git(self).coordinates_to_conandata() trim_conandata(self) # to test it does not affect def layout(self): self.folders.source = "." def source(self): Git(self).checkout_from_conandata_coordinates() self.output.info("MYCMAKE: {}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {}".format(load(self, "src/myfile.h"))) def build(self): cmake = os.path.join(self.source_folder, "CMakeLists.txt") file_h = os.path.join(self.source_folder, "src/myfile.h") self.output.info("MYCMAKE-BUILD: {}".format(load(self, cmake))) self.output.info("MYFILE-BUILD: {}".format(load(self, file_h))) """) @pytest.mark.parametrize("conanfile_scm", [False, True]) def test_full_scm(self, conanfile_scm): conanfile = self.conanfile_scm if conanfile_scm else self.conanfile_full folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"conanfile.py": conanfile, "src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder) c = TestClient(default_server_user=True) c.run_command('git clone "file://{}" .'.format(url)) c.run("create .") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out c.run("upload * -c -r=default") # Do a change and commit, this commit will not be used by package save_files(path=folder, files={"src/myfile.h": "my2header2!"}) git_add_changes_commit(folder=folder) # use another fresh client c2 = TestClient(servers=c.servers) c2.run("install --requires=pkg/0.1@ --build=pkg*") assert "pkg/0.1: MYCMAKE: mycmake" in c2.out assert "pkg/0.1: MYFILE: myheader!" in c2.out # local flow c.run("install .") c.run("build .") assert "conanfile.py (pkg/0.1): MYCMAKE-BUILD: mycmake" in c.out assert "conanfile.py (pkg/0.1): MYFILE-BUILD: myheader!" in c.out @pytest.mark.parametrize("conanfile_scm", [False, True]) def test_branch_flow(self, conanfile_scm): """ Testing that when a user creates a branch, and pushes a commit, the package can still be built from sources, and get_url_and_commit() captures the remote URL and not the local """ conanfile = self.conanfile_scm if conanfile_scm else self.conanfile_full url = git_create_bare_repo() c = TestClient(default_server_user=True) c.run_command('git clone "file://{}" .'.format(url)) c.save({"conanfile.py": conanfile, "src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}) c.run_command("git checkout -b mybranch") git_add_changes_commit(folder=c.current_folder) c.run_command("git push --set-upstream origin mybranch") c.run("create .") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out c.run("upload * -c -r=default") rmdir(c.current_folder) # Remove current folder to make sure things are not used from here # use another fresh client c2 = TestClient(servers=c.servers) c2.run("install --requires=pkg/0.1@ --build=pkg*") assert "pkg/0.1: MYCMAKE: mycmake" in c2.out assert "pkg/0.1: MYFILE: myheader!" in c2.out def test_fetch_commit(self): """ Testing fetch commit """ url = git_create_bare_repo() c = TestClient(default_server_user=True) c.run_command('git clone "file://{}" .'.format(url)) c.save({"conanfile.py": self.conanfile_scm, "src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}) c.run_command("git checkout -b mybranch") git_add_changes_commit(folder=c.current_folder) c.run_command("git push --set-upstream origin mybranch") c.run("create .") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out c.run("upload * -c -r=default") # Create an orphan commit by removing the branch c.run_command("git push origin --delete mybranch") rmdir(c.current_folder) # Remove current folder to make sure things are not used from here # use another fresh client c2 = TestClient(servers=c.servers) c2.run("install --requires=pkg/0.1@ --build=pkg*") assert "pkg/0.1: MYCMAKE: mycmake" in c2.out assert "pkg/0.1: MYFILE: myheader!" in c2.out def test_grafted_commit(self): # https://github.com/conan-io/conan/issues/18295 folder = os.path.join(temp_folder(), "myrepo") conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.scm import Git class ConanRecipe(ConanFile): name = "conan-grafted" version = "1.2.3" def export(self): git = Git(self, self.recipe_folder) git.coordinates_to_conandata(repository=True) """) url, commit = create_local_git_repo(files={"conanfile.py": conanfile}, folder=folder) c = TestClient() c.save({"README.txt": "my readme!"}, path=folder) c.run_command("git checkout -b mybranch", cwd=folder) orphan_commit = git_add_changes_commit(folder) c.run_command(f"git checkout {orphan_commit}", cwd=folder) c.run_command("git branch -D mybranch", cwd=folder) # Now the client does the clone and export c.run_command(f'git clone "file://{url}" . ') c.run_command("git --no-pager log --decorate") assert "grafted" not in c.out c.run("export .") c.run_command("git --no-pager log --decorate") assert "grafted" not in c.out @pytest.mark.tool("git") class TestGitBasicSCMFlowSubfolder: """ Same as above, but conanfile.py put in "conan" subfolder in the root """ conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load, update_conandata class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self, os.path.dirname(self.recipe_folder)) # PARENT! scm_url, scm_commit = git.get_url_and_commit() update_conandata(self, {"sources": {"commit": scm_commit, "url": scm_url}}) def layout(self): self.folders.root = ".." self.folders.source = "." def source(self): git = Git(self) sources = self.conan_data["sources"] git.clone(url=sources["url"], target=".") git.checkout(commit=sources["commit"]) self.output.info("MYCMAKE: {}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {}".format(load(self, "src/myfile.h"))) def build(self): cmake = os.path.join(self.source_folder, "CMakeLists.txt") file_h = os.path.join(self.source_folder, "src/myfile.h") self.output.info("MYCMAKE-BUILD: {}".format(load(self, cmake))) self.output.info("MYFILE-BUILD: {}".format(load(self, file_h))) """) def test_full_scm(self): folder = os.path.join(temp_folder(), "myrepo") url, commit = create_local_git_repo(files={"conan/conanfile.py": self.conanfile, "src/myfile.h": "myheader!", "CMakeLists.txt": "mycmake"}, folder=folder) c = TestClient(default_server_user=True) c.run_command('git clone "{}" .'.format(url)) c.run("create conan") assert "pkg/0.1: MYCMAKE: mycmake" in c.out assert "pkg/0.1: MYFILE: myheader!" in c.out c.run("upload * -c -r=default") # Do a change and commit, this commit will not be used by package save_files(path=folder, files={"src/myfile.h": "my2header2!"}) git_add_changes_commit(folder=folder) # use another fresh client c2 = TestClient(servers=c.servers) c2.run("install --requires=pkg/0.1@ --build=pkg*") assert "pkg/0.1: MYCMAKE: mycmake" in c2.out assert "pkg/0.1: MYFILE: myheader!" in c2.out # local flow c.run("install conan") c.run("build conan") assert "conanfile.py (pkg/0.1): MYCMAKE-BUILD: mycmake" in c.out assert "conanfile.py (pkg/0.1): MYFILE-BUILD: myheader!" in c.out @pytest.mark.tool("git") class TestGitMonorepoSCMFlow: """ Build the full new SCM approach: Same as above but with a monorepo with multiple subprojects """ # TODO: swap_child_folder() not documented, not public usage conanfile = textwrap.dedent(""" import os, shutil from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import load, update_conandata, move_folder_contents class Pkg(ConanFile): name = "{pkg}" version = "0.1" {requires} def export(self): git = Git(self, self.recipe_folder) scm_url, scm_commit = git.get_url_and_commit() self.output.info("CAPTURING COMMIT: {{}}!!!".format(scm_commit)) folder = os.path.basename(self.recipe_folder) update_conandata(self, {{"sources": {{"commit": scm_commit, "url": scm_url, "folder": folder}}}}) def layout(self): self.folders.source = "." def source(self): git = Git(self) sources = self.conan_data["sources"] git.clone(url=sources["url"], target=".") git.checkout(commit=sources["commit"]) move_folder_contents(self, os.path.join(self.source_folder, sources["folder"]), self.source_folder) def build(self): cmake = os.path.join(self.source_folder, "CMakeLists.txt") file_h = os.path.join(self.source_folder, "src/myfile.h") self.output.info("MYCMAKE-BUILD: {{}}".format(load(self, cmake))) self.output.info("MYFILE-BUILD: {{}}".format(load(self, file_h))) """) def test_full_scm(self): folder = os.path.join(temp_folder(), "myrepo") conanfile1 = self.conanfile.format(pkg="pkg1", requires="") conanfile2 = self.conanfile.format(pkg="pkg2", requires="requires = 'pkg1/0.1'") url, commit = create_local_git_repo(files={"sub1/conanfile.py": conanfile1, "sub1/src/myfile.h": "myheader1!", "sub1/CMakeLists.txt": "mycmake1!", "sub2/conanfile.py": conanfile2, "sub2/src/myfile.h": "myheader2!", "sub2/CMakeLists.txt": "mycmake2!" }, folder=folder) c = TestClient(default_server_user=True) c.run_command('git clone "{}" .'.format(url)) c.run("create sub1") commit = re.search(r"CAPTURING COMMIT: (\S+)!!!", str(c.out)).group(1) assert "pkg1/0.1: MYCMAKE-BUILD: mycmake1!" in c.out assert "pkg1/0.1: MYFILE-BUILD: myheader1!" in c.out c.save({"sub2/src/myfile.h": "my2header!"}) git_add_changes_commit(folder=c.current_folder) c.run("create sub2") assert "pkg2/0.1: MYCMAKE-BUILD: mycmake2!" in c.out assert "pkg2/0.1: MYFILE-BUILD: my2header!" in c.out # Exporting again sub1, gives us exactly the same revision as before c.run("export sub1") assert "CAPTURING COMMIT: {}".format(commit) in c.out c.run("upload * -c -r=default") # use another fresh client c2 = TestClient(servers=c.servers) c2.run("install --requires=pkg2/0.1@ --build=*") assert "pkg1/0.1: Checkout: {}".format(commit) in c2.out assert "pkg1/0.1: MYCMAKE-BUILD: mycmake1!" in c2.out assert "pkg1/0.1: MYFILE-BUILD: myheader1!" in c2.out assert "pkg2/0.1: MYCMAKE-BUILD: mycmake2!" in c2.out assert "pkg2/0.1: MYFILE-BUILD: my2header!" in c2.out @pytest.mark.tool("cmake") def test_exports_sources_common_code_layout(self): """ This is a copy of test_exports_sources_common_code_layout in test_in_subfolder.py but instead of using "exports", trying to implement it with Git features """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout, CMake from conan.tools.files import load, copy, save, update_conandata, move_folder_contents from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def export(self): git = Git(self) scm_url, scm_commit = git.get_url_and_commit() update_conandata(self, {"sources": {"commit": scm_commit, "url": scm_url}}) def layout(self): self.folders.root = ".." self.folders.subproject = "pkg" cmake_layout(self) def source(self): git = Git(self) sources = self.conan_data["sources"] git.clone(url=sources["url"], target=".") git.checkout(commit=sources["commit"]) # Layout is pkg/pkg/ and pkg/common/ # Final we want is pkg/ and common/ # NOTE: This abs_path is IMPORTANT to avoid the trailing "." src_folder = os.path.abspath(self.source_folder) move_folder_contents(self, src_folder, os.path.dirname(src_folder)) def build(self): cmake = CMake(self) cmake.configure() cmake.build() self.run(os.path.join(self.cpp.build.bindirs[0], "myapp")) """) cmake_include = "include(${CMAKE_CURRENT_LIST_DIR}/../common/myutils.cmake)" c.save({"pkg/conanfile.py": conanfile, "pkg/app.cpp": gen_function_cpp(name="main", includes=["../common/myheader"], preprocessor=["MYDEFINE"]), "pkg/CMakeLists.txt": gen_cmakelists(appsources=["app.cpp"], custom_content=cmake_include), "common/myutils.cmake": 'message(STATUS "MYUTILS.CMAKE!")', "common/myheader.h": '#define MYDEFINE "MYDEFINEVALUE"'}) c.init_git_repo() c.run("create pkg") assert "MYUTILS.CMAKE!" in c.out assert "main: Release!" in c.out assert "MYDEFINE: MYDEFINEVALUE" in c.out # Local flow c.run("install pkg") c.run("build pkg") assert "MYUTILS.CMAKE!" in c.out assert "main: Release!" in c.out assert "MYDEFINE: MYDEFINEVALUE" in c.out c.run("install pkg -s build_type=Debug") c.run("build pkg -s build_type=Debug") assert "MYUTILS.CMAKE!" in c.out assert "main: Debug!" in c.out assert "MYDEFINE: MYDEFINEVALUE" in c.out class TestConanFileSubfolder: """verify that we can have a conanfile in a subfolder # https://github.com/conan-io/conan/issues/11275 """ conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git from conan.tools.files import update_conandata, load class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self, os.path.dirname(self.recipe_folder)) url, commit = git.get_url_and_commit() # We store the current url and commit in conandata.yml update_conandata(self, {"sources": {"commit": commit, "url": url}}) self.output.info("URL: {}".format(url)) self.output.info("COMMIT: {}".format(commit)) def layout(self): pass # self.folders.source = "source" def source(self): git = Git(self) sources = self.conan_data["sources"] url = sources["url"] commit = sources["commit"] git.clone(url=url, target=".") git.checkout(commit=commit) self.output.info("MYCMAKE: {}".format(load(self, "CMakeLists.txt"))) self.output.info("MYFILE: {}".format(load(self, "src/myfile.h"))) """) def test_conanfile_subfolder(self): """ A local repo, without remote, will have commit, but no URL """ c = TestClient() c.save({"conan/conanfile.py": self.conanfile, "CMakeLists.txt": "mycmakelists", "src/myfile.h": "myheader"}) commit = c.init_git_repo() c.run("export conan") assert "pkg/0.1: COMMIT: {}".format(commit) in c.out assert "pkg/0.1: URL: {}".format(c.current_folder.replace("\\", "/")) in c.out c.run("create conan") assert "pkg/0.1: MYCMAKE: mycmakelists" in c.out assert "pkg/0.1: MYFILE: myheader" in c.out def test_git_run(self): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self) self.output.info(git.run("--version")) """) c = TestClient() c.save({"conan/conanfile.py": conanfile}) c.init_git_repo() c.run("export conan") assert "pkg/0.1: git version" in c.out class TestGitIncluded: def test_git_included(self): conanfile = textwrap.dedent(""" import os import shutil from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" def export_sources(self): git = Git(self) included = git.included_files() for i in included: dst = os.path.join(self.export_sources_folder, i) os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copy2(i, dst) def source(self): self.output.info("SOURCES: {}!!".format(sorted(os.listdir(".")))) self.output.info("SOURCES_SUB: {}!!".format(sorted(os.listdir("sub")))) """) c = TestClient() c.save({"conanfile.py": conanfile, ".gitignore": "*.txt", "myfile.txt": "test", "myfile.other": "othertest", "sub/otherfile": "other"}) c.init_git_repo() c.run("create .") assert "pkg/0.1: SOURCES: ['.gitignore', 'conanfile.py', 'myfile.other', 'sub']!!" in c.out assert "pkg/0.1: SOURCES_SUB: ['otherfile']!!" in c.out def test_git_included_subfolder(self): conanfile = textwrap.dedent(""" import os import shutil from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" def export_sources(self): git = Git(self, "src") included = git.included_files() for i in included: shutil.copy2(i, self.export_sources_folder) def source(self): self.output.info("SOURCES: {}!!".format(sorted(os.listdir(".")))) """) c = TestClient() c.save({"conanfile.py": conanfile, ".gitignore": "*.txt", "somefile": "some", "src/myfile.txt": "test", "src/myfile.other": "othertest"}) c.init_git_repo() c.run("create .") assert "pkg/0.1: SOURCES: ['myfile.other']!!" in c.out def test_capture_git_tag(): """ A local repo, without remote, will have commit, but no URL """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" def set_version(self): git = Git(self, self.recipe_folder) self.version = git.run("describe --tags") """) c.save({"conanfile.py": conanfile}) c.init_git_repo() c.run_command("git tag 1.2") c.run("install .") assert "pkg/1.2" in c.out c.run("create .") assert "pkg/1.2" in c.out c.run("install --requires=pkg/1.2") assert "pkg/1.2" in c.out @pytest.mark.tool("git") class TestGitShallowTagClone: """ When we do a shallow clone of a repo with a specific tag/branch, it doesn't clone any of the git history. When we check to see if a commit is in the repo, we fallback to a git fetch if we can't verify the commit locally. """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self, self.recipe_folder) commit = git.get_commit() url = git.get_remote_url() self.output.info("URL: {}".format(url)) self.output.info("COMMIT: {}".format(commit)) in_remote = git.commit_in_remote(commit) self.output.info("COMMIT IN REMOTE: {}".format(in_remote)) self.output.info("DIRTY: {}".format(git.is_dirty())) """) def test_find_tag_in_remote(self): """ a shallow cloned repo won't have the new commit locally, but can fetch it. """ folder = temp_folder() url, commit = create_local_git_repo(files={"conanfile.py": self.conanfile}, folder=folder) c = TestClient() # Create a tag with c.chdir(folder): c.run_command('git tag 1.0.0') # Do a shallow clone of our tag c.run_command('git clone --depth=1 --branch 1.0.0 "{}" myclone'.format(folder)) with c.chdir("myclone"): c.run("export .") assert "pkg/0.1: COMMIT: {}".format(commit) in c.out assert "pkg/0.1: URL: {}".format(url) in c.out assert "pkg/0.1: COMMIT IN REMOTE: True" in c.out assert "pkg/0.1: DIRTY: False" in c.out def test_detect_commit_not_in_remote(self): """ a shallow cloned repo won't have new commit in remote """ folder = temp_folder() url, commit = create_local_git_repo(files={"conanfile.py": self.conanfile}, folder=folder) c = TestClient() # Create a tag with c.chdir(folder): c.run_command('git tag 1.0.0') # Do a shallow clone of our tag c.run_command('git clone --depth=1 --branch 1.0.0 "{}" myclone'.format(folder)) with c.chdir("myclone"): c.save({"conanfile.py": self.conanfile + "\n# some coment!"}) new_commit = git_add_changes_commit(c.current_folder) c.run("export .") assert "pkg/0.1: COMMIT: {}".format(new_commit) in c.out assert "pkg/0.1: URL: {}".format(url) in c.out assert "pkg/0.1: COMMIT IN REMOTE: False" in c.out assert "pkg/0.1: DIRTY: False" in c.out @pytest.mark.tool("git") class TestGitTreelessRemote: conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Git import os class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.source = "source" def export(self): git = Git(self) git.clone(url="{url}", args=["--filter=tree:0"], target="target") git.folder = "target" cloned_url = git.run("remote -v") self.output.info("git remote: %s ===" % cloned_url) cloned_url = git.get_remote_url() self.output.info("get_remote_url(): %s ===" % cloned_url) """) def test_treeless_clone(self): """ When cloning a git repository with the `--filter=tree:0` option, the Git.get_remote_url() should only the URL of the repository. Validate the issue https://github.com/conan-io/conan/issues/18415 """ repository = temp_folder(path_with_spaces=False) url, commit = create_local_git_repo(files={"README": "Lumen naturale ratum est"}, folder=repository) client = TestClient() client.save({"conanfile.py": self.conanfile.format(url=url)}) client.run("export .") # We expect [tree:0] for regular git remote command. Requires Git +2.43 assert f"git remote: origin\t{url} (fetch) [tree:0]" in client.out # Then get_remote_url filters it to only the URL assert f"get_remote_url(): {url} ===" in client.out def test_treeless_clone_with_parenthesis(self): """ Windows can use C:/Program Files (x86) path, which contains parenthesis. As the URL will have (fetch) or (push), get_remote_url() should be able to parse it. """ repository = os.path.join(temp_folder(), "Program Files (x86)", "myrepo") os.makedirs(repository) url, commit = create_local_git_repo(files={"README": "Pacem in maribus."}, folder=repository) client = TestClient() client.save({"conanfile.py": self.conanfile.format(url=url)}) client.run("export .") assert f"get_remote_url(): {url} ===" in client.out ================================================ FILE: test/functional/tools/scm/test_git_get_commit.py ================================================ import os import pytest from conan.tools.scm import Git from conan.test.utils.mocks import ConanFileMock from conan.test.utils.tools import TestClient from conan.internal.util.files import chdir from conan.internal.util.runners import conan_run @pytest.mark.tool("git") def test_change_branch_in_root_commit(): """ https://github.com/conan-io/conan/issues/10971#issuecomment-1089316912 """ c = TestClient() c.save({"root.txt": "", "subfolder/subfolder.txt": ""}) c.run_command("git init .") c.run_command("git checkout -B master") c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command("git add .") c.run_command('git commit -m "initial commit"') c.run_command("git checkout -b change_branch") c.save({"subfolder/subfolder.txt": "CHANGED"}) c.run_command("git add .") c.run_command('git commit -m "second commit"') c.run_command("git checkout master") c.run_command('git merge --no-ff change_branch -m "Merge branch"') conanfile = ConanFileMock({}, runner=conan_run) git = Git(conanfile, folder=c.current_folder) commit_conan = git.get_commit() c.run_command("git rev-parse HEAD") commit_real = str(c.out).splitlines()[0] assert commit_conan == commit_real @pytest.mark.tool("git") def test_multi_folder_repo(): c = TestClient() c.save({"lib_a/conanfile.py": ""}) c.run_command("git init .") c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command("git add .") c.run_command('git commit -m "lib_a commit"') c.save({"lib_b/conanfile.py": ""}) c.run_command("git add .") c.run_command('git commit -m "lib_b commit"') c.save({"lib_c/conanfile.py": ""}) c.run_command("git add .") c.run_command('git commit -m "lib_c commit"') c.save({"root_change": ""}) c.run_command("git add .") c.run_command('git commit -m "root change"') # Git object for lib_a conanfile = ConanFileMock({}, runner=conan_run) git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_a")) commit_libA = git.get_commit() git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_b")) commit_libB = git.get_commit() git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_c")) commit_libC = git.get_commit() git = Git(conanfile, folder=c.current_folder) commit_root = git.get_commit() # All different assert len({commit_libA, commit_libB, commit_libC, commit_root}) == 4 c.run_command("git rev-parse HEAD") commit_real = str(c.out).splitlines()[0] assert commit_root == commit_real # New commit in A c.save({"lib_a/conanfile.py": "CHANGED"}) c.run_command("git add .") c.run_command('git commit -m "lib_a commit2"') # Git object for lib_a git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_a")) new_commit_libA = git.get_commit() git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_b")) new_commit_libB = git.get_commit() git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_c")) new_commit_libC = git.get_commit() git = Git(conanfile, folder=c.current_folder) new_commit_root = git.get_commit() assert new_commit_libA != commit_libA assert new_commit_libB == commit_libB assert new_commit_libC == commit_libC assert new_commit_root != commit_root c.run_command("git rev-parse HEAD") commit_real = str(c.out).splitlines()[0] assert new_commit_root == commit_real @pytest.mark.tool("git") def test_relative_folder_repo(): c = TestClient() c.save({"lib_a/conanfile.py": ""}) c.run_command("git init .") c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command("git add .") c.run_command('git commit -m "lib_a commit"') c.save({"lib_b/conanfile.py": ""}) c.run_command("git add .") c.run_command('git commit -m "lib_b commit"') c.save({"lib_c/conanfile.py": ""}) c.run_command("git add .") c.run_command('git commit -m "lib_c commit"') c.save({"root_change": ""}) c.run_command("git add .") c.run_command('git commit -m "root change"') conanfile = ConanFileMock({}, runner=conan_run) # Relative paths for folders, from the current_folder with chdir(c.current_folder): git = Git(conanfile, folder="lib_a") commit_libA = git.get_commit() git = Git(conanfile, folder="lib_b") commit_libB = git.get_commit() git = Git(conanfile, folder="./lib_c") commit_libC = git.get_commit() # this is folder default, but be explicit git = Git(conanfile, folder=".") commit_root = git.get_commit() # All different assert len({commit_libA, commit_libB, commit_libC, commit_root}) == 4 # Compare to Full paths git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_a")) full_commit_libA = git.get_commit() git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_b")) full_commit_libB = git.get_commit() git = Git(conanfile, folder=os.path.join(c.current_folder, "lib_c")) full_commit_libC = git.get_commit() git = Git(conanfile, folder=c.current_folder) full_commit_root = git.get_commit() assert full_commit_libA == commit_libA assert full_commit_libB == commit_libB assert full_commit_libC == commit_libC assert full_commit_root == commit_root # Sanity checks c.run_command("git rev-parse HEAD") commit_real_root = str(c.out).splitlines()[0] assert commit_real_root == commit_root c.run_command("git rev-list -n 1 --full-history HEAD -- lib_a") commit_real_libA = str(c.out).splitlines()[0] assert commit_real_libA == commit_libA @pytest.mark.tool("git") def test_submodule_repo(): c = TestClient() c.save({"conanfile.py": ""}) c.run_command("git init .") c.run_command('git config user.name myname') c.run_command('git config user.email myname@mycompany.com') c.run_command("git add .") c.run_command('git commit -m "Initial commit"') c.run_command('git clone . source_subfolder') c.run_command('git submodule add ../ source_subfolder') c.run_command('git commit -m "submodule commit"') c.save({"root_change": ""}) c.run_command("git add .") c.run_command('git commit -m "root change"') conanfile = ConanFileMock({}, runner=conan_run) with chdir(c.current_folder): # default case git = Git(conanfile) commit_root = git.get_commit() # Relative paths git = Git(conanfile, folder="source_subfolder") commit_relA = git.get_commit() git = Git(conanfile, folder="./source_subfolder") commit_relB = git.get_commit() # Full path git = Git(conanfile, folder=os.path.join(c.current_folder, "source_subfolder")) commit_full = git.get_commit() assert commit_relA == commit_relB assert commit_relA == commit_full assert commit_root != commit_full # This is the commit which modified the tree in the containing repo # not the commit which the submodule is at c.run_command("git rev-list HEAD -n 1 --full-history -- source_subfolder") commit_submodule = str(c.out).splitlines()[0] assert commit_submodule != commit_full ================================================ FILE: test/functional/tools/scm/test_version.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_version(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Version class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "compiler" def configure(self): v = Version(self.settings.compiler.version) assert v > "10" v = Version("1.2.3") self.output.warning(f"The major of 1.2.3 is {v.major}") self.output.warning(f"The minor of 1.2.3 is {v.minor}") self.output.warning(f"The patch of 1.2.3 is {v.patch}") """) c.save({"conanfile.py": conanfile}) settings = "-s compiler=gcc -s compiler.libcxx=libstdc++11" c.run("create . {} -s compiler.version=11".format(settings)) assert "The major of 1.2.3 is 1" in c.out assert "The minor of 1.2.3 is 2" in c.out assert "The patch of 1.2.3 is 3" in c.out c.run("create . {} -s compiler.version=9".format(settings), assert_error=True) ================================================ FILE: test/functional/tools/system/__init__.py ================================================ ================================================ FILE: test/functional/tools/system/package_manager_test.py ================================================ import json import platform import textwrap from unittest.mock import patch, MagicMock import pytest from conan.tools.system.package_manager import _SystemPackageManagerTool from conan.test.utils.tools import TestClient @pytest.mark.tool("apt_get") @pytest.mark.skipif(platform.system() != "Linux", reason="Requires apt") def test_apt_check(): client = TestClient() client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.system.package_manager import Apt class MyPkg(ConanFile): settings = "arch", "os" def system_requirements(self): apt = Apt(self) not_installed = apt.check(["non-existing1", "non-existing2"]) print("missing:", not_installed) """)}) client.run("create . --name=test --version=1.0 -s:b arch=armv8 -s:h arch=x86") assert "missing: ['non-existing1', 'non-existing2']" in client.out @pytest.mark.tool("apt_get") @pytest.mark.skipif(platform.system() != "Linux", reason="Requires apt") def test_apt_install_substitutes(): client = TestClient() conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.system.package_manager import Apt class MyPkg(ConanFile): settings = "arch", "os" def system_requirements(self): # FIXME this is needed because the ci-functional apt-get update fails try: self.run("sudo apt-get update") except Exception: pass apt = Apt(self) {} """) installs = 'apt.install_substitutes(["non-existing1", "non-existing2"], ["non-existing3", "non-existing4"])' client.save({"conanfile.py": conanfile_py.format(installs)}) client.run("create . --name=test --version=1.0 -c tools.system.package_manager:mode=install " "-c tools.system.package_manager:sudo=True", assert_error=True) assert "None of the installs for the package substitutes succeeded." in client.out client.run_command("sudo apt remove nano -yy") installs = 'apt.install_substitutes(["non-existing1", "non-existing2"], ["nano"], ["non-existing3"])' client.save({"conanfile.py": conanfile_py.format(installs)}) client.run("create . --name=test --version=1.0 -c tools.system.package_manager:mode=install " "-c tools.system.package_manager:sudo=True") assert "1 newly installed" in client.out @pytest.mark.tool("apt_get") @pytest.mark.skipif(platform.system() != "Linux", reason="Requires apt") def test_build_require(): client = TestClient() client.save({"tool_require.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.system.package_manager import Apt class MyPkg(ConanFile): settings = "arch", "os" def system_requirements(self): apt = Apt(self) not_installed = apt.check(["non-existing1", "non-existing2"]) print("missing:", not_installed) """)}) client.run("export tool_require.py --name=tool_require --version=1.0") client.save({"consumer.py": textwrap.dedent(""" from conan import ConanFile class consumer(ConanFile): settings = "arch", "os" tool_requires = "tool_require/1.0" """)}) client.run("create consumer.py --name=consumer --version=1.0 " "-s:b arch=armv8 -s:h arch=x86 --build=missing") assert "missing: ['non-existing1', 'non-existing2']" in client.out @pytest.mark.tool("brew") @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires brew") def test_brew_check(): client = TestClient() client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.system.package_manager import Brew class MyPkg(ConanFile): settings = "arch" def system_requirements(self): brew = Brew(self) not_installed = brew.check(["non-existing1", "non-existing2"]) print("missing:", not_installed) """)}) client.run("create . --name=test --version=1.0") assert "missing: ['non-existing1', 'non-existing2']" in client.out @pytest.mark.tool("brew") @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires brew") @pytest.mark.skip(reason="brew update takes a lot of time") def test_brew_install_check_mode(): client = TestClient() client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.system.package_manager import Brew class MyPkg(ConanFile): settings = "arch" def system_requirements(self): brew = Brew(self) brew.install(["non-existing1", "non-existing2"]) """)}) client.run("create . test/1.0@", assert_error=True) assert "System requirements: 'non-existing1, non-existing2' are missing but " \ "can't install because tools.system.package_manager:mode is 'check'" in client.out @pytest.mark.tool("brew") @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires brew") @pytest.mark.skip(reason="brew update takes a lot of time") def test_brew_install_install_mode(): client = TestClient() client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.system.package_manager import Brew class MyPkg(ConanFile): settings = "arch" def system_requirements(self): brew = Brew(self) brew.install(["non-existing1", "non-existing2"]) """)}) client.run("create . test/1.0@ -c tools.system.package_manager:mode=install", assert_error=True) assert "Error: No formulae found in taps." in client.out def test_collect_system_requirements(): """ we can know the system_requires for every package because they are part of the graph, this naturally execute at ``install``, but we can also prove that with ``graph info`` we can for it to with the righ ``mode=collect`` mode. """ client = TestClient() client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.system.package_manager import Brew, Apt class MyPkg(ConanFile): settings = "arch" def system_requirements(self): brew = Brew(self) brew.install(["brew1", "brew2"]) apt = Apt(self) apt.install(["pkg1", "pkg2"]) """)}) with patch.object(_SystemPackageManagerTool, '_conanfile_run', MagicMock(return_value=False)): client.run("install . -c tools.system.package_manager:tool=apt-get --format=json", redirect_stdout="graph.json") graph = json.loads(client.load("graph.json")) assert {"apt-get": {"install": ["pkg1", "pkg2"], "missing": []}} == \ graph["graph"]["nodes"]["0"]["system_requires"] # plain report, do not check client.run("graph info . -c tools.system.package_manager:tool=apt-get " "-c tools.system.package_manager:mode=report --format=json", redirect_stdout="graph2.json") graph2 = json.loads(client.load("graph2.json")) # TODO: Unify format of ``graph info`` and ``install`` assert {"apt-get": {"install": ["pkg1", "pkg2"]}} == \ graph2["graph"]["nodes"]["0"]["system_requires"] # Check report-installed with patch.object(_SystemPackageManagerTool, '_conanfile_run', MagicMock(return_value=True)): client.run("graph info . -c tools.system.package_manager:tool=apt-get " "-c tools.system.package_manager:mode=report-installed --format=json", redirect_stdout="graph2.json") graph2 = json.loads(client.load("graph2.json")) assert {"apt-get": {"install": ["pkg1", "pkg2"], 'missing': ['pkg1', 'pkg2']}} == graph2["graph"]["nodes"]["0"]["system_requires"] # Default "check" will fail, as dpkg-query not installed client.run("graph info . -c tools.system.package_manager:tool=apt-get " "-c tools.system.package_manager:mode=check", assert_error=True) assert "ERROR: conanfile.py: Error in system_requirements() method, line 11" in client.out ================================================ FILE: test/functional/tools/system/python_manager_test.py ================================================ import json import sys import textwrap import platform import pytest from conan.test.utils.tools import TestClient from conan.internal.util.files import save_files from conan.test.utils.test_files import temp_folder def _create_py_hello_world(folder): setup_py = textwrap.dedent(""" from setuptools import setup, find_packages setup( name='hello', version='0.1.0', packages=find_packages(include=['hello', 'hello.*']), entry_points={'console_scripts': ['hello-world = hello:hello']} ) """) hello_py = textwrap.dedent(""" def hello(): print("Hello Test World!") """) save_files(folder, {"setup.py": setup_py, "hello/__init__.py": hello_py}) def test_empty_pyenv(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.system import PyEnv class PyenvPackage(ConanFile): def generate(self): PyEnv(self).generate() def build(self): self.run("python -m pip list") """) c = TestClient(path_with_spaces=False) c.save({"conanfile.py": conanfile}) c.run("build") # Test that some Conan common deps are not in this pip list assert "requests" not in c.out assert "colorama" not in c.out assert "Jinja2" not in c.out assert "PyJWT" not in c.out ext = ".bat" if platform.system() == "Windows" else ".sh" script = c.load(f"conan_pyenv{ext}") assert "conan_pyenv" in script def test_build_py_manager(): pip_package_folder = temp_folder(path_with_spaces=True) _create_py_hello_world(pip_package_folder) pip_package_folder = pip_package_folder.replace('\\', '/') conanfile_pyenv = textwrap.dedent(f""" from conan import ConanFile from conan.tools.system import PyEnv from conan.tools.layout import basic_layout class PyenvPackage(ConanFile): def layout(self): basic_layout(self) def generate(self): pyenv = PyEnv(self) pyenv.install(["{pip_package_folder}"]) pyenv.generate() def build(self): self.run("hello-world") """) client = TestClient(path_with_spaces=False) # FIXME: the python shebang inside vitual env packages fails when using path_with_spaces client.save({"pip/conanfile.py": conanfile_pyenv}) client.run("build pip") assert "RUN: hello-world" in client.out assert "Hello Test World!" in client.out client.run("build pip") assert "Found existing installation: hello 0.1.0" in client.out assert "Hello Test World!" in client.out client.run("build pip -verror") assert "Found existing installation" not in client.out def test_install_version_range(): c = TestClient(path_with_spaces=False) # TODO: Maybe we want a pip.run("-m pip install .") that automatically handles python.exe path conanfile_pyenv = textwrap.dedent(""" from conan import ConanFile from conan.tools.system import PyEnv from conan.tools.layout import basic_layout class PyenvPackage(ConanFile): def generate(self): pyenv = PyEnv(self) pyenv.install(["."]) pyenv.install(["hello>=0.0,<1.0"]) pyenv.generate() def build(self): self.run("hello-world") """) # FIXME: the python shebang inside vitual env packages fails when using path_with_spaces c.save({"conanfile.py": conanfile_pyenv}) _create_py_hello_world(c.current_folder) c.run("build") assert "RUN: hello-world" in c.out assert "Hello Test World!" in c.out def test_create_py_manager(): pip_package_folder = temp_folder(path_with_spaces=True) _create_py_hello_world(pip_package_folder) pip_package_folder = pip_package_folder.replace('\\', '/') conanfile_pyenv = textwrap.dedent(f""" from conan import ConanFile from conan.tools.system import PyEnv from conan.tools.layout import basic_layout class PyenvPackage(ConanFile): name = "pip_hello_test" version = "0.1" build_policy = "missing" upload_policy = "skip" def layout(self): basic_layout(self) def finalize(self): PyEnv(self, self.package_folder).install(["{pip_package_folder}"]) def package_info(self): python_env_bin = PyEnv(self, self.package_folder).bin_path self.buildenv_info.prepend_path("PATH", python_env_bin) """) conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def requirements(self): self.tool_requires("pip_hello_test/0.1") def build(self): self.run("hello-world") """) client = TestClient(path_with_spaces=False) # FIXME: the python shebang inside vitual env packages fails when using path_with_spaces client.save({"pip/conanfile.py": conanfile_pyenv, "consumer/conanfile.py": conanfile}) client.run("create pip --version=0.1") client.run("build consumer") assert "RUN: hello-world" in client.out assert "Hello Test World!" in client.out @pytest.mark.skipif(sys.version_info.minor < 8, reason="UV needs Python >= 3.8") def test_build_uv_manager(): pip_package_folder = temp_folder(path_with_spaces=True) _create_py_hello_world(pip_package_folder) pip_package_folder = pip_package_folder.replace('\\', '/') def conanfile_pyenv(py_version): return textwrap.dedent(f""" from conan import ConanFile from conan.tools.system import PyEnv from conan.tools.layout import basic_layout import platform import os class PyenvPackage(ConanFile): name = "pip_hello_test" version = "0.1" def layout(self): basic_layout(self) def generate(self): pip_env = PyEnv(self, py_version="{py_version}") pip_env.install(["{pip_package_folder}"]) pip_env.generate() pip_env.run(["--version"]) def build(self): self.run("hello-world") """) client = TestClient(path_with_spaces=False) # FIXME: the python shebang inside vitual env packages fails when using path_with_spaces client.save({"pip/conanfile.py": conanfile_pyenv("3.11.6")}) client.run("build pip/conanfile.py") assert "Virtual environment for Python 3.11.6 created successfully using UV." in client.out if platform.system() == "Windows": assert "python.exe --version\nPython 3.11.6" in client.out else: assert "python --version\nPython 3.11.6" in client.out assert "RUN: hello-world" in client.out assert "Hello Test World!" in client.out client.run("build pip/conanfile.py") assert "Found existing installation: hello 0.1.0" in client.out assert "RUN: hello-world" in client.out assert "Hello Test World!" in client.out client.save({"pip/conanfile.py": conanfile_pyenv("3.12.3")}) client.run("build pip/conanfile.py") assert "Virtual environment for Python 3.12.3 created successfully using UV." in client.out if platform.system() == "Windows": assert "python.exe --version\nPython 3.12.3" in client.out else: assert "python --version\nPython 3.12.3" in client.out assert "Found existing installation: hello 0.1.0" not in client.out assert "RUN: hello-world" in client.out assert "Hello Test World!" in client.out @pytest.mark.skipif(sys.version_info.minor < 8, reason="UV needs Python >= 3.8") def test_fail_build_uv_manager(): pip_package_folder = temp_folder(path_with_spaces=True) _create_py_hello_world(pip_package_folder) pip_package_folder = pip_package_folder.replace('\\', '/') conanfile_pyenv = textwrap.dedent(f""" from conan import ConanFile from conan.tools.system import PyEnv from conan.tools.layout import basic_layout import platform import os class PyenvPackage(ConanFile): name = "pip_hello_test" version = "0.1" def layout(self): basic_layout(self) def generate(self): pip_env = PyEnv(self, py_version="3.11.86") pip_env.install(["{pip_package_folder}"]) pip_env.generate() def build(self): self.run("hello-world") """) client = TestClient(path_with_spaces=False) # FIXME: the python shebang inside vitual env packages fails when using path_with_spaces client.save({"pip/conanfile.py": conanfile_pyenv}) client.run("build pip/conanfile.py", assert_error=True) assert "PyEnv could not create a Python 3.11.86 virtual environment using UV" in client.out @pytest.mark.skipif(sys.version_info.minor > 7, reason="UV needs Python 3.7 to fail") def test_fail_uv_python_version(): pip_package_folder = temp_folder(path_with_spaces=True) _create_py_hello_world(pip_package_folder) pip_package_folder = pip_package_folder.replace('\\', '/') conanfile_pyenv = textwrap.dedent(f""" from conan import ConanFile from conan.tools.system import PyEnv from conan.tools.layout import basic_layout import platform import os class PyenvPackage(ConanFile): name = "pip_hello_test" version = "0.1" def layout(self): basic_layout(self) def generate(self): pip_env = PyEnv(self, py_version="3.11.86") pip_env.install(["{pip_package_folder}"]) pip_env.generate() def build(self): self.run("hello-world") """) client = TestClient(path_with_spaces=False) # FIXME: the python shebang inside vitual env packages fails when using path_with_spaces client.save({"pip/conanfile.py": conanfile_pyenv}) client.run("build pip/conanfile.py", assert_error=True) assert "needs Python >= 3.8" in client.out def test_build_deprecated_python_manager(): pip_package_folder = temp_folder(path_with_spaces=True) _create_py_hello_world(pip_package_folder) pip_package_folder = pip_package_folder.replace('\\', '/') conanfile_pyenv = textwrap.dedent(f""" from conan import ConanFile from conan.tools.system import PipEnv from conan.tools.layout import basic_layout class PyenvPackage(ConanFile): def layout(self): basic_layout(self) def generate(self): pip = PipEnv(self) pip.install(["{pip_package_folder}"]) pip.generate() def build(self): self.run("hello-world") """) client = TestClient(path_with_spaces=False) # FIXME: the python shebang inside vitual env packages fails when using path_with_spaces client.save({"pip/conanfile.py": conanfile_pyenv}) client.run("build pip") assert "WARN: deprecated: 'PipEnv()' is deprecated, use 'PyEnv()'" in client.out assert "RUN: hello-world" in client.out assert "Hello Test World!" in client.out @pytest.mark.parametrize("verbosity", ["-verror", "-vstatus"]) def test_pyenv_install_error_always_shown(verbosity): conanfile_pyenv = textwrap.dedent(""" from conan import ConanFile from conan.tools.system import PyEnv class PyenvPackage(ConanFile): def generate(self): pyenv = PyEnv(self) pyenv.install(["package_does_not_exist"]) """) client = TestClient(path_with_spaces=False) client.save({"conanfile.py": conanfile_pyenv}) client.run(f"build . {verbosity}", assert_error=True) assert "package_does_not_exist" in client.out assert "ERROR" in client.out def test_cmake_toolchain_configure_find_python(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain from conan.tools.system import PyEnv class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): pyenv = PyEnv(self) pyenv.generate() tc = CMakeToolchain(self) tc.cache_variables["Python_ROOT_DIR"] = pyenv.env_dir tc.cache_variables["Python_EXECUTABLE"] = pyenv.env_exe tc.cache_variables["Python_FIND_UNVERSIONED_NAMES"] = "FIRST" tc.cache_variables["Python_FIND_STRATEGY"] = "LOCATION" tc.cache_variables["Python_FIND_VIRTUALENV"] = "STANDARD" tc.cache_variables["Python_FIND_REGISTRY"] = "NEVER" tc.generate() """) client.save({"conanfile.py": conanfile}) client.run("install .") presets = json.loads(client.load("CMakePresets.json")) cv = presets["configurePresets"][0]["cacheVariables"] assert "Python_ROOT_DIR" in cv assert "Python_EXECUTABLE" in cv assert "\\" not in cv["Python_ROOT_DIR"] assert "\\" not in cv["Python_EXECUTABLE"] assert "conan_pyenv" in cv["Python_ROOT_DIR"] assert cv["Python_FIND_UNVERSIONED_NAMES"] == "FIRST" assert cv["Python_FIND_STRATEGY"] == "LOCATION" assert cv["Python_FIND_VIRTUALENV"] == "STANDARD" assert cv["Python_FIND_REGISTRY"] == "NEVER" ================================================ FILE: test/functional/tools/test_apple_tools.py ================================================ import platform import re import textwrap import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.tools.apple.apple import XCRun from conan.test.utils.mocks import ConanFileMock, MockSettings from conan.internal.util.runners import conan_run @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires Xcode") def test_xcrun(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.apple import XCRun class HelloConan(ConanFile): name = "hello" version = "0.1" settings = "os", "compiler", "build_type", "arch" def build(self): sdk_path = XCRun(self).sdk_path self.output.info(sdk_path) """) client = TestClient(path_with_spaces=False) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create .") assert "MacOSX" in client.out @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires OSX and xcrun tool") def test_xcrun_sdks(): def _common_asserts(xcrun_): assert xcrun_.cc.endswith('clang') assert xcrun_.cxx.endswith('clang++') assert xcrun_.ar.endswith('ar') assert xcrun_.ranlib.endswith('ranlib') assert xcrun_.strip.endswith('strip') assert xcrun_.find('lipo').endswith('lipo') assert os.path.isdir(xcrun_.sdk_path) conanfile = ConanFileMock( runner=conan_run) conanfile.settings = MockSettings( {"os": "Macos", "arch": "x86"}) xcrun = XCRun(conanfile) _common_asserts(xcrun) conanfile.settings = MockSettings( {"os": "iOS", "arch": "x86"}) xcrun = XCRun(conanfile, sdk='macosx') _common_asserts(xcrun) # Simulator assert "iPhoneOS" not in xcrun.sdk_path conanfile.settings = MockSettings( {"os": "iOS", "os.sdk": "iphoneos", "arch": "armv7"}) xcrun = XCRun(conanfile) _common_asserts(xcrun) assert "iPhoneOS" in xcrun.sdk_path conanfile.settings = MockSettings( {"os": "watchOS", "os.sdk": "watchos", "arch": "armv7"}) xcrun = XCRun(conanfile) _common_asserts(xcrun) assert "WatchOS" in xcrun.sdk_path # Default one conanfile.settings = MockSettings({}) xcrun = XCRun(conanfile) _common_asserts(xcrun) @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires Xcode") def test_xcrun_in_tool_requires(): # https://github.com/conan-io/conan/issues/12260 client = TestClient() tool = textwrap.dedent(""" from conan import ConanFile from conan.tools.apple import XCRun class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def package_info(self): xcrun = XCRun(self{}) self.output.info("sdk: %s" % xcrun.sdk_path) """) client.save({"br.py": tool.format(", use_settings_target=True")}) client.run("export br.py --name=br --version=0.1") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" tool_requires = "br/0.1" """) profile_ios = textwrap.dedent(""" include(default) [settings] os=iOS os.version=15.5 os.sdk=iphoneos os.sdk_version=15.5 arch=armv8 """) client.save({"conanfile.py": conanfile, "profile_ios": profile_ios}) client.run("create . --name=pkg --version=1.0 -pr:h=./profile_ios -pr:b=default --build='*'") assert re.search("sdk:.*iPhoneOS", str(client.out)) assert not re.search("sdk:.*MacOSX", str(client.out)) client.save({"br.py": tool.format("")}) client.run("export br.py --name=br --version=0.1") client.run("create . --name=pkg --version=1.0 -pr:h=./profile_ios -pr:b=default --build='*'") assert not re.search("sdk:.*iPhoneOS", str(client.out)) assert re.search("sdk:.*MacOSX", str(client.out)) @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires Xcode") def test_xcrun_in_required_by_tool_requires(): """ ConanCenter case, most typical, openssl builds with autotools so needs the sysroot and is a require by cmake so in the build context it needs the settings_build, not the settings_target, that's why the use_settings_target default is False """ client = TestClient() openssl = textwrap.dedent(""" from conan import ConanFile from conan.tools.apple import XCRun class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def build(self): xcrun = XCRun(self) self.output.info("sdk for building openssl: %s" % xcrun.sdk_path) """) consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" tool_requires = "cmake/1.0" """) profile_ios = textwrap.dedent(""" include(default) [settings] os=iOS os.version=15.5 os.sdk=iphoneos os.sdk_version=15.5 arch=armv8 """) client.save({"cmake.py": GenConanfile("cmake", "1.0").with_requires("openssl/1.0"), "openssl.py": openssl, "consumer.py": consumer, "profile_ios": profile_ios}) client.run("export openssl.py --name=openssl --version=1.0") client.run("export cmake.py") client.run("create consumer.py --name=consumer --version=1.0 -pr:h=./profile_ios -pr:b=default --build='*'") assert re.search("sdk for building openssl:.*MacOSX", str(client.out)) ================================================ FILE: test/functional/tools/test_files.py ================================================ import os import textwrap import patch_ng import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save, load class MockPatchset: apply_args = None def apply(self, strip=0, root=None, fuzz=False): self.apply_args = (root, strip, fuzz) return True @pytest.fixture def mock_patch_ng(monkeypatch): mock = MockPatchset() def mock_fromstring(string): mock.string = string return mock monkeypatch.setattr(patch_ng, "fromfile", lambda _: mock) monkeypatch.setattr(patch_ng, "fromstring", mock_fromstring) return mock class TestConanToolFiles: def test_imports(self): conanfile = GenConanfile().with_import("from conan.tools.files import load, save, " "mkdir, download, get, ftp_download") client = TestClient(light=True) client.save({"conanfile.py": conanfile}) client.run("install .") def test_load_save_mkdir(self): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load, save, mkdir class Pkg(ConanFile): name = "mypkg" version = "1.0" def source(self): mkdir(self, "myfolder") save(self, "./myfolder/myfile", "some_content") assert load(self, "./myfolder/myfile") == "some_content" """) client = TestClient(light=True) client.save({"conanfile.py": conanfile}) client.run("source .") def test_download(self): client = TestClient(light=True) file_server = TestFileServer() client.servers["file_server"] = file_server save(os.path.join(file_server.store, "myfile.txt"), "some content") profile = textwrap.dedent("""\ [conf] tools.files.download:retry=1 tools.files.download:retry_wait=0 """) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): name = "mypkg" version = "1.0" def source(self): download(self, "{}/myfile.txt", "myfile.txt") assert os.path.exists("myfile.txt") """.format(file_server.fake_url)) client.save({"conanfile.py": conanfile}) client.save({"profile": profile}) client.run("create . -pr=profile") def test_download_export_sources(self): client = TestClient(light=True) file_server = TestFileServer() client.servers["file_server"] = file_server save(os.path.join(file_server.store, "myfile.txt"), "some content") save(os.path.join(file_server.store, "myfile2.txt"), "some content") conanfile = textwrap.dedent(f""" import os from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): name = "mypkg" version = "1.0" def export(self): download(self, "{file_server.fake_url}/myfile.txt", "myfile.txt") assert os.path.exists("myfile.txt") def export_sources(self): download(self, "{file_server.fake_url}/myfile2.txt", "myfile2.txt") assert os.path.exists("myfile2.txt") """) client.save({"conanfile.py": conanfile}) client.run("create .") def test_patch(mock_patch_ng): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import patch class Pkg(ConanFile): name = "mypkg" version = "1.0" def build(self): patch(self, patch_file='path/to/patch-file', patch_type='security') """) client = TestClient(light=True) client.save({"conanfile.py": conanfile}) client.run('create .') # Note: This cannot exist anymore, because the path is moved when prev is computed # assert os.path.exists(mock_patch_ng.apply_args[0]) assert mock_patch_ng.apply_args[1:] == (0, False) assert 'mypkg/1.0: Apply patch (security)' in str(client.out) @pytest.mark.parametrize("no_copy_source", [False, True]) def test_patch_real(no_copy_source): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import patch, save, load class Pkg(ConanFile): name = "mypkg" version = "1.0" exports_sources = "*" no_copy_source = %s def layout(self): self.folders.source = "src" self.folders.build = "build" def source(self): save(self, "myfile.h", "//dummy contents") patch(self, patch_file="patches/mypatch_h", patch_type="security") self.output.info("SOURCE: {}".format(load(self, "myfile.h"))) def build(self): save(self, "myfile.cpp", "//dummy contents") if self.no_copy_source: patch_file = os.path.join(self.source_folder, "../patches/mypatch_cpp") else: patch_file = "patches/mypatch_cpp" patch(self, patch_file=patch_file, patch_type="security", base_path=self.build_folder) self.output.info("BUILD: {}".format(load(self, "myfile.cpp"))) """ % no_copy_source) client = TestClient(light=True) patch_contents = textwrap.dedent("""\ --- myfile.{ext} +++ myfile.{ext} @@ -1 +1 @@ -//dummy contents +//smart contents """) client.save({"conanfile.py": conanfile, "patches/mypatch_h": patch_contents.format(ext="h"), "patches/mypatch_cpp": patch_contents.format(ext="cpp")}) client.run('create .') assert "mypkg/1.0: Apply patch (security)" in client.out assert "mypkg/1.0: SOURCE: //smart contents" in client.out assert "mypkg/1.0: BUILD: //smart contents" in client.out # Test local source too client.run("install .") client.run("source .") assert "conanfile.py (mypkg/1.0): Apply patch (security)" in client.out assert "conanfile.py (mypkg/1.0): SOURCE: //smart contents" in client.out client.run("build .") assert "conanfile.py (mypkg/1.0): Apply patch (security)" in client.out assert "conanfile.py (mypkg/1.0): BUILD: //smart contents" in client.out def test_apply_conandata_patches(mock_patch_ng): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import apply_conandata_patches class Pkg(ConanFile): name = "mypkg" version = "1.11.0" def layout(self): self.folders.source = "source_subfolder" def build(self): apply_conandata_patches(self) """) conandata_yml = textwrap.dedent(""" patches: "1.11.0": - patch_file: "patches/0001-buildflatbuffers-cmake.patch" - patch_file: "patches/0002-implicit-copy-constructor.patch" patch_type: backport patch_source: https://github.com/google/flatbuffers/pull/5650 patch_description: Needed to build with modern clang compilers. "1.12.0": - patch_file: "patches/0001-buildflatbuffers-cmake.patch" """) client = TestClient(light=True) client.save({'conanfile.py': conanfile, 'conandata.yml': conandata_yml}) client.run('create .') assert mock_patch_ng.apply_args[0].endswith('source_subfolder') assert mock_patch_ng.apply_args[1:] == (0, False) assert 'mypkg/1.11.0: Apply patch (backport): Needed to build with modern' \ ' clang compilers.' in str(client.out) # Test local methods client.run("install .") client.run("build .") assert 'conanfile.py (mypkg/1.11.0): Apply patch (backport): Needed to build with modern' \ ' clang compilers.' in str(client.out) def test_apply_conandata_patches_relative_base_path(mock_patch_ng): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import apply_conandata_patches class Pkg(ConanFile): name = "mypkg" version = "1.11.0" def layout(self): self.folders.source = "source_subfolder" def build(self): apply_conandata_patches(self) """) conandata_yml = textwrap.dedent(""" patches: "1.11.0": - patch_file: "patches/0001-buildflatbuffers-cmake.patch" base_path: "relative_dir" """) client = TestClient(light=True) client.save({'conanfile.py': conanfile, 'conandata.yml': conandata_yml}) client.run('create .') assert mock_patch_ng.apply_args[0].endswith(os.path.join('source_subfolder', "relative_dir")) assert mock_patch_ng.apply_args[1:] == (0, False) def test_no_patch_file_entry(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import apply_conandata_patches class Pkg(ConanFile): name = "mypkg" version = "1.11.0" def layout(self): self.folders.source = "source_subfolder" def build(self): apply_conandata_patches(self) """) conandata_yml = textwrap.dedent(""" patches: "1.11.0": - wrong_entry: "patches/0001-buildflatbuffers-cmake.patch" "1.12.0": - patch_file: "patches/0001-buildflatbuffers-cmake.patch" """) client = TestClient(light=True) client.save({'conanfile.py': conanfile, 'conandata.yml': conandata_yml}) client.run('create .', assert_error=True) assert "The 'conandata.yml' file needs a 'patch_file' or 'patch_string' entry for every patch" \ " to be applied" in str(client.out) def test_patch_string_entry(mock_patch_ng): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import apply_conandata_patches class Pkg(ConanFile): name = "mypkg" version = "1.11.0" def build(self): apply_conandata_patches(self) """) conandata_yml = textwrap.dedent(""" patches: "1.11.0": - patch_string: mock patch data patch_type: string """) client = TestClient(light=True) client.save({'conanfile.py': conanfile, 'conandata.yml': conandata_yml}) client.run('create .') # Note: This cannot exist anymore, because the path is moved when prev is computed # assert os.path.exists(mock_patch_ng.apply_args[0]) assert mock_patch_ng.apply_args[1:] == (0, False) assert 'mock patch data' == mock_patch_ng.string.decode('utf-8') assert 'mypkg/1.11.0: Apply patch (string)' in str(client.out) def test_relate_base_path_all_versions(mock_patch_ng): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import apply_conandata_patches class Pkg(ConanFile): name = "mypkg" version = "1.0" def layout(self): self.folders.source = "source_subfolder" def build(self): apply_conandata_patches(self) """) conandata_yml = textwrap.dedent(""" patches: - patch_file: "patches/0001-buildflatbuffers-cmake.patch" base_path: "relative_dir" """) client = TestClient(light=True) client.save({'conanfile.py': conanfile, 'conandata.yml': conandata_yml}) client.run('create .') assert mock_patch_ng.apply_args[0].endswith(os.path.join('source_subfolder', "relative_dir")) assert mock_patch_ng.apply_args[1:] == (0, False) def test_export_conandata_patches(): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import export_conandata_patches, load class Pkg(ConanFile): name = "mypkg" version = "1.0" def layout(self): self.folders.source = "source_subfolder" def export_sources(self): export_conandata_patches(self) def source(self): self.output.info(load(self, os.path.join(self.export_sources_folder, "patches/mypatch.patch"))) """) conandata_yml = textwrap.dedent(""" patches: - patch_file: "patches/mypatch.patch" """) client = TestClient(light=True) client.save({"conanfile.py": conanfile}) client.run("create .", assert_error=True) assert "conandata.yml not defined" in client.out # Empty conandata client.save({"conandata.yml": ""}) client.run("create .", assert_error=True) assert "export_conandata_patches(): No patches defined in conandata" in client.out assert "ERROR: mypkg/1.0: Error in source() method" in client.out # wrong patches client.save({"conandata.yml": "patches: 123"}) client.run("create .", assert_error=True) assert "conandata.yml 'patches' should be a list or a dict" in client.out # No patch found client.save({"conandata.yml": conandata_yml}) client.run("create .", assert_error=True) assert "Patch file does not exist: '" in client.out client.save({"patches/mypatch.patch": "mypatch!!!"}) client.run("create .") assert "mypkg/1.0: mypatch!!!" in client.out conandata_yml = textwrap.dedent(""" patches: "1.0": - patch_file: "patches/mypatch.patch" """) client.save({"conandata.yml": conandata_yml}) client.run("create .") assert "mypkg/1.0: mypatch!!!" in client.out def test_export_conandata_patches_no_patches(): # Patch exists but has no contents, this used to hard crash client = TestClient(light=True) conandata_yml = textwrap.dedent(""" patches: "1.0": # - patch_file: "patches/mypatch.patch" """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import export_conandata_patches, apply_conandata_patches class Pkg(ConanFile): name = "mypkg" version = "1.0" def export_sources(self): export_conandata_patches(self) def build(self): apply_conandata_patches(self) """) client.save({"conandata.yml": conandata_yml, "conanfile.py": conanfile}) client.run("create .") assert "No patches defined for version 1.0 in conandata.yml" in client.out @pytest.mark.parametrize("trim", [True, False]) def test_export_conandata_patches_extra_origin(trim): conanfile = textwrap.dedent(f""" import os from conan import ConanFile from conan.tools.files import export_conandata_patches, load, trim_conandata class Pkg(ConanFile): name = "mypkg" version = "1.0" def export(self): if {trim}: trim_conandata(self) def layout(self): self.folders.source = "source_subfolder" def export_sources(self): export_conandata_patches(self) def source(self): self.output.info(load(self, os.path.join(self.export_sources_folder, "patches/mypatch.patch"))) """) client = TestClient(light=True) patches_folder = temp_folder() conandata_yml = textwrap.dedent(""" patches: "1.0": - patch_file: "patches/mypatch.patch" """) save(os.path.join(patches_folder, "mypkg", "conandata.yml"), conandata_yml) save(os.path.join(patches_folder, "mypkg", "patches", "mypatch.patch"), "mypatch!!!") pkg_conandata = textwrap.dedent("""\ patches: "1.1": - patch_file: "patches/mypatch2.patch" """) client.save({"conanfile.py": conanfile, "conandata.yml": pkg_conandata, "patches/mypatch2.patch": ""}) client.run(f'create . -cc core.sources.patch:extra_path="{patches_folder}"') assert "mypkg/1.0: Applying extra patches" in client.out assert "mypkg/1.0: mypatch!!!" in client.out conandata = load(client.exported_layout().conandata()) assert "1.0" in conandata assert "patch_file: patches/mypatch.patch" in conandata if trim: assert "1.1" not in conandata else: assert "1.1" in conandata ================================================ FILE: test/functional/tools_versions_test.py ================================================ import platform import textwrap import pytest from conan.test.assets.sources import gen_function_cpp from test.conftest import tools_locations from conan.test.utils.tools import TestClient class TestToolsCustomVersions: @pytest.mark.tool("cmake") def test_default_cmake(self): client = TestClient() client.run_command('cmake --version') default_cmake_version = tools_locations["cmake"]["default"] assert "cmake version {}".format(default_cmake_version) in client.out @pytest.mark.tool("cmake", "3.19") def test_custom_cmake_3_19(self): client = TestClient() client.run_command('cmake --version') assert "cmake version 3.19" in client.out @pytest.mark.tool("mingw64") @pytest.mark.tool("cmake", "3.19") @pytest.mark.skipif(platform.system() != "Windows", reason="Mingw test") def test_custom_cmake_mingw64(self): client = TestClient() client.run_command('cmake --version') assert "cmake version 3.19" in client.out main = gen_function_cpp(name="main") cmakelist = textwrap.dedent(""" set(CMAKE_CXX_COMPILER_WORKS 1) set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(App CXX) add_executable(app app.cpp) """) client.save({"CMakeLists.txt": cmakelist, "app.cpp": main}) client.run_command('cmake . -G "MinGW Makefiles"') client.run_command("cmake --build .") ================================================ FILE: test/functional/util/__init__.py ================================================ ================================================ FILE: test/functional/util/test_cmd_args_to_string.py ================================================ import os import platform import textwrap import pytest from conan.tools.build import cmd_args_to_string from conan.test.utils.tools import TestClient from conan.internal.util.files import chdir from conan.internal.util.runners import detect_runner @pytest.fixture(scope="module") def application_folder(): """ This is building a simple C app to print the received arguments and see if the obtained value is the expected once it is escaped by the Conan cmd_args_to_string tool """ t = TestClient() main = textwrap.dedent(""" #include int main(int argc, char *argv[]){ int i; for(i=1;i cmake client.save({"conanfile.txt": "[tool_requires]\nmycmake/1.0"}, clean_first=True) client.run("install . -s:h build_type=Debug") assert "mycmake/1.0" in client.out assert "openssl/1.0" in client.out ext = "bat" if platform.system() == "Windows" else "sh" # TODO: Decide on logic .bat vs .sh cmd = environment_wrap_command(ConanFileMock(), "conanbuild", client.current_folder, "mycmake.{}".format(ext)) client.run_command(cmd) assert "MYCMAKE=Release!!" in client.out assert "MYOPENSSL=Release!!" in client.out def test_complete(client): app = textwrap.dedent(""" import platform from conan import ConanFile class Pkg(ConanFile): requires = "openssl/1.0" build_requires = "mycmake/1.0" settings = "os" def build_requirements(self): self.test_requires("mygtest/1.0", run=True) def build(self): mybuild_cmd = "mycmake.bat" if platform.system() == "Windows" else "mycmake.sh" self.run(mybuild_cmd) mytest_cmd = "mygtest.bat" if platform.system() == "Windows" else "mygtest.sh" self.run(mytest_cmd, env="conanrun") """) client.save({"conanfile.py": app}) client.run("install . -s build_type=Debug --build=missing") # Run the BUILD environment ext = "bat" if platform.system() == "Windows" else "sh" # TODO: Decide on logic .bat vs .sh cmd = environment_wrap_command(ConanFileMock(), "conanbuild", client.current_folder, cmd="mycmake.{}".format(ext)) client.run_command(cmd) assert "MYCMAKE=Release!!" in client.out assert "MYOPENSSL=Release!!" in client.out # Run the RUN environment cmd = environment_wrap_command(ConanFileMock(), "conanrun", client.current_folder, cmd="mygtest.{ext} && .{sep}myrunner.{ext}".format(ext=ext, sep=os.sep)) client.run_command(cmd) assert "MYGTEST=Debug!!" in client.out assert "MYGTESTVAR=MyGTestValueDebug!!" in client.out client.run("build . -s:h build_type=Debug") assert "MYCMAKE=Release!!" in client.out assert "MYOPENSSL=Release!!" in client.out assert "MYGTEST=Debug!!" in client.out def test_dependents_new_buildenv(): client = TestClient() boost = textwrap.dedent(""" from conan import ConanFile class Boost(ConanFile): def package_info(self): self.buildenv_info.define_path("PATH", "myboostpath") """) other = textwrap.dedent(""" from conan import ConanFile class Other(ConanFile): def requirements(self): self.requires("boost/1.0") def package_info(self): self.buildenv_info.append_path("PATH", "myotherpath") self.buildenv_info.prepend_path("PATH", "myotherprepend") """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.env import VirtualBuildEnv import os class Lib(ConanFile): requires = {} def generate(self): build_env = VirtualBuildEnv(self).vars() with build_env.apply(): self.output.info("LIB PATH %s" % os.getenv("PATH")) """) client.save({"boost/conanfile.py": boost, "other/conanfile.py": other, "consumer/conanfile.py": consumer.format('"boost/1.0", "other/1.0"'), "profile_define": "[buildenv]\nPATH=(path)profilepath", "profile_append": "[buildenv]\nPATH+=(path)profilepath", "profile_prepend": "[buildenv]\nPATH=+(path)profilepath"}) client.run("create boost --name=boost --version=1.0") client.run("create other --name=other --version=1.0") client.run("install consumer") result = os.pathsep.join(["myotherprepend", "myboostpath", "myotherpath"]) assert "LIB PATH {}".format(result) in client.out # Now test if we declare in different order, still topological order should be respected client.save({"consumer/conanfile.py": consumer.format('"other/1.0", "boost/1.0"')}) client.run("install consumer") assert "LIB PATH {}".format(result) in client.out client.run("install consumer -pr=profile_define") assert "LIB PATH profilepath" in client.out client.run("install consumer -pr=profile_append") result = os.pathsep.join(["myotherprepend", "myboostpath", "myotherpath", "profilepath"]) assert "LIB PATH {}".format(result) in client.out client.run("install consumer -pr=profile_prepend") result = os.pathsep.join(["profilepath", "myotherprepend", "myboostpath", "myotherpath"]) assert "LIB PATH {}".format(result) in client.out def test_tool_requires_conanfile_txt(): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) build_req = textwrap.dedent(""" from conan import ConanFile class BuildReqConan(ConanFile): pass """) client.save({"conanfile.py": build_req}) client.run("export . --name=build_req --version=1.0 --user=test --channel=test") consumer = textwrap.dedent(""" [tool_requires] build_req/1.0@test/test """) client.save({"conanfile.txt": consumer}, clean_first=True) client.run("install . --build=missing") assert "build_req/1.0@test/test: Created package" in client.out def test_profile_override_conflict(): client = TestClient(light=True) test = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def requirements(self): self.tool_requires(self.tested_reference_str) def test(self): pass """) client.save({"conanfile.py": GenConanfile("protoc"), "test_package/conanfile.py": test, "profile": "[tool_requires]\nprotoc/0.1"}) client.run("create . --version 0.1 -pr=profile") client.run("create . --version 0.2 -pr=profile") assert "protoc/0.1: Already installed!" in client.out assert "protoc/0.2 (test package)" in client.out assert "WARN: The package created was 'protoc/0.1' but the reference being tested " \ "is 'protoc/0.2'" in client.out def test_both_context_options_error(): # https://github.com/conan-io/conan/issues/11385 c = TestClient() pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "arch" options = {"neon": [True, "check", False]} default_options = {"neon": True} def config_options(self): if "arm" not in self.settings.arch: del self.options.neon """) c.save({"pkg/conanfile.py": pkg, "consumer/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_build_requires("pkg/0.1")}) c.run("export pkg") c.run("install consumer -s:b arch=x86_64 -s:h arch=armv8 --build=missing") # This failed in Conan 1.X, but now it works c.assert_listed_binary({"pkg/0.1": ("a0a41a189feabff576a535d071858191b90beceb", "Build")}) c.assert_listed_binary({"pkg/0.1": ("62e589af96a19807968167026d906e63ed4de1f5", "Build")}, build=True) assert "Finalizing install" in c.out def test_conditional_require_context(): """ test that we can condition on the context to define a dependency """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def requirements(self): if self.context == "host": self.requires("dep/1.0") """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "consumer/conanfile.py": pkg}) c.run("create dep") c.run("create consumer") c.assert_listed_require({"dep/1.0": "Cache"}) c.run("create consumer --build-require") assert "dep/1.0" not in c.out class TestBuildTrackHost: def test_overriden_host_but_not_build(self): """ Making the ``tool_requires(..., visible=True)`` works, and allows overriding, but propagates the build-requirement to protobuf/protoc down the graph, and VirtualBuildEnv will put ``protoc`` from it in the PATH. Not a problem in majority of cases, but not the cleanest """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class ProtoBuf(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("protobuf/1.0") def build_requirements(self): self.tool_requires("protobuf/1.0", visible=True) """) c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), "pkg/conanfile.py": pkg, "app/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_requirement("protobuf/1.1", override=True) .with_build_requirement("protobuf/1.1", override=True)}) c.run("create protobuf --version=1.0") c.run("create protobuf --version=1.1") c.run("create pkg") c.run("install app") c.assert_listed_require({"protobuf/1.1": "Cache"}) c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) def test_overriden_host_version(self): """ Make the tool_requires follow the regular require with the expression "" """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class ProtoBuf(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("protobuf/1.0") def build_requirements(self): self.tool_requires("protobuf/") """) c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), "pkg/conanfile.py": pkg, "app/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_requirement("protobuf/1.1", override=True)}) c.run("create protobuf --version=1.0") c.run("create protobuf --version=1.1") c.run("create pkg") c.run("install pkg") # make sure it doesn't crash c.run("install app") c.assert_listed_require({"protobuf/1.1": "Cache"}) c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) # verify locks work c.run("lock create app") lock = json.loads(c.load("app/conan.lock")) build_requires = lock["build_requires"] assert len(build_requires) == 1 assert "protobuf/1.1" in build_requires[0] # lock can be used c.run("install app --lockfile=app/conan.lock") c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) def test_overriden_host_version_version_range(self): """ same as above, but using version ranges instead of overrides """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class ProtoBuf(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("protobuf/[*]") def build_requirements(self): self.tool_requires("protobuf/") """) c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), "pkg/conanfile.py": pkg, "app/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) c.run("create protobuf --version=1.0") c.run("create pkg") c.run("install pkg") # make sure it doesn't crash c.run("install app") c.assert_listed_require({"protobuf/1.0": "Cache"}) c.assert_listed_require({"protobuf/1.0": "Cache"}, build=True) c.run("create protobuf --version=1.1") c.run("install pkg") # make sure it doesn't crash c.run("install app") c.assert_listed_require({"protobuf/1.1": "Cache"}) c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) # verify locks work c.run("lock create app") lock = json.loads(c.load("app/conan.lock")) build_requires = lock["build_requires"] assert len(build_requires) == 1 assert "protobuf/1.1" in build_requires[0] # lock can be used c.run("install app --lockfile=app/conan.lock") c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) def test_track_host_error_nothost(self): """ if no host requirement is defined, it will be an error """ c = TestClient(light=True) c.save({"conanfile.py": GenConanfile().with_build_requirement("protobuf/")}) c.run("install .", assert_error=True) assert "ERROR: require 'protobuf/': " \ "didn't find a matching host dependency" in c.out def test_track_host_errors_trait(self): """ It is not possible to make host_version visible too """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class ProtoBuf(ConanFile): name = "protobuf" def requirements(self): self.tool_requires("other/", visible=True) """) c.save({"pkg/conanfile.py": pkg}) c.run("install pkg", assert_error=True) assert "ERROR: protobuf/None require 'other/': 'host_version' " \ "can only be used for non-visible tool_requires" in c.out def test_track_host_error_wrong_context(self): """ it can only be used by tool_requires, not regular requires """ c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg").with_requirement("protobuf/")}) c.run(f"install .", assert_error=True) assert " 'host_version' can only be used for non-visible tool_requires" in c.out def test_host_version_test_package(self): """ https://github.com/conan-io/conan/issues/14704 """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class ProtoBuf(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("protobuf/[>=1.0]") def build_requirements(self): self.tool_requires("protobuf/") """) # regular requires test_package c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), "pkg/conanfile.py": pkg, "pkg/test_package/conanfile.py": GenConanfile().with_test("pass")}) c.run("create protobuf --version=1.0") c.run(f"create pkg") # works without problem test = textwrap.dedent(""" from conan import ConanFile class Test(ConanFile): def build_requirements(self): self.tool_requires(self.tested_reference_str) def test(self): pass """) c.save({"pkg/test_package/conanfile.py": test}) c.run("create protobuf --version=1.0") # This used to fail c.run(f"create pkg") c.assert_listed_binary({"protobuf/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}) c.assert_listed_binary({"protobuf/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}, build=True) def test_overriden_host_version_transitive_deps(self): """ Make the tool_requires follow the regular require with the expression "" for a transitive_deps """ c = TestClient(light=True) c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requirement("protobuf/[>=1.0]"), "app/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_tool_requirement("protobuf/")}) c.run("create protobuf --version=1.0") c.run("create protobuf --version=1.1") c.run("create pkg") c.run("install pkg") # make sure it doesn't crash c.run("install app") c.assert_listed_require({"protobuf/1.1": "Cache"}) c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) # verify locks work c.run("lock create app") lock = json.loads(c.load("app/conan.lock")) build_requires = lock["build_requires"] assert len(build_requires) == 1 assert "protobuf/1.1" in build_requires[0] # lock can be used c.run("install app --lockfile=app/conan.lock") c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) @pytest.mark.parametrize("host_version, assert_error, assert_msg", [ ("libgettext>", False, "gettext/0.2#d9f9eaeac9b6e403b271f04e04149df2"), # Error cases, just checking that we fail gracefully - no tracebacks ("libgettext", True, "Package 'gettext/", True, "app/1.0 require ':/': didn't find a matching host dependency"), (">", True, "app/1.0 require '/': didn't find a matching host dependency"), (":", True, " Package 'gettext/0.1]") .with_tool_requirement(f"gettext/" """ c = TestClient(light=True) user_channel_reference = f"@{requires_tag}" if requires_tag else "" pkg = textwrap.dedent(f""" from conan import ConanFile class ProtoBuf(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("protobuf/1.0{user_channel_reference}") def build_requirements(self): self.tool_requires("protobuf/@{tool_requires_tag}") """) c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), "pkg/conanfile.py": pkg}) for tag in (requires_tag, tool_requires_tag): if "/" in tag: user, channel = tag.split("/", 1) user_channel = f"--user={user} --channel={channel}" else: user_channel = "" c.run(f"create protobuf --version=1.0 {user_channel}") c.run("create pkg") expected_tool_requires_tag = f"@{tool_requires_tag}" if tool_requires_tag else "" c.assert_listed_require({f"protobuf/1.0{user_channel_reference}": "Cache"}) c.assert_listed_require({f"protobuf/1.0{expected_tool_requires_tag}": "Cache"}, build=True) @pytest.mark.parametrize("shared", [True, False]) def test_host_version_transitive_contexts(self, shared): # app ---------------------------------------> protobuf (shared) # \---tool-require-> grpc/ (shared) -> protobuf (shared) # \--tool-require-------(host)----------------/ tc = TestClient(light=True) tc.save({"grpc/conanfile.py": GenConanfile("grpc", "0.1").with_shared_option(shared) .with_requirement("protobuf/0.1"), "protobuf/conanfile.py": GenConanfile("protobuf", "0.1") .with_shared_option(shared), "conanfile.py": GenConanfile("app", "0.1").with_requires("protobuf/[*]") .with_tool_requirement("grpc/[*]") .with_tool_requirement("protobuf/") }) tc.run("export protobuf") tc.run("export grpc") tc.run("graph info .") assert "Conflict between" not in tc.out @pytest.mark.parametrize("shared", [True, False]) def test_host_version_transitive_contexts2(self, shared): # app -> grpc (shared) -> protobuf (shared) # \-----------------------/ # \---tool-require-> grpc/ (shared) -> protobuf (shared) # \--tool-require-------(host)----------------/ tc = TestClient(light=True) tc.save({"grpc/conanfile.py": GenConanfile("grpc", "0.1").with_shared_option(shared) .with_requirement("protobuf/0.1"), "protobuf/conanfile.py": GenConanfile("protobuf", "0.1") .with_shared_option(shared), "conanfile.py": GenConanfile("app", "0.1").with_requires("grpc/0.1") .with_requires("protobuf/[*]") .with_tool_requirement("grpc/") .with_tool_requirement("protobuf/") }) tc.run("export protobuf") tc.run("export grpc") tc.run("graph info .") assert "Conflict between" not in tc.out def test_host_version_transitive_contexts_orphan(self): tc = TestClient(light=True) tc.save({"grpc/conanfile.py": GenConanfile("grpc", "0.1") .with_requirement("protobuf/0.1").with_shared_option(False), "protobuf/conanfile.py": GenConanfile("protobuf", "0.1").with_shared_option(False), "conanfile.py": GenConanfile("app", "0.1").with_requires("grpc/0.1") .with_requires("protobuf/[*]") .with_tool_requirement("protobuf/") .with_tool_requirement("grpc/")}) tc.run("export protobuf") tc.run("export grpc") tc.run("graph info . -o:a=*:shared=True -f=json", redirect_stdout="graph.json") data = json.loads(tc.load("graph.json")) def _assert_no_orphan(deps_graph): ids = set(deps_graph["nodes"].keys()) seen = set(deps_graph["root"].keys()) for node in deps_graph["nodes"].values(): seen.update(node["dependencies"].keys()) assert not ids - seen, f"Orphan nodes found: {ids - seen}" _assert_no_orphan(data["graph"]) def test_user_channel_error(self): lib = textwrap.dedent(""" from conan import ConanFile class LibWithToolConan(ConanFile): name = "lib_with_tool" package_type = "static-library" settings = "os", "arch" # {} """) c = TestClient(default_server_user=True) c.save({"conanfile.py": lib.format(1)}) c.run("create . --version 1.0.0 --user foobar") c.save({"conanfile.py": lib.format(2)}) c.run("create . --version 1.1.0 --user foobar") rev1 = c.exported_recipe_revision() c.save({"conanfile.py": lib.format(3)}) c.run("create . --version 1.1.0 --user foobar") rev2 = c.exported_recipe_revision() assert rev1 != rev2 c.run(f"upload lib_with_tool/1.1.0@foobar#{rev2} -r=default") c.run(f"remove lib_with_tool/1.1.0@foobar#{rev2} -c") dep = textwrap.dedent(""" from conan import ConanFile class DepLibConan(ConanFile): name = "dep_lib" package_type = "static-library" settings = "os", "arch" def build_requirements(self): self.tool_requires("lib_with_tool/@foobar") def requirements(self): self.requires("lib_with_tool/1.1.0@foobar") """) c.save({"conanfile.py": dep}, clean_first=True) c.run("create . --version=1.0.0 --user=foobar") conanfile = textwrap.dedent(f""" [requires] dep_lib/1.0.0@foobar lib_with_tool/1.1.0@foobar#{rev2} """) c.save({"conanfile.txt": conanfile}, clean_first=True) c.run("install .") # No longer produces a conflict def test_build_missing_build_requires(): c = TestClient(light=True) c.save({"tooldep/conanfile.py": GenConanfile("tooldep", "0.1"), "tool/conanfile.py": GenConanfile("tool", "0.1").with_tool_requires("tooldep/0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_tool_requires("tool/0.1"), "app/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) c.run("create tooldep") c.run("create tool") c.run("create pkg") c.run("remove tool*:* -c") c.run("install app") assert "- Build" not in c.out assert re.search(r"Skipped binaries(\s*)tool/0.1, tooldep/0.1", c.out) c.run("install app --build=missing") assert "- Build" not in c.out assert re.search(r"Skipped binaries(\s*)tool/0.1, tooldep/0.1", c.out) def test_requirement_in_wrong_method(): tc = TestClient(light=True) tc.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def configure(self): self.requires("foo/1.0") """)}) tc.run('create . -cc="core:warnings_as_errors=[\'*\']"', assert_error=True) assert ("ERROR: deprecated: Requirements should only be added in the requirements()/" "build_requirements() methods, not configure()/config_options(), which might " "raise errors in the future.") in tc.out def test_transitive_build_scripts_error(): # https://github.com/conan-io/conan/issues/18235 c = TestClient(light=True) meta = textwrap.dedent(""" from conan import ConanFile class Meta(ConanFile): name = "meta" version = "0.1" package_type = "build-scripts" def requirements(self): self.requires("dep/0.1") """) product = textwrap.dedent(""" from conan import ConanFile class Product(ConanFile): name = "product" version = "0.1" package_type = "build-scripts" def requirements(self): self.requires("meta/0.1") """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "meta/conanfile.py": meta, "product/conanfile.py": product}) c.run("create dep") c.run("create meta") c.run("install product") # It doesn't crash def test_transitive_build_scripts_library_error(): # https://github.com/conan-io/conan/issues/18856 c = TestClient(light=True) tool = textwrap.dedent(""" from conan import ConanFile class Product(ConanFile): name = "tool" version = "0.1" package_type = "build-scripts" def requirements(self): self.requires("dep/0.1") """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_package_type("static-library"), "tool/conanfile.py": tool}) c.run("create dep") c.run("create tool") # Tool-requires, not an issue c.run("install --tool-requires=tool/0.1") # requires, it crashed with traceback, now a clear error message c.run("install --requires=tool/0.1", assert_error=True) assert ("ERROR: Package 'tool/0.1' with type 'build-scripts' cannot have " "a 'static-library' dependency to 'dep/0.1'") in c.out ================================================ FILE: test/integration/build_requires/profile_build_requires_test.py ================================================ import os import platform import textwrap import pytest from conan.internal.paths import CONANFILE from conan.test.utils.tools import TestClient, GenConanfile class TestBuildRequires: @pytest.fixture() def client(self): c = TestClient() tool_conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class tool(ConanFile): name = "tool" version = "0.1" exports_sources = "mytool*" def package(self): copy(self, "mytool*", self.source_folder, self.package_folder) def package_info(self): self.buildenv_info.append_path("PATH", self.package_folder) """) name = "mytool.bat" if platform.system() == "Windows" else "mytool" c.save({CONANFILE: tool_conanfile, name: "echo Hello World!"}, clean_first=True) os.chmod(os.path.join(c.current_folder, name), 0o777) c.run("export . --user=lasote --channel=stable") lib_conanfile = textwrap.dedent(""" from conan import ConanFile class mylib(ConanFile): name = "mylib" version = "0.1" def build(self): self.run("mytool") """) profile = """ [tool_requires] tool/0.1@lasote/stable nonexistingpattern*: sometool/1.2@user/channel """ profile2 = """ [tool_requires] tool/0.1@lasote/stable nonexistingpattern*: sometool/1.2@user/channel """ test_conanfile = textwrap.dedent(""" from conan import ConanFile class Testmylib(ConanFile): def build(self): self.run("mytool") def test(self): pass """) c.save({CONANFILE: lib_conanfile, "test_package/conanfile.py": test_conanfile, "profile.txt": profile, "profile2.txt": profile2}, clean_first=True) return c def test_profile_requires(self, client): """ cli -(tool-requires)-> tool/0.1 \\--(requires)->mylib/0.1 -(tool_requires)->tool/0.1 (skipped) """ client.run("export . --user=lasote --channel=stable") client.run("install --requires=mylib/0.1@lasote/stable " "--profile ./profile.txt --build missing") assert "Hello World!" in client.out client.run("install --requires=mylib/0.1@lasote/stable --profile ./profile2.txt --build='*'") assert "Hello World!" in client.out def test_profile_open_requires(self, client): client.run("build . --profile ./profile.txt --build missing") assert "Hello World!" in client.out def test_build_mode_requires(self, client): client.run("install . --profile ./profile.txt", assert_error=True) assert "ERROR: Missing prebuilt package for 'tool/0.1@lasote/stable'" in client.out client.run("install . --profile ./profile.txt --build=Pythontool", assert_error=True) assert "ERROR: Missing prebuilt package for 'tool/0.1@lasote/stable'" in client.out client.run("install . --profile ./profile.txt --build=tool/0.1*") assert "tool/0.1@lasote/stable: Created package" in client.out # now remove packages, ensure --build=missing also creates them client.run('remove "*:*" -c') client.run("install . --profile ./profile.txt --build=missing") assert "tool/0.1@lasote/stable: Created package" in client.out def test_profile_test_requires(self, client): client.run("create . --profile ./profile.txt --build missing") assert 2 == str(client.out).splitlines().count("Hello World!") def test_consumer_patterns(self, client): profile_patterns = """ [tool_requires] &: tool/0.1@lasote/stable nonexistingpattern*: sometool/1.2@user/channel """ client.save({CONANFILE: GenConanfile("mylib", "0.1"), "profile.txt": profile_patterns}) client.run("create . --profile=./profile.txt --build=missing") assert 1 == str(client.out).splitlines().count("Hello World!") def test_build_requires_options(self): client = TestClient() client.save({CONANFILE: GenConanfile("mytool", "0.1")}) client.run("export . --user=lasote --channel=stable") conanfile = textwrap.dedent(""" from conan import ConanFile class mylib(ConanFile): name = "mylib" version = "0.1" build_requires = "mytool/0.1@lasote/stable" options = {"coverage": [True, False]} def build(self): self.output.info("Coverage %s" % self.options.coverage) """) client.save({CONANFILE: conanfile}, clean_first=True) client.run("build . -o mylib*:coverage=True --build missing") client.assert_listed_require({"mytool/0.1@lasote/stable": "Cache"}, build=True) assert "conanfile.py (mylib/0.1): Coverage True" in client.out client.save({CONANFILE: conanfile}, clean_first=True) client.run("build . -o coverage=True") client.assert_listed_require({"mytool/0.1@lasote/stable": "Cache"}, build=True) assert "mytool/0.1@lasote/stable: Already installed!" in client.out assert "conanfile.py (mylib/0.1): Coverage True" in client.out def test_consumer_patterns_loop_error(): client = TestClient() profile_patterns = textwrap.dedent(""" include(default) [tool_requires] tool1/1.0 tool2/1.0 """) client.save({"tool1/conanfile.py": GenConanfile(), "tool2/conanfile.py": GenConanfile().with_build_requires("tool1/1.0"), "consumer/conanfile.py": GenConanfile(), "profile.txt": profile_patterns}) client.run("export tool1 --name=tool1 --version=1.0") client.run("export tool2 --name=tool2 --version=1.0") client.run("install consumer --build=missing -pr:b=profile.txt -pr:h=profile.txt", assert_error=True) assert "There is a cycle/loop in the graph" in client.out # we can fix it with the negation profile_patterns = textwrap.dedent(""" include(default) [tool_requires] tool1/1.0 !tool1*:tool2/1.0 """) client.save({"profile.txt": profile_patterns}) client.run("install consumer --build=missing -pr:b=profile.txt -pr:h=profile.txt") assert "tool1/1.0: Created package" in client.out assert "tool2/1.0: Created package" in client.out def test_tool_requires_revision_profile(): # We shoul be able to explicitly [tool_require] a recipe revision in the profile c = TestClient() build_profile = textwrap.dedent("""\ [settings] os=Linux [tool_requires] *:tool/0.1#2d65f1b4af1ce59028f96adbfe7ed5a2 """) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "cmake/conanfile.py": GenConanfile("cmake", "0.1"), "app/conanfile.py": GenConanfile("app", "0.1").with_tool_requires("cmake/0.1"), "build_profile": build_profile}) c.run("export tool") rev1 = c.exported_recipe_revision() assert rev1 == "2d65f1b4af1ce59028f96adbfe7ed5a2" # Create a new tool revision to proof that we can still require the old one c.save({"tool/conanfile.py": GenConanfile("tool", "0.1").with_class_attribute("myvar=42")}) c.run("export tool") rev2 = c.exported_recipe_revision() assert rev2 != rev1 c.run("export cmake") c.run("graph info app -pr:b=build_profile --build=*") assert f"tool/0.1#{rev1}" in c.out assert rev2 not in c.out def test_tool_requires_version_range_loop(): # https://github.com/conan-io/conan/issues/17930 c = TestClient(light=True) build_profile = textwrap.dedent("""\ [settings] os=Linux [tool_requires] tool/[>=1.0 <2] """) c.save({"tool/conanfile.py": GenConanfile("tool", "1.1"), "app/conanfile.py": GenConanfile("app", "0.1").with_tool_requires("tool/1.1"), "build_profile": build_profile}) c.run("create tool") c.run("install app -pr:b=build_profile") assert "tool/1.1" in c.out # It is skipped ================================================ FILE: test/integration/build_requires/test_build_requires_source_method.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient class TestBuildEnvSource: @pytest.fixture() def client(self): c = TestClient() tool = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import chdir, save class Tool(ConanFile): name = "tool" version = "0.1" def package(self): with chdir(self, self.package_folder): echo = f"@echo off\necho MY-TOOL! {self.name}/{self.version}!!" save(self, "bin/mytool.bat", echo) save(self, "bin/mytool.sh", echo) os.chmod("bin/mytool.sh", 0o777) """) c.save({"conanfile.py": tool}) c.run("create .") return c def test_source_buildenv(self, client): c = client pkg = textwrap.dedent(""" from conan import ConanFile import platform class Pkg(ConanFile): name = "pkg" version = "0.1" tool_requires = "tool/0.1" source_buildenv = True def source(self): cmd = "mytool.bat" if platform.system() == "Windows" else "mytool.sh" self.run(cmd) """) c.save({"conanfile.py": pkg}) c.run("create .") assert "MY-TOOL! tool/0.1" in c.out c.run("install .") # to generate conanbuild script first, so it is available c.run("source .") assert "MY-TOOL! tool/0.1" in c.out def test_source_buildenv_layout(self, client): c = client pkg = textwrap.dedent(""" from conan import ConanFile import platform class Pkg(ConanFile): name = "pkg" version = "0.1" tool_requires = "tool/0.1" settings = "build_type" source_buildenv = True def layout(self): self.folders.source = "mysrc" bt = self.settings.get_safe("build_type") or "Release" self.folders.generators = f"mybuild{bt}" def source(self): cmd = "mytool.bat" if platform.system() == "Windows" else "mytool.sh" self.run(cmd) """) c.save({"conanfile.py": pkg}) c.run("create .") assert "MY-TOOL! tool/0.1" in c.out c.run("install .") # to generate conanbuild script first, so it is available # But they are in a different folder, user can copy them to source folder to make # them available to source() method. This works # shutil.copytree(os.path.join(c.current_folder, "mybuild"), # os.path.join(c.current_folder, "mysrc")) # Another possibility is user directly calling the "conanbuild" script to activate # The current solution defines "generators" to be robust for "conan source" command # defaulting to "Release" config c.run("source .") assert "MY-TOOL! tool/0.1" in c.out def test_source_buildenv_cmake_layout(self, client): c = client pkg = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout import platform class Pkg(ConanFile): name = "pkg" version = "0.1" tool_requires = "tool/0.1" settings = "build_type" source_buildenv = True def layout(self): cmake_layout(self) bt = self.settings.get_safe("build_type") or "Release" self.folders.generators = os.path.join(bt, "generators") def source(self): cmd = "mytool.bat" if platform.system() == "Windows" else "mytool.sh" self.run(cmd) """) c.save({"conanfile.py": pkg}) c.run("create .") assert "MY-TOOL! tool/0.1" in c.out c.run("install .") c.run("source .") assert "MY-TOOL! tool/0.1" in c.out def test_source_buildenv_default_fail(self, client): c = client pkg = textwrap.dedent(""" from conan import ConanFile import platform class Pkg(ConanFile): name = "pkg" version = "0.1" tool_requires = "tool/0.1" def source(self): cmd = "mytool.bat" if platform.system() == "Windows" else "mytool.sh" self.run(cmd) """) c.save({"conanfile.py": pkg}) c.run("create .", assert_error=True) assert "ERROR: pkg/0.1: Error in source() method, line 12" in c.out # Local will still work, because ``install`` generates env-scripts and no layout c.run("install .") c.run("source .") assert "MY-TOOL! tool/0.1" in c.out ================================================ FILE: test/integration/build_requires/test_install_test_build_require.py ================================================ import json import platform import textwrap import pytest from conan.test.utils.mocks import ConanFileMock from conan.tools.env.environment import environment_wrap_command from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def client(): openssl = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): settings = "os" package_type = "shared-library" def package(self): with chdir(self, self.package_folder): echo = "@echo off\necho MYOPENSSL={}!!".format(self.settings.os) save(self, "bin/myopenssl.bat", echo) save(self, "bin/myopenssl.sh", echo) os.chmod("bin/myopenssl.sh", 0o777) """) cmake = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): settings = "os" requires = "openssl/1.0" def package(self): with chdir(self, self.package_folder): echo = "@echo off\necho MYCMAKE={}!!".format(self.settings.os) save(self, "mycmake.bat", echo + "\ncall myopenssl.bat") save(self, "mycmake.sh", echo + "\n myopenssl.sh") os.chmod("mycmake.sh", 0o777) def package_info(self): self.buildenv_info.append_path("PATH", self.package_folder) """) client = TestClient() client.save({"tool/conanfile.py": GenConanfile(), "cmake/conanfile.py": cmake, "openssl/conanfile.py": openssl}) client.run("create tool --name=tool --version=1.0") client.run("create openssl --name=openssl --version=1.0") client.run("create cmake --name=mycmake --version=1.0") return client @pytest.mark.parametrize("build_profile", ["", "-pr:b=default"]) def test_build_require_test_package(build_profile, client): test_cmake = textwrap.dedent(r""" import os, platform, sys from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): self.tool_requires(self.tested_reference_str) def build(self): mybuild_cmd = "mycmake.bat" if platform.system() == "Windows" else "mycmake.sh" self.run(mybuild_cmd) def test(self): pass """) # Test with extra build_requires to check it doesn't interfere or get deleted client.save({"cmake/test_package/conanfile.py": test_cmake}) client.run("create cmake --name=mycmake --version=1.0 {} --build=missing".format(build_profile)) def check(out): system = {"Darwin": "Macos"}.get(platform.system(), platform.system()) assert "MYCMAKE={}!!".format(system) in out assert "MYOPENSSL={}!!".format(system) in out check(client.out) client.run("test cmake/test_package mycmake/1.0@ {}".format(build_profile)) check(client.out) def test_both_types(client): # When testing same package in both contexts, the 2 profiles approach must be used test_cmake = textwrap.dedent(r""" import os, platform from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): self.requires(self.tested_reference_str) self.build_requires(self.tested_reference_str) def build(self): mybuild_cmd = "mycmake.bat" if platform.system() == "Windows" else "mycmake.sh" self.run(mybuild_cmd) def test(self): pass """) # Test with extra build_requires to check it doesn't interfere or get deleted client.save({"cmake/test_package/conanfile.py": test_cmake}) # This must use the build-host contexts to have same dep in different contexts client.run("create cmake --name=mycmake --version=1.0 -pr:b=default --build=missing") def check(out): system = {"Darwin": "Macos"}.get(platform.system(), platform.system()) assert "MYCMAKE={}!!".format(system) in out assert "MYOPENSSL={}!!".format(system) in out check(client.out) client.run("test cmake/test_package mycmake/1.0@ -pr:b=default") check(client.out) def test_create_build_requires(): # test that I can create a package passing the build and host context and package will get both client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def package_info(self): self.output.info("MYOS=%s!!!" % self.settings.os) self.output.info("MYTARGET={}!!!".format(self.settings_target.os)) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=br --version=0.1 --build-require -s:h os=Linux -s:b os=Windows") client.assert_listed_binary({"br/0.1": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Build")}, build=True) assert "br/0.1: MYOS=Windows!!!" in client.out assert "br/0.1: MYTARGET=Linux!!!" in client.out assert "br/0.1: MYOS=Linux!!!" not in client.out def test_build_require_conanfile_text(client): client.save({"conanfile.txt": "[tool_requires]\nmycmake/1.0"}, clean_first=True) client.run("install . -g VirtualBuildEnv") ext = ".bat" if platform.system() == "Windows" else ".sh" cmd = environment_wrap_command(ConanFileMock(), "conanbuild", client.current_folder, f"mycmake{ext}") client.run_command(cmd) system = {"Darwin": "Macos"}.get(platform.system(), platform.system()) assert "MYCMAKE={}!!".format(system) in client.out assert "MYOPENSSL={}!!".format(system) in client.out def test_build_require_command_line_build_context(client): client.run("install --tool-requires=mycmake/1.0@ -g VirtualBuildEnv -pr:b=default") ext = ".bat" if platform.system() == "Windows" else ".sh" cmd = environment_wrap_command(ConanFileMock(), "conanbuild", client.current_folder, f"mycmake{ext}") client.run_command(cmd) system = {"Darwin": "Macos"}.get(platform.system(), platform.system()) assert "MYCMAKE={}!!".format(system) in client.out assert "MYOPENSSL={}!!".format(system) in client.out def test_install_multiple_tool_requires_cli(): c = TestClient() c.save({"conanfile.py": GenConanfile()}) c.run("create . --name=zlib --version=1.1") c.run("create . --name=cmake --version=0.1") c.run("create . --name=gcc --version=0.2") c.run("install --tool-requires=cmake/0.1 --tool-requires=gcc/0.2 --requires=zlib/1.1") c.assert_listed_require({"cmake/0.1": "Cache", "gcc/0.2": "Cache"}, build=True) c.assert_listed_require({"zlib/1.1": "Cache"}) def test_bootstrap_other_architecture(): """ this is the case of libraries as ICU, that needs itself for cross-compiling """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import cross_building class Pkg(ConanFile): name = "tool" version = "1.0" settings = "os" def build_requirements(self): if cross_building(self): self.tool_requires("tool/1.0") """) c.save({"conanfile.py": conanfile}) win_pkg_id = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" linux_pkg_id = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" c.run("create . -s:b os=Windows -s:h os=Windows") c.assert_listed_binary({"tool/1.0": (win_pkg_id, "Build")}) assert "Build requirements" not in c.out # This is smart and knows how to build only the missing one "host" but not "build" c.run("create . -s:b os=Windows -s:h os=Linux --build=missing:tool*") c.assert_listed_binary({"tool/1.0": (linux_pkg_id, "Build")}) c.assert_listed_binary({"tool/1.0": (win_pkg_id, "Cache")}, build=True) # This will rebuild all c.run("create . -s:b os=Windows -s:h os=Linux") c.assert_listed_binary({"tool/1.0": (linux_pkg_id, "Build")}) c.assert_listed_binary({"tool/1.0": (win_pkg_id, "Build")}, build=True) c.run("graph build-order --requires=tool/1.0 -s:b os=Windows -s:h os=Linux --build=* " "--format=json", redirect_stdout="o.json") order = json.loads(c.load("o.json")) package1 = order[0][0]["packages"][0][0] package2 = order[0][0]["packages"][1][0] assert package1["package_id"] == win_pkg_id assert package1["depends"] == [] assert package2["package_id"] == linux_pkg_id assert package2["depends"] == [win_pkg_id] def test_bootstrap_cc(): # https://github.com/conan-io/conan/issues/16758 cc = textwrap.dedent(""" from conan import ConanFile class CcConanfile(ConanFile): name = "cc" version = "1.0" options = {"bootstrap": [True, False]} default_options = {"bootstrap": True} def build_requirements(self): if self.options.bootstrap: self.tool_requires("cc/1.0", options={"*:bootstrap": False}) """) foo = GenConanfile("foo", "1.0").with_tool_requirement("cc/1.0") c = TestClient() c.save({"cc/conanfile.py": cc, "foo/conanfile.py": foo}) c.run("create cc --build-require") # Both are build-requires c.assert_listed_binary({"cc/1.0": ("826727aac60b5956d1df5121a3921f26a6984f15", "Build")}, build=True) c.assert_listed_binary({"cc/1.0": ("96ae1d965ce6f2e2256ac0cfc3a35dcd4860c389", "Build")}, build=True) # This works fine, being able to build the 2 different binaries both bootstrapped and not c.run("list cc/1.0:*") assert "bootstrap: False" in c.out assert "bootstrap: True" in c.out c.run("create foo") c.assert_listed_binary({"cc/1.0": ("96ae1d965ce6f2e2256ac0cfc3a35dcd4860c389", "Cache")}, build=True) ================================================ FILE: test/integration/build_requires/test_relocatable_toolchain.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_relocatable_toolchain(): """ Implements the following use case: - base/1.0 implements an SDK that needs to be relocated in every maching, but this package contains the non relocatable part and the relocation scripts - sdk/1.0 --tool_requires-> base/1.0 and implements just the relocation execution, copying whatever is necessary from base, and relocating it. It defines build_policy = "missing" and upload policy = "skip" https://github.com/conan-io/conan/issues/5059 """ c = TestClient(default_server_user=True) base = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "base" version = "1.0" settings = "arch" def build(self): save(self, "sdk.txt", f"arch:{self.settings.arch}=>{self.settings_target.arch}") def package(self): copy(self, "*", self.build_folder, self.package_folder) """) sdk = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load, copy, save class Pkg(ConanFile): name = "sdk" version = "1.0" settings = "arch" build_policy = "missing" upload_policy = "skip" def build_requirements(self): self.tool_requires("base/1.0") def package(self): # Whatever modification, customization, RPATHs, symlinks, etc pkg_folder = self.dependencies.build["base"].package_folder sdk = load(self, os.path.join(pkg_folder, "sdk.txt")) save(self, os.path.join(self.package_folder, "sdk.txt"), "CUSTOM PATH: " + sdk) def package_info(self): self.output.info(f"SDK INFO: {load(self, 'sdk.txt')}!!!") """) c.save({"linux": "[settings]\nos=Linux\narch=x86_64", "embedded": "[settings]\narch=armv8", "base/conanfile.py": base, "sdk/conanfile.py": sdk, "consumer/conanfile.py": GenConanfile("app", "1.0").with_tool_requires("sdk/1.0")}) c.run("create base -pr:h=embedded -pr:b=linux --build-require") c.run("export sdk") c.run("install consumer -pr:h=embedded -pr:b=linux") c.assert_listed_binary({"base/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Cache"), "sdk/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Build")}, build=True) assert "sdk/1.0: Calling package()" in c.out assert "sdk/1.0: SDK INFO: CUSTOM PATH: arch:x86_64=>armv8!!!" in c.out c.run("install consumer -pr:h=embedded -pr:b=linux -v") c.assert_listed_binary({"base/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Skip"), "sdk/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Cache")}, build=True) assert "sdk/1.0: Calling package()" not in c.out assert "sdk/1.0: SDK INFO: CUSTOM PATH: arch:x86_64=>armv8!!!" in c.out # If I upload everything and remove: c.run("upload * -r=default -c") c.run("remove * -c") c.run("install consumer -pr:h=embedded -pr:b=linux") c.assert_listed_binary( {"base/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Download (default)"), "sdk/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Build")}, build=True) assert "sdk/1.0: Calling package()" in c.out assert "sdk/1.0: SDK INFO: CUSTOM PATH: arch:x86_64=>armv8!!!" in c.out # we can even remove the binary! c.run("remove base/1.0:* -c") c.run("install consumer -pr:h=embedded -pr:b=linux -v") c.assert_listed_binary({"base/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Skip"), "sdk/1.0": ("62e589af96a19807968167026d906e63ed4de1f5", "Cache")}, build=True) assert "sdk/1.0: SDK INFO: CUSTOM PATH: arch:x86_64=>armv8!!!" in c.out ================================================ FILE: test/integration/build_requires/test_toolchain_packages.py ================================================ import json import textwrap from conan.api.model import PkgReference from conan.test.utils.tools import TestClient def test_android_ndk(): """ emulates the androidndk, a single package per OS-arch, that can target any android architecture (not especialized binary per target) """ c = TestClient() windows = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 """) linux = textwrap.dedent("""\ [settings] os=Linux arch=x86_64 """) android = textwrap.dedent("""\ [settings] os=Android os.api_level=14 arch = armv7 build_type = Release compiler=clang compiler.version=11 compiler.libcxx=c++_shared compiler.cppstd=14 [tool_requires] androidndk/0.1 """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "androidndk" version = "0.1" settings = "os", "arch" def build(self): save(self, "bin/ndk.compiler", f"MYNDK-{self.settings.os}-{self.settings.arch} exe!") def package(self): copy(self, "*", src=self.build_folder, dst=self.package_folder) def package_info(self): arch = self.settings_target.arch self.cpp_info.libs = [f"libndklib-{arch}"] self.buildenv_info.define("MY_ANDROID_ARCH", f"android-{arch}") """) test = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load from conan.tools.env import VirtualBuildEnv class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): ndk = self.dependencies.build["androidndk"] self.output.info(f"NDK LIBS: {ndk.cpp_info.libs}!!!") compiler = os.path.join(ndk.package_folder, "bin/ndk.compiler") self.output.info(load(self, compiler)) env = VirtualBuildEnv(self).vars() self.output.info(f"MY-VAR: {env.get('MY_ANDROID_ARCH')}") def test(self): pass """) c.save({"conanfile.py": conanfile, "windows": windows, "linux": linux, "android": android}) # IMPORTANT: The consumption via test_package define the relation. If not existing # I need to pass --build-require # Creating the NDK packages for Windows, Linux c.run("create . -pr:b=windows -pr:h=android --build-require") c.assert_listed_binary({"androidndk/0.1": ("522dcea5982a3f8a5b624c16477e47195da2f84f", "Build")}, build=True) # The same NDK can be used for different architectures, this should not require a new NDK build c.run("create . -pr:b=windows -pr:h=android -s:h arch=armv8 --build=missing --build-require") c.assert_listed_binary({"androidndk/0.1": ("522dcea5982a3f8a5b624c16477e47195da2f84f", "Cache")}, build=True) assert "androidndk/0.1: Already installed!" in c.out # But a different build architecture is a different NDK executable c.run("create . -pr:b=windows -s:b arch=x86 -pr:h=android --build-require") c.assert_listed_binary({"androidndk/0.1": ("c11e463c49652ba9c5adc62573ee49f966bd8417", "Build")}, build=True) assert "androidndk/0.1: Calling build()" in c.out # But a different build OS is a different NDK executable c.run("create . -pr:b=linux -pr:h=android --build-require") c.assert_listed_binary({"androidndk/0.1": ("63fead0844576fc02943e16909f08fcdddd6f44b", "Build")}, build=True) assert "androidndk/0.1: Calling build()" in c.out # IMPORTANT: The consumption via test_package allows specifying the type of requires # in this case: None, as this is intended to be injected via profile [tool_requires] # can be tested like that c.run("remove * -c") c.save({"test_package/conanfile.py": test}) # Creating the NDK packages for Windows, Linux c.run("create . -pr:b=windows -pr:h=android --build-require") c.assert_listed_binary({"androidndk/0.1": ("522dcea5982a3f8a5b624c16477e47195da2f84f", "Build")}, build=True) assert "androidndk/0.1 (test package): NDK LIBS: ['libndklib-armv7']!!!" in c.out assert "androidndk/0.1 (test package): MYNDK-Windows-x86_64 exe!" in c.out assert "androidndk/0.1 (test package): MY-VAR: android-armv7" in c.out # The same NDK can be used for different architectures, this should not require a new NDK build c.run("create . -pr:b=windows -pr:h=android -s:h arch=armv8 --build=missing --build-require") c.assert_listed_binary({"androidndk/0.1": ("522dcea5982a3f8a5b624c16477e47195da2f84f", "Cache")}, build=True) assert "androidndk/0.1: Already installed!" in c.out assert "androidndk/0.1 (test package): NDK LIBS: ['libndklib-armv8']!!!" in c.out assert "androidndk/0.1 (test package): MYNDK-Windows-x86_64 exe!" in c.out assert "androidndk/0.1 (test package): MY-VAR: android-armv8" in c.out # But a different build architecture is a different NDK executable c.run("create . -pr:b=windows -s:b arch=x86 -pr:h=android --build=missing --build-require") c.assert_listed_binary({"androidndk/0.1": ("c11e463c49652ba9c5adc62573ee49f966bd8417", "Build")}, build=True) assert "androidndk/0.1: Calling build()" in c.out assert "androidndk/0.1 (test package): NDK LIBS: ['libndklib-armv7']!!!" in c.out assert "androidndk/0.1 (test package): MYNDK-Windows-x86 exe!" in c.out assert "androidndk/0.1 (test package): MY-VAR: android-armv7" in c.out # But a different build OS is a different NDK executable c.run("create . -pr:b=linux -pr:h=android --build=missing --build-require") c.assert_listed_binary({"androidndk/0.1": ("63fead0844576fc02943e16909f08fcdddd6f44b", "Build")}, build=True) assert "androidndk/0.1: Calling build()" in c.out assert "androidndk/0.1 (test package): NDK LIBS: ['libndklib-armv7']!!!" in c.out assert "androidndk/0.1 (test package): MYNDK-Linux-x86_64 exe!" in c.out assert "androidndk/0.1 (test package): MY-VAR: android-armv7" in c.out # Now any other package can use it c.save({"conanfile.py": test, "windows": windows, "linux": linux, "android": android}, clean_first=True) c.run("install . -pr:b=windows -pr:h=android") c.assert_listed_binary({"androidndk/0.1": ("522dcea5982a3f8a5b624c16477e47195da2f84f", "Cache")}, build=True) assert "conanfile.py: NDK LIBS: ['libndklib-armv7']!!!" in c.out assert "conanfile.py: MYNDK-Windows-x86_64 exe!" in c.out # And build on the fly the NDK if not binary exists c.run("install . -pr:b=linux -s:b arch=x86 -pr:h=android -s:h arch=armv8 --build=missing") c.assert_listed_binary({"androidndk/0.1": ("f14494c6e4810a63f050d6f5f37e9776ed48d3c9", "Build")}, build=True) assert "conanfile.py: NDK LIBS: ['libndklib-armv8']!!!" in c.out assert "conanfile.py: MYNDK-Linux-x86 exe!" in c.out assert "conanfile.py: MY-VAR: android-armv8" in c.out def test_libcxx(): """ emulates a package for libcxx, containing only a library to link with """ c = TestClient() macos = textwrap.dedent(""" [settings] os=Macos arch = x86_64 build_type = Release compiler=apple-clang compiler.version=12.0 compiler.cppstd=14 compiler.libcxx=libc++ """) ios = textwrap.dedent("""\ [settings] os=iOS os.version = 14.3 os.sdk = iphoneos arch = armv7 build_type = Release compiler=apple-clang compiler.version=11.0 compiler.cppstd=14 compiler.libcxx=libc++ [tool_requires] libcxx/0.1 """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "libcxx" version = "0.1" settings = "os", "arch", "compiler", "build_type" def build(self): # HERE IT MUST USE THE SETTINGS-TARGET for CREATING THE BINARIES arch = self.settings_target.arch os_ = self.settings_target.os phone_sdk = self.settings_target.get_safe("os.sdk") or "" save(self, f"lib/libcxx-{arch}", f"libcxx{phone_sdk}-{os_}-{arch}!") def package(self): copy(self, "*", src=self.build_folder, dst=self.package_folder) def package_info(self): arch = self.settings_target.arch self.cpp_info.libs = [f"libcxx-{arch}"] def package_id(self): self.info.settings.clear() self.info.settings_target = self.settings_target """) test = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): libcxx = self.dependencies.build["libcxx"] libcxx_lib = libcxx.cpp_info.libs[0] self.output.info(f"LIBCXX LIBS: {libcxx_lib}!!!") libcxx_path = os.path.join(libcxx.package_folder, "lib", libcxx_lib) self.output.info(load(self, libcxx_path)) def test(self): pass """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test, "macos": macos, "ios": ios}) c.run("create . -pr:b=macos -pr:h=ios --build-require") c.assert_listed_binary({"libcxx/0.1": ("cd83035dfa71d83329e31a13b54b03fd38836815", "Build")}, build=True) assert "libcxx/0.1 (test package): LIBCXX LIBS: libcxx-armv7!!!" in c.out assert "libcxx/0.1 (test package): libcxxiphoneos-iOS-armv7!" in c.out # Same host profile should be same binary, the build profile is not factored in c.run("create . -pr:b=macos -s:b build_type=Debug -s:b arch=armv8 -pr:h=ios " "--build=missing --build-require") c.assert_listed_binary({"libcxx/0.1": ("cd83035dfa71d83329e31a13b54b03fd38836815", "Cache")}, build=True) assert "libcxx/0.1 (test package): LIBCXX LIBS: libcxx-armv7!!!" in c.out assert "libcxx/0.1 (test package): libcxxiphoneos-iOS-armv7!" in c.out # But every change in host, is a different binary c.run("create . -pr:b=macos -pr:h=ios -s:h arch=armv8 --build=missing --build-require") c.assert_listed_binary({"libcxx/0.1": ("af2d5e0458816f1d7e61c82315d143c20e3db2e3", "Build")}, build=True) assert "libcxx/0.1 (test package): LIBCXX LIBS: libcxx-armv8!!!" in c.out assert "libcxx/0.1 (test package): libcxxiphoneos-iOS-armv8!" in c.out # But every change in host, is a different binary c.run("create . -pr:b=macos -pr:h=ios -s:h arch=armv8 -s:h os.sdk=iphonesimulator " "--build-require") c.assert_listed_binary({"libcxx/0.1": ("c7e3b0fbb4fac187f59bf97e7958631b96fda2a6", "Build")}, build=True) assert "libcxx/0.1 (test package): LIBCXX LIBS: libcxx-armv8!!!" in c.out assert "libcxx/0.1 (test package): libcxxiphonesimulator-iOS-armv8!" in c.out # Now any other package can use it c.save({"conanfile.py": test, "macos": macos, "ios": ios}, clean_first=True) c.run("install . -pr:b=macos -pr:h=ios") c.assert_listed_binary({"libcxx/0.1": ("cd83035dfa71d83329e31a13b54b03fd38836815", "Cache")}, build=True) assert "conanfile.py: LIBCXX LIBS: libcxx-armv7!!!" in c.out assert "conanfile.py: libcxxiphoneos-iOS-armv7!" in c.out def test_compiler_gcc(): """ this is testing a gcc-like cross-compiler that needs the gcc.exe binary to compile and can also contain a specific libcxx for the target architecture """ c = TestClient() # build machine linux = textwrap.dedent(""" [settings] os=Linux arch = x86_64 build_type = Release compiler=gcc compiler.version=11 compiler.cppstd=14 compiler.libcxx=libstdc++11 """) rpi = textwrap.dedent("""\ [settings] os=Linux arch = armv7 build_type = Release [tool_requires] gcc/0.1 """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "gcc" version = "0.1" settings = "os", "arch", "compiler", "build_type" def build(self): # HERE IT MUST USE THE SETTINGS-TARGET for CREATING THE LIBCXX # BUT SETTINGS for CREATING THE GCC.EXE arch = self.settings_target.arch os_ = self.settings_target.os save(self, f"lib/libcxx-{arch}", f"libcxx-{os_}-{arch}!") save(self, "bin/gcc", f"gcc-{self.settings.os}-{self.settings.arch}") def package(self): copy(self, "*", src=self.build_folder, dst=self.package_folder) def package_info(self): arch = self.settings_target.arch self.cpp_info.libs = [f"libcxx-{arch}"] def package_id(self): self.info.settings_target = self.settings_target """) test = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class Pkg(ConanFile): settings = "os", "arch", "build_type" def generate(self): gcc = self.dependencies.build["gcc"] libcxx_lib = gcc.cpp_info.libs[0] self.output.info(f"LIBCXX LIBS: {libcxx_lib}!!!") libcxx_path = os.path.join(gcc.package_folder, "lib", libcxx_lib) self.output.info(load(self, libcxx_path)) gcc_path = os.path.join(gcc.package_folder, "bin/gcc") self.output.info(load(self, gcc_path)) def test(self): pass """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test, "linux": linux, "rpi": rpi}) c.run("create . -pr:b=linux -pr:h=rpi --build-require") c.assert_listed_binary({"gcc/0.1": ("6190ea2804cd4777609ec7174ccfdee22c6318c3", "Build")}, build=True) assert "gcc/0.1 (test package): LIBCXX LIBS: libcxx-armv7!!!" in c.out assert "gcc/0.1 (test package): libcxx-Linux-armv7!" in c.out assert "gcc/0.1 (test package): gcc-Linux-x86_64" in c.out # Same host profile, but different build profile is a different binary c.run("create . -pr:b=linux -s:b os=Windows -s:b arch=armv8 -pr:h=rpi --build-require") c.assert_listed_binary({"gcc/0.1": ("687fdc5f43017300a98643948869b4c5560ca82c", "Build")}, build=True) assert "gcc/0.1 (test package): LIBCXX LIBS: libcxx-armv7!!!" in c.out assert "gcc/0.1 (test package): libcxx-Linux-armv7!" in c.out assert "gcc/0.1 (test package): gcc-Windows-armv8" in c.out # Same build but different host is also a new binary c.run("create . -pr:b=linux -pr:h=rpi -s:h arch=armv8 --build=missing --build-require") c.assert_listed_binary({"gcc/0.1": ("0521de9a6b94083bd47474a51570a3b856b77406", "Build")}, build=True) assert "gcc/0.1 (test package): LIBCXX LIBS: libcxx-armv8!!!" in c.out assert "gcc/0.1 (test package): libcxx-Linux-armv8!" in c.out assert "gcc/0.1 (test package): gcc-Linux-x86_64" in c.out # check the list packages c.run("list gcc/0.1:* --format=json", redirect_stdout="packages.json") pkgs_json = json.loads(c.load("packages.json")) pref = PkgReference.loads("gcc/0.1#a8d725d9988de633accf410fb04cd162:" "6190ea2804cd4777609ec7174ccfdee22c6318c3") settings_target = { "arch": "armv7", "build_type": "Release", "os": "Linux" } revision_dict = pkgs_json["Local Cache"]["gcc/0.1"]["revisions"][pref.ref.revision] assert settings_target == revision_dict["packages"][pref.package_id]["info"]["settings_target"] ================================================ FILE: test/integration/cache/__init__.py ================================================ ================================================ FILE: test/integration/cache/backup_sources_test.py ================================================ import json import os import textwrap import uuid from unittest import mock import bottle import pytest from bottle import static_file, HTTPError, request from webtest import TestApp from conan.internal.errors import NotFoundException from conan.errors import ConanException from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save, load, rmdir, mkdir, remove class TestDownloadCacheBackupSources: def test_users_download_cache_summary(self): def custom_download(this, url, filepath, *args, **kwargs): # noqa assert not url.startswith("http://myback") save(filepath, f"Hello, world!") with mock.patch("conan.internal.rest.file_downloader.FileDownloader.download", custom_download): client = TestClient(default_server_user=True, light=True) tmp_folder = temp_folder() client.save_home( {"global.conf": f"core.sources:download_cache={tmp_folder}\n" "core.sources:download_urls=['origin', 'http://myback']"}) sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): def source(self): download(self, "http://localhost:5000/myfile.txt", "myfile.txt", sha256="{sha256}") """) client.save({"conanfile.py": conanfile}) client.run("source .") assert 2 == len(os.listdir(os.path.join(tmp_folder, "s"))) content = json.loads(load(os.path.join(tmp_folder, "s", sha256 + ".json"))) assert "http://localhost:5000/myfile.txt" in content["references"]["unknown"] assert len(content["references"]["unknown"]) == 1 conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "http://localhost.mirror:5000/myfile.txt", "myfile.txt", sha256="{sha256}") """) client.save({"conanfile.py": conanfile}) client.run("source .") assert 2 == len(os.listdir(os.path.join(tmp_folder, "s"))) content = json.loads(load(os.path.join(tmp_folder, "s", sha256 + ".json"))) assert "http://localhost.mirror:5000/myfile.txt" in content["references"]["pkg/1.0"] assert len(content["references"]["pkg/1.0"]) == 1 # Ensure the cache is working and we didn't break anything by modifying the summary client.run("source .") assert "Downloading file" not in client.out client.run("create . --user=barbarian") content = json.loads(load(os.path.join(tmp_folder, "s", sha256 + ".json"))) assert content["references"]["pkg/1.0@barbarian"] == \ ["http://localhost.mirror:5000/myfile.txt"] client.run("create . --user=barbarian --channel=stable") content = json.loads(load(os.path.join(tmp_folder, "s", sha256 + ".json"))) assert content["references"]["pkg/1.0@barbarian/stable"] == \ ["http://localhost.mirror:5000/myfile.txt"] @pytest.fixture(autouse=True) def _setup(self): self.client = TestClient(default_server_user=True, light=True) self.file_server = TestFileServer() self.client.servers["file_server"] = self.file_server self.download_cache_folder = temp_folder() def test_upload_sources_backup(self): http_server_base_folder_backups = os.path.join(self.file_server.store, "backups") http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") save(os.path.join(self.file_server.store, "mycompanystorage", "mycompanyfile.txt"), "Business stuff") save(os.path.join(http_server_base_folder_internet, "duplicated1.txt"), "I am duplicated #1") save(os.path.join(self.file_server.store, "mycompanystorage2", "duplicated2.txt"), "I am duplicated #2") hello_world_sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" mycompanyfile_sha256 = "7f1d5d6ae44eb93061b0e07661bd8cbac95a7c51fa204570bf7e24d877d4a224" duplicated1_sha256 = "744aca0436e2e355c285fa926a37997df488544589cec2260fc9db969a3c78df" duplicated2_sha256 = "66ba2ba05211da3b0b0cb0a08f18e1d9077e7321c2be27887371ec37fade376d" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{hello_world_sha256}") download(self, "{self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt", "mycompanyfile.txt", sha256="{mycompanyfile_sha256}") download(self, ["{self.file_server.fake_url}/mycompanystorage/duplicated1.txt", "{self.file_server.fake_url}/internet/duplicated1.txt"], "duplicated1.txt", sha256="{duplicated1_sha256}") download(self, ["{self.file_server.fake_url}/internet/duplicated1.txt"], "duplicated1.txt", sha256="{duplicated1_sha256}") download(self, ["{self.file_server.fake_url}/mycompanystorage/duplicated2.txt", "{self.file_server.fake_url}/mycompanystorage2/duplicated2.txt"], "duplicated2.txt", sha256="{duplicated2_sha256}") download(self, "{self.file_server.fake_url}/mycompanystorage2/duplicated2.txt", "duplicated2.txt", sha256="{duplicated2_sha256}") """) # Ensure that a None url is only warned about but no exception is thrown, # this is possible in CI systems that substitute an env var and fail to give it a value self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=[None, 'origin', '{self.file_server.fake_url}/backups/']\n" f"core.sources:upload_url={self.file_server.fake_url}/backups/"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create .", assert_error=True) assert "Trying to download sources from None backup remote" in self.client.out self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['origin', '{self.file_server.fake_url}/backups/']\n" f"core.sources:upload_url={self.file_server.fake_url}/backups/\n" f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', " f"'{self.file_server.fake_url}/mycompanystorage2/']"}) self.client.run("create .") self.client.run("upload * -c -r=default") # To test #15501 that it works even with extra files in the local cache extra_file_path = os.path.join(self.download_cache_folder, "s", "extrafile") save(extra_file_path, "Hello, world!") self.client.run("upload * -c -r=default", assert_error=True) assert "Missing metadata file for backup source" in self.client.out os.unlink(extra_file_path) server_contents = os.listdir(http_server_base_folder_backups) assert hello_world_sha256 in server_contents assert hello_world_sha256 + ".json" in server_contents # Its only url is excluded, should not be there assert mycompanyfile_sha256 not in server_contents assert mycompanyfile_sha256 + ".json" not in server_contents # It has an excluded url, but another that is not, it should be there assert duplicated1_sha256 in server_contents assert duplicated1_sha256 + ".json" in server_contents # All its urls are excluded, it shoud not be there assert duplicated2_sha256 not in server_contents assert duplicated2_sha256 + ".json" not in server_contents self.client.run("upload * -c -r=default") assert "already in server, skipping upload" in self.client.out # Clear de local cache rmdir(self.download_cache_folder) mkdir(self.download_cache_folder) # Everything the same, but try to download from backup first self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/backups/', 'origin']\n" f"core.sources:upload_url={self.file_server.fake_url}/backups/\n" f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', '{self.file_server.fake_url}/mycompanystorage2/']"}) self.client.run("source .") assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup" in self.client.out assert f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not found in {self.file_server.fake_url}/backups/" in self.client.out # Ensure defaults backup folder works if it's not set in global.conf # (The rest is needed to exercise the rest of the code) self.client.save_home( {"global.conf": f"core.sources:download_urls=['{self.file_server.fake_url}/backups/', 'origin']\n" f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', '{self.file_server.fake_url}/mycompanystorage2/']"}) self.client.run("source .") assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup" in self.client.out assert f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not found in {self.file_server.fake_url}/backups/" in self.client.out def test_unknown_handling(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['origin']\n" f"core.sources:upload_url={self.file_server.fake_url}/backups/"}) self.client.save({"conanfile.py": conanfile}) self.client.run("source .") json_path = os.path.join(self.download_cache_folder, "s", sha256 + ".json") contents = json.loads(load(json_path)) assert "unknown" in contents["references"] s_folder = os.path.join(self.download_cache_folder, "s") assert len(os.listdir(s_folder)) == 2 self.client.run("export . --version=1.0") self.client.run("upload * -c -r=default") assert "No backup sources files to upload" in self.client.out self.client.run("cache clean --backup-sources") assert len(os.listdir(s_folder)) == 0 def test_download_origin_first(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['origin', '{self.file_server.fake_url}/backup/']\n" f"core.sources:upload_url={self.file_server.fake_url}/backup/"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create .") self.client.run("upload * -c -r=default") rmdir(self.download_cache_folder) self.client.run("source .") assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in origin" in self.client.out self.client.run("source .") assert f"Source {self.file_server.fake_url}/internet/myfile.txt retrieved from local download cache" in self.client.out def test_download_origin_last(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/backup', 'origin']\n" f"core.sources:upload_url={self.file_server.fake_url}/backup"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create . -vv") assert f"WARN: File {self.file_server.fake_url}/internet/myfile.txt not found in {self.file_server.fake_url}/backup/" in self.client.out assert f"Downloaded {self.file_server.fake_url}/internet/myfile.txt from {self.file_server.fake_url}/internet/myfile.txt" self.client.run("upload * -c -r=default") rmdir(self.download_cache_folder) self.client.run("source .") assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup" in self.client.out self.client.run("source .") assert f"Source {self.file_server.fake_url}/internet/myfile.txt retrieved from local download cache" in self.client.out def test_sources_backup_server_error_500(self): conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="unused") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/internal_error/', " f"'{self.file_server.fake_url}/unused/']\n"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create .", assert_error=True) assert "ConanException: Error 500 downloading file" in self.client.out def test_sources_backup_server_not_found(self): conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="unused") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.download:retry=0\n" f"core.sources:download_urls=['error_server', 'origin']\n"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create .", assert_error=True) assert "ConanException: Error downloading file" in self.client.out def test_upload_sources_backup_creds_needed(self): client = TestClient(default_server_user=True, light=True) download_cache_folder = temp_folder() http_server_base_folder_backups = temp_folder() http_server_base_folder_internet = temp_folder() save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") class TestFileServer2: def __init__(self): self.fake_url = "http://fake%s.com" % str(uuid.uuid4()).replace("-", "") self.root_app = bottle.Bottle() self.app = TestApp(self.root_app) self._attach_to(self.root_app) @staticmethod def _attach_to(app): def valid_auth(token): auth = request.headers.get("Authorization") if auth == f"Bearer {token}": return return HTTPError(401, "Authentication required") @app.route("/internet/", method=["GET"]) def get_internet_file(file): return static_file(file, http_server_base_folder_internet) @app.route("/downloader/", method=["GET"]) def get_file(file): ret = valid_auth("mytoken") return ret or static_file(file, http_server_base_folder_backups) @app.route("/uploader/", method=["PUT"]) def put_file(file): ret = valid_auth("myuploadtoken") if ret: return ret dest = os.path.join(http_server_base_folder_backups, file) with open(dest, 'wb') as f: f.write(request.body.read()) http_server = TestFileServer2() client.servers["file_server"] = http_server sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download, load class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{http_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") self.output.info(f"CONTENT: {{load(self, 'myfile.txt')}}") """) client.save_home( {"global.conf": f"core.sources:download_cache={download_cache_folder}\n" f"core.sources:download_urls=['{http_server.fake_url}/downloader/', 'origin']\n" f"core.sources:upload_url={http_server.fake_url}/uploader/"}) client.save({"conanfile.py": conanfile}) client.run("create .", assert_error=True) assert f"ConanException: Authentication to source backup server '{http_server.fake_url}" \ f"/downloader/' failed" in client.out content = {"credentials": [ {"url": f"{http_server.fake_url}", "token": "mytoken"} ]} save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content)) client.run("create .") assert "CONTENT: Hello, world!" in client.out client.run("upload * -c -r=default", assert_error=True) assert f"Authentication to source backup server '{http_server.fake_url}" \ f"/uploader/' failed" in client.out content = {"credentials": [ {"url": f"{http_server.fake_url}", "token": "myuploadtoken"} ]} # Now use the correct UPLOAD token save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content)) client.run("upload * -c -r=default --force") # need --force to guarantee cached updated server_contents = os.listdir(http_server_base_folder_backups) assert sha256 in server_contents assert sha256 + ".json" in server_contents client.run("upload * -c -r=default") assert "already in server, skipping upload" in client.out content = {"credentials": [ {"url": f"{http_server.fake_url}", "token": "mytoken"} ]} save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content)) rmdir(download_cache_folder) # Remove the "remote" myfile.txt so if it raises # we know it tried to download the original source os.remove(os.path.join(http_server_base_folder_internet, "myfile.txt")) client.run("source .") assert f"Sources for {http_server.fake_url}/internet/myfile.txt found in remote backup" in client.out assert "CONTENT: Hello, world!" in client.out client.run("source .") assert f"Source {http_server.fake_url}/internet/myfile.txt retrieved from local download cache" in client.out assert "CONTENT: Hello, world!" in client.out def test_download_sources_multiurl(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") http_server_base_folder_backup = os.path.join(self.file_server.store, "backup") http_server_base_folder_downloader = os.path.join(self.file_server.store, "downloader") save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:upload_url={self.file_server.fake_url}/backup/\n" f"core.sources:download_urls=['origin', '{self.file_server.fake_url}/downloader/', " f"'{self.file_server.fake_url}/backup/']"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create .") self.client.run("upload * -c -r=default") # We upload files to second backup, # to ensure that the first one gets skipped in the list but finds in the second one server_contents = os.listdir(http_server_base_folder_backup) assert sha256 in server_contents assert sha256 + ".json" in server_contents rmdir(self.download_cache_folder) # Remove the "remote" myfile.txt so if it raises # we know it tried to download the original source remove(os.path.join(http_server_base_folder_internet, "myfile.txt")) self.client.run("source .") assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup {self.file_server.fake_url}/backup/" in self.client.out # And if the first one has them, prefer it before others in the list save(os.path.join(http_server_base_folder_downloader, sha256), load(os.path.join(http_server_base_folder_backup, sha256))) save(os.path.join(http_server_base_folder_downloader, sha256 + ".json"), load(os.path.join(http_server_base_folder_backup, sha256 + ".json"))) rmdir(self.download_cache_folder) self.client.run("source .") assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup {self.file_server.fake_url}/downloader/" in self.client.out def test_list_urls_miss(self): def custom_download(this, url, *args, **kwargs): # noqa raise NotFoundException() with mock.patch("conan.internal.rest.file_downloader.FileDownloader.download", custom_download): client = TestClient(default_server_user=True, light=True) download_cache_folder = temp_folder() sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "http://fake/myfile.txt", "myfile.txt", sha256="{sha256}") """) client.save_home( {"global.conf": f"core.sources:download_cache={download_cache_folder}\n" f"core.sources:download_urls=['origin', 'http://extrafake/']\n"}) client.save({"conanfile.py": conanfile}) client.run("source .", assert_error=True) assert "WARN: Sources for http://fake/myfile.txt failed in 'origin'" in client.out assert "WARN: Checking backups" in client.out assert "NotFoundException: File http://fake/myfile.txt " \ "not found in ['origin', 'http://extrafake/']" in client.out def test_ok_when_origin_breaks_midway_list(self): http_server_base_folder_backup2 = os.path.join(self.file_server.store, "backup2") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_backup2, sha256), "Hello, world!") save(os.path.join(http_server_base_folder_backup2, sha256 + ".json"), '{"references": {}}') conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internal_error/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": "tools.files.download:retry=0\n" f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/empty/', " f"'origin', '{self.file_server.fake_url}/backup2/']\n"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create .") assert f"Sources for {self.file_server.fake_url}/internal_error/myfile.txt found in remote backup {self.file_server.fake_url}/backup2/" in self.client.out def test_ok_when_origin_authorization_error(self): client = TestClient(default_server_user=True, light=True) download_cache_folder = temp_folder() http_server_base_folder_backup1 = temp_folder() http_server_base_folder_backup2 = temp_folder() sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_backup2, sha256), "Hello, world!") save(os.path.join(http_server_base_folder_backup2, sha256 + ".json"), '{"references": {}}') class TestFileServer2: def __init__(self, store=None): self.store = store or temp_folder(path_with_spaces=False) mkdir(self.store) self.fake_url = "http://fake%s.com" % str(uuid.uuid4()).replace("-", "") self.root_app = bottle.Bottle() self.app = TestApp(self.root_app) self._attach_to(self.root_app, self.store) @staticmethod def _attach_to(app, store): # noqa @app.route("/internet/", method=["GET"]) def get_internet_file(file): return HTTPError(401, "You Are Not Allowed Here") @app.route("/downloader1/", method=["GET"]) def get_file(file): return static_file(file, http_server_base_folder_backup1) @app.route("/downloader1/", method=["PUT"]) def put_file(file): if file in os.listdir(http_server_base_folder_backup1): return HTTPError(401, "You Are Not Allowed Here") dest = os.path.join(http_server_base_folder_backup1, file) with open(dest, 'wb') as f: f.write(request.body.read()) @app.route("/downloader2/", method=["GET"]) def get_file_2(file): return static_file(file, http_server_base_folder_backup2) http_server = TestFileServer2() client.servers["file_server"] = http_server conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{http_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) client.save_home({"global.conf": f"core.sources:download_cache={download_cache_folder}\n" f"core.sources:download_urls=['{http_server.fake_url}/downloader1/', " f"'origin', '{http_server.fake_url}/downloader2/']\n" f"core.sources:upload_url={http_server.fake_url}/downloader1/\n" "core.upload:retry=0\ncore.download:retry=0"}) client.save({"conanfile.py": conanfile}) client.run("create .") assert f"Sources for {http_server.fake_url}/internet/myfile.txt found in remote backup {http_server.fake_url}/downloader2/" in client.out # TODO: Check better message with Authentication error message assert "failed in 'origin'" in client.out # Now try to upload once to the first backup server. It's configured so it has write permissions but no overwrite client.run("upload * -c -r=default") upload_server_contents = os.listdir(http_server_base_folder_backup1) assert sha256 in upload_server_contents assert sha256 + ".json" in upload_server_contents client.run("remove * -c -r=default") client.run("upload * -c -r=default") assert f"Could not update summary '{sha256}.json' in backup sources server" in client.out def test_ok_when_origin_bad_sha256(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") http_server_base_folder_backup2 = os.path.join(self.file_server.store, "backup2") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" # This file's sha is not the one in the line above save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Bye, world!") save(os.path.join(http_server_base_folder_backup2, sha256), "Hello, world!") save(os.path.join(http_server_base_folder_backup2, sha256 + ".json"), '{"references": {}}') conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/backup1/', " f"'origin', '{self.file_server.fake_url}/backup2/']\n"}) self.client.save({"conanfile.py": conanfile}) self.client.run("create .") assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup {self.file_server.fake_url}/backup2/" in self.client.out assert "sha256 hash failed for" in self.client.out def test_export_then_upload_workflow(self): mkdir(os.path.join(self.download_cache_folder, "s")) http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") http_server_base_folder_backup = os.path.join(self.file_server.store, "backup") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/backup/', 'origin']\n" f"core.sources:upload_url={self.file_server.fake_url}/backup/"}) self.client.save({"conanfile.py": conanfile}) self.client.run("export .") self.client.run("upload * -c -r=default") # This used to crash because we were trying to list a missing dir if only exports were made assert "[Errno 2] No such file or directory" not in self.client.out self.client.run("create .") self.client.run("upload * -c -r=default") assert sha256 in os.listdir(http_server_base_folder_backup) self.client.run("cache clean * -bs") backups = os.listdir(os.path.join(self.download_cache_folder, "s")) assert len(backups) == 0 def test_export_then_upload_recipe_only_workflow(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/backup/', 'origin']\n" f"core.sources:upload_url={self.file_server.fake_url}/backup/"}) self.client.save({"conanfile.py": conanfile}) self.client.run("export .") exported_rev = self.client.exported_recipe_revision() self.client.run("upload * --only-recipe -c -r=default") # This second run used to crash because we thought there would be some packages always self.client.run("upload * --only-recipe -c -r=default") # Ensure we are testing for an already uploaded recipe assert f"Recipe 'pkg/1.0#{exported_rev}' already in server, skipping upload" in self.client.out @pytest.mark.parametrize("unknown", [True, False]) def test_source_then_upload_workflow(self, unknown): mkdir(os.path.join(self.download_cache_folder, "s")) http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") http_server_base_folder_backup = os.path.join(self.file_server.store, "backup") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"core.sources:download_urls=['{self.file_server.fake_url}/backup/', 'origin']\n" f"core.sources:upload_url={self.file_server.fake_url}/backup/"}) self.client.save({"conanfile.py": conanfile}) ref_args = "" if unknown else "--name foo --version 1.0" reference_key = "unknown" if unknown else "foo/1.0" self.client.run(f"source . {ref_args}") content = json.loads(load(os.path.join(self.download_cache_folder, "s", sha256 + ".json"))) assert len(content["references"][reference_key]) == 1 self.client.run("cache backup-upload") # This used to crash because we were trying to list a missing dir if only exports were made assert "[Errno 2] No such file or directory" not in self.client.out assert sha256 in os.listdir(http_server_base_folder_backup) def test_backup_source_corrupted_download_handling(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}"}) self.client.save({"conanfile.py": conanfile}) self.client.run("source .") # Now simulate a dirty download, the file is there but the sha256 is wrong save(os.path.join(self.download_cache_folder, "s", sha256), "Bye bye, world!") # Now try to source again, it should eat it up self.client.run("source .") @pytest.mark.parametrize("exception", [Exception, ConanException]) @pytest.mark.parametrize("upload", [True, False]) def test_backup_source_dirty_download_handle(self, exception, upload): def custom_download(this, *args, **kwargs): raise exception() http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" f"tools.files.download:retry=0"}) self.client.save({"conanfile.py": conanfile}) with mock.patch("conan.internal.rest.file_downloader.FileDownloader._download_file", custom_download): self.client.run("source .", assert_error=True) # The mock does not actually download a file, let's add it for the test save(os.path.join(self.download_cache_folder, "s", sha256), "__corrupted download__") # Check that the dirty file was not removed. # This check should go away once we refactor dirty handling assert os.path.exists(os.path.join(self.download_cache_folder, "s", f"{sha256}.dirty")) if upload: self.client.run("cache backup-upload") else: self.client.run("source .") assert f"{sha256} is dirty, removing it" in self.client.out @pytest.mark.parametrize("exception", [Exception, ConanException]) def test_backup_source_upload_when_dirty(self, exception): def custom_download(this, *args, **kwargs): # noqa raise exception() mkdir(os.path.join(self.download_cache_folder, "s")) http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): def source(self): download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt", sha256="{sha256}") """) self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" "tools.files.download:retry=0\n" f"core.sources:upload_url={self.file_server.fake_url}/backup/\n"}) self.client.save({"conanfile.py": conanfile}) with mock.patch("conan.internal.rest.file_downloader.FileDownloader._download_file", custom_download): self.client.run("source .", assert_error=True) # A .dirty file was created, now try to source again, # it should detect the dirty download and clean it, but not re-source it self.client.run("export . --name=pkg2 --version=1.0") self.client.run("upload * -c -r=default") assert "No backup sources files to upload" in self.client.out assert sha256 + ".dirty" not in os.listdir(os.path.join(self.download_cache_folder, "s")) ================================================ FILE: test/integration/cache/cache2_update_test.py ================================================ import copy import json import textwrap from collections import OrderedDict import pytest from unittest.mock import patch from conan.api.model import RecipeReference from conans.server.revision_list import RevisionList from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer, NO_SETTINGS_PACKAGE_ID class TestUpdateFlows: @pytest.fixture(autouse=True) def _setup(self): self.liba = RecipeReference.loads("liba/1.0.0") servers = OrderedDict() for index in range(3): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) self.client = TestClient(servers=servers, inputs=3*["user", "password"]) self.client2 = TestClient(servers=servers, inputs=3*["user", "password"]) self.the_time = 0.0 self.server_times = {} def _upload_ref_to_all_servers(self, ref, client): # we are patching the time all these revisions uploaded to the servers # will be older than the ones we create in local for index in range(3): self.the_time = self.the_time + 10 self._upload_ref_to_server(ref, f"server{index}", client) def _upload_ref_to_server(self, ref, remote, client): # we are patching the time all these revisions uploaded to the servers # will be older than the ones we create in local self.server_times[remote] = self.the_time with patch.object(RevisionList, '_now', return_value=self.the_time): client.run(f"upload {ref} -r {remote} -c") def test_revision_fixed_version(self): # NOTES: # - When a revision is installed from a remote it takes the date from the remote, not # updating the date to the current time # - If we want to install the revision and create it with a new date use --update-date # (name to be decided) # - Revisions are considered inmutable: if for example we do a conan install --update of a # revision that is already in the cache, but has a newer date in the remote, we will not install # anything, just updating the date in the cache to the one in the remote, so if you want to # get what the remote has you have to re-install you will have to remove the local # package and install from server # - In conan 2.X no remote means search in all remotes # create a revision 0 in client2, client2 will have an older revision than all the servers self.client2.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV0")}) self.client2.run("create .") # other revision created in client self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV")}) self.client.run("create .") self._upload_ref_to_all_servers("liba/1.0.0", self.client) # upload other revision 1 we create in client self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV1")}) self.client.run("create .") self._upload_ref_to_all_servers("liba/1.0.0", self.client) # NOW WE HAVE: # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV1 (1020)| REV0 (1000)| REV1(40) | REV1(50) | REV1 (60) | # | REV (1010)| | REV (10) | REV (20) | REV (30) | # | | | | | | # 1. TESTING WITHOUT SPECIFIC REVISIONS AND WITH NO REMOTES: "conan install --requires=liba/1.0.0" # client2 already has a revision for this recipe, don't install anything self.client2.run("install --requires=liba/1.0.0@") self.client2.assert_listed_require({"liba/1.0.0": "Cache"}) assert "liba/1.0.0: Already installed!" in self.client2.out self.client.run("remove * -c") # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | | REV0 (1000)| REV1(40) | REV1(50) | REV1 (60) | # | | | REV (10) | REV (20) | REV (30) | # | | | | | | self.client.run("install --requires=liba/1.0.0@") # will not find revisions for the recipe -> search remotes by order and install the # first match that is rev1 from server0 # --> result: install rev from server0 self.client.assert_listed_require({"liba/1.0.0": "Downloaded (server0)"}) assert f"liba/1.0.0: Retrieving package {NO_SETTINGS_PACKAGE_ID}" \ " from remote 'server0'" in self.client.out latest_rrev = self.client.cache.get_latest_recipe_revision(RecipeReference.loads("liba/1.0.0@")) # check that we have stored REV1 in client with the same date from the server0 assert latest_rrev.timestamp == self.server_times["server0"] assert latest_rrev.timestamp == self.server_times["server0"] # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # |REV1 (40) | REV0 (1000)| REV1(40) | REV1(50) | REV1 (60) | # | | | REV (10) | REV (20) | REV (30) | # | | | | | | self.client.run("install --requires=liba/1.0.0@ --update") # It will first check all the remotes and # will find the latest revision: REV1 from server2 we already have that # revision but the date is newer # --> result: do not download anything, but update REV1 date in cache self.client.assert_listed_require({"liba/1.0.0": "Cache (Updated date) (server2)"}) assert "liba/1.0.0: Already installed!" in self.client.out # now create a newer REV2 in server2 and if we do --update it should update the date # to the date in server0 and associate that remote but not install anything # we create a newer revision in client2 self.client2.run("remove * -c") self.client2.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV2")}) self.client2.run("create .") self.the_time = 100.0 self._upload_ref_to_server("liba/1.0.0", "server2", self.client2) # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # |REV1 (60) | REV2 (1000)| REV1(100) | REV1(50) | REV2 (100) | # | | | REV (10) | REV (20) | REV1 (60) | # | | | | | REV (30) | self.client.run("install --requires=liba/1.0.0@ --update") # --> result: Update date and server because server0 has a newer date latest_rrev = self.client.cache.get_latest_recipe_revision(self.liba) self.client.assert_listed_require({"liba/1.0.0": "Updated (server2)"}) assert "liba/1.0.0: Downloaded package" in self.client.out assert latest_rrev.timestamp == self.server_times["server2"] # we create a newer revision in client self.client.run("remove * -c") self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV2")}) self.client.run("create .") self.client.run(f"remove {latest_rrev.repr_notime()} -c -r server2") # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # |REV2 (2002) | REV2 (2000)| REV1(100) | REV1(50) | REV1 (60) | # | | | REV (10) | REV (20) | REV (30) | # | | | | | | self.client.run("install --requires=liba/1.0.0@") # we already have a revision for liba/1.0.0 so don't install anything # --> result: don't install anything assert "liba/1.0.0: Already installed!" in self.client.out self.client.run("install --requires=liba/1.0.0@ --update") # we already have a newer revision in the client # we will check all the remotes, find the latest revision # this revision will be oldest than the one in the cache # --> result: don't install anything self.client.assert_listed_require({"liba/1.0.0": "Newer"}) assert "liba/1.0.0: Already installed!" in self.client.out # create newer revisions in servers so that the ones from the clients are older self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV3")}) self.client.run("create .") rev_to_upload = self.client.cache.get_latest_recipe_revision(self.liba) # the future self.the_time = 3000000000.0 self._upload_ref_to_all_servers(repr(rev_to_upload), self.client) # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV2 (2002) | REV2 (2000)| REV3(3010) | REV3(3020)| REV3 (3030)| # | | | REV1(100) | REV1(50) | REV1 (60) | # | | | REV (10) | REV (20) | REV (30) | # | | | | | | self.client2.run("install --requires=liba/1.0.0@ --update") # now check for newer references with --update for client2 that has an older revision # when we use --update: first check all remotes (no -r argument) get latest revision # check if it is in cache, if it is --> stop, if it is not --> check date and install # --> result: install rev from server2 self.client2.assert_listed_require({"liba/1.0.0": "Updated (server2)"}) assert f"liba/1.0.0: Downloaded recipe revision {rev_to_upload.revision}" in self.client2.out assert f"liba/1.0.0: Retrieving package {NO_SETTINGS_PACKAGE_ID}" \ " from remote 'server2'" in self.client2.out check_ref = RecipeReference.loads(str(rev_to_upload)) # without revision rev_to_upload = self.client2.cache.get_latest_recipe_revision(check_ref) assert rev_to_upload.timestamp == self.server_times["server2"] # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV2 (2000) | REV3 (3030)| REV3(3010) | REV3(3020)| REV3 (3030)| # | | REV0 (1000)| REV1(100) | REV1(50) | REV1 (60) | # | | | REV (10) | REV (20) | REV (30) | # | | | | | | # TESTING WITH SPECIFIC REVISIONS AND WITH NO REMOTES: "conan install --requires=liba/1.0.0#rrev" # - In conan 2.X no remote means search in all remotes # check one revision we already have will not be installed # we search for that revision in the cache, we found it # --> result: don't install that latest_rrev = self.client.cache.get_latest_recipe_revision(self.liba) self.client.run(f"install --requires={latest_rrev}@#{latest_rrev.revision}") self.client.assert_listed_require({"liba/1.0.0": "Cache"}) assert "liba/1.0.0: Already installed!" in self.client.out self.client.run("remove * -c") self.client.run("remove '*' -c -r server0") self.client.run("remove '*' -c -r server1") self.client.run("remove '*' -c -r server2") # create new older revisions in servers self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV4")}) self.client.run("create .") server_rrev = self.client.cache.get_latest_recipe_revision(self.liba) self.the_time = 0.0 self._upload_ref_to_all_servers("liba/1.0.0", self.client) self.client.run("remove * -c") # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | | REV3 (3030)| REV4(10) | REV4(20) | REV4 (30) | # | | REV0 (1000)| | | | # | | | | | | self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV5")}) self.client.run("create .") # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV5 (2001) | REV3 (3030)| REV4(10) | REV4(20) | REV4 (30) | # | | REV0 (1000)| | | | # | | | | | | # install REV4 self.client.run(f"install --requires={server_rrev}@#{server_rrev.revision}") # have a newer different revision in the cache, but ask for an specific revision that is in # the servers, will try to find that revision and install it from the first server found # will not check all the remotes for the latest because we consider revisions completely # immutable so all of them are the same # --> result: install new revision asked, but the latest revision remains the other one, # because the one installed took the date from the server and it's older assert "liba/1.0.0: Not found in local cache, looking in remotes..." in self.client.out assert "liba/1.0.0: Checking remote: server0" in self.client.out assert "liba/1.0.0: Checking remote: server1" not in self.client.out assert "liba/1.0.0: Checking remote: server2" not in self.client.out server_rrev_norev = copy.copy(server_rrev) server_rrev_norev.revision = None latest_cache_revision = self.client.cache.get_latest_recipe_revision(server_rrev_norev) assert latest_cache_revision != server_rrev # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV5 (2001) | REV3 (3030)| REV4(10) | REV4(20) | REV4 (30) | # | REV4 (10) | REV0 (1000)| | | | # | | | | | | self.client.run(f"install --requires={server_rrev}@#{server_rrev.revision} --update") # last step without --update it took the REV4 from server0 but now # we tell conan to search for newer recipes of an specific revision # it will go to server2 and update the local date with the one # from the remote # --> result: update REV4 date to 30 but it won't be latest latest_cache_revision = self.client.cache.get_latest_recipe_revision(server_rrev_norev) assert latest_cache_revision != server_rrev latest_cache_revision = self.client.cache.recipe_layout(server_rrev).reference assert self.the_time == latest_cache_revision.timestamp self.client.assert_listed_require({"liba/1.0.0": "Cache (Updated date) (server2)"}) self.client.run("remove * -c") self.client.run("remove '*' -c -r server0") self.client.run("remove '*' -c -r server1") self.client.run("remove '*' -c -r server2") self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV6")}) self.client.run("create .") server_rrev = self.client.cache.get_latest_recipe_revision(self.liba) self.the_time = 3000000020.0 self._upload_ref_to_all_servers(repr(server_rrev), self.client) latest_server_time = self.the_time # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV6(2002) | REV3 (3030)| REV6(3030) |REV6(3040) | REV6(3050) | # | | REV0 (1000)| | | | # | | | | | | self.client.run(f"install --requires={server_rrev}@#{server_rrev.revision} --update") # now we have the same revision with different dates in the servers and in the cache # in this case, if we specify --update we will check all the remotes, if that revision # has a newer date in the servers we will take that date from the server but we will not # install anything, we are considering revisions fully immutable in 2.0 # --> results: update revision date in cache, do not install anything latest_rrev_cache = self.client.cache.get_latest_recipe_revision(self.liba) assert latest_server_time == latest_rrev_cache.timestamp self.client.assert_listed_require({"liba/1.0.0": "Cache (Updated date) (server2)"}) # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV6(2002) | REV3 (3050)| REV6(3030) |REV6(3040) | REV6(3050) | # | | REV0 (1000)| | | | # | | | | | | self.client.run("remove * -c") # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | | REV3 (3050)| REV6(3030) |REV6(3040) | REV6(3050) | # | | REV0 (1000)| | | | # | | | | | | self.client.run(f"install --requires={server_rrev}@#{server_rrev.revision} --update") # now we have the same revision with different dates in the servers and in the cache # in this case, if we specify --update we will check all the remotes and will install # the revision from the server that has the latest date # --> results: install from server2 latest_rrev_cache = self.client.cache.get_latest_recipe_revision(self.liba) assert latest_server_time == latest_rrev_cache.timestamp self.client.assert_listed_require({"liba/1.0.0": "Downloaded (server2)"}) # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |-------------|------------|------------|-----------|------------| # | REV6(3050) | REV3 (3050)| REV6(3030) |REV6(3040) | REV6(3050) | # | | REV0 (1000)| | | | # | | | | | | def test_version_ranges(self): # create a revision 0 in client2, client2 will have an older revision than all the servers for minor in range(3): self.client2.save({"conanfile.py": GenConanfile("liba", f"1.{minor}.0").with_build_msg("REV0")}) self.client2.run("create .") self.the_time = 10.0 + minor*10.0 self._upload_ref_to_server(f"liba/1.{minor}.0", f"server{minor}", self.client2) self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV0")}) self.client.run("create .") # NOW WE HAVE: # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |----------------|----------------|----------------|----------------|----------------| # | 1.0 REV0 (1000)| 1.0 REV0 (1000)| 1.0 REV0 (10) | 1.1 REV0 (10) | 1.2 REV0 (10) | # | | 1.1 REV0 (1000)| | | | # | | 1.2 REV0 (1000)| | | | self.client.run("install --requires=liba/[>0.9.0]@") assert "liba/[>0.9.0]: liba/1.0.0" in self.client.out assert "liba/1.0.0: Already installed!" in self.client.out self.client.run("remove * -c") # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |----------------|----------------|----------------|----------------|----------------| # | | 1.0 REV0 (1000)| 1.0 REV0 (10) | 1.1 REV0 (20) | 1.2 REV0 (30) | # | | 1.1 REV0 (1000)| | | | # | | 1.2 REV0 (1000)| | | | self.client.run("install --requires=liba/[>0.9.0]@") # will not find versions for the recipe in cache -> search remotes by order and install the # first match that is 1.0 from server0 # --> result: install 1.0 from server0 assert "liba/[>0.9.0]: liba/1.0.0" in self.client.out self.client.assert_listed_require({"liba/1.0.0": "Downloaded (server0)"}) latest_rrev = self.client.cache.get_latest_recipe_revision(RecipeReference.loads("liba/1.0.0@")) assert latest_rrev.timestamp == self.server_times["server0"] # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |----------------|----------------|----------------|----------------|----------------| # | 1.0 REV0 (10)| 1.0 REV0 (1000) | 1.0 REV0 (10) | 1.1 REV0 (20) | 1.2 REV0 (30) | # | | 1.1 REV0 (1000)| | | | # | | 1.2 REV0 (1000)| | | | self.client.run("install --requires=liba/[>1.0.0]@") # first match that is 1.1 from server1 # --> result: install 1.1 from server1 assert "liba/[>1.0.0]: liba/1.1.0" in self.client.out self.client.assert_listed_require({"liba/1.1.0": "Downloaded (server1)"}) # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |----------------|----------------|----------------|----------------|----------------| # | 1.1 REV0 (10) | 1.0 REV0 (1000)| 1.0 REV0 (10) | 1.1 REV0 (20) | 1.2 REV0 (30) | # | | 1.1 REV0 (1000)| | | | # | | 1.2 REV0 (1000)| | | | self.client.run("install --requires=liba/[>1.0.0]@ --update") # check all servers # --> result: install 1.2 from server2 assert "liba/[>1.0.0]: liba/1.2.0" in self.client.out self.client.assert_listed_require({"liba/1.2.0": "Downloaded (server2)"}) # If we have multiple revisions with different names for the same version and we # do a --update we are going to first resolver the version range agains server0 # then in the proxy we will install rev2 that is the latest # | CLIENT | CLIENT2 | SERVER0 | SERVER1 | SERVER2 | # |----------------|----------------|----------------|----------------|----------------| # | 1.0 REV0 (10) | | 1.2 REV0 (10) | 1.2 REV1 (20) | 1.2 REV2 (30) | # | | | | | | # | | | | | | self.client.run("remove * -c") self.client2.run("remove * -c") # now we are uploading different revisions with different dates, but the same version for minor in range(3): self.client2.save({"conanfile.py": GenConanfile("liba", f"1.2.0").with_build_msg(f"REV{minor}")}) self.client2.run("create .") self.the_time = 10.0 + minor*10.0 self._upload_ref_to_server(f"liba/1.2.0", f"server{minor}", self.client2) self.client.save({"conanfile.py": GenConanfile("liba", "1.0.0").with_build_msg("REV0")}) self.client.run("create .") self.client.run("install --requires=liba/[>1.0.0]@ --update") assert "liba/[>1.0.0]: liba/1.2.0" in self.client.out self.client.assert_listed_require({"liba/1.2.0": "Downloaded (server2)"}) assert f"liba/1.2.0: Retrieving package {NO_SETTINGS_PACKAGE_ID} " \ "from remote 'server2' " in self.client.out @pytest.mark.parametrize("update,result", [ # Not a real pattern, works to support legacy syntax ["*", {"liba/1.1": "Downloaded (default)", "libb/1.1": "Downloaded (default)"}], ["libc", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], ["liba", {"liba/1.1": "Downloaded (default)", "libb/1.0": "Cache"}], ["libb", {"liba/1.0": "Cache", "libb/1.1": "Downloaded (default)"}], ["libb/1.0", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], ["libb/1.0#7e88fd43dc3c8171b6f38f8d1b139641", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], ["", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], # Patterns not supported, only full name match ["lib*", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], ["liba/*", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], # None only passes legacy --update without args, # to ensure it works, it should be the same as passing * [None, {"liba/1.1": "Downloaded (default)", "libb/1.1": "Downloaded (default)"}] ]) def test_muliref_update_pattern(update, result): tc = TestClient(light=True, default_server_user=True) tc.save({"liba/conanfile.py": GenConanfile("liba"), "libb/conanfile.py": GenConanfile("libb")}) tc.run("create liba --version=1.0") tc.run("create libb --version=1.0") tc.run("create liba --version=1.1") tc.run("create libb --version=1.1") tc.run("upload * -c -r default") tc.run('remove "*/1.1" -c') update_flag = f"--update={update}" if update is not None else "--update" tc.run(f'install --requires="liba/[>=1.0]" --requires="libb/[>=1.0]" -r default {update_flag}') tc.assert_listed_require(result) def test_update_remote_older_revision(): # https://github.com/conan-io/conan/issues/19313 c = TestClient(light=True, default_server_user=True) zlib = textwrap.dedent(""" from conan import ConanFile class Zlib(ConanFile): name = "zlib" version = "1.2.11" exports_sources = "*" """) c.save({"conanfile.py": zlib, "file.h": "//myheader"}) c.run("export") c.run("upload * -r=default -c") c2 = TestClient(light=True, servers=c.servers) c2.run("graph info --requires=zlib/[*]") rev1 = "2e87959c586811f8a4eaf12a327cc042" c2.assert_listed_require({f"zlib/1.2.11#{rev1}": "Downloaded (default)"}) # Modify zlib code c.save({"file.h": "//myheader 2"}) c.run("export") c.run("upload * -r=default -c") c2.run("graph info --requires=zlib/[*] --update") rev2 = "934b3de03768d9030b61127d588d0a96" c2.assert_listed_require({f"zlib/1.2.11#{rev2}": "Updated (default)"}) # revert this to old, make it latestlatest c.save({"file.h": "//myheader"}) c.run("export") c.run("remove * -r=default -c") c.run("upload * -r=default -c") c2.run("graph info --requires=zlib/[*] --update") assert ("Latest from 'default' was found in " "the cache, using it and updating its timestamp") in c2.out c2.assert_listed_require({f"zlib/1.2.11#{rev1}": "Updated (default)"}) c2.run("list *#* --format=json") revs = json.loads(c2.stdout)["Local Cache"]["zlib/1.2.11"]["revisions"] # we check that the update from the server has made rev1 the latest in the cache again assert revs[rev1]["timestamp"] > revs[rev2]["timestamp"] ================================================ FILE: test/integration/cache/download_cache_test.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save, set_dirty class TestDownloadCache: def test_download_skip(self): """ basic proof that enabling download_cache avoids downloading things again """ client = TestClient(default_server_user=True) # generate large random package file conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): def package(self): fileSizeInBytes = 11000000 with open(os.path.join(self.package_folder, "data.txt"), 'wb') as fout: fout.write(os.urandom(fileSizeInBytes)) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=mypkg --version=0.1 --user=user --channel=testing") client.run("upload * --confirm -r default") client.run("remove * -c") # enable cache tmp_folder = temp_folder() client.save_home({"global.conf": f"core.download:download_cache={tmp_folder}"}) client.run("install --requires=mypkg/0.1@user/testing") assert "mypkg/0.1@user/testing: Downloading" in client.out client.run("remove * -c") client.run("install --requires=mypkg/0.1@user/testing") assert "mypkg/0.1@user/testing: Downloading" not in client.out assert "conan_package.tgz from download cache, instead of downloading it" in client.out # removing the config downloads things client.save_home({"global.conf": ""}) client.run("remove * -c") client.run("install --requires=mypkg/0.1@user/testing") assert "mypkg/0.1@user/testing: Downloading" in client.out client.save_home({"global.conf": f"core.download:download_cache={tmp_folder}"}) client.run("remove * -c") client.run("install --requires=mypkg/0.1@user/testing") assert "mypkg/0.1@user/testing: Downloading" not in client.out assert "conan_package.tgz from download cache, instead of downloading it" in client.out def test_dirty_download(self): # https://github.com/conan-io/conan/issues/8578 client = TestClient(default_server_user=True) tmp_folder = temp_folder() client.save_home({"global.conf": f"core.download:download_cache={tmp_folder}"}) client.save({"conanfile.py": GenConanfile().with_package_file("file.txt", "content")}) client.run("create . --name=pkg --version=0.1") client.run("upload * -c -r default") client.run("remove * -c") client.run("install --requires=pkg/0.1@") # Make the cache dirty # The "c" internal folder must exist, it is the actual storage of blobs cache_folder = os.path.join(tmp_folder, "c") for f in os.listdir(cache_folder): # damage the file path = os.path.join(cache_folder, f) assert os.path.isfile(path) save(path, "broken!") set_dirty(path) client.run("remove * -c") client.run("install --requires=pkg/0.1@") assert "Downloading" in client.out client.run("remove * -c") client.run("install --requires=pkg/0.1@") # TODO assert "Downloading" not in client.out def test_user_downloads_cached_newtools(self): client = TestClient() file_server = TestFileServer() client.servers["file_server"] = file_server save(os.path.join(file_server.store, "myfile.txt"), "some content") save(os.path.join(file_server.store, "myfile2.txt"), "some query") save(os.path.join(file_server.store, "myfile3.txt"), "some content 3") tmp_folder = temp_folder() client.save_home({"global.conf": f"core.sources:download_cache={tmp_folder}"}) # badchecksums are not cached conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): def source(self): download(self, "%s/myfile.txt", "myfile.txt", md5="kk") """ % file_server.fake_url) client.save({"conanfile.py": conanfile}) client.run("source .", assert_error=True) assert "ConanException: md5 hash failed for" in client.out assert "Provided hash: kk" in client.out # There are 2 things in the cache, not sha256, no caching assert 0 == len(os.listdir(tmp_folder)) # Nothing was cached # This is the right checksum conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): def source(self): md5 = "9893532233caff98cd083a116b013c0b" md5_2 = "0dc8a17658b1c7cfa23657780742a353" sha256 = "bcc23055e479c1050455f5bb457088cfae3cbb2783f7579a7df9e33ea9f43429" download(self, "{0}/myfile.txt", "myfile.txt", md5=md5) download(self, "{0}/myfile3.txt", "myfile3.txt", sha256=sha256) download(self, "{0}/myfile.txt?q=myfile2.txt", "myfile2.txt", md5=md5_2) """).format(file_server.fake_url) client.save({"conanfile.py": conanfile}) client.run("source .") assert "some content" in client.load("myfile.txt") assert "some query" in client.load("myfile2.txt") assert "some content 3" in client.load("myfile3.txt") # remove remote and local files os.remove(os.path.join(file_server.store, "myfile3.txt")) os.remove(os.path.join(client.current_folder, "myfile.txt")) os.remove(os.path.join(client.current_folder, "myfile2.txt")) os.remove(os.path.join(client.current_folder, "myfile3.txt")) # Will use the cached one client.run("source .") assert "some content" == client.load("myfile.txt") assert "some query" == client.load("myfile2.txt") assert "some content 3" in client.load("myfile3.txt") # disabling cache will make it fail client.save_home({"global.conf": ""}) client.run("source .", assert_error=True) assert "ERROR: conanfile.py: Error in source() method, line 10" in client.out assert "Not found" in client.out def test_download_relative_error(self): """ relative paths are not allowed """ c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile().with_package_file("file.txt", "content")}) c.run("create . --name=mypkg --version=0.1 --user=user --channel=testing") c.run("upload * --confirm -r default") c.run("remove * -c") # enable cache c.save_home({"global.conf": f"core.download:download_cache=mytmp_folder"}) c.run("install --requires=mypkg/0.1@user/testing", assert_error=True) assert 'core.download:download_cache must be an absolute path' in c.out ================================================ FILE: test/integration/cache/rmdir_fail_test.py ================================================ import os import platform import pytest from conan.test.utils.tools import TestClient, GenConanfile @pytest.mark.skipif(platform.system() != "Windows", reason="needs windows") def test_fail_rmdir(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=mypkg --version=0.1 --user=lasote --channel=testing") build_folder = client.created_layout().build() f = open(os.path.join(build_folder, "myfile"), "wb") f.write(b"Hello world") client.run("install --requires=mypkg/0.1@lasote/testing --build=*", assert_error=True) assert "ERROR: Couldn't remove folder" in client.out ================================================ FILE: test/integration/cache/storage_path_test.py ================================================ import os import textwrap from hashlib import sha256 from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, GenConanfile def test_storage_path(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) tmp_folder = temp_folder(path_with_spaces=True) client.save_home({"global.conf": f"core.cache:storage_path={tmp_folder}"}) client.run("create . --name=mypkg --version=0.1") assert f"mypkg/0.1: Package folder {tmp_folder}" in client.out assert os.path.isfile(os.path.join(tmp_folder, "cache.sqlite3")) client.run("cache path mypkg/0.1") assert tmp_folder in client.out def test_wrong_home_error(): client = TestClient(light=True) client.save_home({"global.conf": "core.cache:storage_path=//"}) client.run("list *", assert_error=True) assert "Couldn't initialize storage in" in client.out def test_short_storage_path(): c = TestClient() global_conf = textwrap.dedent("""\ {% set h = hashlib.new("sha256", conan_home_folder.encode(), usedforsecurity=False).hexdigest() %} core.cache:storage_path=C:/conan_{{h[:6]}} """) c.save_home({"global.conf": global_conf}) c.run("config show *") myhash = sha256() myhash.update(c.cache_folder.replace("\\", "/").encode()) myhash = myhash.hexdigest()[:6] assert f"core.cache:storage_path: C:/conan_{myhash}" in c.out ================================================ FILE: test/integration/cache/test_home_special_char.py ================================================ import os import platform import pytest from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient import textwrap _path_chars = "päthñç$" @pytest.fixture(scope="module") def client_with_special_chars(): """ the path with special characters is creating a conanbuild.bat that fails """ cache_folder = os.path.join(temp_folder(), _path_chars) current_folder = os.path.join(temp_folder(), _path_chars) c = TestClient(cache_folder, current_folder) tool = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): name = "mytool" version = "1.0" def package(self): with chdir(self, self.package_folder): echo = "@echo off\necho MYTOOL WORKS!!" save(self, "bin/mytool.bat", echo) save(self, "bin/mytool.sh", echo) os.chmod("bin/mytool.sh", 0o777) """) c.save({"conanfile.py": tool}) c.run("create .") conan_file = textwrap.dedent(""" import platform from conan import ConanFile class App(ConanFile): name="failure" version="0.1" settings = 'os', 'arch', 'compiler', 'build_type' generators = "VirtualBuildEnv" tool_requires = "mytool/1.0" def build(self): mycmd = "mytool.bat" if platform.system() == "Windows" else "mytool.sh" self.run(mycmd) """) c.save({"conanfile.py": conan_file}) return c def test_reuse_buildenv(client_with_special_chars): c = client_with_special_chars # Need the 2 profile to work correctly buildenv c.run("create .") assert _path_chars in c.out assert "MYTOOL WORKS!!" in c.out @pytest.mark.skipif(platform.system() != "Windows", reason="powershell only win") def test_reuse_buildenv_powershell(client_with_special_chars): c = client_with_special_chars c.run("create . -c tools.env.virtualenv:powershell=True") assert _path_chars in c.out assert "MYTOOL WORKS!!" in c.out ================================================ FILE: test/integration/cache/test_package_revisions.py ================================================ from unittest import mock from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update def test_package_revision_latest(): # https://github.com/conan-io/conan/issues/14945 c = TestClient() c.save({"tool/conanfile.py": GenConanfile("tool", "0.1").with_package_file("file.txt", env_var="MYVAR"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_tool_requires("tool/0.1")}) with environment_update({"MYVAR": "MYVALUE1"}): # Just in case dates were not correctly processed with mock.patch("conan.internal.cache.cache.revision_timestamp_now", return_value="1691760295"): c.run("create tool") prev1 = c.created_package_revision("tool/0.1") with environment_update({"MYVAR": "MYVALUE2"}): # Just in case dates were not correctly processed with mock.patch("conan.internal.cache.cache.revision_timestamp_now", return_value="1697442658"): c.run("create tool") prev2 = c.created_package_revision("tool/0.1") c.run("create pkg") # The latest is used assert prev2 in c.out assert prev1 not in c.out c.run("install --requires=pkg/0.1 --build=pkg*") # The latest is used assert prev2 in c.out assert prev1 not in c.out ================================================ FILE: test/integration/cache/test_same_pref_removal.py ================================================ import os from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_same_pref_removal(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") c.run("install --requires=pkg/0.1 --build=pkg*") layout = c.created_layout() pkg_folder1 = layout.package() assert os.path.exists(os.path.join(pkg_folder1, "conanmanifest.txt")) assert os.path.exists(os.path.join(pkg_folder1, "conaninfo.txt")) c.run("install --requires=pkg/0.1 --build=pkg*") layout = c.created_layout() pkg_folder2 = layout.package() assert pkg_folder1 == pkg_folder2 assert os.path.exists(os.path.join(pkg_folder1, "conanmanifest.txt")) assert os.path.exists(os.path.join(pkg_folder1, "conaninfo.txt")) ================================================ FILE: test/integration/command/__init__.py ================================================ ================================================ FILE: test/integration/command/alias_test.py ================================================ import os import textwrap from conan.api.model import RecipeReference from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.utils.tools import TestClient, GenConanfile DepsGraphBuilder.ALLOW_ALIAS = True class TestConanAlias: def test_repeated_alias(self): client = TestClient(light=True) client.alias("hello/0.x@lasote/channel", "hello/0.1@lasote/channel") client.alias("hello/0.x@lasote/channel", "hello/0.2@lasote/channel") client.alias("hello/0.x@lasote/channel", "hello/0.3@lasote/channel") def test_basic(self): client = TestClient(light=True, default_server_user=True) for i in (1, 2): client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("0.%s" % i)}) client.run("export . --user=lasote --channel=channel") client.alias("hello/0.x@lasote/channel", "hello/0.1@lasote/channel") conanfile_chat = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): name = "chat" version = "1.0" requires = "hello/(0.x)@lasote/channel" """) client.save({"conanfile.py": conanfile_chat}, clean_first=True) client.run("export . --user=lasote --channel=channel") client.save({"conanfile.txt": "[requires]\nchat/1.0@lasote/channel"}, clean_first=True) client.run("install . --build=missing") assert ("chat/1.0@lasote/channel: WARN: legacy: Requirement 'alias' is provided in Conan 2" in client.out) client.assert_listed_require({"hello/0.1@lasote/channel": "Cache"}) assert "hello/0.x@lasote/channel: hello/0.1@lasote/channel" in client.out ref = RecipeReference.loads("chat/1.0@lasote/channel") pref = client.get_latest_package_reference(ref) pkg_folder = client.get_latest_pkg_layout(pref).package() conaninfo = client.load(os.path.join(pkg_folder, "conaninfo.txt")) assert "hello/0.1" in conaninfo assert "hello/0.x" not in conaninfo client.run('upload "*" --confirm -r default') client.run('remove "*" -c') client.run("install .") assert "'alias' is a Conan 1.X legacy" in client.out client.assert_listed_require({"hello/0.1@lasote/channel": "Downloaded (default)"}) assert "hello/0.x@lasote/channel from" not in client.out client.alias("hello/0.x@lasote/channel", "hello/0.2@lasote/channel") client.run("install . --build=missing") assert "hello/0.2" in client.out assert "hello/0.1" not in client.out def test_not_override_package(self): """ Do not override a package with an alias If we create an alias with the same name as an existing package, it will override the package without any warning. """ t = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): description = "{}" """) # Create two packages reference1 = "pkga/0.1@user/testing" t.save({"conanfile.py": conanfile.format(reference1)}) t.run("export . --name=pkga --version=0.1 --user=user --channel=testing") reference2 = "pkga/0.2@user/testing" t.save({"conanfile.py": conanfile.format(reference2)}) t.run("export . --name=pkga --version=0.2 --user=user --channel=testing") ================================================ FILE: test/integration/command/cache/__init__.py ================================================ ================================================ FILE: test/integration/command/cache/test_cache_clean.py ================================================ import os.path import re import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save @pytest.mark.parametrize("use_pkglist", [True, False]) def test_cache_clean(use_pkglist): c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports("*").with_exports_sources("*"), "sorces/file.txt": ""}) c.run("create .") ref_layout = c.exported_layout() pkg_layout = c.created_layout() c.run("upload * -c -r=default") # Force creation of tgzs assert os.path.exists(ref_layout.source()) assert os.path.exists(ref_layout.download_export()) assert os.path.exists(pkg_layout.build()) assert os.path.exists(pkg_layout.download_package()) if use_pkglist: c.run("list *:*#* -f=json", redirect_stdout="pkglist.json") arg = "--list=pkglist.json" if use_pkglist else "*" c.run(f'cache clean {arg} -s -b -v') assert f"{ref_layout.reference.repr_notime()}: Cleaning recipe cache contents" in c.out assert f"{pkg_layout.reference}: Cleaning package cache contents" in c.out assert not os.path.exists(pkg_layout.build()) assert not os.path.exists(ref_layout.source()) assert os.path.exists(ref_layout.download_export()) assert os.path.exists(pkg_layout.download_package()) c.run('cache clean -d') assert not os.path.exists(ref_layout.download_export()) assert not os.path.exists(pkg_layout.download_package()) c.run("cache clean -bs -v") assert "Cleaning 0 backup sources" in c.out def test_cache_clean_all(): c = TestClient(light=True) c.save({"pkg1/conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("revision_mode='scm'"), "pkg2/conanfile.py": GenConanfile("pkg", "0.2").with_package("error"), "pkg3/conanfile.py": GenConanfile("pkg", "0.3")}) c.run("create pkg1", assert_error=True) c.run("create pkg2", assert_error=True) c.run("create pkg3") pref = c.created_package_reference("pkg/0.3") tmp_folder = os.path.join(c.cache_folder, "p", "t") assert len(os.listdir(tmp_folder)) == 1 # Failed export was here builds_folder = os.path.join(c.cache_folder, "p", "b") assert len(os.listdir(builds_folder)) == 2 # both builds are here c.run('cache clean') assert not os.path.exists(tmp_folder) assert len(os.listdir(builds_folder)) == 1 # only correct pkg/0.3 remains # Check correct package removed all ref_layout = c.get_latest_ref_layout(pref.ref) pkg_layout = c.get_latest_pkg_layout(pref) assert not os.path.exists(ref_layout.source()) assert not os.path.exists(ref_layout.download_export()) assert not os.path.exists(pkg_layout.build()) assert not os.path.exists(pkg_layout.download_package()) # A second clean like this used to crash # as it tried to delete a folder that was not there and tripped shutils up c.run('cache clean') assert not os.path.exists(tmp_folder) def test_cache_multiple_builds_same_prev_clean(): """ Different consecutive builds will create exactly the same folder, for the same exact prev, not leaving trailing non-referenced folders """ c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") create_out = c.out c.run("cache path pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709") path1 = str(c.stdout) assert path1 in create_out c.run("create .") create_out = c.out c.run("cache path pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709") path2 = str(c.stdout) assert path2 in create_out assert path1 == path2 builds_folder = os.path.join(c.cache_folder, "p", "b") assert len(os.listdir(builds_folder)) == 1 # only one build c.run('cache clean') assert len(os.listdir(builds_folder)) == 1 # one build not cleaned c.run('remove * -c') assert len(os.listdir(builds_folder)) == 0 # no folder remain def test_cache_multiple_builds_diff_prev_clean(): """ Different consecutive builds will create different folders, even if for the same exact prev, leaving trailing non-referenced folders """ c = TestClient(light=True) package_lines = 'save(self, os.path.join(self.package_folder, "foo.txt"), str(time.time()))' gen = GenConanfile("pkg", "0.1").with_package(package_lines).with_import("import os, time") \ .with_import("from conan.tools.files import save") c.save({"conanfile.py": gen}) c.run("create .") create_out = c.out c.run("cache path pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709") path1 = str(c.stdout) assert path1 in create_out c.run("create .") create_out = c.out c.run("cache path pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709") path2 = str(c.stdout) assert path2 in create_out assert path1 != path2 builds_folder = os.path.join(c.cache_folder, "p", "b") assert len(os.listdir(builds_folder)) == 2 # both builds are here c.run('cache clean') assert len(os.listdir(builds_folder)) == 2 # two builds will remain, both are valid c.run('remove * -c') assert len(os.listdir(builds_folder)) == 0 # no folder remain def test_cache_clean_custom_storage(): c = TestClient() t = temp_folder(path_with_spaces=False) save(c.paths.global_conf_path, f"core.cache:storage_path={t}") c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_cmake_build()}) c.run("create .", assert_error=True) build_folder = re.search(r"pkg/0.1: Building your package in (\S+)", str(c.out)).group(1) assert os.listdir(build_folder) # now clean c.run("cache clean") assert not os.path.exists(build_folder) ================================================ FILE: test/integration/command/cache/test_cache_integrity.py ================================================ import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save @pytest.mark.parametrize("use_pkglist", [True, False]) @pytest.mark.parametrize("output_pkglist", [True, False]) def test_cache_integrity(use_pkglist, output_pkglist): t = TestClient(light=True) t.save({"conanfile.py": GenConanfile()}) t.run("create . --name pkg1 --version 1.0") t.run("create . --name pkg2 --version=2.0") layout = t.created_layout() conaninfo = os.path.join(layout.package(), "conaninfo.txt") save(conaninfo, "[settings]") t.run("create . --name pkg3 --version=3.0") layout = t.created_layout() conaninfo = os.path.join(layout.package(), "conaninfo.txt") save(conaninfo, "[settings]") t.run("create . --name pkg4 --version=4.0") layout = t.created_layout() conaninfo = os.path.join(layout.package(), "conaninfo.txt") save(conaninfo, "[settings]") t.run("create . --name pkg5 --version=5.0") layout = t.exported_layout() save(layout.conanfile(), "empty") if use_pkglist: t.run("list *:*#* -f=json", redirect_stdout="pkglist.json") arg = "--list=pkglist.json" if use_pkglist else "*" if output_pkglist: arg += " --format=json" t.run( f"cache check-integrity {arg}", assert_error=True, redirect_stdout="pkglist.json" if output_pkglist else None, ) assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: Integrity check: ok" in t.out assert "pkg2/2.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: ERROR: \nManifest mismatch" in t.out assert "pkg3/3.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: ERROR: \nManifest mismatch" in t.out assert "pkg4/4.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: ERROR: \nManifest mismatch" in t.out assert "pkg5/5.0#4d670581ccb765839f2239cc8dff8fbd: ERROR: \nManifest mismatch" in t.out if output_pkglist: t.run("remove --list=pkglist.json -c") else: t.run("remove pkg2/2.0:da39a3ee5e6b4b0d3255bfef95601890afd80709 -c") t.run("remove pkg3/3.0:da39a3ee5e6b4b0d3255bfef95601890afd80709 -c") t.run("remove pkg4/4.0:da39a3ee5e6b4b0d3255bfef95601890afd80709 -c") t.run("remove pkg5/5.0 -c") t.run("cache check-integrity *") assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "pkg2/2.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "pkg3/3.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "pkg4/4.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out @pytest.mark.parametrize("output_pkglist", [True, False]) def test_cache_integrity_missing_recipe_manifest(output_pkglist): t = TestClient() t.save({"conanfile.py": GenConanfile()}) t.run("create . --name pkg1 --version 1.0") t.run("create . --name pkg2 --version=2.0") layout = t.exported_layout() manifest = os.path.join(layout.export(), "conanmanifest.txt") os.remove(manifest) t.run("create . --name pkg3 --version=3.0") if output_pkglist: t.run("cache check-integrity * -f json", assert_error=True, redirect_stdout="pkglist.json") else: t.run("cache check-integrity *", assert_error=True) assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "pkg2/2.0#4d670581ccb765839f2239cc8dff8fbd: ERROR: Manifest missing" in t.out assert "pkg3/3.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "ERROR: There are corrupted artifacts, check the error logs" in t.out t.run(f"remove {'--list pkglist.json' if output_pkglist else 'pkg2*'} -c") t.run("cache check-integrity *") assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: Integrity check: ok" in t.out assert "pkg3/3.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "Integrity check: ok" in t.out @pytest.mark.parametrize("output_pkglist", [True, False]) def test_cache_integrity_missing_package_manifest(output_pkglist): t = TestClient() t.save({"conanfile.py": GenConanfile()}) t.run("create . --name pkg1 --version 1.0") t.run("create . --name pkg2 --version=2.0") layout = t.created_layout() manifest = os.path.join(layout.package(), "conanmanifest.txt") os.remove(manifest) t.run("create . --name pkg3 --version=3.0") if output_pkglist: t.run("cache check-integrity * -f json", assert_error=True, redirect_stdout="pkglist.json") else: t.run("cache check-integrity *", assert_error=True) assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "pkg2/2.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: ERROR: Manifest missing" in t.out assert "pkg3/3.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "ERROR: There are corrupted artifacts, check the error logs" in t.out t.run(f"remove {'--list pkglist.json' if output_pkglist else 'pkg2*'} -c") t.run("cache check-integrity *") assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: Integrity check: ok" in t.out assert "pkg3/3.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "Integrity check: ok" in t.out @pytest.mark.parametrize("output_pkglist", [True, False]) def test_cache_integrity_missing_package_conaninfo(output_pkglist): t = TestClient() t.save({"conanfile.py": GenConanfile()}) t.run("create . --name pkg1 --version 1.0") t.run("create . --name pkg2 --version=2.0") layout = t.created_layout() conaninfo = os.path.join(layout.package(), "conaninfo.txt") os.remove(conaninfo) if output_pkglist: t.run("cache check-integrity * -f json", assert_error=True, redirect_stdout="pkglist.json") else: t.run("cache check-integrity *", assert_error=True) assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd: Integrity check: ok" in t.out assert "pkg2/2.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: ERROR: \nManifest mismatch" in t.out t.run(f"remove {'--list pkglist.json' if output_pkglist else 'pkg2*'} -c") t.run("cache check-integrity *") assert "pkg1/1.0#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#0ba8627bd47edc3a501e8f0eb9a79e5e: Integrity check: ok" in t.out def test_cache_integrity_missing_package_file(): t = TestClient() t.save({"conanfile.py": GenConanfile().with_package_file("myfile", "mycontents!")}) t.run("create . --name pkg --version 1.0") layout = t.created_layout() os.remove(os.path.join(layout.package(), "myfile")) t.run("cache check-integrity *", assert_error=True) assert "pkg/1.0#2f2609c8e5c87bf836c3fdaa6096b55d:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ "#d950d0cd76f6bba62c8add9c68d1aeb3: ERROR: \nManifest mismatch" in t.out def test_cache_integrity_export_sources(): # https://github.com/conan-io/conan/issues/14840 t = TestClient(default_server_user=True) t.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports_sources("src/*"), "src/mysource.cpp": ""}) t.run("create .") t.run("cache check-integrity *") assert "pkg/0.1#ae07161b81ab07f7f1f746391668df0e: Integrity check: ok" in t.out # If we download, integrity should be ok # (it failed before, because the manifest is not complete) t.run("upload * -r=default -c") t.run("remove * -c") t.run("install --requires=pkg/0.1") t.run("cache check-integrity *") assert "pkg/0.1#ae07161b81ab07f7f1f746391668df0e: Integrity check: ok" in t.out ================================================ FILE: test/integration/command/cache/test_cache_path.py ================================================ import json import os import platform import pytest from conan.api.model import RecipeReference, PkgReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def created_package(): t = TestClient(light=True) t.save({"conanfile.py": GenConanfile()}) t.run("create . --name foo --version 1.0") recipe_layout = t.exported_layout() pkg_layout = t.created_layout() return t, recipe_layout, pkg_layout def test_cache_path(created_package): t, recipe_layout, pkg_layout = created_package # Basic recipe paths, without specifying revision recipe_revision = recipe_layout.reference.revision pref = pkg_layout.reference t.run("cache path foo/1.0") assert recipe_layout.export() == str(t.out).rstrip() t.run("cache path foo/1.0#latest") assert recipe_layout.export() == str(t.out).rstrip() t.run("cache path foo/1.0 --folder=export_source") assert recipe_layout.export_sources() == str(t.out).rstrip() t.run("cache path foo/1.0 --folder=source") assert recipe_layout.source() == str(t.out).rstrip() # Basic recipe paths, with revision t.run(f"cache path foo/1.0#{recipe_revision}") assert recipe_layout.export() == str(t.out).rstrip() t.run(f"cache path foo/1.0#{recipe_revision} --folder=export_source") assert recipe_layout.export_sources() == str(t.out).rstrip() t.run(f"cache path foo/1.0#{recipe_revision} --folder=source") assert recipe_layout.source() == str(t.out).rstrip() # Basic package paths, without specifying revision t.run(f"cache path foo/1.0:{pref.package_id}") assert pkg_layout.package() == str(t.out).rstrip() t.run(f"cache path foo/1.0:{pref.package_id} --folder=build") assert pkg_layout.build() == str(t.out).rstrip() # Basic package paths, with recipe-revision t.run(f"cache path foo/1.0#{recipe_revision}:{pref.package_id}") assert pkg_layout.package() == str(t.out).rstrip() t.run(f"cache path foo/1.0#{recipe_revision}:{pref.package_id}#latest") assert pkg_layout.package() == str(t.out).rstrip() t.run(f"cache path foo/1.0#{recipe_revision}:{pref.package_id} --folder=build") assert pkg_layout.build() == str(t.out).rstrip() # Basic package paths, with both revisions t.run(f"cache path foo/1.0#{recipe_revision}:{pref.package_id}#{pref.revision}") assert pkg_layout.package() == str(t.out).rstrip() t.run(f"cache path foo/1.0#{recipe_revision}:{pref.package_id}#{pref.revision} --folder=build") assert pkg_layout.build() == str(t.out).rstrip() def test_cache_path_exist_errors(created_package): t, recipe_layout, pkg_layout = created_package recipe_revision = recipe_layout.reference.revision pref = pkg_layout.reference t.run("cache path nonexist/1.0", assert_error=True) assert "ERROR: Recipe 'nonexist/1.0' not found" in t.out t.run("cache path nonexist/1.0#rev", assert_error=True) assert "ERROR: Recipe 'nonexist/1.0#rev' not found" in t.out t.run("cache path foo/1.0#rev", assert_error=True) # TODO: Improve this error message assert "ERROR: Recipe 'foo/1.0#rev' not found" in t.out t.run(f"cache path foo/1.0:pid1", assert_error=True) assert f"ERROR: 'foo/1.0#{recipe_revision}:pid1' not found in cache" in t.out t.run(f"cache path foo/1.0#{recipe_revision}:pid1", assert_error=True) assert f"ERROR: 'foo/1.0#{recipe_revision}:pid1' not found in cache" in t.out t.run(f"cache path foo/1.0#{recipe_revision}:{pref.package_id}#rev2", assert_error=True) assert f"ERROR: No entry for package 'foo/1.0#{recipe_revision}:{pref.package_id}#rev2" in t.out def test_cache_path_arg_errors(): t = TestClient() # build, cannot obtain build without pref t.run("cache path foo/1.0 --folder build", assert_error=True) assert "ERROR: '--folder build' requires a valid package reference" in t.out # Invalid reference t.run("cache path patata", assert_error=True) assert "ERROR: patata is not a valid recipe reference" in t.out # source, cannot obtain build without pref t.run("cache path foo/1.0:pid --folder source", assert_error=True) assert "ERROR: '--folder source' requires a recipe reference" in t.out def test_cache_path_does_not_exist_folder(): client = TestClient(default_server_user=True) conanfile = GenConanfile() client.save({"conanfile.py": conanfile}) client.run("create . --name=mypkg --version=0.1") pref = client.created_package_reference("mypkg/0.1") client.run("upload * --confirm -r default") client.run("remove * -c") client.run(f"install --requires mypkg/0.1") client.run(f"cache path {pref} --folder build", assert_error=True) assert f"ERROR: 'build' folder does not exist for the reference {pref}" in client.out def test_cache_path_output_json(): client = TestClient() conanfile = GenConanfile("mypkg", "0.1") client.save({"conanfile.py": conanfile}) client.run("export .") layout = client.exported_layout() client.run("cache path mypkg/0.1 --format=json") output = json.loads(client.stdout) assert output == {"cache_path": os.path.join(layout.base_folder, "e")} class TestCacheRef: def test_cache_path(self, created_package): t, recipe_layout, pkg_layout = created_package t.run(f'cache ref "{os.path.dirname(recipe_layout.export())}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{recipe_layout.export()}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{recipe_layout.source()}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{os.path.dirname(pkg_layout.package())}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{pkg_layout.build()}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{pkg_layout.package()}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out def test_downloaded_paths(self): t = TestClient(light=True, default_server_user=True) t.save({"conanfile.py": GenConanfile()}) t.run("create . --name foo --version 1.0") t.run("upload * -c -r=default") t.run("remove * -c") t.run("install --requires=foo/1.0") ref = RecipeReference.loads("foo/1.0#4d670581ccb765839f2239cc8dff8fbd") pref = PkgReference(ref, "da39a3ee5e6b4b0d3255bfef95601890afd80709") recipe_layout = t.get_latest_ref_layout(ref) pkg_layout = t.get_latest_pkg_layout(pref) t.run(f'cache ref "{os.path.dirname(recipe_layout.export())}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{recipe_layout.export()}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{os.path.dirname(pkg_layout.package())}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out t.run(f'cache ref "{pkg_layout.package()}"') assert f"foo/1.0#{recipe_layout.reference.revision}" in t.out def test_errors(self): t = TestClient(light=True) t.run('cache ref "/some/non/existing/path"', assert_error=True) assert "ERROR: Reference for this path not found in cache" in t.out if platform.system() == "Windows": t.run("cache ref J:/not/path", assert_error=True) assert "ERROR: Invalid path: J:/not/path" in t.out ================================================ FILE: test/integration/command/cache/test_cache_save_restore.py ================================================ import json import os import platform import shutil import sys import tarfile import time import pytest from conan.api.model import PkgReference, RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID from conan.internal.util.files import save, load def test_cache_save_restore(): c = TestClient() c.save({"conanfile.py": GenConanfile().with_settings("os")}) c.run("create . --name=pkg --version=1.0 -s os=Linux") c.run("create . --name=pkg --version=1.1 -s os=Linux") c.run("create . --name=other --version=2.0 -s os=Linux") # Force the compress level just to make sure it doesn't crash c.run("cache save pkg/*:* -cc core.gzip:compresslevel=9") cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz") assert os.path.exists(cache_path) _validate_restore(cache_path) # Lets test that the pkglist does not contain windows backslash paths to make it portable with open(cache_path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) fileobj = the_tar.extractfile("pkglist.json") pkglist = fileobj.read() the_tar.close() package_list = json.loads(pkglist) assert "\\" not in package_list def test_cache_save_restore_with_package_file(): """If we have some sources in the root (like the CMakeLists.txt) we don't declare folders.source""" conan_file = GenConanfile() \ .with_settings("os") \ .with_package_file("bin/file.txt", "content!!") client = TestClient() client.save({"conanfile.py": conan_file}) client.run("create . --name=pkg --version=1.0 -s os=Linux") client.run("cache save pkg/*:* ") cache_path = os.path.join(client.current_folder, "conan_cache_save.tgz") assert os.path.exists(cache_path) c2 = TestClient() shutil.copy2(cache_path, c2.current_folder) c2.run("cache restore conan_cache_save.tgz") c2.run("list *:*#*") assert "pkg/1.0" in c2.out tree = _get_directory_tree(c2.base_folder) # Restore again, expect the tree to be unchanged c2.run("cache restore conan_cache_save.tgz") c2.run("list *:*#*") assert "pkg/1.0" in c2.out tree2 = _get_directory_tree(c2.base_folder) assert tree2 == tree def test_cache_save_downloaded_restore(): """ what happens if we save packages downloaded from server, not created """ c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile().with_settings("os")}) c.run("create . --name=pkg --version=1.0 -s os=Linux") c.run("create . --name=pkg --version=1.1 -s os=Linux") c.run("create . --name=other --version=2.0 -s os=Linux") c.run("upload * -r=default -c") c.run("remove * -c") c.run("download *:* -r=default --metadata=*") c.run("cache save pkg/*:* ") cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz") assert os.path.exists(cache_path) _validate_restore(cache_path) def _get_directory_tree(base_folder): tree = [] for d, _, fs in os.walk(base_folder): rel_d = os.path.relpath(d, base_folder) if d != base_folder else "" if rel_d: tree.append(rel_d) for f in fs: tree.append(os.path.join(rel_d, f)) tree.sort() return tree def _validate_restore(cache_path): c2 = TestClient() # Create a package in the cache to check put doesn't interact badly c2.save({"conanfile.py": GenConanfile().with_settings("os")}) c2.run("create . --name=pkg2 --version=3.0 -s os=Windows") shutil.copy2(cache_path, c2.current_folder) c2.run("cache restore conan_cache_save.tgz") c2.run("list *:*#*") assert "pkg2/3.0" in c2.out assert "pkg/1.0" in c2.out assert "pkg/1.1" in c2.out assert "other/2.0" not in c2.out tree = _get_directory_tree(c2.base_folder) # Restore again, just in case c2.run("cache restore conan_cache_save.tgz") c2.run("list *:*#*") assert "pkg2/3.0" in c2.out assert "pkg/1.0" in c2.out assert "pkg/1.1" in c2.out assert "other/2.0" not in c2.out tree2 = _get_directory_tree(c2.base_folder) assert tree2 == tree def test_cache_save_excluded_folders(): # https://github.com/conan-io/conan/issues/18234 c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile().with_exports("*.py").with_exports_sources("*.c"), "somefile.py": "", "mysrc.c": ""}) c.run("create . --name=pkg --version=1.0") ref_layout = c.exported_layout() pkg_layout = c.created_layout() c.run("upload * --dry-run -r=default -c") assert os.path.exists(os.path.join(ref_layout.download_export(), "conan_export.tgz")) assert os.path.exists(os.path.join(ref_layout.download_export(), "conan_sources.tgz")) assert os.path.exists(os.path.join(ref_layout.source(), "mysrc.c")) assert os.path.exists(os.path.join(pkg_layout.download_package(), "conan_package.tgz")) assert os.path.exists(pkg_layout.build()) c.run("cache save *:*") cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz") c2 = TestClient() shutil.copy2(cache_path, c2.current_folder) c2.run("cache restore conan_cache_save.tgz") ref = RecipeReference.loads("pkg/1.0") ref_layout = c2.get_latest_ref_layout(ref) pkg_layout = c2.get_latest_pkg_layout(PkgReference(ref_layout.reference, NO_SETTINGS_PACKAGE_ID)) assert os.path.exists(os.path.join(ref_layout.source(), "mysrc.c")) assert not os.path.exists(os.path.join(ref_layout.download_export(), "conan_export.tgz")) assert not os.path.exists(os.path.join(ref_layout.download_export(), "conan_sources.tgz")) assert not os.path.exists(os.path.join(pkg_layout.download_package(), "conan_package.tgz")) assert not os.path.exists(pkg_layout.build()) # exclude source c.run("cache save * --no-source") c3 = TestClient() shutil.copy2(cache_path, c3.current_folder) c3.run("cache restore conan_cache_save.tgz") ref_layout = c3.get_latest_ref_layout(ref) assert not os.path.exists(os.path.join(ref_layout.source(), "mysrc.c")) def test_cache_save_restore_metadata(): c = TestClient() c.save({"conanfile.py": GenConanfile().with_settings("os")}) c.run("create . --name=pkg --version=1.0 -s os=Linux") pid = c.created_package_id("pkg/1.0") # Add some metadata c.run("cache path pkg/1.0 --folder=metadata") metadata_path = str(c.stdout).strip() myfile = os.path.join(metadata_path, "logs", "mylogs.txt") save(myfile, "mylogs!!!!") c.run(f"cache path pkg/1.0:{pid} --folder=metadata") pkg_metadata_path = str(c.stdout).strip() myfile = os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt") save(myfile, "mybuildlogs!!!!") c.run("cache save pkg/*:* ") cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz") assert os.path.exists(cache_path) # restore and check c2 = TestClient() shutil.copy2(cache_path, c2.current_folder) c2.run("cache restore conan_cache_save.tgz") c2.run("cache path pkg/1.0 --folder=metadata") metadata_path = str(c2.stdout).strip() myfile = os.path.join(metadata_path, "logs", "mylogs.txt") assert load(myfile) == "mylogs!!!!" c2.run(f"cache path pkg/1.0:{pid} --folder=metadata") pkg_metadata_path = str(c2.stdout).strip() myfile = os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt") assert load(myfile) == "mybuildlogs!!!!" # FIXME: check the timestamps of the conan cache restore @pytest.mark.skipif(platform.system() == "Windows", reason="Fails in windows in ci because of the low precission of the clock") def test_cache_save_restore_multiple_revisions(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") rrev1 = c.exported_recipe_revision() time.sleep(0.2) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("var=42")}) c.run("create .") rrev2 = c.exported_recipe_revision() time.sleep(0.2) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("var=123")}) c.run("create .") rrev3 = c.exported_recipe_revision() def check_ordered_revisions(client): client.run("list *#* --format=json") revisions = json.loads(client.stdout)["Local Cache"]["pkg/0.1"]["revisions"] assert revisions[rrev1]["timestamp"] < revisions[rrev2]["timestamp"] assert revisions[rrev2]["timestamp"] < revisions[rrev3]["timestamp"] check_ordered_revisions(c) c.run("cache save pkg/*#*:* ") cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz") # restore and check c2 = TestClient() shutil.copy2(cache_path, c2.current_folder) c2.run("cache restore conan_cache_save.tgz") check_ordered_revisions(c2) def test_cache_save_restore_graph(): """ It is possible to save package list """ c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1")}) c.run("create dep") c.run("create pkg --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --format=json", redirect_stdout="list.json") c.run("cache save --file=cache.tgz --list=list.json") cache_path = os.path.join(c.current_folder, "cache.tgz") assert os.path.exists(cache_path) c2 = TestClient() # Create a package in the cache to check put doesn't interact badly c2.save({"conanfile.py": GenConanfile().with_settings("os")}) c2.run("create . --name=pkg2 --version=3.0 -s os=Windows") shutil.copy2(cache_path, c2.current_folder) c2.run("cache restore cache.tgz") c2.run("list *:*#*") assert "pkg/0.1" in c2.out assert "dep/0.1" in c2.out def test_cache_save_subfolder(): """ It is possible to save package list in subfolder that doesn't exist https://github.com/conan-io/conan/issues/15362 """ c = TestClient() c.save({"conanfile.py": GenConanfile("dep", "0.1")}) c.run("export .") c.run("cache save * --file=subfolder/cache.tgz") assert os.path.exists(os.path.join(c.current_folder, "subfolder", "cache.tgz")) def test_error_restore_not_existing(): c = TestClient() c.run("cache restore potato.tgz", assert_error=True) assert "ERROR: Restore archive doesn't exist in " in c.out @pytest.mark.parametrize("src_store", (False, True)) @pytest.mark.parametrize("dst_store", (False, True)) def test_cache_save_restore_custom_storage_path(src_store, dst_store): c = TestClient() if src_store: tmp_folder = temp_folder() c.save_home({"global.conf": f"core.cache:storage_path={tmp_folder}"}) c.save({"conanfile.py": GenConanfile()}) c.run("create . --name=pkg --version=1.0") c.run("cache save *:*") cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz") c2 = TestClient() if dst_store: tmp_folder = temp_folder() c2.save_home({"global.conf": f"core.cache:storage_path={tmp_folder}"}) shutil.copy2(cache_path, c2.current_folder) c2.run("cache restore conan_cache_save.tgz") c2.run("list *:*") assert "pkg/1.0" in c2.out @pytest.mark.parametrize("compress", ["gz", "xz", "zst"]) def test_cache_save_restore_compressions(compress): """ we accept different compressions formats""" if compress == "zst" and sys.version_info.minor < 14: pytest.skip("Skipping zst compression tests") conan_file = GenConanfile() \ .with_settings("os") \ .with_package_file("bin/file.txt", "content!!") client = TestClient() client.save({"conanfile.py": conan_file}) client.run("create . --name=pkg --version=1.0 -s os=Linux") client.run(f"cache save pkg/*:* --file=mysave.t{compress}") if compress in ("xz", "zst"): assert f"WARN: experimental: The '{compress}' compression is experimental." in client.out cache_path = os.path.join(client.current_folder, f"mysave.t{compress}") assert os.path.exists(cache_path) c2 = TestClient() shutil.copy2(cache_path, c2.current_folder) c2.run(f"cache restore mysave.t{compress}") c2.run("list *:*#*") assert "pkg/1.0" in c2.out ================================================ FILE: test/integration/command/cache/test_cache_sign.py ================================================ import json import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient PLUGIN_CONTENT = textwrap.dedent(""" import os from conan.internal.util.files import save # Only for testing purposes def sign(ref, artifacts_folder, signature_folder, **kwargs): save(os.path.join(signature_folder, "signature.sig"), "signed-content") return [{ "method": "dummy-method", "provider": "dummy-provider", "sign_artifacts": {"signature": "signature.sig"} }] def verify(ref, artifacts_folder, signature_folder, files, **kwargs): pass """) def test_pkg_sign_no_plugin(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run("cache sign *", assert_error=True) assert "ERROR: The sign() function in the package sign plugin is not defined." in c.out c.run("cache verify *", assert_error=True) assert "ERROR: The verify() function in the package sign plugin is not defined." in c.out def test_pkg_sign_no_plugin_functions(): c = TestClient() c.save_home({"extensions/plugins/sign/sign.py": ""}) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run("cache sign *", assert_error=True) assert "ERROR: The sign() function in the package sign plugin is not defined." in c.out c.run("cache verify *", assert_error=True) assert "ERROR: The verify() function in the package sign plugin is not defined." in c.out def test_pkg_sign_verify_basic(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.save_home({"extensions/plugins/sign/sign.py": PLUGIN_CONTENT}) c.run("create .") c.run("cache sign *") assert textwrap.dedent("""\ [Package sign] Results: pkg/0.1 revisions 485dad6cb11e2fa99d9afbe44a57a164 packages da39a3ee5e6b4b0d3255bfef95601890afd80709 revisions 0ba8627bd47edc3a501e8f0eb9a79e5e [Package sign] Summary: OK=2, FAILED=0""") in c.out c.run("cache sign * -f json") conanfile_dict = json.loads(c.stdout)["pkg/0.1"]["revisions"]["485dad6cb11e2fa99d9afbe44a57a164"] package_dict = conanfile_dict["packages"]["da39a3ee5e6b4b0d3255bfef95601890afd80709"] \ ["revisions"]["0ba8627bd47edc3a501e8f0eb9a79e5e"] assert list(conanfile_dict["files"].keys()) == ["conanfile.py", "conanmanifest.txt"] assert list(package_dict["files"].keys()) == ["conan_package.tgz", "conaninfo.txt", "conanmanifest.txt"] c.run("cache verify *") assert textwrap.dedent(""" [Package sign] Results: pkg/0.1 revisions 485dad6cb11e2fa99d9afbe44a57a164 packages da39a3ee5e6b4b0d3255bfef95601890afd80709 revisions 0ba8627bd47edc3a501e8f0eb9a79e5e [Package sign] Summary: OK=2, FAILED=0""") in c.out c.run("cache verify * -f json") conanfile_dict = json.loads(c.stdout)["pkg/0.1"]["revisions"]["485dad6cb11e2fa99d9afbe44a57a164"] package_dict = conanfile_dict["packages"]["da39a3ee5e6b4b0d3255bfef95601890afd80709"] \ ["revisions"]["0ba8627bd47edc3a501e8f0eb9a79e5e"] assert list(conanfile_dict["files"].keys()) == ["conanfile.py", "conanmanifest.txt"] assert list(package_dict["files"].keys()) == ["conan_package.tgz", "conaninfo.txt", "conanmanifest.txt"] def test_pkg_sign_no_packages(): c = TestClient() c.save_home({"extensions/plugins/sign/sign.py": PLUGIN_CONTENT}) c.run("cache sign other-pkg/*", assert_error=True) assert "ERROR: No packages to process in the package list provided" in c.out c.run("cache verify other-pkg/*", assert_error=True) assert "ERROR: No packages to process in the package list provided" in c.out def test_pkg_sign_exception(): c = TestClient() signer = textwrap.dedent(r""" import os from conan.errors import ConanException from conan.tools.files import save def sign(ref, artifacts_folder, signature_folder, **kwargs): if "lib" in ref.repr_notime(): raise ConanException("Error signing package") save(None, os.path.join(signature_folder, "signature.sig"), "signed-content") return [{ "method": "dummy-method", "provider": "dummy-provider", "sign_artifacts": {"signature": "signature.sig"} }] """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") c.save({"conanfile.py": GenConanfile("lib", "0.1")}) c.run("export .") c.save({"conanfile.py": GenConanfile("package", "0.1")}) c.run("export .") c.run("cache sign *", assert_error=True) assert textwrap.dedent("""\ [Package sign] Results: lib/0.1 revisions dbe307e08b1a344fef76f60c85c0c4e8 ERROR: Error signing package package/0.1 revisions 1fd0e5bcc411dcd3ff5b16024e2d7c04 pkg/0.1 revisions 485dad6cb11e2fa99d9afbe44a57a164 [Package sign] Summary: OK=2, FAILED=1""") in c.out # test json output c.run("cache sign * -f json", assert_error=True) assert "ERROR: There were some errors in the package signing process. " \ "Please check the output." in c.out results = json.loads(c.stdout) assert results["lib/0.1"]["revisions"]["dbe307e08b1a344fef76f60c85c0c4e8"]["pkgsign_error"] == \ "Error signing package" def test_pkg_verify_exception(): c = TestClient() signer = textwrap.dedent(r""" import os from conan.internal.util.files import save # Only for testing purposes from conan.errors import ConanException def sign(ref, artifacts_folder, signature_folder, **kwargs): save(os.path.join(signature_folder, "signature.sig"), "signed-content") return [{ "method": "dummy-method", "provider": "dummy-provider", "sign_artifacts": {"signature": "signature.sig"} }] def verify(ref, artifacts_folder, signature_folder, files, **kwargs): if "lib" in ref.repr_notime(): raise ConanException("Wrong signature") """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") c.save({"conanfile.py": GenConanfile("lib", "0.1")}) c.run("export .") c.save({"conanfile.py": GenConanfile("package", "0.1")}) c.run("export .") c.run("cache sign" " *") # First sign all packages to generate manifests c.run("cache verify *", assert_error=True) assert textwrap.dedent("""\ [Package sign] Results: lib/0.1 revisions dbe307e08b1a344fef76f60c85c0c4e8 ERROR: Wrong signature package/0.1 revisions 1fd0e5bcc411dcd3ff5b16024e2d7c04 pkg/0.1 revisions 485dad6cb11e2fa99d9afbe44a57a164 [Package sign] Summary: OK=2, FAILED=1""") in c.out # test json output c.run("cache verify * -f json", assert_error=True) assert "ERROR: There were some errors in the package signing process. " \ "Please check the output." in c.out results = json.loads(c.stdout) assert results["lib/0.1"]["revisions"]["dbe307e08b1a344fef76f60c85c0c4e8"]["pkgsign_error"] == \ "Wrong signature" def test_pkg_sign_verify_pkglist(): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.save_home({"extensions/plugins/sign/sign.py": PLUGIN_CONTENT}) c.run("create .") # test empty package list c.run("list no-exist/* -f json", redirect_stdout="pkglist.json") c.run("cache sign -l pkglist.json", assert_error=True) assert "ERROR: No packages to process in the package list provided" in c.out c.run("cache verify -l pkglist.json", assert_error=True) assert "ERROR: No packages to process in the package list provided" in c.out # test incomplete package list c.run("list */* -f json", redirect_stdout="pkglist.json") c.run("cache sign -l pkglist.json", assert_error=True) assert "ERROR: No packages to process in the package list provided" in c.out c.run("cache verify -l pkglist.json", assert_error=True) assert "ERROR: No packages to process in the package list provided" in c.out # test recipe latest package list c.run("list */*#latest -f json", redirect_stdout="pkglist.json") c.run("cache sign -l pkglist.json") expected = textwrap.dedent("""\ [Package sign] Results: pkg/0.1 revisions 485dad6cb11e2fa99d9afbe44a57a164 [Package sign] Summary: OK=1, FAILED=0""") assert expected in c.out c.run("cache verify -l pkglist.json") assert expected in c.out # test packages without prev package list c.run("list */*:* -f json", redirect_stdout="pkglist.json") # FIXME: list command is returning packages without package revision, so packages are not signed c.run("cache sign -l pkglist.json") expected = textwrap.dedent("""\ [Package sign] Results: pkg/0.1 revisions 485dad6cb11e2fa99d9afbe44a57a164 packages da39a3ee5e6b4b0d3255bfef95601890afd80709 [Package sign] Summary: OK=1, FAILED=0""") assert expected in c.out c.run("cache verify -l pkglist.json") assert expected in c.out # test packages with prev package list c.run("list */*:*#latest -f json", redirect_stdout="pkglist.json") c.run("cache sign -l pkglist.json") expected = textwrap.dedent("""\ [Package sign] Results: pkg/0.1 revisions 485dad6cb11e2fa99d9afbe44a57a164 packages da39a3ee5e6b4b0d3255bfef95601890afd80709 revisions 0ba8627bd47edc3a501e8f0eb9a79e5e [Package sign] Summary: OK=2, FAILED=0""") assert expected in c.out c.run("cache verify -l pkglist.json") assert expected in c.out ================================================ FILE: test/integration/command/config_test.py ================================================ import json import os import textwrap import pytest from conan.api.conan_api import ConanAPI from conan.test.assets.genconanfile import GenConanfile from conan.internal.model.conf import BUILT_IN_CONFS from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update def test_missing_subarguments(): """ config MUST run with a subcommand. Otherwise, it MUST exits with error. """ client = TestClient() client.run("config", assert_error=True) assert "ERROR: Exiting with code: 2" in client.out class TestConfigHome: """ The test framework cannot test the CONAN_HOME env-var because it is not using it (it will break tests for maintainers that have the env-var defined) """ def test_config_home_default(self): client = TestClient() client.run("config home") assert f"{client.cache_folder}\n" == client.stdout client.run("config home --format=text", assert_error=True) # It is not possible to use --format=text explicitly assert "--format=text" in client.out def test_api_uses_env_var_home(self): cache_folder = os.path.join(temp_folder(), "custom") with environment_update({"CONAN_HOME": cache_folder}): api = ConanAPI() assert api.cache_folder == cache_folder def test_config_list(): """ 'conan config list' shows all the built-in Conan configurations """ client = TestClient() client.run("config list") for k, v in BUILT_IN_CONFS.items(): assert f"{k}: {v}" in client.out client.run("config list --format=json") assert f"{json.dumps(BUILT_IN_CONFS, indent=4)}\n" == client.stdout client.run("config list cmake") assert "tools.cmake:cmake_program: Path to CMake executable" in client.out assert "core.download:parallel" not in client.out assert "tools.build:verbosity" not in client.out def test_config_install(): tc = TestClient() tc.save({'config/foo': ''}) # This should not fail (insecure flag exists) tc.run("config install config --insecure") assert "foo" in os.listdir(tc.cache_folder) # Negative test, ensure we would be catching a missing arg if it did not exist tc.run("config install config --superinsecure", assert_error=True) def test_config_install_conanignore(): tc = TestClient() conanignore = textwrap.dedent(""" a/* # This is a tests b/c/* d/* tests/* # Next line is commented out, so it should be ignored # other_tests/* !b/c/important_file !b/c/important_folder/* """) tc.save({ 'config_folder/.conanignore': conanignore, 'config_folder/a/test': '', 'config_folder/abracadabra': '', 'config_folder/b/bison': '', 'config_folder/b/a/test2': '', 'config_folder/b/c/helmet': '', 'config_folder/b/c/important_file': '', 'config_folder/b/c/important_folder/contents': '', 'config_folder/d/prix': '', 'config_folder/d/foo/bar': '', 'config_folder/foo': '', 'config_folder/tests/tester': '', 'config_folder/other_tests/tester2': '' }) def _assert_config_exists(path): assert os.path.exists(os.path.join(tc.cache_folder, path)) def _assert_config_not_exists(path): assert not os.path.exists(os.path.join(tc.cache_folder, path)) tc.run('config install config_folder') _assert_config_not_exists(".conanignore") _assert_config_not_exists("a") _assert_config_not_exists("a/test") _assert_config_exists("abracadabra") _assert_config_exists("b") _assert_config_exists("b/bison") _assert_config_exists("b/a/test2") _assert_config_not_exists("b/c/helmet") _assert_config_exists("b/c") _assert_config_exists("b/c/important_file") _assert_config_exists("b/c/important_folder/contents") _assert_config_not_exists("d/prix") _assert_config_not_exists("d/foo/bar") _assert_config_not_exists("d") _assert_config_exists("foo") _assert_config_not_exists("tests/tester") _assert_config_exists("other_tests/tester2") def test_config_install_conanignore_ignore_all_allow_specific_workflow(): tc = TestClient() conanignore = textwrap.dedent(""" * !important_folder/* !important_file # We can even include the conanignore that we skip by default! !.conanignore """) tc.save({ 'config_folder/.conanignore': conanignore, 'config_folder/a/test': '', 'config_folder/abracadabra': '', 'config_folder/important_folder/contents': '', 'config_folder/important_file': '', }) def _assert_config_exists(path): assert os.path.exists(os.path.join(tc.cache_folder, path)) def _assert_config_not_exists(path): assert not os.path.exists(os.path.join(tc.cache_folder, path)) tc.run('config install config_folder') _assert_config_exists(".conanignore") _assert_config_not_exists("a") _assert_config_not_exists("abracadabra") _assert_config_exists("important_folder/contents") _assert_config_exists("important_file") @pytest.mark.parametrize("has_conanignore", [True, False]) @pytest.mark.parametrize("folder", [None, "myfolder", "myfolder/subfolder"]) def test_config_install_conanignore_walk_directories(has_conanignore, folder): tc = TestClient(light=True) if has_conanignore: conanignore = "*" tc.save({"config_folder/.conanignore": conanignore}) tc.save({"config_folder/myfolder/subfolder/item.py": ""}) folder_arg = f"-sf {folder} -tf {folder}" if folder else "" tc.run(f"config install config_folder {folder_arg}") if has_conanignore: assert not os.path.exists(os.path.join(tc.cache_folder, "myfolder", "subfolder", "item.py")) else: assert os.path.exists(os.path.join(tc.cache_folder, "myfolder", "subfolder", "item.py")) def test_config_show(): globalconf = textwrap.dedent(""" tools.build:jobs=42 tools.files.download:retry_wait=10 tools.files.download:retry=7 core.net.http:timeout=30 core.net.http:max_retries=5 zlib/*:user.mycategory:retry=True zlib/*:user.mycategory:foo=0 zlib/*:user.myothercategory:foo=0 """) tc = TestClient() tc.save_home({"global.conf": globalconf}) tc.run("config show tools.build:jobs") assert "42" in tc.out tc.run("config show core*") assert "core.net.http:timeout" in tc.out assert "30" in tc.out assert "core.net.http:max_retries" in tc.out assert "5" in tc.out tc.run("config show *retr*") assert "tools.files.download:retry_wait" in tc.out assert "tools.files.download:retry" in tc.out assert "core.net.http:max_retries" in tc.out assert "zlib/*:user.mycategory:retry" in tc.out tc.run("config show zlib*") assert "zlib/*:user.mycategory:retry" in tc.out assert "zlib/*:user.mycategory:foo" in tc.out assert "zlib/*:user.myothercategory:foo" in tc.out tc.run("config show zlib/*") assert "zlib/*:user.mycategory:retry" in tc.out assert "zlib/*:user.mycategory:foo" in tc.out assert "zlib/*:user.myothercategory:foo" in tc.out tc.run("config show zlib/*:foo") assert "zlib/*:user.mycategory:foo" in tc.out assert "zlib/*:user.myothercategory:foo" in tc.out @pytest.mark.parametrize("storage_path", [None, "p", "../foo"]) def test_config_clean(storage_path): tc = TestClient(light=True) absolut_storage_path = os.path.abspath(os.path.join(tc.current_folder, storage_path)) if storage_path else os.path.join(tc.cache_folder, "p") storage = f"core.cache:storage_path={storage_path}" if storage_path else "" tc.save_home({"global.conf": f"core.upload:retry=7\n{storage}", "extensions/compatibility/mycomp.py": "", "extensions/commands/cmd_foo.py": "", }) tc.run("profile detect --name=foo") tc.run("remote add bar http://fakeurl") tc.save({"conanfile.py": GenConanfile("pkg", "0.1")}) tc.run("create .") assert os.path.exists(absolut_storage_path) tc.run("config clean") tc.run("profile list") assert "foo" not in tc.out tc.run("remote list") assert "bar" not in tc.out tc.run("config show core.upload:retry") assert "7" not in tc.out assert os.path.exists(os.path.join(tc.cache_folder, "extensions")) assert not os.path.exists(os.path.join(tc.cache_folder, "extensions", "compatibility", "mycomp.py")) assert os.path.exists(absolut_storage_path) # This will error because the call to clean will remove the profiles tc.run("create .", assert_error=True) # Works after regenerating them! tc.run("profile detect") tc.run("create .") def test_config_reinit(): custom_global_conf = "core.upload:retry=7" global_conf_folder = temp_folder() with open(os.path.join(global_conf_folder, "global.conf"), "w") as f: f.write(custom_global_conf) cache_folder = temp_folder() conan_api = ConanAPI(cache_folder=cache_folder) assert conan_api._api_helpers.global_conf.get("core.upload:retry", check_type=int) != 7 conan_api.config.install(global_conf_folder, verify_ssl=False) # Already has an effect, the config installation reinitializes the config assert conan_api._api_helpers.global_conf.get("core.upload:retry", check_type=int) == 7 def test_config_reinit_core_conf(): tc = TestClient(light=True) tc.save_home({"extensions/commands/cmd_foo.py": textwrap.dedent(""" from conan.cli.command import conan_command from conan.api.output import ConanOutput @conan_command() def foo(conan_api, parser, *args, **kwargs): ''' Foo ''' parser.parse_args(*args) ConanOutput().info(f"Retry: {conan_api.config.get('core.upload:retry', check_type=int)}") """)}) tc.run("foo -cc core.upload:retry=7") assert "Retry: 7" in tc.out ================================================ FILE: test/integration/command/create_test.py ================================================ import json import os import re import textwrap import pytest from conan.api.model import RecipeReference from conan.cli.exit_codes import ERROR_GENERAL from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID, GenConanfile from conan.internal.util.files import load def test_dependencies_order_matches_requires(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkga --version=0.1 --user=user --channel=testing") client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkgb --version=0.1 --user=user --channel=testing") conanfile = textwrap.dedent(""" [requires] pkgb/0.1@user/testing pkga/0.1@user/testing """) client.save({"conanfile.txt": conanfile}, clean_first=True) client.run("install . -g MSBuildDeps -s build_type=Release -s arch=x86") conandeps = client.load("conandeps.props") assert conandeps.find("pkgb") < conandeps.find("pkga") def test_create(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyPkg(ConanFile): def source(self): assert(self.version=="0.1") assert(self.name=="pkg") def configure(self): assert(self.version=="0.1") assert(self.name=="pkg") def requirements(self): assert(self.version=="0.1") assert(self.name=="pkg") def build(self): assert(self.version=="0.1") assert(self.name=="pkg") def package(self): assert(self.version=="0.1") assert(self.name=="pkg") def package_info(self): assert(self.version=="0.1") assert(self.name=="pkg") def system_requirements(self): assert(self.version=="0.1") assert(self.name=="pkg") self.output.info("Running system requirements!!") """) client.save({"conanfile.py": conanfile}) client.run("create --name=pkg --version=0.1 --user=lasote --channel=testing") assert "Profile host:\n[settings]" in client.out assert "pkg/0.1@lasote/testing: Generating the package" in client.out assert "Running system requirements!!" in client.out client.run('list -c *') assert "pkg/0.1@lasote/testing" in client.out # Create with only user will raise an error because of no name/version client.run("create conanfile.py --user=lasote --channel=testing", assert_error=True) assert "ERROR: conanfile didn't specify name" in client.out # Create with user but no channel should be valid client.run("create . --name=pkg --version=0.1 --user=lasote") assert "pkg/0.1@lasote:" in client.out def test_error_create_name_version(): client = TestClient() client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("1.2")}) client.run("create . --name=hello --version=1.2 --user=lasote --channel=stable") client.run("create . --name=pkg", assert_error=True) assert "ERROR: Package recipe with name pkg!=hello" in client.out client.run("create . --version=1.1", assert_error=True) assert "ERROR: Package recipe with version 1.1!=1.2" in client.out def test_create_user_channel(): client = TestClient() client.save({"conanfile.py": GenConanfile().with_name("pkg").with_version("0.1")}) client.run("create . --user=lasote --channel=channel") assert "pkg/0.1@lasote/channel: Generating the package" in client.out client.run("list * -c") assert "pkg/0.1@lasote/channel" in client.out # test default without user and channel client.run("create . ") assert "pkg/0.1: Generating the package" in client.out def test_create_in_subfolder(): client = TestClient() client.save({"subfolder/conanfile.py": GenConanfile().with_name("pkg").with_version("0.1")}) client.run("create subfolder --user=lasote --channel=channel") assert "pkg/0.1@lasote/channel: Generating the package" in client.out client.run("list * -c") assert "pkg/0.1@lasote/channel" in client.out def test_create_in_subfolder_with_different_name(): # Now with a different name client = TestClient() client.save({"subfolder/Custom.py": GenConanfile().with_name("pkg").with_version("0.1")}) client.run("create subfolder/Custom.py --user=lasote --channel=channel") assert "pkg/0.1@lasote/channel: Generating the package" in client.out client.run("list * -c") assert "pkg/0.1@lasote/channel" in client.out def test_create_test_package(): client = TestClient() client.save({"conanfile.py": GenConanfile().with_name("pkg").with_version("0.1"), "test_package/conanfile.py": GenConanfile().with_test('self.output.info("TESTING!!!")')}) client.run("create . --user=lasote --channel=testing") assert "pkg/0.1@lasote/testing: Generating the package" in client.out assert "pkg/0.1@lasote/testing (test package): TESTING!!!" in client.out def test_create_skip_test_package(): # Skip the test package stage if explicitly disabled with --test-folder=None # https://github.com/conan-io/conan/issues/2355 client = TestClient() client.save({"conanfile.py": GenConanfile().with_name("pkg").with_version("0.1"), "test_package/conanfile.py": GenConanfile().with_test('self.output.info("TESTING!!!")')}) client.run("create . --user=lasote --channel=testing --test-folder=\"\"") assert "pkg/0.1@lasote/testing: Generating the package" in client.out assert "TESTING!!!" not in client.out def test_create_package_requires(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=dep --version=0.1 --user=user --channel=channel") client.run("create . --name=other --version=1.0 --user=user --channel=channel") conanfile = GenConanfile().with_require("dep/0.1@user/channel") test_conanfile = textwrap.dedent(""" from conan import ConanFile class MyPkg(ConanFile): requires = "other/1.0@user/channel" def requirements(self): self.requires(self.tested_reference_str) def build(self): for r in self.requires.values(): self.output.info("build() Requires: %s" % str(r.ref)) import os for dep in self.dependencies.host.values(): self.output.info("build() cpp_info dep: %s" % dep) def test(self): pass """) client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing -vv") assert "pkg/0.1@lasote/testing (test package): build() " \ "Requires: other/1.0@user/channel" in client.out assert "pkg/0.1@lasote/testing (test package): build() " \ "Requires: pkg/0.1@lasote/testing" in client.out assert "pkg/0.1@lasote/testing (test package): build() cpp_info dep: other" in client.out assert "pkg/0.1@lasote/testing (test package): build() cpp_info dep: dep" in client.out assert "pkg/0.1@lasote/testing (test package): build() cpp_info dep: pkg" in client.out # Check that the additional info shows up when creating pkg # Graph info, package creation, and test package graph info assert client.out.count("requires: dep/0.1@user/channel") == 3 def test_package_folder_build_error(): """ Check package folder is not created if the build step fails """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyPkg(ConanFile): def build(self): raise Exception("Build error") """) client.save({"conanfile.py": conanfile}) ref = RecipeReference("pkg", "0.1", "danimtb", "testing") client.run("create . --name=pkg --version=0.1 --user=danimtb --channel=testing", assert_error=True) assert "Build error" in client.out pref = client.get_latest_package_reference(ref, NO_SETTINGS_PACKAGE_ID) assert pref is None def test_create_with_name_and_version(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run('create . --name=lib --version=1.0') assert "lib/1.0: Created package revision" in client.out def test_create_with_only_user_channel(): """This should be the recommended way and only from Conan 2.0""" client = TestClient() client.save({"conanfile.py": GenConanfile().with_name("lib").with_version("1.0")}) client.run('create . --user=user --channel=channel') assert "lib/1.0@user/channel: Created package revision" in client.out client.run('create . --user=user --channel=channel') assert "lib/1.0@user/channel: Created package revision" in client.out def test_requires_without_user_channel(): client = TestClient() conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hellobar" version = "0.1" def package_info(self): self.output.warning("Hello, I'm hellobar") ''') client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": GenConanfile().with_require("hellobar/0.1")}) client.run("create . --name=consumer --version=1.0") assert "hellobar/0.1: WARN: Hello, I'm hellobar" in client.out assert "consumer/1.0: Created package revision" in client.out def test_conaninfo_contents_without_user_channel(): client = TestClient() client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("0.1")}) client.run("create .") client.save({"conanfile.py": GenConanfile().with_name("bye").with_version("0.1") .with_require("hello/0.1")}) client.run("create .") package_folder = client.created_layout().package() conaninfo = load(os.path.join(package_folder, "conaninfo.txt")) # The user and channel nor None nor "_/" appears in the conaninfo assert "None" not in conaninfo assert "_/" not in conaninfo assert "/_" not in conaninfo assert "[requires]\nhello/0.1\n" in conaninfo def test_components_json_output(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.1" settings = "build_type" def package_info(self): self.cpp_info.components["pkg1"].libs = ["libpkg1"] self.cpp_info.components["pkg2"].libs = ["libpkg2"] self.cpp_info.components["pkg2"].requires = ["pkg1"] """) client.save({"conanfile.py": conanfile}) client.run("create . --format=json") data = json.loads(client.stdout) cpp_info_data = data["graph"]["nodes"]["1"]["cpp_info"] assert "libpkg1" in cpp_info_data["pkg1"]["libs"] assert cpp_info_data["pkg1"]["requires"] == [] assert "libpkg2" in cpp_info_data["pkg2"]["libs"] assert cpp_info_data["pkg2"]["requires"] == ["pkg1"] def test_lockfile_input_not_specified(): client = TestClient() client.save({"conanfile.py": GenConanfile().with_name("foo").with_version("1.0")}) client.run("lock create . --lockfile-out locks/conan.lock") client.run("create . --lockfile-out locks/conan.lock") assert "Generated lockfile:" in client.out def test_create_build_missing(): """ test the --build=missing:pattern syntax """ c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_settings("os"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_settings("os") .with_requires("dep/1.0")}) c.run("create dep -s os=Windows") # Wrong pattern will not build it c.run("create pkg -s os=Windows --build=missing:kk", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkg/1.0'" in c.out # Pattern missing * will not build it c.run("create pkg -s os=Windows --build=missing:pkg", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkg/1.0'" in c.out # Correct pattern pkg* will build it c.run("create pkg -s os=Windows --build=missing:pkg*") c.assert_listed_binary({"pkg/1.0": ("90887fdbe22295dfbe41afe0a45f960c6a72b650", "Build")}) # Now anything that is not an explicit --build=pkg* will avoid rebuilding c.run("create pkg -s os=Windows --build=missing:kk") c.assert_listed_binary({"pkg/1.0": ("90887fdbe22295dfbe41afe0a45f960c6a72b650", "Cache")}) assert "Calling build()" not in c.out # but dependency without binary will fail, even if right pkg* pattern c.run("create pkg -s os=Linux --build=missing:pkg*", assert_error=True) c.assert_listed_binary({"pkg/1.0": ("4c0c198b627f9af3e038af4da5e6b3ae205c2435", "Build")}) c.assert_listed_binary({"dep/1.0": ("9a4eb3c8701508aa9458b1a73d0633783ecc2270", "Missing")}) assert "ERROR: Missing prebuilt package for 'dep/1.0'" in c.out # The & placeholder also works c.run("create pkg -s os=Linux --build=missing:&", assert_error=True) c.assert_listed_binary({"pkg/1.0": ("4c0c198b627f9af3e038af4da5e6b3ae205c2435", "Build")}) c.assert_listed_binary({"dep/1.0": ("9a4eb3c8701508aa9458b1a73d0633783ecc2270", "Missing")}) assert "ERROR: Missing prebuilt package for 'dep/1.0'" in c.out def test_create_no_user_channel(): """ test the --build=pattern and --build=missing:pattern syntax to build missing packages without user/channel """ c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep1/0.1", "dep2/0.1@user", "dep3/0.1@user/channel")}) c.run("export dep --name=dep1 --version=0.1") c.run("export dep --name=dep2 --version=0.1 --user=user") c.run("export dep --name=dep3 --version=0.1 --user=user --channel=channel") # First test the ``--build=missing:pattern`` c.run("create pkg --build=missing:*@", assert_error=True) c.assert_listed_binary({"dep1/0.1": (NO_SETTINGS_PACKAGE_ID, "Build"), "dep2/0.1": (NO_SETTINGS_PACKAGE_ID, "Missing"), "dep3/0.1": (NO_SETTINGS_PACKAGE_ID, "Missing")}) c.run("create pkg --build=missing:!*@", assert_error=True) c.assert_listed_binary({"dep1/0.1": (NO_SETTINGS_PACKAGE_ID, "Missing"), "dep2/0.1": (NO_SETTINGS_PACKAGE_ID, "Build"), "dep3/0.1": (NO_SETTINGS_PACKAGE_ID, "Build")}) # Now lets make sure they exist c.run("create pkg --build=missing") # Now test the --build=pattern c.run("create pkg --build=*@") c.assert_listed_binary({"dep1/0.1": (NO_SETTINGS_PACKAGE_ID, "Build"), "dep2/0.1": (NO_SETTINGS_PACKAGE_ID, "Cache"), "dep3/0.1": (NO_SETTINGS_PACKAGE_ID, "Cache")}) # The --build=* needs to be said: "build all except those that have user/channel c.run("create pkg --build=* --build=!*@") c.assert_listed_binary({"dep1/0.1": (NO_SETTINGS_PACKAGE_ID, "Cache"), "dep2/0.1": (NO_SETTINGS_PACKAGE_ID, "Build"), "dep3/0.1": (NO_SETTINGS_PACKAGE_ID, "Build")}) def test_create_build_missing_negation(): tc = TestClient(light=True) tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "lib/conanfile.py": GenConanfile("lib", "1.0").with_requires("dep/1.0"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_requires("lib/1.0")}) tc.run("export dep") tc.run("export lib") tc.run("create pkg --build=missing:~dep/*", assert_error=True) tc.assert_listed_binary({"pkg/1.0": ("a72376edfbbdaf97c8608b5fda53cadebac46a20", "Build"), "lib/1.0": ("abfcc78fa8242cabcd1e3d92896aa24808c789a3", "Build"), "dep/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Missing")}) tc.run("create pkg --build=missing:~dep/* --build=missing:~lib/*", assert_error=True) tc.assert_listed_binary({"pkg/1.0": ("a72376edfbbdaf97c8608b5fda53cadebac46a20", "Build"), "lib/1.0": ("abfcc78fa8242cabcd1e3d92896aa24808c789a3", "Missing"), "dep/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Missing")}) def test_create_format_json(): """ Tests the ``conan create . -f json`` result The result should be something like: { 'graph': { 'nodes': [ {'ref': '', # consumer 'recipe': 'Virtual', .... }, {'ref': 'hello/0.1#18d5440ae45afc4c36139a160ac071c7', 'dependencies': {'1': {'ref': 'hello/0.1', 'visible': 'True', ...}}, .... }, {'ref': 'pkg/0.2#44a1a27ac2ea1fbcf434a05c4d57388d', .... } ], 'root': {'0': 'None'} } } """ client = TestClient() profile_build = textwrap.dedent("""\ [settings] arch=x86_64 build_type=Release compiler=gcc compiler.libcxx=libstdc++ compiler.version=12 os=Linux [conf] user.first:value="my value" user.second:value=["my value"] user.second:value+=["other value"] [buildenv] VAR1=myvalue1 """) profile_host = textwrap.dedent("""\ [settings] arch=x86 build_type=Debug compiler=gcc compiler.libcxx=libstdc++ compiler.version=12 os=Linux """) conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.2" settings = "build_type", "compiler" author = "John Doe" license = "MIT" url = "https://foo.bar.baz" homepage = "https://foo.bar.site" topics = "foo", "bar", "qux" provides = "libjpeg", "libjpg" deprecated = "other-pkg" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} """) client.save({"conanfile.py": conanfile, "host": profile_host, "build": profile_build}) client.run("create . -pr:h host -pr:b build") client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("0.1") .with_require("pkg/0.2"), "host": profile_host, "build": profile_build}, clean_first=True) client.run("create . -f json -pr:h host -pr:b build") info = json.loads(client.stdout) nodes = info["graph"]['nodes'] consumer_ref = 'conanfile' hello_pkg_ref = 'hello/0.1#18d5440ae45afc4c36139a160ac071c7' pkg_pkg_ref = 'pkg/0.2#db78b8d06a78af5c3ac56706f133098d' consumer_info = hello_pkg_info = pkg_pkg_info = None for n in nodes.values(): ref = n["ref"] if ref == consumer_ref: consumer_info = n elif ref == hello_pkg_ref: hello_pkg_info = n else: assert ref == pkg_pkg_ref pkg_pkg_info = n # Consumer information assert consumer_info["recipe"] == "Cli" assert consumer_info["package_id"] is None assert consumer_info["prev"] is None assert consumer_info["options"] == {} assert consumer_info["settings"] == {'arch': 'x86', 'build_type': 'Debug', 'compiler': 'gcc', 'compiler.libcxx': 'libstdc++', 'compiler.version': '12', 'os': 'Linux'} consumer_deps = { '1': {'ref': 'hello/0.1', 'run': False, 'libs': True, 'skip': False, 'test': False, 'force': False, 'direct': True, 'build': False, 'transitive_headers': None, 'transitive_libs': None, 'headers': True, 'package_id_mode': None, 'visible': True, 'require': 'hello/0.1'}, '2': {'ref': 'pkg/0.2', 'run': False, 'libs': True, 'skip': False, 'test': False, 'force': False, 'direct': False, 'build': False, 'transitive_headers': None, 'transitive_libs': None, 'headers': True, 'package_id_mode': None, 'visible': True, 'require': 'pkg/0.2'} } assert consumer_info["dependencies"] == consumer_deps # hello/0.1 pkg information assert hello_pkg_info["package_id"] == "8eba237c0fb239fcb7daa47979ab99258eaaa7d1" assert hello_pkg_info["prev"] == "d95380a07c35273509dfc36b26f6cec1" assert hello_pkg_info["settings"] == {} assert hello_pkg_info["options"] == {} hello_pkg_info_deps = { "2": { "ref": "pkg/0.2", "run": False, "libs": True, "skip": False, "test": False, "force": False, "direct": True, "build": False, "transitive_headers": None, "transitive_libs": None, "headers": True, "package_id_mode": "semver_mode", "visible": True, 'require': 'pkg/0.2' } } assert hello_pkg_info["dependencies"] == hello_pkg_info_deps # pkg/0.2 pkg information assert pkg_pkg_info["package_id"] == "fb1439470288b15b2da269ed97b1a5f2f5d1f766" assert pkg_pkg_info["prev"] == "6949b0f89941d2a5994f9e6e4a89a331" assert pkg_pkg_info["author"] == 'John Doe' assert pkg_pkg_info["settings"] == {'build_type': 'Debug', 'compiler': 'gcc', 'compiler.libcxx': 'libstdc++', 'compiler.version': '12'} assert pkg_pkg_info["options"] == {'fPIC': 'True', 'shared': 'False'} assert pkg_pkg_info["dependencies"] == {} def test_create_format_json_and_deps_cpp_info(): """ Tests the ``conan create . -f json`` result, but ``cpp_info`` object only. The goal is to get something like: ``` { .... 'cpp_info': {'cmp1': {'bindirs': None, 'builddirs': None, 'cflags': None, 'cxxflags': None, 'defines': None, 'exelinkflags': None, 'frameworkdirs': None, 'frameworks': None, 'includedirs': None, 'libdirs': None, 'libs': ['libcmp1'], 'objects': None, 'properties': {'pkg_config_aliases': ['compo1_alias'], 'pkg_config_name': 'compo1'}, 'dependencies': None, 'resdirs': None, 'sharedlinkflags': None, 'srcdirs': None, 'sysroot': '/another/sysroot', 'system_libs': None}, 'root': {'bindirs': ['bin'], 'builddirs': [], 'cflags': ['pkg_a_c_flag'], 'cxxflags': ['pkg_a_cxx_flag'], 'defines': ['pkg_onedefinition', 'pkg_twodefinition'], 'exelinkflags': ['pkg_exe_link_flag'], 'frameworkdirs': ['framework/path/pkg'], 'frameworks': ['pkg_oneframework', 'pkg_twoframework'], 'includedirs': ['path/includes/pkg', 'include/path/pkg'], 'libdirs': ['lib/path/pkg'], 'libs': ['pkg'], 'objects': None, 'properties': {'pkg_config_aliases': ['pkg_alias1', 'pkg_alias2'], 'pkg_config_name': 'pkg_other_name'}, 'dependencies': None, 'resdirs': ['/path ' 'with ' 'spaces/.conan2/p/d15a235e212166d9/p/res'], 'sharedlinkflags': ['pkg_shared_link_flag'], 'srcdirs': None, 'sysroot': '/path/to/folder/pkg', 'system_libs': ['pkg_onesystemlib', 'pkg_twosystemlib']} }} ``` """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.2" def package_info(self): self.cpp_info.libs = ["pkg"] self.cpp_info.includedirs = ["path/includes/pkg", "other/include/path/pkg"] self.cpp_info.libdirs = ["one/lib/path/pkg"] self.cpp_info.defines = ["pkg_onedefinition", "pkg_twodefinition"] self.cpp_info.cflags = ["pkg_a_c_flag"] self.cpp_info.cxxflags = ["pkg_a_cxx_flag"] self.cpp_info.sharedlinkflags = ["pkg_shared_link_flag"] self.cpp_info.exelinkflags = ["pkg_exe_link_flag"] self.cpp_info.sysroot = "/path/to/folder/pkg" self.cpp_info.frameworks = ["pkg_oneframework", "pkg_twoframework"] self.cpp_info.system_libs = ["pkg_onesystemlib", "pkg_twosystemlib"] self.cpp_info.frameworkdirs = ["one/framework/path/pkg"] self.cpp_info.set_property("pkg_config_name", "pkg_other_name") self.cpp_info.set_property("pkg_config_aliases", ["pkg_alias1", "pkg_alias2"]) self.cpp_info.components["cmp1"].libs = ["libcmp1"] self.cpp_info.components["cmp1"].set_property("pkg_config_name", "compo1") self.cpp_info.components["cmp1"].set_property("pkg_config_aliases", ["compo1_alias"]) self.cpp_info.components["cmp1"].sysroot = "/another/sysroot" """) client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("0.1") .with_require("pkg/0.2")}, clean_first=True) client.run("create . -f json") info = json.loads(client.stdout) nodes = info["graph"]["nodes"] hello_pkg_ref = 'hello/0.1#18d5440ae45afc4c36139a160ac071c7' pkg_pkg_ref = 'pkg/0.2#926714b5fb0a994f47ec37e071eba1da' hello_cpp_info = pkg_cpp_info = None for n in nodes.values(): ref = n["ref"] if ref == hello_pkg_ref: assert n['binary'] == "Build" hello_cpp_info = n['cpp_info'] elif ref == pkg_pkg_ref: assert n['binary'] == "Cache" pkg_cpp_info = n['cpp_info'] assert hello_cpp_info and pkg_cpp_info # hello/0.1 cpp_info assert hello_cpp_info['root']["libs"] is None assert len(hello_cpp_info['root']["bindirs"]) == 1 assert len(hello_cpp_info['root']["libdirs"]) == 1 assert hello_cpp_info['root']["sysroot"] is None assert hello_cpp_info['root']["properties"] is None # pkg/0.2 cpp_info # root info assert pkg_cpp_info['root']["libs"] == ['pkg'] assert len(pkg_cpp_info['root']["bindirs"]) == 1 assert len(pkg_cpp_info['root']["libdirs"]) == 1 assert pkg_cpp_info['root']["sysroot"] == '/path/to/folder/pkg' assert pkg_cpp_info['root']["system_libs"] == ['pkg_onesystemlib', 'pkg_twosystemlib'] assert pkg_cpp_info['root']['cflags'] == ['pkg_a_c_flag'] assert pkg_cpp_info['root']['cxxflags'] == ['pkg_a_cxx_flag'] assert pkg_cpp_info['root']['defines'] == ['pkg_onedefinition', 'pkg_twodefinition'] assert pkg_cpp_info['root']["properties"] == {'pkg_config_aliases': ['pkg_alias1', 'pkg_alias2'], 'pkg_config_name': 'pkg_other_name'} # component info assert pkg_cpp_info['cmp1']["libs"] == ['libcmp1'] assert pkg_cpp_info['cmp1']["bindirs"][0].endswith("bin") # Abs path /bin assert pkg_cpp_info['cmp1']["libdirs"][0].endswith("lib") # Abs path /lib assert pkg_cpp_info['cmp1']["sysroot"] == "/another/sysroot" assert pkg_cpp_info['cmp1']["properties"] == {'pkg_config_aliases': ['compo1_alias'], 'pkg_config_name': 'compo1'} def test_default_framework_dirs(): conanfile = textwrap.dedent(""" from conan import ConanFile class LibConan(ConanFile): name = "lib" version = "1.0" def package_info(self): self.output.warning("FRAMEWORKS: {}".format(self.cpp_info.frameworkdirs))""") client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") assert "FRAMEWORKS: []" in client.out def test_default_framework_dirs_with_layout(): conanfile = textwrap.dedent(""" from conan import ConanFile class LibConan(ConanFile): name = "lib" version = "1.0" def layout(self): pass def package_info(self): self.output.warning("FRAMEWORKS: {}".format(self.cpp_info.frameworkdirs))""") client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") assert "FRAMEWORKS: []" in client.out def test_defaults_in_components(): """In Conan 2, declaring or not the layout has no influence in how cpp_info behaves. It was only 1.X""" lib_conan_file = textwrap.dedent(""" from conan import ConanFile class LibConan(ConanFile): name = "lib" version = "1.0" def layout(self): pass def package_info(self): self.cpp_info.components["foo"].libs = ["foolib"] """) client = TestClient() client.save({"conanfile.py": lib_conan_file}) client.run("create . ") consumer_conanfile = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): name = "consumer" version = "1.0" requires = "lib/1.0" def layout(self): pass def generate(self): cppinfo = self.dependencies["lib"].cpp_info components = cppinfo.components self.output.warning("BINDIRS: {}".format(cppinfo.bindirs)) self.output.warning("LIBDIRS: {}".format(cppinfo.libdirs)) self.output.warning("INCLUDEDIRS: {}".format(cppinfo.includedirs)) self.output.warning("RESDIRS: {}".format(cppinfo.resdirs)) self.output.warning("FOO LIBDIRS: {}".format(components["foo"].libdirs)) self.output.warning("FOO INCLUDEDIRS: {}".format(components["foo"].includedirs)) self.output.warning("FOO RESDIRS: {}".format(components["foo"].resdirs)) """) client.save({"conanfile.py": consumer_conanfile}) client.run("create . ") # The paths are absolute and the components have defaults # ".+" Check that there is a path, not only "lib" assert re.search(r"BINDIRS: \['.+bin']", client.out) assert re.search(r"LIBDIRS: \['.+lib']", client.out) assert re.search(r"INCLUDEDIRS: \['.+include']", client.out) assert "WARN: RES DIRS: []" assert re.search(r"WARN: FOO LIBDIRS: \['.+lib']", client.out) assert re.search(r"WARN: FOO INCLUDEDIRS: \['.+include']", client.out) assert "WARN: FOO RESDIRS: []" in client.out # The paths are absolute and the components have defaults # ".+" Check that there is a path, not only "lib" assert re.search(r"BINDIRS: \['.+bin']", client.out) assert re.search(r"LIBDIRS: \['.+lib']", client.out) assert re.search(r"INCLUDEDIRS: \['.+include']", client.out) assert "WARN: RES DIRS: []" assert bool(re.search(r"WARN: FOO LIBDIRS: \['.+lib']", client.out)) assert bool(re.search(r"WARN: FOO INCLUDEDIRS: \['.+include']", client.out)) assert "WARN: FOO RESDIRS: []" in client.out def test_name_never(): """ check that a package can be named equal to a build policy --build=never, because --build are now patterns Close https://github.com/conan-io/conan/issues/12430 """ c = TestClient() c.save({"conanfile.py": GenConanfile("never", "0.1")}) c.run("create .") assert "never/0.1: Created package" in c.out def test_create_both_host_build_require(): c = TestClient() c.save({"conanfile.py": GenConanfile("protobuf", "0.1").with_settings("build_type"), "test_package/conanfile.py": GenConanfile().with_build_requires("protobuf/0.1") .with_test("pass")}) c.run("create . -s:b build_type=Release -s:h build_type=Debug", assert_error=True) # The main "host" Debug binary will be correctly build c.assert_listed_binary({"protobuf/0.1": ("9e186f6d94c008b544af1569d1a6368d8339efc5", "Build")}) # But test_package will fail because of the missing "tool_require" in Release c.assert_listed_binary({"protobuf/0.1": ("efa83b160a55b033c4ea706ddb980cd708e3ba1b", "Missing")}, build=True, test_package=True) c.run("remove * -c") # make sure that previous binary is removed c.run("create . -s:b build_type=Release -s:h build_type=Debug --build-test=missing") c.assert_listed_binary({"protobuf/0.1": ("9e186f6d94c008b544af1569d1a6368d8339efc5", "Build")}) # it used to fail, now it works and builds the test_package "tools_requires" in Release c.assert_listed_binary({"protobuf/0.1": ("9e186f6d94c008b544af1569d1a6368d8339efc5", "Cache")}, test_package=True) c.assert_listed_binary({"protobuf/0.1": ("efa83b160a55b033c4ea706ddb980cd708e3ba1b", "Build")}, build=True, test_package=True) # we can be more explicit about the current package only with "missing:protobuf/*" c.run("remove * -c") # make sure that previous binary is removed c.run("create . -s:b build_type=Release -s:h build_type=Debug --build-test=missing:protobuf/*") c.assert_listed_binary({"protobuf/0.1": ("9e186f6d94c008b544af1569d1a6368d8339efc5", "Build")}) # it used to fail, now it works and builds the test_package "tools_requires" in Release c.assert_listed_binary({"protobuf/0.1": ("efa83b160a55b033c4ea706ddb980cd708e3ba1b", "Build")}, build=True, test_package=True) def test_python_requires_json_format(): """Check python requires does not crash when calling conan create . --format=json See https://github.com/conan-io/conan/issues/14577""" c = TestClient() c.save({"conanfile.py": GenConanfile("pyreq", "1.0") .with_package_type("python-require")}) c.run("create . --format=json", redirect_stdout="output.json") data = json.loads(load(os.path.join(c.current_folder, "output.json"))) # There's a graph and the python requires is there assert len(data["graph"]["nodes"]["0"]["python_requires"]) == 1 def test_python_requires_with_test_package(): c = TestClient() # Code comes from the docs conanfile = textwrap.dedent(""" from conan import ConanFile def mynumber(): return 42 class PyReq(ConanFile): name = "pyreq" version = "1.0" package_type = "python-require" """) test_conanfile = textwrap.dedent(""" from conan import ConanFile class Tool(ConanFile): def test(self): pyreq = self.python_requires["pyreq"].module mynumber = pyreq.mynumber() self.output.info("{}!!!".format(mynumber)) """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("create .") # Ensure that creating a deps graph does not break the testing assert "pyreq/1.0 (test package): 42!!!" in c.out def test_create_test_package_only_build(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "test_package/conanfile.py": GenConanfile().with_test("self.output.info('TEST1!!!')"), "test_package2/conanfile.py": GenConanfile().with_test("self.output.info('TEST2!!!')")}) # As it doesn't exist, it builds and test it c.run("create . -tm") assert "Testing the package" in c.out assert "TEST1!!!" in c.out # this will not create the binary, so it won't test it c.run("create . --build=missing --test-missing") assert "Testing the package" not in c.out assert "TEST" not in c.out c.run("create . -tf=test_package2 -tm") assert "Testing the package" in c.out assert "TEST2!!!" in c.out assert "TEST1!!!" not in c.out c.run("create . -tf=test_package2 --build=missing --test-missing") assert "Testing the package" not in c.out assert "TEST2!!!" not in c.out assert "TEST1!!!" not in c.out # error c.run("create . -tm -tf=", assert_error=True) assert '--test-folder="" is incompatible with --test-missing' in c.out def test_create_test_package_only_build_python_require(): c = TestClient() test = textwrap.dedent(""" from conan import ConanFile class Tool(ConanFile): python_requires = "tested_reference_str" def test(self): self.output.info("TEST!!!!") """) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_package_type("python-require"), "test_package/conanfile.py": test}) c.run("create .") assert "Testing the package" in c.out assert "pkg/0.1 (test package): TEST!!!" in c.out c.run("create . -tm") assert "Testing the package" in c.out assert "pkg/0.1 (test package): TEST!!!" in c.out c.run("create . -tm --build=missing") assert "Testing the package" in c.out assert "pkg/0.1 (test package): TEST!!!" in c.out @pytest.mark.parametrize("command", ["create", "install"]) @pytest.mark.parametrize("out_file", [False, True]) def test_create_build_fail_generate_outfile(command, out_file): c = TestClient() c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1") .with_requires("pkgb/0.1") .with_package("raise Exception('myerror')"), "pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requires("pkgc/0.1") .with_settings("build_type") .with_generator("CMakeDeps"), }) c.run("export pkga") c.run("export pkgb") c.run("export pkgc") if out_file: error = c.run(f"{command} pkgd --build=missing --format=json --out-file=graph.json", assert_error=True) else: error = c.run(f"{command} pkgd --build=missing --format=json", assert_error=True, redirect_stdout="graph.json") assert error == ERROR_GENERAL assert "pkgc/0.1: Error in package() method, line 8" in c.out graph = json.loads(c.load("graph.json")) nodeid = "1" if command == "create" else "0" assert graph["graph"]["nodes"][nodeid]["name"] == "pkgd" # We can construct a package list from it c.run("list -g=graph.json --graph-binaries=Build --format=json") pkglist = json.loads(c.stdout) # not built packages don't have revisions rrev = pkglist["Local Cache"]["pkgc/0.1"]["revisions"]["b7f74fa20b19f1daac67db49318b7197"] assert "revisions" not in rrev["packages"]["4a8d7d78a454700be1ab74b4a77fd7f36a44d122"] # built packages do have package revisions rrev = pkglist["Local Cache"]["pkgb/0.1"]["revisions"]["5b1ae5e3c1f718c0fd90d4dd8d9b57fb"] assert "revisions" in rrev["packages"]["47a5f20ec8fb480e1c5794462089b01a3548fdc5"] rrev = pkglist["Local Cache"]["pkga/0.1"]["revisions"]["57ece23aeb368b634896004ad579767a"] assert "revisions" in rrev["packages"]["da39a3ee5e6b4b0d3255bfef95601890afd80709"] ================================================ FILE: test/integration/command/custom_commands_test.py ================================================ import json import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update class TestCustomCommandsErrors: def test_import_error_custom_command(self): mycommand = textwrap.dedent(""" import this_doesnt_exist """) client = TestClient() command_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'cmd_mycommand.py') client.save({f"{command_file_path}": mycommand}) # Call to any other command, it will fail loading the custom command client.run("list *") assert "ERROR: Error loading custom command 'cmd_mycommand.py': " \ "No module named 'this_doesnt_exist'" in client.out # But it won't break the whole conan and you can still use the rest of it client.run("config home") assert client.cache_folder in client.out def test_import_error_custom_command_subfolder(self): """ used to break, this is handled differently in conan """ mycommand = textwrap.dedent(""" import this_doesnt_exist """) client = TestClient() command_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'mycompany', 'cmd_mycommand.py') client.save({f"{command_file_path}": mycommand}) # Call to any other command, it will fail loading the custom command, client.run("list *") assert "ERROR: Error loading custom command mycompany.cmd_mycommand" in client.out # But it won't break the whole conan and you can still use the rest of it client.run("config home") assert client.cache_folder in client.out def test_import_error_bad_name(self): mycommand = textwrap.dedent(""" from conan.cli.command import conan_command, conan_subcommand @conan_command(group="custom commands") def mycommand(conan_api, parser, *args, **kwargs): \""" custom \""" @conan_subcommand() def mysubcmd(conan_api, parser, *args, **kwargs): \""" mysubcmd \""" """) c = TestClient() c.save_home({"extensions/commands/cmd_mycommand.py": mycommand}) # Call to any other command, it will fail loading the custom command c.run("list *") assert "The name for the subcommand method should begin with the main command name" in c.out class TestCustomCommands: def test_simple_custom_command(self): mycommand = textwrap.dedent(""" import json import os from conan.cli.command import conan_command from conan.api.output import cli_out_write def output_mycommand_cli(info): cli_out_write(f"Conan cache folder is: {info.get('cache_folder')}") def output_mycommand_json(info): cli_out_write(json.dumps(info)) @conan_command(group="custom commands", formatters={"cli": output_mycommand_cli, "json": output_mycommand_json}) def mycommand(conan_api, parser, *args, **kwargs): \""" this is my custom command, it will print the location of the cache folder \""" info = {"cache_folder": os.path.basename(conan_api.cache_folder)} return info """) client = TestClient() command_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'cmd_mycommand.py') client.save({f"{command_file_path}": mycommand}) client.run("mycommand -f cli") foldername = os.path.basename(client.cache_folder) assert f'Conan cache folder is: {foldername}' in client.out client.run("mycommand -f json") assert f'{{"cache_folder": "{foldername}"}}' in client.out def test_command_layer(self): myhello = textwrap.dedent(""" from conan.api.output import cli_out_write from conan.cli.command import conan_command @conan_command(group="custom commands") def hello(conan_api, parser, *args, **kwargs): ''' My Hello doc ''' cli_out_write("Hello {}!") """) mybye = textwrap.dedent(""" from conan.api.output import cli_out_write from conan.cli.command import conan_command, conan_subcommand @conan_command(group="custom commands") def bye(conan_api, parser, *args, **kwargs): ''' My Bye doc ''' @conan_subcommand() def bye_say(conan_api, parser, *args, **kwargs): ''' My bye say doc ''' cli_out_write("Bye!") """) client = TestClient() layer_path = os.path.join(client.cache_folder, 'extensions', 'commands') client.save({os.path.join(layer_path, 'cmd_hello.py'): myhello.format("world"), os.path.join(layer_path, "greet", 'cmd_hello.py'): myhello.format("moon"), os.path.join(layer_path, "greet", 'cmd_bye.py'): mybye}) # Test that the root "hello" without subfolder still works and no conflict client.run("hello") assert "Hello world!" in client.out client.run("greet:hello") assert "Hello moon!" in client.out client.run("greet:bye say") assert "Bye!" in client.out client.run("-h") assert "greet:bye" in client.out # Ensure the prog has the full command name client.run("hello -h") assert "conan hello" in client.out client.run("greet:bye -h") assert "conan greet:bye" in client.out client.run("greet:bye say -h") assert "conan greet:bye say" in client.out def test_custom_command_with_subcommands(self): complex_command = textwrap.dedent(""" import json from conan.cli.command import conan_command, conan_subcommand from conan.api.output import cli_out_write def output_cli(info): cli_out_write(f"{info.get('argument1')}") def output_json(info): cli_out_write(json.dumps(info)) @conan_subcommand(formatters={"cli": output_cli, "json": output_json}) def complex_sub1(conan_api, parser, subparser, *args): \""" sub1 subcommand \""" subparser.add_argument("argument1", help="This is argument number 1") args = parser.parse_args(*args) info = {"argument1": args.argument1} return info @conan_command() def complex(conan_api, parser, *args, **kwargs): \""" this is a command with subcommands \""" """) client = TestClient() command_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'cmd_complex.py') client.save({f"{command_file_path}": complex_command}) client.run("complex sub1 myargument -f=cli") assert "myargument" in client.out client.run("complex sub1 myargument -f json") assert f'{{"argument1": "myargument"}}' in client.out def test_custom_command_with_subcommands_with_underscore(self): complex_command = textwrap.dedent(""" import json from conan.cli.command import conan_command, conan_subcommand from conan.api.output import cli_out_write @conan_command() def command_with_underscores(conan_api, parser, *args, **kwargs): \""" this is a command with subcommands \""" @conan_subcommand() def command_with_underscores_subcommand_with_underscores_too(conan_api, parser, subparser, *args): \""" sub1 subcommand \""" subparser.add_argument("argument1", help="This is argument number 1") args = parser.parse_args(*args) cli_out_write(args.argument1) """) client = TestClient() command_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'cmd_command_with_underscores.py') client.save({f"{command_file_path}": complex_command}) client.run("command-with-underscores subcommand-with-underscores-too myargument") assert "myargument" in client.out def test_overwrite_builtin_command(self): complex_command = textwrap.dedent(""" import json from conan.cli.command import conan_command from conan.api.output import cli_out_write @conan_command() def install(conan_api, parser, *args, **kwargs): \""" this is a command with subcommands \""" cli_out_write("Hello world") """) client = TestClient() command_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'myteam', 'cmd_install.py') client.save({f"{command_file_path}": complex_command}) command_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'cmd_install.py') client.save({f"{command_file_path}": complex_command}) client.run("myteam:install") assert "Hello world" in client.out client.run("install") assert "Hello world" in client.out def test_custom_command_local_import(self): mycode = textwrap.dedent(""" from conan.api.output import cli_out_write def write_output(folder): cli_out_write(f"Conan cache folder from cmd_mycode: {folder}") """) mycommand = textwrap.dedent(""" import os from conan.cli.command import conan_command from mycode import write_output @conan_command(group="custom commands") def mycommand(conan_api, parser, *args, **kwargs): \""" this is my custom command, it will print the location of the cache folder \""" folder = os.path.basename(conan_api.cache_folder) write_output(folder) """) client = TestClient() mycommand_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'danimtb', 'cmd_mycommand.py') mycode_file_path = os.path.join(client.cache_folder, 'extensions', 'commands', 'danimtb', 'mycode.py') client.save({ mycode_file_path: mycode, mycommand_file_path: mycommand }) client.run("danimtb:mycommand") foldername = os.path.basename(client.cache_folder) assert f'Conan cache folder from cmd_mycode: {foldername}' in client.out def test_custom_command_from_other_location(self): """ Tests that setting developer env variable ``_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH`` will append that folder to the Conan custom command default location. """ myhello = textwrap.dedent(""" from conan.api.output import cli_out_write from conan.cli.command import conan_command @conan_command(group="custom commands") def hello(conan_api, parser, *args, **kwargs): ''' My Hello doc ''' cli_out_write("Hello {}!") """) client = TestClient() my_local_layer_path = temp_folder(path_with_spaces=False) layer_path = os.path.join(client.cache_folder, 'extensions', 'commands') client.save({os.path.join(layer_path, 'cmd_hello.py'): myhello.format("world")}) client.save({"cmd_hello.py": myhello.format("Overridden")}, path=my_local_layer_path) with environment_update({"_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH": my_local_layer_path}): client.run("hello") # Local commands have preference over Conan custom ones if they collide assert "Hello Overridden!" in client.out # Without the variable it only loads the default custom commands location client.run("hello") assert "Hello world!" in client.out class TestCommandAPI: @pytest.mark.parametrize("argument", ['["list", "pkg*", "-c"]', '"list pkg* -c"']) def test_command_reuse_interface(self, argument): mycommand = textwrap.dedent(f""" import json from conan.cli.command import conan_command from conan.api.output import cli_out_write @conan_command(group="custom commands") def mycommand(conan_api, parser, *args, **kwargs): \""" mycommand help \""" result = conan_api.command.run({argument}) cli_out_write(json.dumps(result["results"], indent=2)) """) c = TestClient() command_file_path = os.path.join(c.cache_folder, 'extensions', 'commands', 'cmd_mycommand.py') c.save({f"{command_file_path}": mycommand}) c.run("mycommand", redirect_stdout="file.json") assert json.loads(c.load("file.json")) == {"Local Cache": {}} def test_command_reuse_other_custom(self): cmd1 = textwrap.dedent(f""" from conan.cli.command import conan_command from conan.api.output import cli_out_write @conan_command(group="custom commands") def mycmd1(conan_api, parser, *args, **kwargs): \"""mycommand help \""" # result = conan_api.command.run("") cli_out_write("MYCMD1!!!!!") conan_api.command.run("mycmd2") """) cmd2 = textwrap.dedent(f""" from conan.cli.command import conan_command from conan.api.output import cli_out_write @conan_command(group="custom commands") def mycmd2(conan_api, parser, *args, **kwargs): \"""mycommand help\""" cli_out_write("MYCMD2!!!!!") """) c = TestClient() cmds = os.path.join(c.cache_folder, 'extensions', 'commands') c.save({os.path.join(cmds, "cmd_mycmd1.py"): cmd1, os.path.join(cmds, "cmd_mycmd2.py"): cmd2}) c.run("mycmd1") assert "MYCMD1!!!!!" in c.out assert "MYCMD2!!!!!" in c.out def test_command_verbosity_leak(self): mycommand = textwrap.dedent(f""" import json from conan.cli.command import conan_command from conan.api.output import ConanOutput @conan_command(group="custom commands") def mycommand(conan_api, parser, *args, **kwargs): \""" mycommand help \""" parser.add_argument("foo", help="foo") args = parser.parse_args(*args) out = ConanOutput() out.title("This is my first title") conan_api.command.run("config home") out.title("This is my second title") """) c = TestClient() command_file_path = os.path.join(c.cache_folder, 'extensions', 'commands', 'cmd_mycommand.py') c.save({f"{command_file_path}": mycommand}) c.run("mycommand foo -vquiet") assert "This is my first title" not in c.out assert "This is my second title" not in c.out def test_command_reuse_interface_create(self): mycommand = textwrap.dedent(""" import json from conan.cli.command import conan_command from conan.cli.formatters.graph import format_graph_json @conan_command(group="custom commands", formatters={"json": format_graph_json}) def mycommand(conan_api, parser, *args, **kwargs): \""" mycommand help \""" result = conan_api.command.run(["create", ".", "--version=1.0.0"]) return result """) c = TestClient() command_file_path = os.path.join(c.cache_folder, 'extensions', 'commands', 'cmd_mycommand.py') c.save({f"{command_file_path}": mycommand, "conanfile.py": GenConanfile("mylib")}) c.run("mycommand --format=json", redirect_stdout="file.json") create_output = json.loads(c.load("file.json")) assert create_output['graph']['nodes']['1']['label'] == "mylib/1.0.0" def test_subcommand_reuse_interface(self): mycommand = textwrap.dedent(""" import json from conan.cli.command import conan_command from conan.api.output import cli_out_write @conan_command(group="custom commands") def mycommand(conan_api, parser, *args, **kwargs): \""" mycommand help \""" parser.add_argument("remote", help="remote") parser.add_argument("url", help="url") args = parser.parse_args(*args) conan_api.command.run(["remote", "add", args.remote, args.url]) result = conan_api.command.run(["remote", "list"]) result = {r.name: r.url for r in result} cli_out_write(json.dumps(result, indent=2)) """) c = TestClient() command_file_path = os.path.join(c.cache_folder, 'extensions', 'commands', 'cmd_mycommand.py') c.save({f"{command_file_path}": mycommand}) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") c.run("mycommand myremote myurl") assert json.loads(c.stdout) == {"myremote": "myurl"} class TestCommandsRemoteCaching: def test_remotes_cache(self): complex_command = textwrap.dedent(""" import json from conan.cli.command import conan_command from conan.api.output import ConanOutput @conan_command() def mycache(conan_api, parser, *args, **kwargs): \""" this is a command with subcommands \""" remotes = conan_api.remotes.list() host = conan_api.profiles.get_profile(["default"]) build = conan_api.profiles.get_profile(["default"]) deps_graph = conan_api.graph.load_graph_requires(["pkg/0.1"], None, host, build, None, remotes, None, None) conan_api.graph.analyze_binaries(deps_graph, None, remotes=remotes) # SECOND RUN!!! # This will break if the caching is not working remotes[0].url = "broken" deps_graph = conan_api.graph.load_graph_requires(["pkg/0.1"], None, host, build, None, remotes, None, None) conan_api.graph.analyze_binaries(deps_graph, None, remotes=remotes) # Now invalidate the cache and see how it breaks try: remotes[0].invalidate_cache() deps_graph = conan_api.graph.load_graph_requires(["pkg/0.1"], None, host, build, None, remotes, None, None) conan_api.graph.analyze_binaries(deps_graph, None, remotes=remotes) except Exception as e: ConanOutput().warning(f"Cache invalidated, as expected: {e}") """) c = TestClient(default_server_user=True) c.save_home({"extensions/commands/cmd_mycache.py": complex_command}) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_require("dep/0.1")}) c.run("create dep") c.run("create pkg") c.run("upload * -r=default -c") c.run("remove * -c") c.run("mycache") # Does not break unexpectedly, caching is working assert "WARN: Cache invalidated, as expected: Invalid URL 'broken" in c.out def test_custom_command_settings_access(): tc = TestClient() mycommand = textwrap.dedent(""" from conan.cli.command import conan_command from conan.api.output import ConanOutput @conan_command(group="custom commands") def mycommand(conan_api, parser, *args, **kwargs): \""" test \""" settings = conan_api.config.settings_yml ConanOutput().info(f"settings.fields: {settings.fields}") ConanOutput().info(f"settings.possible_values(): {settings.possible_values()}") ConanOutput().info(f"settings.compiler: {settings.compiler}") ConanOutput().info(f"settings.compiler.possible_values(): {settings.compiler.possible_values()}") """) command_file_path = os.path.join('extensions', 'commands', 'cmd_mycommand.py') tc.save_home({f"{command_file_path}": mycommand}) tc.run("mycommand") assert "settings.fields: ['arch', 'build_type', 'compiler', 'os']" in tc.out # No values here, but as it does not fail, the interface is working fine assert "settings.compiler: None" in tc.out ================================================ FILE: test/integration/command/download/__init__.py ================================================ ================================================ FILE: test/integration/command/download/download_parallel_test.py ================================================ from conan.test.utils.tools import GenConanfile, TestClient def test_basic_parallel_download(): client = TestClient(light=True, default_server_user=True) threads = 2 packages = 2 per_package = 4 client.save_home({"global.conf": f"core.download:parallel={threads}"}) client.save({"conanfile.py": GenConanfile().with_option("myoption", '["ANY"]')}) package_ids = [] for i in range(packages): for n in range(per_package): client.run(f"create . --name=pkg{i} --version=0.1 --user=user --channel=testing -o pkg{i}/*:myoption={n}") package_id = client.created_package_id(f"pkg{i}/0.1@user/testing") package_ids.append((i, package_id)) client.run("upload * --confirm -r default") client.run("remove * -c") # Lets download the packages client.run("download pkg*/0.1@user/testing#*:* -r default") assert f"Downloading with {threads} parallel threads" in client.out for i, package_id in package_ids: assert f"pkg{i}/0.1@user/testing: Package installed {package_id}" in client.out ================================================ FILE: test/integration/command/download/download_selected_packages_test.py ================================================ import os import pytest from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import load @pytest.fixture(scope="module") def setup(): client = TestClient(default_server_user=True) conanfile = GenConanfile().with_settings("os", "arch").with_package_file("hellohello0.h", "x") client.save({"conanfile.py": conanfile}) ref = RecipeReference.loads("hello0/0.1@lasote/stable") client.run("export . --name=hello0 --version=0.1 --user=lasote --channel=stable") client.run("install --requires={} -s os=Windows --build missing".format(ref)) client.run("install --requires={} -s os=Linux --build missing".format(ref)) client.run("install --requires={} -s os=Linux -s arch=x86 --build missing".format(ref)) client.run("upload {} -r default".format(ref)) latest_rrev = client.cache.get_latest_recipe_revision(ref) packages = client.cache.get_package_references(latest_rrev) package_ids = [package.package_id for package in packages] return client, ref, package_ids, str(conanfile) @pytest.mark.artifactory_ready def test_download_recipe_twice(setup): client, ref, package_ids, conanfile = setup new_client = TestClient(servers=client.servers, inputs=["admin", "password"]) new_client.run("download hello0/0.1@lasote/stable -r default") ref = RecipeReference.loads("hello0/0.1@lasote/stable") conanfile_path = new_client.get_latest_ref_layout(ref).conanfile() assert conanfile == load(conanfile_path) new_client.run("download hello0/0.1@lasote/stable -r default") assert conanfile == load(conanfile_path) new_client.run("download hello0/0.1@lasote/stable -r default") assert conanfile == load(conanfile_path) @pytest.mark.artifactory_ready def test_download_packages_twice(setup): client, ref, package_ids, _ = setup new_client = TestClient(servers=client.servers, inputs=["admin", "password"]) expected_header_contents = "x" new_client.run("download hello0/0.1@lasote/stable:* -r default") pref = client.get_latest_package_reference("hello0/0.1@lasote/stable", package_id=package_ids[0]) package_folder = new_client.get_latest_pkg_layout(pref).package() got_header = load(os.path.join(package_folder, "hellohello0.h")) assert expected_header_contents == got_header new_client.run("download hello0/0.1@lasote/stable:* -r default") got_header = load(os.path.join(package_folder, "hellohello0.h")) assert expected_header_contents == got_header new_client.run("download hello0/0.1@lasote/stable:* -r default") got_header = load(os.path.join(package_folder, "hellohello0.h")) assert expected_header_contents == got_header ================================================ FILE: test/integration/command/download/download_test.py ================================================ import os import textwrap from collections import OrderedDict from unittest import mock from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient, TestServer, NO_SETTINGS_PACKAGE_ID, GenConanfile from conan.internal.util.files import load def test_download_with_sources(): client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports_sources("*"), "file.h": "myfile.h", "otherfile.cpp": "C++code"}) client.run("export . --user=lasote --channel=stable") ref = RecipeReference.loads("pkg/0.1@lasote/stable") client.run("upload pkg/0.1@lasote/stable -r default") client.run("remove pkg/0.1@lasote/stable -c") client.run("download pkg/0.1@lasote/stable -r default") assert "Downloading 'pkg/0.1@lasote/stable' sources" in client.out source = client.get_latest_ref_layout(ref).export_sources() assert "myfile.h" == load(os.path.join(source, "file.h")) assert "C++code" == load(os.path.join(source, "otherfile.cpp")) def test_no_user_channel(): # https://github.com/conan-io/conan/issues/6009 client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=1.0") client.run("upload * --confirm -r default") client.run("remove * -c") client.run("download pkg/1.0:{} -r default".format(NO_SETTINGS_PACKAGE_ID)) assert f"Downloading package 'pkg/1.0#4d670581ccb765839f2239cc8dff8fbd:{NO_SETTINGS_PACKAGE_ID}" in client.out # All client.run("remove * -c") client.run("download pkg/1.0#*:* -r default") assert f"Downloading package 'pkg/1.0#4d670581ccb765839f2239cc8dff8fbd:{NO_SETTINGS_PACKAGE_ID}" in client.out def test_download_with_python_requires(): """ In the past, when having a python_require in a different repo, it cannot be ``conan download`` as the download runs from a single repo. Now, from https://github.com/conan-io/conan/issues/14260, "conan download" doesn't really need to load conanfile, so it doesn't fail because of this. """ # https://github.com/conan-io/conan/issues/9548 servers = OrderedDict([("tools", TestServer()), ("pkgs", TestServer())]) c = TestClient(servers=servers, inputs=["admin", "password", "admin", "password"]) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("tool/0.1")}) c.run("export tool") c.run("create pkg") c.run("upload tool* -r tools -c") c.run("upload pkg* -r pkgs -c") c.run("remove * -c") c.run("install --requires=pkg/0.1 -r pkgs -r tools") assert "Downloading" in c.out c.run("remove * -c") c.run("download pkg/0.1 -r pkgs") assert "pkg/0.1: Downloaded package revision" in c.out def test_download_verify_ssl_conf(): client = TestClient() client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "http://verify.true", "", verify=True) download(self, "http://verify.false", "", verify=False) """)}) did_verify = {} def custom_download(this, url, filepath, *args, **kwargs): did_verify[url] = args[2] with mock.patch("conan.internal.rest.file_downloader.FileDownloader.download", custom_download): client.run("create . -c tools.files.download:verify=True") assert did_verify["http://verify.true"] assert did_verify["http://verify.false"] did_verify.clear() client.run("remove pkg/1.0 -c") client.run("create . -c tools.files.download:verify=False") assert not did_verify["http://verify.true"] assert not did_verify["http://verify.false"] did_verify.clear() client.run("remove pkg/1.0 -c") client.run("create .") assert did_verify["http://verify.true"] assert not did_verify["http://verify.false"] def test_download_list_only_recipe(): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("liba", "0.1")}) c.run("create .") c.run("upload * -r=default -c") c.run("remove * -c") c.run("list *:* -r=default --format=json", redirect_stdout="pkgs.json") c.run("download --list=pkgs.json --only-recipe -r=default") assert "packages" not in c.out ================================================ FILE: test/integration/command/download/test_download_patterns.py ================================================ import re import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update @pytest.mark.artifactory_ready class TestDownloadPatterns: # The fixture is very similar from TestUploadPatterns, but not worth extracting @pytest.fixture(scope="class") def client(self): """ create a few packages, with several recipe revisions, several pids, several prevs """ client = TestClient(default_server_user=True) for pkg in ("pkga", "pkgb"): for version in ("1.0", "1.1"): for rrev in ("rev1", "rev2"): client.save({"conanfile.py": GenConanfile().with_settings("os") .with_class_attribute(f"potato='{rrev}'") .with_package_file("file", env_var="MYVAR")}) for the_os in ("Windows", "Linux"): for prev in ("prev1", "prev2"): with environment_update({"MYVAR": prev}): client.run(f"create . --name={pkg} --version={version} " f"-s os={the_os}") client.run("upload *#*:*#* -r=default -c") return client @staticmethod def assert_downloaded(pattern, result, client, only_recipe=False, query=None): def ref_map(r): rev1 = "ad55a66b62acb63ffa99ea9b75c16b99" rev2 = "127fb537a658ad6a57153a038960dc53" pid1 = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" pid2 = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" if "Linux" in r: prev1 = "7ce684c6109943482b9174dd089e717b" prev2 = "772234192e8e4ba71b018d2e7c02423e" else: prev1 = "45f88f3c318bd43d1bc48a5d408a57ef" prev2 = "c5375d1f517ecb1ed6c9532b0f4d86aa" r = r.replace("prev1", prev1).replace("prev2", prev2).replace("Windows", pid1) r = r.replace("Linux", pid2).replace("rev1", rev1).replace("rev2", rev2) return r pattern = ref_map(pattern) only_recipe = "" if not only_recipe else "--only-recipe" query = "" if not query else f"-p={query}" client.run(f"download {pattern} -r=default {only_recipe} {query}") out = str(client.out) downloaded_recipes = [f"{p}/{v}#{rr}" for p in result[0] for v in result[1] for rr in result[2]] downloaded_packages = [f"{r}:{pid}#{pr}" for r in downloaded_recipes for pid in result[3] for pr in result[4]] # Checks skipped_recipe_count = len(re.findall("Skip recipe .+ download, already in cache", out)) assert skipped_recipe_count == len(downloaded_recipes) for recipe in downloaded_recipes: recipe = ref_map(recipe) existing = f"Skip recipe {recipe} download, already in cache" in out assert existing skipped_pkg_count = len(re.findall("Skip package .+ download, already in cache", out)) assert skipped_pkg_count == len(downloaded_packages) for pkg in downloaded_packages: pkg = ref_map(pkg) existing = f"Skip package {pkg} download, already in cache" in out assert existing def test_all_latest(self, client): result = ("pkga", "pkgb"), ("1.0", "1.1"), ("rev2",), ("Windows", "Linux"), ("prev2",) self.assert_downloaded("*", result, client) def test_all(self, client): result = ("pkga", "pkgb"), ("1.0", "1.1"), ("rev1", "rev2",), ("Windows", "Linux"), \ ("prev1", "prev2") self.assert_downloaded("*#*:*#*", result, client) def test_pkg(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), ("Windows", "Linux"), ("prev2",) self.assert_downloaded("pkga", result, client) def test_pkg_rrev(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), ("Windows", "Linux"), ("prev2",) self.assert_downloaded("pkga#rev1", result, client) def test_pkg_rrevs(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1", "rev2"), ("Windows", "Linux"), ("prev2",) self.assert_downloaded("pkga#*", result, client) def test_pkg_pid(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), ("Windows",), ("prev2",) self.assert_downloaded("pkga:Windows", result, client) def test_pkg_rrev_pid(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), ("Windows",), ("prev2",) self.assert_downloaded("pkga#rev1:Windows", result, client) def test_pkg_rrevs_pid(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1", "rev2"), ("Windows",), ("prev2",) self.assert_downloaded("pkga#*:Windows", result, client) def test_pkg_rrev_pid_prev(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), ("Windows",), ("prev1",) self.assert_downloaded("pkga#rev1:Windows#prev1", result, client) # Only recipes def test_all_latest_only_recipe(self, client): result = ("pkga", "pkgb"), ("1.0", "1.1"), ("rev2",), (), () self.assert_downloaded("*", result, client, only_recipe=True) def test_pkg_only_recipe(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), (), () self.assert_downloaded("pkga", result, client, only_recipe=True) def test_pkg_rrev_only_recipe(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), (), () self.assert_downloaded("pkga#rev1", result, client, only_recipe=True) def test_pkg_rrevs_only_recipe(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1", "rev2"), (), () self.assert_downloaded("pkga#*", result, client, only_recipe=True) # Package query def test_all_query(self, client): result = ("pkga", "pkgb"), ("1.0", "1.1"), ("rev2",), ("Windows",), ("prev2",) self.assert_downloaded("*", result, client, query="os=Windows") def test_pkg_query(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), ("Windows",), ("prev2",) self.assert_downloaded("pkga", result, client, query="os=Windows") class TestDownloadPatterErrors: @pytest.fixture(scope="class") def client(self): client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile("pkg", "0.1")}) client.run(f"create .") client.run("upload *#*:*#* -r=default -c") return client @staticmethod def assert_error(pattern, error, client, only_recipe=False, query=None): only_recipe = "" if not only_recipe else "--only-recipe" query = "" if not query else f"-p={query}" client.run(f"download {pattern} -r=default {only_recipe} {query}", assert_error=True) assert error in client.out def test_recipe_not_found(self, client): # FIXME: This error is ugly error = "ERROR: Recipe not found: 'zlib/1.2.11@_/_'. [Remote: default]" self.assert_error("zlib/1.2.11", error, client) def test_rrev_not_found(self, client): error = "ERROR: Recipe revision 'pkg/0.1#rev1' not found" self.assert_error("pkg/0.1#rev1", error, client) def test_pid_not_found(self, client): # FIXME: Ugly error, improve it rrev = "485dad6cb11e2fa99d9afbe44a57a164" error = f"Binary package not found: 'pkg/0.1@_/_#{rrev}:pid1'. [Remote: default]" self.assert_error(f"pkg/0.1#{rrev}:pid1", error, client) def test_prev_not_found(self, client): rrev = "485dad6cb11e2fa99d9afbe44a57a164" pid = "da39a3ee5e6b4b0d3255bfef95601890afd80709" error = f"ERROR: Package revision 'pkg/0.1#{rrev}:{pid}#prev' not found" self.assert_error(f"pkg/0.1#{rrev}:{pid}#prev", error, client) ================================================ FILE: test/integration/command/export/__init__.py ================================================ ================================================ FILE: test/integration/command/export/export_dirty_test.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient from conan.internal.util.files import load class TestSourceDirty: def test_keep_failing_source_folder(self): # https://github.com/conan-io/conan/issues/4025 client = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): def source(self): save(self, "somefile.txt", "hello world!!!") raise Exception("boom") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=1.0", assert_error=True) assert "ERROR: pkg/1.0: Error in source() method, line 6" in client.out # Check that we can debug and see the folder source_file = os.path.join(client.exported_layout().source(), "somefile.txt") assert load(source_file) == "hello world!!!" # Without any change, the export will generate same recipe revision, reuse source folder client.run("create . --name=pkg --version=1.0", assert_error=True) assert "pkg/1.0: Source folder is corrupted, forcing removal" in client.out assert "ERROR: pkg/1.0: Error in source() method, line 6" in client.out # The install also removes corrupted source folder before proceeding, then call source client.run("install --requires=pkg/1.0 --build=missing", assert_error=True) assert "pkg/1.0: WARN: Trying to remove corrupted source folder" in client.out assert "ERROR: pkg/1.0: Error in source() method, line 6" in client.out # This creates a new revision that doesn't need removal, different source folder client.save({"conanfile.py": conanfile.replace("source(", "source2(")}) client.run("create . --name=pkg --version=1.0") assert "corrupted, forcing removal" not in client.out # Check that it is empty assert os.listdir(os.path.join(client.exported_layout().source())) == [] ================================================ FILE: test/integration/command/export/export_path_test.py ================================================ import os import textwrap import pytest from conan.internal.model.manifest import FileTreeManifest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("relative_path", [False, True]) def test_basic(relative_path): client = TestClient() source_folder = os.path.join(client.current_folder, "source") files = {"conanfile.py": GenConanfile().with_exports("*"), "main.cpp": "mymain"} client.save(files, path=source_folder) if relative_path: with client.chdir("current"): client.run("export ../source --name=hello --version=0.1 --user=lasote --channel=stable") else: client.run("export source --name=hello --version=0.1 --user=lasote --channel=stable") # The result should be the same in both cases ref_layoyt = client.exported_layout() ref = ref_layoyt.reference reg_path = ref_layoyt.export() manif = FileTreeManifest.load(reg_path) assert '%s: Exported' % str(ref) in client.out assert '%s: Exported to cache folder: %s' % (str(ref), reg_path) in client.out for name in list(files.keys()): assert os.path.exists(os.path.join(reg_path, name)) expected_sums = {'conanfile.py': '7fbb7e71f5b665780ff8c407fe0b1f9e', 'main.cpp': '76c0a7a9d385266e27d69d3875f6ac19'} assert expected_sums == manif.file_sums @pytest.mark.parametrize("relative_path", [False, True]) def test_path(relative_path): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): def export(self): copy(self, "*", src=os.path.join(self.recipe_folder, "..", "source"), dst=os.path.join(self.export_folder, "source")) """) if relative_path: client.save({"conan/conanfile.py": conanfile, "source/main.cpp": "mymain"}) with client.chdir("current"): client.run("export ../conan --name=hello --version=0.1 --user=lasote --channel=stable") else: client.save({"current/conanfile.py": conanfile, "source/main.cpp": "mymain"}) with client.chdir("current"): client.run("export --name=hello --version=0.1 --user=lasote --channel=stable") ref_layoyt = client.exported_layout() reg_path = ref_layoyt.export() manif = FileTreeManifest.load(reg_path) for name in ['conanfile.py', 'conanmanifest.txt', 'source/main.cpp']: assert os.path.exists(os.path.join(reg_path, name)) expected_sums = {'conanfile.py': '6cdb33126a0408bffc0ad0ada66cb061', 'source/main.cpp': '76c0a7a9d385266e27d69d3875f6ac19'} assert expected_sums == manif.file_sums ================================================ FILE: test/integration/command/export/export_sources_test.py ================================================ import os import textwrap from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_exports(): """ Check that exported files go to the right folder """ conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" exports = "*.h" """) c = TestClient(light=True) c.save({"conanfile.py": conanfile, "hello.h": "hello", "data.txt": "data"}) c.run("create .") ref = RecipeReference.loads("hello/0.1") ref_layout = c.get_latest_ref_layout(ref) def assert_files(folder, files): assert sorted(os.listdir(folder)) == sorted(files) assert_files(ref_layout.source(), []) assert_files(ref_layout.export(), ['conanfile.py', 'conanmanifest.txt', 'hello.h']) assert_files(ref_layout.export_sources(), []) def test_exports_sources(): """ Check that exported-sources files go to the right folder AND to the source folder """ conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" exports_sources = "*.h" """) c = TestClient(light=True) c.save({"conanfile.py": conanfile, "hello.h": "hello", "data.txt": "data"}) c.run("create .") ref = RecipeReference.loads("hello/0.1") ref_layout = c.get_latest_ref_layout(ref) def assert_files(folder, files): assert sorted(os.listdir(folder)) == sorted(files) assert_files(ref_layout.source(), ['hello.h']) assert_files(ref_layout.export(), ['conanfile.py', 'conanmanifest.txt', ]) assert_files(ref_layout.export_sources(), ['hello.h']) def test_test_package_copied(): """The exclusion of the test_package folder have been removed so now we test that indeed is exported""" client = TestClient(light=True) conanfile = GenConanfile().with_exports("*").with_exports_sources("*") client.save({"conanfile.py": conanfile, "test_package/foo.txt": "bar"}) client.run("export . --name foo --version 1.0") assert "Copied 2 '.txt' file" in client.out def test_source_changes_generate_new_revisions(): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("lib", "1.0").with_exports_sources("file.h"), "file.h": "Hello World!"}) tc.run("export .") exported_rev = tc.exported_recipe_revision() tc.save({"file.h": "Bye World!"}) tc.run("export .") exported_rev_new = tc.exported_recipe_revision() assert exported_rev != exported_rev_new ================================================ FILE: test/integration/command/export/export_test.py ================================================ import json import os import platform import stat import textwrap import pytest from conan.internal.model.manifest import FileTreeManifest from conan.api.model import RecipeReference from conan.internal.paths import CONANFILE, CONAN_MANIFEST from conan.test.utils.tools import TestClient, GenConanfile from conan.internal.util.files import load, save class TestExportSettings: def test_export_without_full_reference(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("export . --user=lasote --channel=stable", assert_error=True) assert "conanfile didn't specify name" in client.out client.save({"conanfile.py": GenConanfile("lib")}) client.run("export . --user=lasote --channel=stable", assert_error=True) assert "conanfile didn't specify version" in client.out client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=lib --version=1.0 --user=lasote --channel=channel") assert "lib/1.0@lasote/channel: Exported" in client.out client.save({"conanfile.py": GenConanfile("lib", "1.0")}) client.run("export . --user=lasote") assert "lib/1.0@lasote: Exporting package recipe" in client.out client.run("export . --channel=channel", assert_error=True) assert "Can't specify channel 'channel' without user" in client.out client.save({"conanfile.py": GenConanfile("lib", "1.0").with_class_attribute('channel = "channel"')}) client.run("export .", assert_error=True) assert "Can't specify channel 'channel' without user" in client.out client.run("export . --user=user") assert "Exported: lib/1.0@user/channel" in client.out client.save({"conanfile.py": GenConanfile("lib", "1.0").with_class_attribute('user = "user"')}) client.run("export .") assert "lib/1.0@user: Exported" in client.out client.run("export . --channel=channel") assert "lib/1.0@user/channel: Exported" in client.out def test_export_read_only(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): name = "hello" version = "1.2" exports = "file1.txt" exports_sources = "file2.txt" """) client.save({CONANFILE: conanfile, "file1.txt": "", "file2.txt": ""}) mode1 = os.stat(os.path.join(client.current_folder, "file1.txt")).st_mode mode2 = os.stat(os.path.join(client.current_folder, "file2.txt")).st_mode os.chmod(os.path.join(client.current_folder, "file1.txt"), mode1 & ~stat.S_IWRITE) os.chmod(os.path.join(client.current_folder, "file2.txt"), mode2 & ~stat.S_IWRITE) client.run("export . --user=lasote --channel=stable") layout = client.exported_layout() export_path = layout.export() export_src_path = layout.export_sources() assert load(os.path.join(export_path, "file1.txt")) == "" assert load(os.path.join(export_src_path, "file2.txt")) == "" with pytest.raises(IOError): save(os.path.join(export_path, "file1.txt"), "") with pytest.raises(IOError): save(os.path.join(export_src_path, "file2.txt"), "") os.chmod(os.path.join(client.current_folder, "file1.txt"), mode1 | stat.S_IWRITE) os.chmod(os.path.join(client.current_folder, "file2.txt"), mode2 | stat.S_IWRITE) client.save({CONANFILE: conanfile, "file1.txt": "file1", "file2.txt": "file2"}) client.run("export . --user=lasote --channel=stable") layout = client.exported_layout() export_path = layout.export() export_src_path = layout.export_sources() assert load(os.path.join(export_path, "file1.txt")) == "file1" assert load(os.path.join(export_src_path, "file2.txt")) == "file2" client.run("install --requires=hello/1.2@lasote/stable --build=missing") assert "hello/1.2@lasote/stable: Generating the package" in client.out client.save({CONANFILE: conanfile, "file1.txt": "", "file2.txt": ""}) os.chmod(os.path.join(client.current_folder, "file1.txt"), mode1 & ~stat.S_IWRITE) os.chmod(os.path.join(client.current_folder, "file2.txt"), mode2 & ~stat.S_IWRITE) client.run("export . --user=lasote --channel=stable") layout = client.exported_layout() export_path = layout.export() export_src_path = layout.export_sources() assert load(os.path.join(export_path, "file1.txt")) == "" assert load(os.path.join(export_src_path, "file2.txt")) == "" client.run("install --requires=hello/1.2@lasote/stable --build=hello*") assert "hello/1.2@lasote/stable: Generating the package" in client.out def test_code_parent(self): # when referencing the parent, the relative folder "sibling" will be kept base = """ import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): name = "hello" version = "1.2" def export(self): copy(self, "*.txt", src=os.path.join(self.recipe_folder, ".."), dst=self.export_folder) """ for conanfile in (base, base.replace("../*.txt", "../sibling*")): client = TestClient(light=True) client.save({"recipe/conanfile.py": conanfile, "sibling/file.txt": "Hello World!"}) client.current_folder = os.path.join(client.current_folder, "recipe") client.run("export . --user=lasote --channel=stable") layout = client.exported_layout() export_path = layout.export() content = load(os.path.join(export_path, "sibling/file.txt")) assert "Hello World!" == content def test_code_sibling(self): # if provided a path with slash, it will use as a export base client = TestClient(light=True) conanfile = """ import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): name = "hello" version = "1.2" def export(self): copy(self, "*.txt", src=os.path.join(self.recipe_folder, "..", "sibling"), dst=self.export_folder) """ files = {"recipe/conanfile.py": conanfile, "sibling/file.txt": "Hello World!"} client.save(files) client.current_folder = os.path.join(client.current_folder, "recipe") client.run("export . --user=lasote --channel=stable") layout = client.exported_layout() export_path = layout.export() content = load(os.path.join(export_path, "file.txt")) assert "Hello World!" == content def test_code_several_sibling(self): # if provided a path with slash, it will use as a export base client = TestClient(light=True) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): name = "hello" version = "1.2" # exports_sources = "../test/src/*", "../cpp/*", "../include/*" def export_sources(self): copy(self, "*", src=os.path.join(self.recipe_folder, "..", "test", "src"), dst=self.export_sources_folder) copy(self, "*", src=os.path.join(self.recipe_folder, "..", "cpp"), dst=self.export_sources_folder) copy(self, "*", src=os.path.join(self.recipe_folder, "..", "include"), dst=self.export_sources_folder) """) client.save({"recipe/conanfile.py": conanfile, "test/src/file.txt": "Hello World!", "cpp/file.cpp": "Hello World!", "include/file.h": "Hello World!"}) client.current_folder = os.path.join(client.current_folder, "recipe") client.run("export . --user=lasote --channel=stable") layout = client.exported_layout() export_path = layout.export_sources() assert sorted(['file.txt', 'file.cpp', 'file.h']) == sorted(os.listdir(export_path)) @pytest.mark.parametrize("filename", ["myconanfile.py", "Conanfile.py"]) def test_filename(self, filename): client = TestClient(light=True) client.save({filename: GenConanfile("hello", "1.2")}) client.run("export %s --user=user --channel=stable" % filename) assert "hello/1.2@user/stable: Exported" in client.out layout = client.exported_layout() export_path = layout.export() conanfile = load(os.path.join(export_path, "conanfile.py")) assert "name = 'hello'" in conanfile manifest = load(os.path.join(export_path, "conanmanifest.txt")) assert 'conanfile.py: 5dc49e518e15f3889cb2e097ce4d1dff' in manifest def test_exclude_basic(self): client = TestClient(light=True) conanfile = """ from conan import ConanFile class TestConan(ConanFile): name = "hello" version = "1.2" exports = "*.txt", "!*file1.txt" exports_sources = "*.cpp", "!*temp.cpp" """ client.save({CONANFILE: conanfile, "file.txt": "", "file1.txt": "", "file.cpp": "", "file_temp.cpp": ""}) client.run("export .") layout = client.exported_layout() export_path = layout.export() exports_sources_path = layout.export_sources() assert os.path.exists(os.path.join(export_path, "file.txt")) assert not os.path.exists(os.path.join(export_path, "file1.txt")) assert os.path.exists(os.path.join(exports_sources_path, "file.cpp")) assert not os.path.exists(os.path.join(exports_sources_path, "file_temp.cpp")) def test_exclude_folders(self): client = TestClient(light=True) client.save({CONANFILE: GenConanfile("hello", "1.2").with_exports("*.txt", "!*/temp/*"), "file.txt": "", "any/temp/file1.txt": "", "other/sub/file2.txt": ""}) client.run("export .") layout = client.exported_layout() export_path = layout.export() assert os.path.exists(os.path.join(export_path, "file.txt")) assert not os.path.exists(os.path.join(export_path, "any/temp/file1.txt")) assert os.path.exists(os.path.join(export_path, "other/sub/file2.txt")) class TestExport: @pytest.fixture(autouse=True) def setup(self): self.client = TestClient(light=True) self.files = {"conanfile.py": GenConanfile("hello0", "0.1").with_exports("*"), "main.cpp": "MyMain", "CMakeLists.txt": "MyCmake", "executable": "myexe"} self.ref = RecipeReference("hello0", "0.1", "lasote", "stable") self.client.save(self.files) self.client.run("export . --user=lasote --channel=stable") def test_basic(self): reg_path = self.client.exported_layout().export() manif = FileTreeManifest.load(reg_path) assert '%s: Exported' % str(self.ref) in self.client.out assert '%s: Exported to cache folder: %s' % (str(self.ref), reg_path) in self.client.out assert os.path.exists(reg_path) for name in list(self.files.keys()): assert os.path.exists(os.path.join(reg_path, name)) expected_sums = {'CMakeLists.txt': '3cf710785270c7e98a30d4a90ea66492', 'conanfile.py': '5dbbe4328efa3342baba2a7ca961ede1', 'executable': 'db299d5f0d82f113fad627a21f175e59', 'main.cpp': 'd9c03c934a4b3b1670775c17c26f39e9'} assert expected_sums == manif.file_sums def test_case_sensitive(self): self.ref = RecipeReference("hello0", "0.1", "lasote", "stable") self.client.save({"conanfile.py": GenConanfile("hello0", "0.1").with_exports("*")}) self.client.run("export . --user=lasote --channel=stable") assert "hello0/0.1@lasote/stable: Exported" in self.client.out def test_export_filter(self): self.client.save({CONANFILE: GenConanfile("openssl", "2.0.1")}) self.client.run("export . --user=lasote --channel=stable") layout = self.client.exported_layout() export_path = layout.export() assert sorted(os.listdir(export_path)) == [CONANFILE, CONAN_MANIFEST] content = """ from conan import ConanFile class OpenSSLConan(ConanFile): name = "openssl" version = "2.0.1" exports = ('*.txt', '*.h') """ self.client.save({CONANFILE: content}) self.client.run("export . --user=lasote --channel=stable") layout = self.client.exported_layout() export_path = layout.export() assert sorted(os.listdir(export_path)) == ['CMakeLists.txt', CONANFILE, CONAN_MANIFEST] # Now exports being a list instead a tuple content = """ from conan import ConanFile class OpenSSLConan(ConanFile): name = "openssl" version = "2.0.1" exports = ['*.txt', '*.h'] """ self.client.save({CONANFILE: content}) self.client.run("export . --user=lasote --channel=stable") layout = self.client.exported_layout() export_path = layout.export() assert sorted(os.listdir(export_path)) == ['CMakeLists.txt', CONANFILE, CONAN_MANIFEST] class TestExportMetadata: def test_revision_mode_hash(self): t = TestClient(light=True) t.save({'conanfile.py': GenConanfile().with_revision_mode("hash")}) t.run(f"export . --name=name --version=version") layout = t.exported_layout() assert layout.reference.revision == "99c241d3d47396772cd1ac5a36307a29" def test_revision_mode_invalid(self): t = TestClient(light=True) t.save({'conanfile.py': GenConanfile().with_revision_mode("auto")}) t.run("export . --name=name --version=version", assert_error=True) assert "ERROR: Revision mode should be one of 'hash' (default) or 'scm'" in t.out def test_export_no_params(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("lib", "1.0")}) client.run('export .') assert "lib/1.0: Exported" in client.out def test_export_with_name_and_version(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run('export . --name=lib --version=1.0') assert "lib/1.0: Exported" in client.out def test_export_with_only_user_channel(self): """This should be the recommended way and only from Conan 2.0""" client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("lib", "1.0")}) client.run('export . --version= --user=user --channel=channel') assert "lib/1.0@user/channel: Exported" in client.out def test_export_conflict_no_user_channel(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run('export . --name=pkg --version=0.1 --user=user --channel=channel') assert "pkg/0.1@user/channel: Exported" in client.out client.run('export . --name=pkg --version=0.1 --user=other --channel=stable') assert "pkg/0.1@other/stable: Exported" in client.out client.run('export . --name=pkg --version=0.1') assert "pkg/0.1: Exported" in client.out client.run('export . --name=pkg --version=0.1') assert "pkg/0.1: Exported" in client.out @pytest.mark.skipif(platform.system() != "Linux", reason="Needs case-sensitive filesystem") def test_export_casing(): # https://github.com/conan-io/conan/issues/8583 client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): exports = "file1", "FILE1" exports_sources = "test", "TEST" """) client.save({"conanfile.py": conanfile, "test": "some lowercase", "TEST": "some UPPERCASE", "file1": "file1 lowercase", "FILE1": "file1 UPPERCASE" }) assert client.load("test") == "some lowercase" assert client.load("TEST") == "some UPPERCASE" assert client.load("file1") == "file1 lowercase" assert client.load("FILE1") == "file1 UPPERCASE" client.run("export . --name=pkg --version=0.1") layout = client.exported_layout() export_src_folder = layout.export_sources() assert load(os.path.join(export_src_folder, "test")) == "some lowercase" assert load(os.path.join(export_src_folder, "TEST")) == "some UPPERCASE" exports_folder = layout.export() assert load(os.path.join(exports_folder, "file1")) == "file1 lowercase" assert load(os.path.join(exports_folder, "FILE1")) == "file1 UPPERCASE" def test_export_invalid_refs(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile()}) c.run("export . --name=pkg% --version=0.1", assert_error=True) assert "ERROR: Invalid package name 'pkg%'" in c.out c.run("export . --name=pkg --version=0.1%", assert_error=True) assert "ERROR: Invalid package version '0.1%'" in c.out c.run("export . --name=pkg --version=0.1 --user=user%", assert_error=True) assert "ERROR: Invalid package user 'user%'" in c.out c.run("export . --name=pkg --version=0.1 --user=user --channel=channel%", assert_error=True) assert "ERROR: Invalid package channel 'channel%'" in c.out def test_allow_temp_uppercase(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile()}) c.run("export . --name=Pkg --version=0.1", assert_error=True) assert "ERROR: Conan packages names 'Pkg/0.1' must be all lowercase" in c.out c.save_home({"global.conf": "core:allow_uppercase_pkg_names=True"}) c.run("export . --name=Pkg --version=0.1") assert "WARN: Package name 'Pkg/0.1' has uppercase, " \ "and has been allowed by temporary config." in c.out def test_warn_special_chars_refs(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile()}) c.run("export . --name=pkg.name --version=0.1") assert "WARN: Name containing special chars is discouraged 'pkg.name'" in c.out c.run("export . --name=name --version=0.1 --user=user+some") assert "WARN: User containing special chars is discouraged 'user+some'" in c.out c.run("export . --name=pkg.name --version=0.1 --user=user --channel=channel+some") assert "WARN: Channel containing special chars is discouraged 'channel+some'" in c.out def test_export_json(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile()}) c.run("export . --name=foo --version=0.1 --format json") info = json.loads(c.stdout) assert info["reference"] == "foo/0.1#4d670581ccb765839f2239cc8dff8fbd" assert len(info) == 1 # Only "reference" key yet ================================================ FILE: test/integration/command/export/exports_method_test.py ================================================ import os import textwrap from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save_files, load class TestExportsMethod: def test_export_method(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class MethodConan(ConanFile): exports = "file.txt" def export(self): copy(self, "LICENSE.md", self.recipe_folder, self.export_folder) """) client.save({"conanfile.py": conanfile, "LICENSE.md": "license", "file.txt": "file"}) client.run("export . --name=pkg --version=0.1") assert "pkg/0.1: Copied 1 '.txt' file: file.txt" in client.out assert "pkg/0.1: Calling export()" in client.out assert "Copied 1 '.md' file: LICENSE.md" in client.out layout = client.exported_layout() exported_files = os.listdir(layout.export()) assert "file.txt" in exported_files assert "LICENSE.md" in exported_files def test_export_method_parent_folder(self): folder = temp_folder() save_files(folder, {"subdir/subdir2/file2.txt": "", "subdir/file1.txt": ""}) client = TestClient(current_folder=os.path.join(folder, "recipe")) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class MethodConan(ConanFile): def export(self): self.output.info("Executing export() method") copy(self, "*.txt", os.path.join(self.recipe_folder, "../subdir"), self.export_folder) """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=0.1") assert "Copied 2 '.txt' files: file1.txt, file2.txt" in client.out layout = client.exported_layout() assert os.path.isfile(os.path.join(layout.export(), "file1.txt")) assert os.path.isfile(os.path.join(layout.export(), "subdir2", "file2.txt")) def test_export_no_settings_options_method(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class MethodConan(ConanFile): settings = "os" def export(self): if self.settings.os == "Windows": copy(self, "LICENSE.md", self.recipe_folder, self.export_folder) """) client.save({"conanfile.py": conanfile, "LICENSE.md": "license"}) client.run("export . --name=pkg --version=0.1", assert_error=True) assert "ERROR: pkg/0.1: Error in export() method, line 8" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class MethodConan(ConanFile): options = {"myopt": ["myval", "other"]} default_options = {"myopt": "myval"} def export(self): if self.default_options["myopt"] == "myval": copy(self, "LICENSE.md", self.recipe_folder, self.export_folder) """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=0.1", assert_error=True) assert "ERROR: pkg/0.1: Error in export() method, line 9" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile class MethodConan(ConanFile): options = {"myopt": ["myval"]} default_options = {"myopt": "myval"} def export(self): pass def export_sources(self): pass def build(self): self.output.info("MYOPT: %s" % self.options.myopt) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") assert "pkg/0.1: MYOPT: myval" in client.out def test_export_folders(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, load import os class MethodConan(ConanFile): def export(self): content = load(self, os.path.join(os.getcwd(), "data.txt")) save(self, os.path.join(self.export_folder, "myfile.txt"), content) """) client.save({"recipe/conanfile.py": conanfile, "recipe/data.txt": "mycontent"}) client.run("export recipe --name=pkg --version=0.1") layout = client.exported_layout() assert "mycontent" == load(os.path.join(layout.export(), "myfile.txt")) client.current_folder = os.path.join(client.current_folder, "recipe") client.run("export . --name=pkg --version=0.1") layout = client.exported_layout() assert "mycontent" == load(os.path.join(layout.export(), "myfile.txt")) def test_export_attribute_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MethodConan(ConanFile): export = "file.txt" """) client.save({"conanfile.py": conanfile, "file.txt": "file"}) client.run("export . --name=pkg --version=0.1", assert_error=True) assert "ERROR: conanfile 'export' must be a method" in client.out def test_exports_method_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MethodConan(ConanFile): def exports(self): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=0.1", assert_error=True) assert "ERROR: conanfile 'exports' shouldn't be a method, use 'export()' instead" in client.out class TestExportsSourcesMethod: def test_export_sources_method(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class MethodConan(ConanFile): exports_sources = "file.txt" def export_sources(self): copy(self, "LICENSE.md", self.recipe_folder, self.export_sources_folder) """) client.save({"conanfile.py": conanfile, "LICENSE.md": "license", "file.txt": "file"}) client.run("export . --name=pkg --version=0.1") assert "pkg/0.1: Copied 1 '.txt' file: file.txt" in client.out assert "Copied 1 '.md' file: LICENSE.md" in client.out layout = client.exported_layout() exported_files = os.listdir(layout.export_sources()) assert "file.txt" in exported_files assert "LICENSE.md" in exported_files def test_export_source_folders(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, load import os class MethodConan(ConanFile): def export_sources(self): content = load(self, os.path.join(os.getcwd(), "data.txt")) save(self, os.path.join(self.export_sources_folder, "myfile.txt"), content) """) client.save({"recipe/conanfile.py": conanfile, "recipe/data.txt": "mycontent"}) client.run("export recipe --name=pkg --version=0.1") layout = client.exported_layout() assert "mycontent" == load(os.path.join(layout.export_sources(), "myfile.txt")) client.current_folder = os.path.join(client.current_folder, "recipe") client.run("export . --name=pkg --version=0.1") layout = client.exported_layout() assert "mycontent" == load(os.path.join(layout.export_sources(), "myfile.txt")) def test_export_sources_attribute_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MethodConan(ConanFile): export_sources = "file.txt" """) client.save({"conanfile.py": conanfile, "file.txt": "file"}) client.run("export . --name=pkg --version=0.1", assert_error=True) assert "ERROR: conanfile 'export_sources' must be a method" in client.out def test_exports_sources_method_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MethodConan(ConanFile): def exports_sources(self): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=0.1", assert_error=True) assert ("ERROR: conanfile 'exports_sources' shouldn't be a method, " "use 'export_sources()' instead") in client.out def test_exports_sources_upload_error(self): # https://github.com/conan-io/conan/issues/7377 client = TestClient(default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load, copy class MethodConan(ConanFile): def export_sources(self): copy(self, "*", self.recipe_folder, self.export_sources_folder) def build(self): self.output.info("CONTENT: %s" % load(self, "myfile.txt")) """) client.save({"conanfile.py": conanfile, "myfile.txt": "mycontent"}) client.run("export . --name=pkg --version=0.1") assert "Copied 1 '.txt' file: myfile.txt" in client.out client.run("upload pkg/0.1 -r default") client.run("remove * -c") client.run("install --requires=pkg/0.1@ --build='*'") assert "pkg/0.1: Sources downloaded from 'default'" in client.out assert "pkg/0.1: CONTENT: mycontent" in client.out ================================================ FILE: test/integration/command/export/test_export.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_basic(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): name = "hello" version = "1.2" user = "myuser" channel = "mychannel" """) client.save({"conanfile.py": conanfile}) client.run("export .") assert "hello/1.2@myuser/mychannel" in client.out client.run("list *") assert "hello/1.2@myuser/mychannel" in client.out client.run("create .") assert "hello/1.2@myuser/mychannel" in client.out assert "hello/1.2:" not in client.out def test_export_cci(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("mypkg", "cci.123")}) c.run("export .") assert ("WARN: risk: Version 'cci.123' contains an alphanumeric " "major alongside a minor version") in c.out c.save({"conanfile.py": GenConanfile("mypkg", "develop")}) c.run("export .") assert "WARN" not in c.out c.save({"conanfile.py": GenConanfile("mypkg", "cci.123") .with_class_attribute("package_id_embed_mode='full_mode'")}) c.run("export .") assert "WARN" not in c.out ================================================ FILE: test/integration/command/export_pkg_test.py ================================================ import json import os import re import textwrap from textwrap import dedent from conan.internal.paths import CONANFILE from conan.test.utils.tools import TestClient, GenConanfile from conan.internal.util.files import load class TestExportPkg: def test_dont_touch_server(self): # https://github.com/conan-io/conan/issues/3432 client = TestClient(servers={"default": None}, requester_class=None, inputs=["admin", "password"]) client.save({"conanfile.py": GenConanfile().with_name("pkg").with_version("0.1")}) client.run("install .") client.run("export-pkg . --user=lasote --channel=stable ") def test_transitive_without_settings(self): # https://github.com/conan-io/conan/issues/3367 client = TestClient() client.save({"pkgc/conanfile.py": GenConanfile("pkgc", "0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkgc/0.1"), "pkga/conanfile.py": GenConanfile("pkga", "0.1").with_requires("pkgb/0.1")}) client.run("create pkgc") client.run("create pkgb") client.run("build pkga -bf=build") client.run("export-pkg pkga ") package_id = re.search(r"Packaging to (\S+)", str(client.out)).group(1) assert f"conanfile.py (pkga/0.1): Package '{package_id}' created" in client.out # we can export-pkg without the dependencies binaries if we need to optimize client.run("remove pkgc*:* -c") client.run("remove pkgb*:* -c") client.run("export-pkg pkga --skip-binaries") package_id = re.search(r"Packaging to (\S+)", str(client.out)).group(1) assert f"conanfile.py (pkga/0.1): Package '{package_id}' created" in client.out def test_package_folder_errors(self): # https://github.com/conan-io/conan/issues/2350 client = TestClient() client.save({CONANFILE: GenConanfile()}) client.run("export-pkg --name=hello --version=0.1 --user=lasote --channel=stable") assert ("conanfile.py (hello/0.1@lasote/stable): package(): " "WARN: No files in this package!") in client.out def test_options(self): # https://github.com/conan-io/conan/issues/2242 conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): name = "hello" version = "0.1" options = { "optionOne": [True, False, 123] } default_options = {"optionOne": True} """) client = TestClient() client.save({CONANFILE: conanfile}) client.run("export-pkg .") client.run("list hello/0.1:*") assert "optionOne: True" in client.out assert "optionOne: False" not in client.out assert "optionOne: 123" not in client.out client.run("export-pkg . -o optionOne=False") client.run("list hello/0.1:*") assert "optionOne: True" in client.out assert "optionOne: False" in client.out assert "optionOne: 123" not in client.out client.run("export-pkg . -o hello/*:optionOne=123") client.run("list hello/0.1:*") assert "optionOne: True" in client.out assert "optionOne: False" in client.out assert "optionOne: 123" in client.out def test_profile_environment(self): # https://github.com/conan-io/conan/issues/4832 conanfile = dedent(""" import os from conan import ConanFile from conan.tools.env import VirtualBuildEnv class helloPythonConan(ConanFile): def package(self): build_env = VirtualBuildEnv(self).vars() with build_env.apply(): self.output.info("ENV-VALUE: %s!!!" % os.getenv("MYCUSTOMVAR")) """) profile = dedent(""" [buildenv] MYCUSTOMVAR=MYCUSTOMVALUE """) client = TestClient() client.save({CONANFILE: conanfile, "myprofile": profile}) client.run("export-pkg . --name=hello --version=0.1 --user=lasote --channel=stable " " -pr=myprofile") assert "conanfile.py (hello/0.1@lasote/stable): ENV-VALUE: MYCUSTOMVALUE!!!" in client.out def test_build_folders(self): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): name = "hello" version = "0.1" settings = "os" def package(self): copy(self, "*.h", os.path.join(self.source_folder, "include"), os.path.join(self.package_folder, "inc")) copy(self, "*.lib", os.path.join(self.build_folder, "lib"), os.path.join(self.package_folder, "lib")) """) client.save({CONANFILE: conanfile, "include/header.h": "//Windows header", "include/header.txt": "", "libs/what": "", "lib/hello.lib": "My Lib", "lib/bye.txt": ""}, clean_first=True) client.run("export-pkg . --user=lasote --channel=stable -s os=Windows -vvv") assert "copy(pattern=*.h) copied 1 files" in client.out assert "copy(pattern=*.lib) copied 1 files" in client.out package_folder = client.created_layout().package() inc = os.path.join(package_folder, "inc") assert os.listdir(inc) == ["header.h"] assert load(os.path.join(inc, "header.h")) == "//Windows header" lib = os.path.join(package_folder, "lib") assert os.listdir(lib) == ["hello.lib"] assert load(os.path.join(lib, "hello.lib")) == "My Lib" def test_default_source_folder(self): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): def package(self): copy(self, "*.h", os.path.join(self.source_folder, "src"), os.path.join(self.package_folder, "include")) copy(self, "*.lib", self.build_folder, os.path.join(self.package_folder, "lib"), keep_path=False) """) client.save({CONANFILE: conanfile, "src/header.h": "contents", "build/lib/hello.lib": "My Lib"}) client.run("export-pkg . --name=hello --version=0.1 --user=lasote --channel=stable " "-s os=Windows") package_folder = client.created_layout().package() header = os.path.join(package_folder, "include/header.h") assert os.path.exists(header) hello_path = os.path.join(package_folder, "lib", "hello.lib") assert os.path.exists(hello_path) def test_build_source_folders(self): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): settings = "os" name = "hello" version = "0.1" def layout(self): self.folders.build = "build" self.folders.source = "src" def package(self): copy(self, "*.h", os.path.join(self.source_folder, "include"), os.path.join(self.package_folder, "inc")) copy(self, "*.lib", os.path.join(self.build_folder, "lib"), os.path.join(self.package_folder, "lib")) """) client.save({CONANFILE: conanfile, "src/include/header.h": "//Windows header", "src/include/header.txt": "", "build/libs/what": "", "build/lib/hello.lib": "My Lib", "build/lib/bye.txt": ""}) client.run("export-pkg . --user=lasote --channel=stable -s os=Windows") package_folder = client.created_layout().package() inc = os.path.join(package_folder, "inc") assert os.listdir(inc) == ["header.h"] assert load(os.path.join(inc, "header.h")) == "//Windows header" lib = os.path.join(package_folder, "lib") assert os.listdir(lib) == ["hello.lib"] assert load(os.path.join(lib, "hello.lib")) == "My Lib" def test_partial_references(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): name = "hello" version = "0.1" settings = "os" def package(self): copy(self, "*", self.source_folder, self.package_folder) """) # Partial reference is ok client.save({CONANFILE: conanfile, "file.txt": "txt contents"}) client.run("export-pkg . --user=conan --channel=stable") assert ("conanfile.py (hello/0.1@conan/stable): package(): " "Packaged 1 '.txt' file: file.txt") in client.out # Specify different name or version is not working client.run("export-pkg . --name=lib", assert_error=True) assert "ERROR: Package recipe with name lib!=hello" in client.out client.run("export-pkg . --version=1.1", assert_error=True) assert "ERROR: Package recipe with version 1.1!=0.1" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): settings = "os" def package(self): copy(self, "*", self.source_folder, self.package_folder) """) # Partial reference is ok client.save({CONANFILE: conanfile, "file.txt": "txt contents"}) client.run("export-pkg . --name=anyname --version=1.222 --user=conan --channel=stable") assert ("conanfile.py (anyname/1.222@conan/stable): package(): " "Packaged 1 '.txt' file: file.txt") in client.out def test_with_deps(self): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=hello --version=0.1 --user=lasote --channel=stable") conanfile = GenConanfile().with_name("hello1").with_version("0.1")\ .with_import("from conan.tools.files import copy, collect_libs") \ .with_require("hello/0.1@lasote/stable") conanfile = str(conanfile) + """\n def package_info(self): self.cpp_info.libs = collect_libs(self) def layout(self): self.folders.build = "Release_x86" def package(self): copy(self, "*", self.source_folder, self.package_folder) copy(self, "*", self.build_folder, self.package_folder) """ client.save({"conanfile.py": conanfile}, clean_first=True) client.save({"Release_x86/lib/libmycoollib.a": ""}) settings = ('-s os=Windows -s compiler=gcc -s compiler.version=4.9 ' '-s compiler.libcxx=libstdc++ -s build_type=Release -s arch=x86') client.run("export-pkg . --name=hello1 --version=0.1 --user=lasote --channel=stable %s" % settings) consumer = (GenConanfile().with_requires("hello1/0.1@lasote/stable") .with_settings("os", "build_type")) client.save({CONANFILE: consumer}, clean_first=True) client.run("install conanfile.py -g CMakeDeps") assert "hello/0.1@lasote/stable: Already installed!" in client.out assert "hello1/0.1@lasote/stable: Already installed!" in client.out cmakeinfo = client.load("hello1-release-data.cmake") assert "set(hello1_LIBS_RELEASE mycoollib)" in cmakeinfo def test_export_pkg_version_type(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Hello(ConanFile): def requirements(self): assert type(self.version) is str, "Version should be a string" """) client.save({"conanfile.py": conanfile}) client.run("create . --name=hello --version=0.1") # This used to trigger the assertion error client.run("export-pkg . --name=hello --version=0.1") def test_export_pkg_json(self): client = TestClient() client.save({"conanfile.py": GenConanfile("pkg", "0.1")}) # Wrong folders client.run("export-pkg . --format=json", redirect_stdout="exported.json") graph = json.loads(client.load("exported.json")) node = graph["graph"]["nodes"]["0"] assert "pkg/0.1" in node["ref"] # https://github.com/conan-io/conan/issues/15041 assert "da39a3ee5e6b4b0d3255bfef95601890afd80709" == node["package_id"] assert "485dad6cb11e2fa99d9afbe44a57a164" == node["rrev"] assert "0ba8627bd47edc3a501e8f0eb9a79e5e" == node["prev"] assert "Build" == node["binary"] assert node["rrev_timestamp"] is not None assert node["prev_timestamp"] is not None # Make sure the exported json file can be used for ``conan lsit --graph`` input to upload client.run("list --graph=exported.json -gb=build --format=json") pkglist = json.loads(client.stdout) revs = pkglist["Local Cache"]["pkg/0.1"]["revisions"]["485dad6cb11e2fa99d9afbe44a57a164"] assert "da39a3ee5e6b4b0d3255bfef95601890afd80709" in revs["packages"] def test_export_pkg_no_ref(self): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): name = "hello" version = "0.1" def package(self): copy(self, "*.h", os.path.join(self.source_folder, "src"), os.path.join(self.package_folder, "include")) """) client.save({CONANFILE: conanfile, "src/header.h": "contents"}) client.run("export-pkg . -s os=Windows") package_folder = client.created_layout().package() header = os.path.join(package_folder, "include/header.h") assert os.path.exists(header) def test_build_policy_never(): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class TestConan(ConanFile): build_policy = "never" def package(self): copy(self, "*.h", os.path.join(self.source_folder, "src"), os.path.join(self.package_folder, "include")) """) client.save({CONANFILE: conanfile, "src/header.h": "contents"}) client.run("export-pkg . --name=pkg --version=1.0") assert "conanfile.py (pkg/1.0): package(): Packaged 1 '.h' file: header.h" in client.out # check for https://github.com/conan-io/conan/issues/10736 client.run("export-pkg . --name=pkg --version=1.0") assert "conanfile.py (pkg/1.0): package(): Packaged 1 '.h' file: header.h" in client.out client.run("install --requires=pkg/1.0@ --build='*'") client.assert_listed_require({"pkg/1.0": "Cache"}) assert "conanfile.py (pkg/1.0): Calling build()" not in client.out def test_build_policy_never_missing(): # https://github.com/conan-io/conan/issues/9798 client = TestClient() client.save({"conanfile.py": GenConanfile().with_class_attribute('build_policy = "never"'), "consumer.txt": "[requires]\npkg/1.0"}) client.run("export . --name=pkg --version=1.0") client.run("install --requires=pkg/1.0@ --build='*'", assert_error=True) assert "ERROR: Missing binary: pkg/1.0" in client.out client.run("install --requires=pkg/1.0@ --build=missing", assert_error=True) assert "ERROR: Missing binary: pkg/1.0" in client.out def test_export_pkg_json_formatter(): """ Tests the ``conan export-pkg . -f json`` result """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.2" def package_info(self): self.cpp_info.libs = ["pkg"] self.cpp_info.includedirs = ["path/includes/pkg", "other/include/path/pkg"] self.cpp_info.libdirs = ["one/lib/path/pkg"] self.cpp_info.defines = ["pkg_onedefinition", "pkg_twodefinition"] self.cpp_info.cflags = ["pkg_a_c_flag"] self.cpp_info.cxxflags = ["pkg_a_cxx_flag"] self.cpp_info.sharedlinkflags = ["pkg_shared_link_flag"] self.cpp_info.exelinkflags = ["pkg_exe_link_flag"] self.cpp_info.sysroot = "/path/to/folder/pkg" self.cpp_info.frameworks = ["pkg_oneframework", "pkg_twoframework"] self.cpp_info.system_libs = ["pkg_onesystemlib", "pkg_twosystemlib"] self.cpp_info.frameworkdirs = ["one/framework/path/pkg"] self.cpp_info.set_property("pkg_config_name", "pkg_other_name") self.cpp_info.set_property("pkg_config_aliases", ["pkg_alias1", "pkg_alias2"]) self.cpp_info.components["cmp1"].libs = ["libcmp1"] self.cpp_info.components["cmp1"].set_property("pkg_config_name", "compo1") self.cpp_info.components["cmp1"].set_property("pkg_config_aliases", ["compo1_alias"]) self.cpp_info.components["cmp1"].sysroot = "/another/sysroot" """) client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("0.1") .with_require("pkg/0.2")}, clean_first=True) client.run("export-pkg . -f json") info = json.loads(client.stdout) nodes = info["graph"]["nodes"] hello_pkg_ref = 'hello/0.1#18d5440ae45afc4c36139a160ac071c7' pkg_pkg_ref = 'pkg/0.2#926714b5fb0a994f47ec37e071eba1da' hello_cpp_info = pkg_cpp_info = None for n in nodes.values(): ref = n["ref"] if ref == hello_pkg_ref: assert n['binary'] == "Build" hello_cpp_info = n['cpp_info'] elif ref == pkg_pkg_ref: assert n['binary'] == "Cache" pkg_cpp_info = n['cpp_info'] assert hello_cpp_info and pkg_cpp_info # hello/0.1 cpp_info assert hello_cpp_info['root']["libs"] is None assert len(hello_cpp_info['root']["bindirs"]) == 1 assert len(hello_cpp_info['root']["libdirs"]) == 1 assert hello_cpp_info['root']["sysroot"] is None assert hello_cpp_info['root']["properties"] is None # pkg/0.2 cpp_info # root info assert pkg_cpp_info['root']["libs"] == ['pkg'] assert len(pkg_cpp_info['root']["bindirs"]) == 1 assert len(pkg_cpp_info['root']["libdirs"]) == 1 assert pkg_cpp_info['root']["sysroot"] == '/path/to/folder/pkg' assert pkg_cpp_info['root']["system_libs"] == ['pkg_onesystemlib', 'pkg_twosystemlib'] assert pkg_cpp_info['root']['cflags'] == ['pkg_a_c_flag'] assert pkg_cpp_info['root']['cxxflags'] == ['pkg_a_cxx_flag'] assert pkg_cpp_info['root']['defines'] == ['pkg_onedefinition', 'pkg_twodefinition'] assert pkg_cpp_info['root']["properties"] == {'pkg_config_name': 'pkg_other_name', 'pkg_config_aliases': ['pkg_alias1', 'pkg_alias2']} # component info assert pkg_cpp_info["cmp1"]["libs"] == ["libcmp1"] def test_export_pkg_dont_update_src(): """ There was a bug in 1.X and sources were not updated correctly in export-pkg close https://github.com/conan-io/conan/issues/6041 """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class Hello(ConanFile): name = "hello" version = "0.1" exports_sources = "*.cpp" def build(self): content = load(self, "src/hello.cpp") self.output.info("CONTENT: {}".format(content)) """) c.save({"conanfile.py": conanfile, "src/hello.cpp": "old code!"}) c.run("install .") c.run("build .") c.run("export-pkg .") c.run("install --requires=hello/0.1@ --build=hello*") assert "hello/0.1: CONTENT: old code!" in c.out # Now locally change the source code c.save({"src/hello.cpp": "updated code!"}) c.run("install .") c.run("build .") c.run("export-pkg .") c.run("install --requires=hello/0.1@ --build=hello*") assert "hello/0.1: CONTENT: updated code!" in c.out def test_negate_tool_requires(): c = TestClient() profile = textwrap.dedent(""" [tool_requires] !mypkg/*:cmake/3.24 """) c.save({"myprofile": profile, "conanfile.py": GenConanfile("mypkg", "0.1")}) c.run("export-pkg . -pr=myprofile") assert "conanfile.py (mypkg/0.1): Created package" in c.out def test_export_pkg_tool_requires(): """ when a package has "tool_requires" that it needs at the package() method, like typical cmake.install() or autotools.install() (tool_require msys2), then it is necessary: - to install the dependencies - to inject the tool-requirements - to propagate the environment and the conf """ c = TestClient(default_server_user=True) tool = textwrap.dedent(""" from conan import ConanFile class Tool(ConanFile): name = "tool" version = "0.1" def package_info(self): self.buildenv_info.define("MYVAR", "MYVALUE") self.conf_info.define("user.team:conf", "CONF_VALUE") """) consumer = textwrap.dedent(""" import platform from conan import ConanFile class Consumer(ConanFile): name = "consumer" version = "0.1" tool_requires = "tool/0.1" def package(self): self.output.info(f"MYCONF {self.conf.get('user.team:conf')}") cmd = "set MYVAR" if platform.system() == "Windows" else "echo MYVAR=$MYVAR" self.run(cmd) """) c.save({"tool/conanfile.py": tool, "consumer/conanfile.py": consumer}) c.run("create tool") c.run("export-pkg consumer") assert "conanfile.py (consumer/0.1): MYCONF CONF_VALUE" in c.out assert "MYVAR=MYVALUE" in c.out c.run("upload tool* -r=default -c") c.run("remove tool* -c") c.run("export-pkg consumer") assert "conanfile.py (consumer/0.1): MYCONF CONF_VALUE" in c.out assert "MYVAR=MYVALUE" in c.out def test_export_pkg_output_folder(): """ If the local build is using a different output-folder, it should work and export it """ c = TestClient() consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, copy class Consumer(ConanFile): name = "consumer" version = "0.1" def build(self): save(self, "myfile.txt", "") def package(self): copy(self, "*", src=self.build_folder, dst=self.package_folder) """) c.save({"conanfile.py": consumer}) c.run("build . -of=mytmp") c.run("export-pkg . -of=mytmp") assert "Packaged 1 '.txt' file: myfile.txt" in c.out assert os.path.exists(os.path.join(c.current_folder, "mytmp", "myfile.txt")) def test_export_pkg_test_package(): """ If there is a test_package, run it """ c = TestClient() test_conanfile = textwrap.dedent(""" from conan import ConanFile class Test(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def test(self): self.output.info("RUN TEST PACKAGE!!!!") """) c.save({"conanfile.py": GenConanfile("pkg", "1.0"), "test_package/conanfile.py": test_conanfile}) c.run("export-pkg . ") assert "test_package" in c.out assert "RUN TEST PACKAGE!!!!" in c.out c.run('export-pkg . -tf=""') assert "test_package" not in c.out assert "RUN TEST" not in c.out def test_export_pkg_test_package_build_require(): """ Test --build-require """ c = TestClient() test_conanfile = textwrap.dedent(""" from conan import ConanFile class Test(ConanFile): def build_requirements(self): self.tool_requires(self.tested_reference_str) def test(self): self.output.info(f"RUN TEST PACKAGE!!!!") """) c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_setting("os"), "test_package/conanfile.py": test_conanfile}) c.run("export-pkg . -s:b os=Windows -s:h os=Linux --build-require --lockfile-out=conan.lock") assert "test_package" in c.out assert "RUN TEST PACKAGE!!!!" in c.out lock = json.loads(c.load("conan.lock")) assert "pkg/1.0" in lock["build_requires"][0] def test_export_pkg_remote_python_requires(): """ Test that remote python-requires can be resolved """ c = TestClient(default_server_user=True) c.save({"tool/conanfile.py": GenConanfile("tool", "1.0"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_python_requires("tool/1.0")}) c.run("create tool") c.run("upload tool* -r=default -c") c.run("remove * -c") c.run("export-pkg pkg") assert "conanfile.py (pkg/1.0): Exported package binary" in c.out def test_remote_none(): # https://github.com/conan-io/conan/pull/14705 c = TestClient(default_server_user=True) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1"), "pkg/test_package/conanfile.py": GenConanfile().with_test("pass").with_requires("dep/0.1")}) c.run("create dep") c.run("upload dep* -r=default -c") c.run("build pkg") c.run("remove dep*:* -c") c.run("export-pkg pkg") # This used to crash # No longer crash assert "pkg/0.1 (test package): Running test()" in c.out def test_remote_none_tool_requires(): # https://github.com/conan-io/conan/pull/14705 c = TestClient(default_server_user=True) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1").with_settings("compiler"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_tool_requires("tool/0.1"), "pkg/test_package/conanfile.py": GenConanfile().with_test("pass")}) settings = "-s:b compiler=gcc -s:b compiler.version=9 -s:b compiler.libcxx=libstdc++11" c.run(f"create tool {settings} -s:b compiler.cppstd=20 --build-require") c.run(f"build pkg {settings} -s:b compiler.cppstd=17") c.run(f"export-pkg pkg {settings} -s:b compiler.cppstd=17") # This used to crash # No longer crash assert "pkg/0.1 (test package): Running test()" in c.out def test_export_pkg_set_version_python_requires(): # https://github.com/conan-io/conan/issues/16223 c = TestClient(light=True) py_require = textwrap.dedent(""" from conan import ConanFile class TestBuild: def set_version(self): assert self.version class TestModule(ConanFile): name = "pyreq" version = "0.1" package_type = "python-require" """) pkg = textwrap.dedent(""" from conan import ConanFile class TestPkgConan(ConanFile): name="testpkg" python_requires = "pyreq/0.1" python_requires_extend = "pyreq.TestBuild" """) c.save({"pyreq/conanfile.py": py_require, "pkg/conanfile.py": pkg}) c.run("create pyreq") c.run("export-pkg pkg --version=1.0+0") assert "conanfile.py (testpkg/1.0+0): Exported package binary" in c.out ================================================ FILE: test/integration/command/help_test.py ================================================ from unittest.mock import patch from conan import __version__ from conan.test.utils.env import environment_update from conan.test.utils.tools import TestClient class TestHelp: def test_version(self): c = TestClient() c.run("--version") assert "Conan version %s" % __version__ in c.out def test_unknown_command(self): c = TestClient() c.run("some_unknown_command123", assert_error=True) assert "'some_unknown_command123' is not a Conan command" in c.out def test_similar(self): c = TestClient() c.run("instal", assert_error=True) assert "The most similar command is" in c.out assert "install" in c.out c.run("remole", assert_error=True) assert "The most similar commands are" in c.out assert "remove" in c.out assert "remote" in c.out def test_help(self): c = TestClient() c.run("-h") assert "Creator commands" in c.out assert "Consumer commands" in c.out def test_help_command(self): c = TestClient() c.run("new -h") assert "Create a new example recipe" in c.out def test_help_subcommand(self): c = TestClient() c.run("cache -h") # When listing subcommands, but line truncated assert "Perform file operations in the local cache (of recipes and/or packages)" in c.out c.run("cache path -h") assert "Show the path to the Conan cache for a given reference" in c.out def test_all_commands_call_args_parse(): tc = TestClient(light=True) tc.run("-h") commands = tc.api.command.cli._builtin_commands with environment_update({"CONAN_WORKSPACE_ENABLE": "will_break_next"}): for command, info in commands.items(): if len(info._subcommands) > 0: for subcommand in info._subcommands.values(): with patch("conan.cli.command.ConanArgumentParser.parse_args", side_effect=Exception("called")) as mock_run: try: tc.run(f"{command} {subcommand.name} -h") except: pass finally: assert mock_run.called, f'Command "conan {command} {subcommand.name}" did not call parse_args()' else: with patch("conan.cli.command.ConanArgumentParser.parse_args", side_effect=Exception("called")) as mock_run: try: tc.run(f"{command} -h") except: pass finally: assert mock_run.called, f'Command "conan {command}" did not call parse_args()' ================================================ FILE: test/integration/command/info/__init__.py ================================================ ================================================ FILE: test/integration/command/info/info_options_test.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_info_options(): # packages with dash client = TestClient() client.save({"conanfile.py": GenConanfile("my-package", "1.3") .with_option("shared", [True, False]) .with_default_option("shared", False)}) # local client.run("graph info .") assert "shared: False" in client.out client.run("graph info . -o shared=True") assert "shared: True" in client.out client.run("graph info . -o my-package*:shared=True") assert "shared: True" in client.out # in cache client.run("export .") client.run("graph info --requires=my-package/1.3") assert "shared: False" in client.out client.run("graph info --requires=my-package/1.3 -o shared=True") assert "shared: True" in client.out client.run("graph info --requires=my-package/1.3 -o my-package*:shared=True") assert "shared: True" in client.out ================================================ FILE: test/integration/command/info/info_test.py ================================================ import json import os import textwrap from conan.cli.exit_codes import ERROR_GENERAL from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, GenConanfile class TestBasicCliOutput: def test_info_settings(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.2" settings = "build_type", "compiler" author = "John Doe" license = "MIT" url = "https://foo.bar.baz" homepage = "https://foo.bar.site" topics = "foo", "bar", "qux" provides = "libjpeg", "libjpg" deprecated = "other-pkg" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} """) client.save({"conanfile.py": conanfile}) client.run("graph info . -s build_type=Debug -s compiler=gcc -s compiler.version=11") assert "build_type: Debug" in client.out assert "context: host" in client.out assert "license: MIT" in client.out assert "homepage: https://foo.bar.site" in client.out assert "compiler: gcc" in client.out assert "compiler.version: 11" in client.out assert "prev: None" in client.out assert "fPIC: True" in client.out assert "shared: False" in client.out # Create the package client.run("create .") pref = client.get_latest_package_reference("pkg/0.2") client.run("graph info .") # It's a consumer so we can not get the prev because it's not even a package yet assert "prev: None" in client.out # Now, let's create another consumer requiring the previous package client.save({"conanfile.txt": "[requires]\npkg/0.2"}, clean_first=True) client.run("graph info .") assert textwrap.dedent(f""" {repr(pref.ref)}: ref: {repr(pref.ref)} id: 1 recipe: Cache package_id: {pref.package_id} prev: {pref.revision}""") in client.out def test_nontuple_topics(self): client = TestClient() # This is the representation that should always happen, # we wouldn't expect topics not to be a tuple here conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.2" provides = ("bar",) topics = ("foo",) """) client.save({"conanfile.py": conanfile}) client.run("graph info . --format=json") recipe = json.loads(client.stdout)["graph"]["nodes"]["0"] assert type(recipe["topics"]) is list assert recipe["topics"] == ["foo"] assert type(recipe["provides"]) is list assert recipe["provides"] == ["bar"] # But this used to fail, # topics were not converted to a list internally if one was not provided client2 = TestClient() conanfile2 = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.2" provides = "bar" topics = "foo" """) client2.save({"conanfile.py": conanfile2}) client2.run("graph info . --format=json") recipe = json.loads(client2.stdout)["graph"]["nodes"]["0"] assert type(recipe["topics"]) is list assert recipe["topics"] == ["foo"] assert type(recipe["provides"]) is list assert recipe["provides"] == ["bar"] class TestConanfilePath: def test_cwd(self): # Check the first positional argument is a relative path client = TestClient() conanfile = GenConanfile("pkg", "0.1").with_setting("build_type") client.save({"subfolder/conanfile.py": conanfile}) client.run("graph info ./subfolder -s build_type=Debug") assert "build_type: Debug" in client.out def test_wrong_path_parameter(self): # check that the positional parameter must exist and file must be found client = TestClient() client.run("graph info", assert_error=True) assert "ERROR: Conanfile not found" in client.out client.run("graph info not_real_path", assert_error=True) assert "ERROR: Conanfile not found" in client.out client.run("graph info conanfile.txt", assert_error=True) assert "ERROR: Conanfile not found" in client.out client.run("graph info --requires=foo/1.0", assert_error=True) assert "ERROR: Package 'foo/1.0' not resolved: No remote defined." in client.out client.run("graph info . --requires=foo/1.0", assert_error=True) assert ("--requires and --tool-requires arguments are incompatible " "with [path] '.' argument") in client.out class TestFilters: def test_filter_fields(self): # The --filter arg should work, specifying which fields to show only c = TestClient() c.save({"conanfile.py": GenConanfile() .with_class_attribute("author = 'myself'") .with_class_attribute("license = 'MIT'") .with_class_attribute("url = 'https://url.com'")}) c.run("graph info . ") assert "license: MIT" in c.out assert "author: myself" in c.out c.run("graph info . --filter=license") assert "license: MIT" in c.out assert "author" not in c.out c.run("graph info . --filter=author") assert "license" not in c.out assert "author: myself" in c.out c.run("graph info . --filter=author --filter=license") assert "license" in c.out assert "author: myself" in c.out def test_filter_fields_json(self): # The --filter arg should work, specifying which fields to show only c = TestClient() c.save({"conanfile.py": GenConanfile() .with_class_attribute("author = 'myself'") .with_class_attribute("license = 'MIT'") .with_class_attribute("url = 'https://url.com'")}) c.run("graph info . --filter=license --format=json") assert "author" not in c.out assert '"license": "MIT"' in c.out c.run("graph info . --filter=license --format=html", assert_error=True) assert "Formatted output 'html' cannot filter fields" in c.out class TestJsonOutput: def test_json_package_filter(self): client = TestClient() conanfile = GenConanfile("pkg", "0.1").with_setting("build_type").with_requires("dep/0.1") client.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "conanfile.py": conanfile}) client.run("create dep") client.run("graph info . --package-filter=nothing --format=json") assert '"nodes": {}' in client.out client.run("graph info . --package-filter=pkg* --format=json") graph = json.loads(client.stdout) assert len(graph["graph"]["nodes"]) == 1 assert graph["graph"]["nodes"]["0"]["ref"] == "pkg/0.1" # Consumer & filtering client.run("graph info . --package-filter=& --format=json") graph = json.loads(client.stdout) assert len(graph["graph"]["nodes"]) == 1 assert graph["graph"]["nodes"]["0"]["ref"] == "pkg/0.1" # This returns nothing, doesn't work that way, serialized graph doesn't have the consumer client.run("graph info --requires=dep/0.1 --package-filter=& --format=json") graph = json.loads(client.stdout) assert len(graph["graph"]["nodes"]) == 1 assert graph["graph"]["nodes"]["1"]["name"] == "dep" # Filter matches all client.run("graph info . --package-filter=* --format=json") graph = json.loads(client.stdout) assert len(graph["graph"]["nodes"]) == 2 assert graph["graph"]["nodes"]["0"]["ref"] == "pkg/0.1" assert graph["graph"]["nodes"]["1"]["name"] == "dep" def test_json_info_outputs(self): client = TestClient() conanfile = GenConanfile("pkg", "0.1").with_setting("build_type") client.save({"conanfile.py": conanfile}) client.run("graph info . -s build_type=Debug --format=json") graph = json.loads(client.stdout) assert graph["graph"]["nodes"]["0"]["settings"]["build_type"] == "Debug" def test_json_info_requirements(self): c = TestClient() c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"), "app/conanfile.py": GenConanfile().with_require("pkg/[>=0.0 <1.0]")}) c.run("create pkg") c.run("graph info app --format=json") graph = json.loads(c.stdout) assert graph["graph"]["nodes"]["0"]["dependencies"]["1"]["require"] == "pkg/[>=0.0 <1.0]" class TestAdvancedCliOutput: """ Testing more advanced fields output, like SCM or PYTHON-REQUIRES """ def test_python_requires(self): # https://github.com/conan-io/conan/issues/9277 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=tool --version=0.1") conanfile = textwrap.dedent(""" from conan import ConanFile class pkg(ConanFile): python_requires = "tool/0.1" """) client.save({"conanfile.py": conanfile}) client.run("graph info .") assert "python_requires:\n tool/0.1#4d670581ccb765839f2239cc8dff8fbd" in client.out client.run("graph info . --format=json") info = json.loads(client.stdout) pyrequires = info["graph"]["nodes"]["0"]["python_requires"] tool = pyrequires["tool/0.1#4d670581ccb765839f2239cc8dff8fbd"] info = info["graph"]["nodes"]["0"]["info"] assert info["python_requires"] == ["tool/0.1.Z"] # lets make sure the path exists assert tool["recipe"] == "Cache" assert tool["remote"] is None assert os.path.exists(tool["path"]) def test_build_id_info(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "build_type" def build_id(self): self.info_build.settings.build_type = "Any" """) client.save({"conanfile.py": conanfile}) client.run("export .") client.save({"conanfile.py": GenConanfile().with_requires("pkg/0.1")}, clean_first=True) client.run("graph info . -s build_type=Release") assert "build_id: ec0cd314abe055f7de86cd6493e31977d2b87884" in client.out assert "package_id: efa83b160a55b033c4ea706ddb980cd708e3ba1b" in client.out client.run("graph info . -s build_type=Debug") assert "build_id: ec0cd314abe055f7de86cd6493e31977d2b87884" in client.out assert "package_id: 9e186f6d94c008b544af1569d1a6368d8339efc5" in client.out class TestEditables: def test_info_paths(self): # https://github.com/conan-io/conan/issues/7054 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def layout(self): self.folders.source = "." self.folders.build = "." """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": GenConanfile().with_require("pkg/0.1")}) c.run("editable add pkg --name=pkg --version=0.1") # TODO: Check this --package-filter with * c.run("graph info consumer --package-filter=pkg*") # FIXME: Paths are not diplayed yet assert "source_folder: None" in c.out class TestInfoTestPackage: # https://github.com/conan-io/conan/issues/10714 def test_tested_reference_str(self): client = TestClient() client.save({"conanfile.py": GenConanfile("tool", "0.1")}) client.run("export .") conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def build_requirements(self): self.build_requires(self.tested_reference_str) """) client.save({"conanfile.py": conanfile}) for args in ["", " --build=*"]: client.run("graph info . " + args) assert "AttributeError: 'HelloConan' object has no attribute 'tested_reference_str'"\ not in client.out class TestDeployers: def test_custom_deploy(self): c = TestClient() conanfile = GenConanfile("pkg", "0.1").with_class_attribute("license = 'MIT'") c.save({"conanfile.py": conanfile}) c.run("create .") collectlicenses = textwrap.dedent(r""" from conan.tools.files import save def deploy(graph, output_folder, **kwargs): contents = [] conanfile = graph.root.conanfile for pkg in graph.nodes: d = pkg.conanfile contents.append("LICENSE {}: {}!".format(d.ref, d.license)) contents = "\n".join(contents) conanfile.output.info(contents) save(conanfile, "licenses.txt", contents) """) c.save({"conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_class_attribute("license='GPL'"), "collectlicenses.py": collectlicenses}) c.run("graph info . --deployer=collectlicenses") assert "conanfile.py: LICENSE : GPL!" in c.out assert "LICENSE pkg/0.1: MIT!" in c.out contents = c.load("licenses.txt") assert "LICENSE pkg/0.1: MIT!" in contents assert "LICENSE : GPL!" in contents class TestErrorsInGraph: # https://github.com/conan-io/conan/issues/12735 def test_error_in_recipe(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_id(self): a = b """) c.save({"dep/conanfile.py": dep, "consumer/conanfile.py": GenConanfile().with_requires("dep/0.1")}) c.run("export dep") exit_code = c.run("graph info consumer", assert_error=True) assert "name 'b' is not defined" in c.out assert exit_code == ERROR_GENERAL def test_error_exports(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import replace_in_file class Pkg(ConanFile): name = "dep" version = "0.1" def export(self): replace_in_file(self, "conanfile.py", "from conan", "from conans") """) c.save({"dep/conanfile.py": dep, "consumer/conanfile.py": GenConanfile().with_requires("dep/0.1")}) c.run("export dep") exit_code = c.run("graph info consumer", assert_error=True) assert "ERROR: Package 'dep/0.1' not resolved: dep/0.1: Cannot load" in c.out assert exit_code == ERROR_GENERAL def test_missing_invalid(self): c = TestClient() dep_invalid = textwrap.dedent( """ from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): name = "dep_invalid" settings = "os", "build_type" version = "0.1" def validate(self): raise ConanInvalidConfiguration("Invalid package") """ ) c.save({ "dep/conanfile.py": GenConanfile("dep").with_version("0.1"), "dep_invalid/conanfile.py": dep_invalid, "consumer/conanfile.py": GenConanfile("consumer").with_requires("dep/0.1") .with_requires("dep_invalid/0.1"), }) c.run("export dep") c.run("export dep_invalid") exit_code = c.run("graph info consumer") assert "WARN: There are some error(s) in the graph:" in c.out assert "- Missing packages: dep" in c.out assert "- Invalid packages: dep_invalid" in c.out assert exit_code == 0 class TestInfoUpdate: def test_update(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("tool")}) c.run("create . --version=1.0") c.run("create . --version=1.1") c.run("upload tool/1.1 -r=default -c") c.run("remove tool/1.1 -c") c.save({"conanfile.py": GenConanfile().with_requires("tool/[*]")}) c.run("graph info . --filter=recipe") assert "tool/1.0#7fbd52996f34447f4a4c362edb5b4af5 - Cache" in c.out c.run("graph info . --update --filter=recipe") assert "tool/1.1#7fbd52996f34447f4a4c362edb5b4af5 - Downloaded (default)" in c.out def test_info_not_hit_server(): """ the command graph info shouldn't be hitting the server if packages are in the Conan cache :return: """ c = TestClient(default_server_user=True) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"), "consumer/conanfile.py": GenConanfile("consumer", "0.1").with_require("pkg/0.1")}) c.run("create pkg") c.run("create consumer") c.run("upload * -r=default -c") c.run("remove * -c") c.run("install --requires=consumer/0.1@") assert "Downloaded" in c.out # break the server to make sure it is not being contacted at all c.servers["default"] = None c.run("graph info --requires=consumer/0.1@") assert "Downloaded" not in c.out # Now we remove the local, so it will raise errors c.run("remove pkg* -c") c.run("graph info --requires=consumer/0.1@", assert_error=True) assert "'NoneType' object " in c.out c.run("remote disable *") c.run("graph info --requires=consumer/0.1@", assert_error=True) assert "'NoneType' object " not in c.out assert "No remote defined" in c.out def test_graph_info_user(): """ https://github.com/conan-io/conan/issues/15791 """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" user = "user" """) c.save({"conanfile.py": conanfile}) c.run("graph info .") assert "pkg/0.1@user" in c.out c.run("graph info . --channel=channel") assert "pkg/0.1@user/channel" in c.out def test_graph_info_bundle(): c = TestClient(light=True) c.save({"subfolder/conanfile.py": GenConanfile("liba", "1.0")}) c.run("create ./subfolder") conanfile = textwrap.dedent(""" from conan import ConanFile class RepackageRecipe(ConanFile): name = "lib" version = "1.0" def requirements(self): self.requires("liba/1.0") vendor = True """) c.save({"conanfile.py": conanfile}) c.run("create .") c.save({"conanfile.py": GenConanfile("consumer", "1.0").with_requires("lib/1.0")}) c.run("graph info . --build='lib*'") c.assert_listed_binary({"lib/1.0": (NO_SETTINGS_PACKAGE_ID, "Invalid")}) c.run("graph info . -c tools.graph:vendor=build --build='lib*'") c.assert_listed_binary({"lib/1.0": (NO_SETTINGS_PACKAGE_ID, "Build")}) def test_graph_info_provides_conflict_html(): """ This test also makes sure that the serialization of the GraphProvidesConflict works""" tc = TestClient(light=True) tc.save({"bar/conanfile.py": GenConanfile("bar", "1.0"), "foo/conanfile.py": GenConanfile("foo", "1.0").with_provides("bar")}) tc.run("export bar") tc.run("export foo") tc.run("graph info --requires=bar/1.0 --requires=foo/1.0 --format=html", assert_error=True) # It got properly serialized assert '"type": "provide_conflict"' in tc.out assert "Both 'bar/1.0' and 'foo/1.0' provide '['bar']'" ================================================ FILE: test/integration/command/info/test_graph_info_graphical.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient, GenConanfile class TestInfo: def _create(self, name, version, deps=None, export=True): conanfile = textwrap.dedent(""" from conan import ConanFile class pkg(ConanFile): name = "{name}" version = "{version}" license = {license} description = "blah" url = "myurl" {requires} """) requires = "" if deps: requires = "requires = {}".format(", ".join('"{}"'.format(d) for d in deps)) conanfile = conanfile.format(name=name, version=version, requires=requires, license='"MIT"') self.client.save({"conanfile.py": conanfile}, clean_first=True) if export: self.client.run("export . --user=lasote --channel=stable") def test_graph(self): self.client = TestClient() test_deps = { "hello0": ["hello1", "hello2", "hello3"], "hello1": ["hello4"], "hello2": [], "hello3": ["hello7"], "hello4": ["hello5", "hello6"], "hello5": [], "hello6": [], "hello7": ["hello8"], "hello8": ["hello9", "hello10"], "hello9": [], "hello10": [], } def create_export(testdeps, name): deps = testdeps[name] for dep in deps: create_export(testdeps, dep) expanded_deps = ["%s/0.1@lasote/stable" % dep for dep in deps] export = False if name == "hello0" else True self._create(name, "0.1", expanded_deps, export=export) create_export(test_deps, "hello0") # arbitrary case - file will be named according to argument self.client.run("graph info . --format=dot") contents = self.client.stdout expected = textwrap.dedent(""" "hello8/0.1@lasote/stable" -> "hello9/0.1@lasote/stable" "hello8/0.1@lasote/stable" -> "hello10/0.1@lasote/stable" "hello4/0.1@lasote/stable" -> "hello5/0.1@lasote/stable" "hello4/0.1@lasote/stable" -> "hello6/0.1@lasote/stable" "hello3/0.1@lasote/stable" -> "hello7/0.1@lasote/stable" "hello7/0.1@lasote/stable" -> "hello8/0.1@lasote/stable" "conanfile.py (hello0/0.1)" -> "hello1/0.1@lasote/stable" "conanfile.py (hello0/0.1)" -> "hello2/0.1@lasote/stable" "conanfile.py (hello0/0.1)" -> "hello3/0.1@lasote/stable" "hello1/0.1@lasote/stable" -> "hello4/0.1@lasote/stable" """) for line in expected.splitlines(): assert line in contents def test_graph_html(self): self.client = TestClient() test_deps = { "hello0": ["hello1"], "hello1": [], } def create_export(testdeps, name): deps = testdeps[name] for dep in deps: create_export(testdeps, dep) expanded_deps = ["%s/0.1@lasote/stable" % dep for dep in deps] export = False if name == "hello0" else True self._create(name, "0.1", expanded_deps, export=export) create_export(test_deps, "hello0") # arbitrary case - file will be named according to argument self.client.run("graph info . --format=html") html = self.client.stdout # Just make sure it doesn't crash assert "" in html def test_user_templates(): """ Test that a user can override the builtin templates putting templates/graph.html and templates/graph.dot in the home """ c = TestClient() c.save({'lib.py': GenConanfile("lib", "0.1")}) c.run("create lib.py") template_folder = os.path.join(c.cache_folder, 'templates') c.save({"graph.html": '{{ base_template_path }}', "graph.dot": '{{ base_template_path }}'}, path=template_folder) c.run("graph info --requires=lib/0.1 --format=html") assert template_folder in c.stdout c.run("graph info --requires=lib/0.1 --format=dot") assert template_folder in c.stdout def test_graph_info_html_error_reporting_output(): tc = TestClient() tc.save({"lib/conanfile.py": GenConanfile("lib"), "ui/conanfile.py": GenConanfile("ui", "1.0").with_requirement("lib/1.0"), "math/conanfile.py": GenConanfile("math", "1.0").with_requirement("lib/2.0")}) tc.run("export lib/ --version=1.0") tc.run("export lib/ --version=2.0") tc.run("export ui") tc.run("export math") tc.run("graph info --requires=math/1.0 --requires=ui/1.0 --format=html", assert_error=True, redirect_stdout="graph.html") assert "ERROR: Version conflict:" in tc.out # check that it doesn't crash # change order, just in case tc.run("graph info --requires=ui/1.0 --requires=math/1.0 --format=html", assert_error=True, redirect_stdout="graph.html") assert "ERROR: Version conflict:" in tc.out # check that it doesn't crash # direct conflict also doesn't crash tc.run("graph info --requires=ui/1.0 --requires=lib/2.0 --format=html", assert_error=True, redirect_stdout="graph.html") assert "ERROR: Version conflict:" in tc.out # check that it doesn't crash # Check manually # tc.run_command(f"{tc.current_folder}/graph.html") def test_graph_conflict_diamond(): c = TestClient() c.save({"math/conanfile.py": GenConanfile("math"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("math/1.0"), "ai/conanfile.py": GenConanfile("ai", "1.0").with_requires("math/1.0.1"), "game/conanfile.py": GenConanfile("game", "1.0").with_requires("engine/1.0", "ai/1.0"), }) c.run("create math --version=1.0") c.run("create math --version=1.0.1") c.run("create math --version=1.0.2") c.run("create engine") c.run("create ai") c.run("graph info game --format=html", assert_error=True, redirect_stdout="graph.html") # check that it doesn't crash assert "ERROR: Version conflict: Conflict between math/1.0.1 and math/1.0 in the graph." in c.out def test_graph_conflict_loop(): c = TestClient(light=True) c.save({"lib_a/conanfile.py": GenConanfile("lib_a", "1.0").with_requires("lib_b/1.0"), "lib_b/conanfile.py": GenConanfile("lib_b", "1.0").with_requires("lib_c/1.0"), "lib_c/conanfile.py": GenConanfile("lib_c", "1.0").with_requires("lib_a/1.0"), "lib_x/conanfile.py": GenConanfile("lib_x", "1.0").with_requires("lib_a/1.0"), }) c.run("export lib_a") c.run("export lib_b") c.run("export lib_c") c.run("graph info lib_x --format=html", assert_error=True, redirect_stdout="graph.html") # checked manually #c.open("graph.html") def test_graph_missing_error(): c = TestClient() c.save({"engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("math/1.0"), "game/conanfile.py": GenConanfile("game", "1.0").with_requires("engine/1.0"), }) c.run("export engine") c.run("graph info game --format=html", assert_error=True, redirect_stdout="graph.html") # check that it doesn't crash, tested manually assert "ERROR: Package 'math/1.0' not resolved: No remote defined" in c.out ================================================ FILE: test/integration/command/info/test_info_build_order.py ================================================ import json import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_info_build_order(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"), "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("graph build-order consumer --build=missing --format=json") bo_json = json.loads(c.stdout) result = [ [ { "ref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd", "depends": [], "packages": [[ { "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", 'prev': None, 'filenames': [], 'info': {}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=dep/0.1 --build=dep/0.1', "options": [], "overrides": {} } ]] } ], [ { "ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032", "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd" ], "packages": [[ { "package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1", 'prev': None, 'filenames': [], 'info': {'requires': ['dep/0.1']}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=pkg/0.1 --build=pkg/0.1', "options": [], "overrides": {} } ]] } ] ] assert bo_json == result c.run("graph build-order consumer --order-by=recipe --build=missing --format=json") bo_json = json.loads(c.stdout) assert bo_json["order_by"] == "recipe" assert bo_json["order"] == result c.run("graph build-order consumer --build=missing --order-by=recipe --reduce --format=json") bo_json = json.loads(c.stdout) assert bo_json["order_by"] == "recipe" assert bo_json["order"] == result # test html format c.run("graph build-order consumer --build=missing --format=html") assert "" in c.stdout c.run("graph build-order consumer --order-by=recipe --build=missing --format=html") assert "" in c.stdout c.run("graph build-order consumer --order-by=configuration --build=missing --format=html") assert "" in c.stdout def test_info_build_order_configuration(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"), "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("graph build-order consumer --build=missing --order=configuration --format=json") bo_json = json.loads(c.stdout) assert bo_json["order_by"] == "configuration" result = [ [ { "ref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd", "pref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709", "depends": [], "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", 'prev': None, 'filenames': [], 'info': {}, "context": "host", "binary": "Build", 'build_args': '--requires=dep/0.1 --build=dep/0.1', "options": [], "overrides": {} } ], [ { "ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032", "pref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1", "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" ], "package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1", 'prev': None, 'filenames': [], 'info': {'requires': ['dep/0.1']}, "context": "host", "binary": "Build", 'build_args': '--requires=pkg/0.1 --build=pkg/0.1', "options": [], "overrides": {} } ] ] assert bo_json["order"] == result c.run("graph build-order consumer --build=missing --order=configuration --reduce --format=json") bo_json = json.loads(c.stdout) assert bo_json["order"] == result def test_info_build_order_configuration_text_formatter(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"), "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("graph build-order consumer --build=missing --order=configuration --format=text") assert textwrap.dedent("""\ ======== Computing the build order ======== dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709 - Build pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1 - Build """) in c.out def test_info_build_order_build_require(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_tool_requires("dep/0.1"), "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("graph build-order consumer --build=missing --format=json") bo_json = json.loads(c.stdout) result = [ [ { "ref": 'dep/0.1#4d670581ccb765839f2239cc8dff8fbd', "depends": [], "packages": [[ { "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", 'prev': None, 'filenames': [], 'info': {}, "context": "build", 'depends': [], "binary": "Build", 'build_args': '--tool-requires=dep/0.1 --build=dep/0.1', "options": [], "overrides": {} } ]] } ], [ { "ref": "pkg/0.1#b5a40d7314ce57ebdcf8fa31257f3de1", "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd" ], "packages": [[ { "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", 'prev': None, 'filenames': [], 'info': {}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=pkg/0.1 --build=pkg/0.1', "options": [], "overrides": {} } ]] } ] ] assert bo_json == result def test_info_build_order_options(): c = TestClient() # The normal default_options do NOT propagate to build_requires, it is necessary to use # self.requires(..., options=xxx) c.save({"tool/conanfile.py": GenConanfile().with_option("myopt", [1, 2, 3]), "dep1/conanfile.py": GenConanfile().with_tool_requirement("tool/0.1", options={"myopt": 1}), "dep2/conanfile.py": GenConanfile().with_tool_requirement("tool/0.1", options={"myopt": 2}), "consumer/conanfile.txt": "[requires]\ndep1/0.1\ndep2/0.1"}) c.run("export tool --name=tool --version=0.1") c.run("export dep1 --name=dep1 --version=0.1") c.run("export dep2 --name=dep2 --version=0.1") c.run("graph build-order consumer --build=missing --format=json") bo_json = json.loads(c.stdout) result = [ [ {'ref': 'tool/0.1#b4c19a1357b43877a2019dd2804336a9', 'depends': [], 'packages': [[ {'package_id': '1124b99dc8cd3c8bbf79121c7bf86ce40c725a40', 'prev': None, 'context': 'build', 'depends': [], "overrides": {}, 'binary': 'Build', 'options': ['tool/0.1:myopt=2'], 'filenames': [], 'info': {'options': {'myopt': '2'}}, 'build_args': '--tool-requires=tool/0.1 --build=tool/0.1 -o:b="tool/0.1:myopt=2"'}, {'package_id': 'a9035d84c5880b26c4b44acf70078c9a7dd37412', 'prev': None, 'context': 'build', 'depends': [], "overrides": {}, 'info': {'options': {'myopt': '1'}}, 'binary': 'Build', 'options': ['tool/0.1:myopt=1'], 'filenames': [], 'build_args': '--tool-requires=tool/0.1 --build=tool/0.1 -o:b="tool/0.1:myopt=1"'} ]]} ], [ {'ref': 'dep1/0.1#7f0d80f9cb8c6bab06def7f6fb8f3b86', 'depends': ['tool/0.1#b4c19a1357b43877a2019dd2804336a9'], 'packages': [[ {'package_id': 'da39a3ee5e6b4b0d3255bfef95601890afd80709', 'prev': None, 'context': 'host', 'depends': [], 'binary': 'Build', 'options': [], 'filenames': [], 'info': {}, "overrides": {}, 'build_args': '--requires=dep1/0.1 --build=dep1/0.1'} ]]}, {'ref': 'dep2/0.1#23c789d2b36f0461e52cd6f139f97f5e', 'depends': ['tool/0.1#b4c19a1357b43877a2019dd2804336a9'], 'packages': [[ {'package_id': 'da39a3ee5e6b4b0d3255bfef95601890afd80709', 'prev': None, 'context': 'host', 'depends': [], 'binary': 'Build', 'options': [], 'filenames': [], 'info': {}, "overrides": {}, 'build_args': '--requires=dep2/0.1 --build=dep2/0.1'} ]]} ] ] assert bo_json == result def test_info_build_order_merge_multi_product(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"), "consumer1/conanfile.txt": "[requires]\npkg/0.1", "consumer2/conanfile.txt": "[requires]\npkg/0.2"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("export pkg --name=pkg --version=0.2") c.run("graph build-order consumer1 --build=missing --format=json", redirect_stdout="bo1.json") c.run("graph build-order consumer2 --build=missing --format=json", redirect_stdout="bo2.json") c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=json", redirect_stdout="bo3.json") bo_json = json.loads(c.load("bo3.json")) result = [ [ { "ref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd", "depends": [], "packages": [[ { "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", 'prev': None, 'filenames': ["bo1", "bo2"], 'info': {}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=dep/0.1 --build=dep/0.1', "options": [], "overrides": {} } ]] } ], [ { "ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032", "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd" ], "packages": [[ { "package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1", 'prev': None, 'filenames': ["bo1"], 'info': {'requires': ['dep/0.1']}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=pkg/0.1 --build=pkg/0.1', "options": [], "overrides": {} } ]] }, { "ref": "pkg/0.2#1ac8dd17c0f9f420935abd3b6a8fa032", "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd" ], "packages": [[ { "package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1", 'prev': None, 'filenames': ["bo2"], 'info': {'requires': ['dep/0.1']}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=pkg/0.2 --build=pkg/0.2', "options": [], "overrides": {} } ]] } ] ] assert bo_json == result # test that html format for build-order-merge generates something c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=html") assert "" in c.stdout def test_info_build_order_merge_multi_product_configurations(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"), "consumer1/conanfile.txt": "[requires]\npkg/0.1", "consumer2/conanfile.txt": "[requires]\npkg/0.2"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("export pkg --name=pkg --version=0.2") c.run("graph build-order consumer1 --build=missing --order=configuration --format=json", redirect_stdout="bo1.json") c.run("graph build-order consumer2 --build=missing --order=configuration --format=json", redirect_stdout="bo2.json") c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=json", redirect_stdout="bo3.json") bo_json = json.loads(c.load("bo3.json")) assert bo_json["order_by"] == "configuration" result = [ [ { "ref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd", "pref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709", "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "prev": None, "context": "host", "binary": "Build", "options": [], "filenames": [ "bo1", "bo2" ], 'info': {}, "depends": [], "overrides": {}, "build_args": "--requires=dep/0.1 --build=dep/0.1" } ], [ { "ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032", "pref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1", "package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1", "prev": None, "context": "host", "binary": "Build", "options": [], "filenames": [ "bo1" ], 'info': {'requires': ['dep/0.1']}, "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" ], "overrides": {}, "build_args": "--requires=pkg/0.1 --build=pkg/0.1" }, { "ref": "pkg/0.2#1ac8dd17c0f9f420935abd3b6a8fa032", "pref": "pkg/0.2#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1", "package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1", "prev": None, "context": "host", "binary": "Build", "options": [], "filenames": [ "bo2" ], 'info': {'requires': ['dep/0.1']}, "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" ], "overrides": {}, "build_args": "--requires=pkg/0.2 --build=pkg/0.2" } ] ] assert bo_json["order"] == result def test_info_build_order_merge_conditionals(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): if self.settings.os == "Windows": self.requires("depwin/[>0.0 <1.0]") else: self.requires("depnix/[>0.0 <1.0]") """) c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": conanfile, "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=depwin --version=0.1") c.run("export dep --name=depnix --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("graph build-order consumer --format=json --build=missing -s os=Windows", redirect_stdout="bo_win.json") c.run("graph build-order consumer --format=json --build=missing -s os=Linux", redirect_stdout="bo_nix.json") c.run("graph build-order-merge --file=bo_win.json --file=bo_nix.json --format=json", redirect_stdout="bo3.json") bo_json = json.loads(c.load("bo3.json")) result = [ [ { "ref": "depwin/0.1#4d670581ccb765839f2239cc8dff8fbd", "depends": [], "packages": [[ { "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", 'prev': None, 'filenames': ["bo_win"], 'info': {}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=depwin/0.1 --build=depwin/0.1', "options": [], "overrides": {} } ]] }, { "ref": "depnix/0.1#4d670581ccb765839f2239cc8dff8fbd", "depends": [], "packages": [[ { "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", 'prev': None, 'filenames': ["bo_nix"], 'info': {}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=depnix/0.1 --build=depnix/0.1', "options": [], "overrides": {} } ]] } ], [ { "ref": "pkg/0.1#b615ac4c7cd16631cd9e924b68596fce", "depends": [ "depwin/0.1#4d670581ccb765839f2239cc8dff8fbd", "depnix/0.1#4d670581ccb765839f2239cc8dff8fbd" ], "packages": [[ { "package_id": "b23846b9b10455081d89a9dfacd01f7712d04b95", 'prev': None, 'filenames': ["bo_win"], 'info': {'requires': ['depwin/0.1'], 'settings': {'os': 'Windows'}}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=pkg/0.1 --build=pkg/0.1', "options": [], "overrides": {} }, { "package_id": "dc29fa55ec82fab6bd820398c7a152ae5f7d4e28", 'prev': None, 'filenames': ["bo_nix"], 'info': {'requires': ['depnix/0.1'], 'settings': {'os': 'Linux'}}, "context": "host", 'depends': [], "binary": "Build", 'build_args': '--requires=pkg/0.1 --build=pkg/0.1', "options": [], "overrides": {} } ]] } ] ] assert bo_json == result def test_info_build_order_lockfile_location(): """ the lockfile should be in the caller cwd https://github.com/conan-io/conan/issues/13850 """ c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1")}) c.run("create dep") c.run("lock create pkg --lockfile-out=myconan.lock") assert os.path.exists(os.path.join(c.current_folder, "myconan.lock")) c.run("graph build-order pkg --lockfile=myconan.lock --lockfile-out=myconan2.lock") assert os.path.exists(os.path.join(c.current_folder, "myconan2.lock")) def test_build_order_missing_package_check_error(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"), "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") exit_code = c.run("graph build-order consumer --build='pkg/*' --order=configuration --format=json", assert_error=True) bo_json = json.loads(c.stdout) assert bo_json["order_by"] == "configuration" assert exit_code != 0 assert "dep/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Missing binary" in c.out result = [ [ { "ref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd", "pref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709", "package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "prev": None, "context": "host", "binary": "Missing", "options": [], "filenames": [], 'info': {}, "depends": [], "overrides": {}, "build_args": None, } ], [ { "ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032", "pref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1", "package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1", "prev": None, "context": "host", "binary": "Build", "options": [], "filenames": [], 'info': {'requires': ['dep/0.1']}, "depends": [ "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709" ], "overrides": {}, "build_args": "--requires=pkg/0.1 --build=pkg/0.1", } ], ] assert bo_json["order"] == result def test_info_build_order_broken_recipe(): # https://github.com/conan-io/conan/issues/14104 c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import replace_in_file class Pkg(ConanFile): name = "dep" version = "0.1" def export(self): replace_in_file(self, "conanfile.py", "from conan", "from conans") """) c.save({"conanfile.py": dep}) c.run("export .") c.run("graph build-order --requires=dep/0.1 --format=json", assert_error=True) assert "ImportError" in c.out assert "It is possible that this recipe is not Conan 2.0 ready" in c.out class TestBuildOrderReduce: @pytest.mark.parametrize("order", ["recipe", "configuration"]) def test_build_order_reduce(self, order): c = TestClient() c.save({"liba/conanfile.py": GenConanfile("liba", "0.1"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requires("liba/0.1"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requires("libb/0.1"), "consumer/conanfile.txt": "[requires]\nlibc/0.1"}) c.run("create liba") c.run("create libb") c.run("create libc") c.run("remove liba:* -c") c.run("remove libc:* -c") c.run(f"graph build-order consumer --order={order} --build=missing --reduce --format=json") bo_json = json.loads(c.stdout) order_json = bo_json["order"] assert len(order_json) == 2 # 2 levels level0, level1 = order_json assert len(level0) == 1 assert level0[0]["ref"] == "liba/0.1#a658e7beaaae5d6be0b6f67dcc9859e2" # then libc -> directly on liba, no libb involved assert len(level1) == 1 assert level1[0]["ref"] == "libc/0.1#c04c370ad966390e67388565b56f019a" depends = "liba/0.1#a658e7beaaae5d6be0b6f67dcc9859e2" if order == "configuration": depends += ":da39a3ee5e6b4b0d3255bfef95601890afd80709" assert level1[0]["depends"] == [depends] @pytest.mark.parametrize("order", ["recipe", "configuration"]) def test_build_order_merge_reduce(self, order): c = TestClient() c.save({"liba/conanfile.py": GenConanfile("liba", "0.1").with_settings("os"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_settings("os") .with_requires("liba/0.1"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_settings("os") .with_requires("libb/0.1"), "consumer/conanfile.txt": "[requires]\nlibc/0.1"}) for _os in ("Windows", "Linux"): c.run(f"create liba -s os={_os}") c.run(f"create libb -s os={_os}") c.run(f"create libc -s os={_os}") c.run("remove liba:* -c") c.run("remove libc:* -c") c.run(f"graph build-order consumer --order={order} --build=missing -s os=Windows " "--format=json", redirect_stdout="windows.json") c.run(f"graph build-order consumer --order={order} --build=missing -s os=Linux " "--format=json", redirect_stdout="linux.json") c.run(f"graph build-order-merge --file=windows.json --file=linux.json --reduce " "--format=json") bo_json = json.loads(c.stdout) order_json = bo_json["order"] assert len(order_json) == 2 # 2 levels level0, level1 = order_json if order == "recipe": assert len(level0) == 1 assert level0[0]["ref"] == "liba/0.1#8c6ed89c12ab2ce78b239224bd7cb79e" # then libc -> directly on liba, no libb involved assert len(level1) == 1 assert level1[0]["ref"] == "libc/0.1#66db2600b9d6a2a61c9051fcf47da4a3" depends = "liba/0.1#8c6ed89c12ab2ce78b239224bd7cb79e" assert level1[0]["depends"] == [depends] else: assert len(level0) == 2 liba1 = "liba/0.1#8c6ed89c12ab2ce78b239224bd7cb79e:" \ "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" liba2 = "liba/0.1#8c6ed89c12ab2ce78b239224bd7cb79e:" \ "9a4eb3c8701508aa9458b1a73d0633783ecc2270" assert level0[0]["pref"] == liba1 assert level0[1]["pref"] == liba2 # then libc -> directly on liba, no libb involved assert len(level1) == 2 assert level1[0]["ref"] == "libc/0.1#66db2600b9d6a2a61c9051fcf47da4a3" assert level1[0]["depends"] == [liba1] assert level1[1]["ref"] == "libc/0.1#66db2600b9d6a2a61c9051fcf47da4a3" assert level1[1]["depends"] == [liba2] def test_error_reduced(self): c = TestClient() c.save({"conanfile.py": GenConanfile("liba", "0.1")}) c.run("graph build-order . --format=json", redirect_stdout="bo1.json") c.run("graph build-order . --order-by=recipe --reduce --format=json", redirect_stdout="bo2.json") c.run(f"graph build-order-merge --file=bo1.json --file=bo2.json", assert_error=True) assert "ERROR: Reduced build-order file cannot be merged: bo2.json" # different order c.run(f"graph build-order-merge --file=bo2.json --file=bo1.json", assert_error=True) assert "ERROR: Reduced build-order file cannot be merged: bo2.json" def test_error_different_orders(self): c = TestClient() c.save({"conanfile.py": GenConanfile("liba", "0.1")}) c.run("graph build-order . --format=json", redirect_stdout="bo1.json") c.run("graph build-order . --order-by=recipe --format=json", redirect_stdout="bo2.json") c.run("graph build-order . --order-by=configuration --format=json", redirect_stdout="bo3.json") c.run(f"graph build-order-merge --file=bo1.json --file=bo2.json") # Not error c.run(f"graph build-order-merge --file=bo1.json --file=bo3.json", assert_error=True) assert "ERROR: Cannot merge build-orders of recipe!=configuration" in c.out c.run(f"graph build-order-merge --file=bo2.json --file=bo3.json", assert_error=True) assert "ERROR: Cannot merge build-orders of recipe!=configuration" in c.out # different order c.run(f"graph build-order-merge --file=bo3.json --file=bo2.json", assert_error=True) assert "ERROR: Cannot merge build-orders of configuration!=recipe" in c.out def test_merge_missing_error(self): tc = TestClient(light=True) tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0")}) tc.run("export dep") tc.run("graph build-order --order=recipe --requires=dep/1.0 --format=json", assert_error=True, redirect_stdout="order.json") tc.run("graph build-order-merge --file=order.json --file=order.json --format=json", assert_error=True) assert "dep/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709: Missing binary" in tc.out assert "IndexError: list index out of range" not in tc.out def test_merge_invalid_error(self): tc = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): name = "dep" version = "1.0" def validate(self): raise ConanInvalidConfiguration("This configuration is not valid") """) tc.save({"dep/conanfile.py": conanfile}) tc.run("export dep") tc.run("graph build-order --order=recipe --requires=dep/1.0 --format=json", assert_error=True, redirect_stdout="order.json") tc.run("graph build-order-merge --file=order.json --file=order.json --format=json", assert_error=True) assert "dep/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709: Invalid configuration" in tc.out assert "IndexError: list index out of range" not in tc.out def test_reduce_should_remove_recipe(self): tc = TestClient() tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_settings("os")}) tc.run("export dep") tc.run("create dep -s os=Windows") tc.run("graph build-order -s os=Windows --build=missing --order=recipe --requires=dep/1.0 " "--format=json", redirect_stdout="windows.json") tc.run("graph build-order -s os=Linux --build=missing --order=recipe --requires=dep/1.0 " "--format=json", redirect_stdout="linux.json") tc.run("graph build-order-merge --file=windows.json --file=linux.json --reduce " "--format=json") order = json.loads(tc.stdout) assert order["order"][0][0]["ref"] == "dep/1.0#1674c18bb63f0c9778d2811c21f581a0" assert len(order["order"][0][0]["packages"][0]) == 1 assert order["order"][0][0]["packages"][0][0]["binary"] == "Build" def test_multi_configuration_profile_args(): c = TestClient() c.save({"pkg/conanfile.py": GenConanfile().with_settings("os"), "consumer/conanfile.txt": "[requires]\npkg/0.1", "mypr": ""}) c.run("export pkg --name=pkg --version=0.1") args = "-pr=mypr -s:b os=Linux -o:h *:shared=True -c:h user.my:conf=1" c.run(f"graph build-order consumer --format=json --build=missing -s os=Windows {args} " "--order-by=recipe", redirect_stdout="bo_win.json") c.run(f"graph build-order consumer --format=json --build=missing -s os=Linux {args} " "--order-by=recipe", redirect_stdout="bo_nix.json") c.run("graph build-order-merge --file=bo_win.json --file=bo_nix.json --format=json", redirect_stdout="bo3.json") bo_json = json.loads(c.load("bo3.json")) win = '-pr:h="mypr" -s:h="os=Windows" -o:h="*:shared=True" -c:h="user.my:conf=1" -s:b="os=Linux"' nix = '-pr:h="mypr" -s:h="os=Linux" -o:h="*:shared=True" -c:h="user.my:conf=1" -s:b="os=Linux"' assert bo_json["profiles"] == {"bo_win": {"args": win}, "bo_nix": {"args": nix}} def test_build_order_space_in_options(): tc = TestClient(light=True) tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_option("flags", ["ANY", None]) .with_option("extras", ["ANY", None]), "conanfile.txt": textwrap.dedent(""" [requires] dep/1.0 [options] dep/*:flags=define=FOO define=BAR define=BAZ dep/*:extras=cxx="yes" gnuext='no' """)}) tc.run("create dep") tc.run("graph build-order . --order-by=configuration --build=dep/1.0 -f=json", redirect_stdout="order.json") order = json.loads(tc.load("order.json")) assert order["order"][0][0]["build_args"] == '''--requires=dep/1.0 --build=dep/1.0 -o="dep/*:extras=cxx="yes" gnuext='no'" -o="dep/*:flags=define=FOO define=BAR define=BAZ"''' def test_build_order_build_context_compatible(): c = TestClient() foo = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "foo" version = "1.0" settings = "os", "compiler" def validate_build(self): check_min_cppstd(self, 17) def validate(self): check_min_cppstd(self, 14) """) bar = GenConanfile("bar", "1.0").with_settings("os", "compiler").with_tool_requirement("foo/1.0") profile = textwrap.dedent(""" [settings] compiler=gcc compiler.cppstd=gnu14 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) c.save({"conanfile_foo.py": foo, "conanfile_bar.py": bar, "profile": profile}) c.run("export conanfile_foo.py") c.run("export conanfile_bar.py") # "--require/bar.1.0" and "require=foo/1.0" along with `--build=missing` would cause both # packages to be built in the host context - with foo being built with cppstd=17 # (because the default cppstd=14 is not enough) bar requires foo in the "build" context - # where cppstd=14 - but it can reuse the one built for cppstd=17 # (via compatibility plugin) # The three approaches are equivalent: # - Using "--build=missing" and forcing "-s foo/*:compiler.cppstd=17", means, build missing # binaries, and for foo I want the compiler.cppstd=17 binary. This approach could build other # missing binaries too # - Using "--build=compatible:foo/*" and "--build=missing:bar/*" means, build only missing # binary for bar, and for "foo", build a compatible one if the main one is missing. This # approach prevents other packages (not foo/bar) from accidentally being built. # - Last approach, passing both "--build=missing:foo/*" and "--build=compatible:foo/*" is # similar to the other two in final behavior, but the --build=missing:foo avoids doing the # compatibility check for consumption of "foo", and goes directly to the build check for approach in ("--build=missing -s foo/*:compiler.cppstd=17", '--build="compatible:foo/*" --build="missing:bar/*"', '--build="missing:foo/*" --build="compatible:foo/*" --build="missing:bar/*"'): c.run(f'graph build-order --require=foo/1.0 --require=bar/1.0 -pr:a profile {approach}') c.assert_listed_binary({"foo/1.0": ["4e2ae338231ae18d0d43b9e119404d2b2c416758", "Build"], "bar/1.0": ["5e4ffcc1ff33697a4ee96f66f0d2228ec458f25c", "Build"]}) c.assert_listed_binary({"foo/1.0": ["4e2ae338231ae18d0d43b9e119404d2b2c416758", "Build"]}, build=True) def test_info_build_order_editable(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1"), "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("editable add dep") c.run("export pkg") c.run("graph build-order consumer --build=missing --build=editable -f=json --order-by=recipe") bo_json = json.loads(c.stdout) pkg = bo_json["order"][0][0]["packages"][0][0] assert pkg["binary"] == "EditableBuild" assert pkg["build_args"] == "--requires=dep/0.1 --build=dep/0.1" c.run("graph build-order consumer --build=missing --build=editable -f=json " "--order-by=configuration") bo_json = json.loads(c.stdout) pkg = bo_json["order"][0][0] assert pkg["binary"] == "EditableBuild" assert pkg["build_args"] == "--requires=dep/0.1 --build=dep/0.1" def test_build_order_path_reqs_mixed_args(): # This used not to crash in previous versions # Also make sure we can properly used them separately tc = TestClient(light=True) tc.run("graph build-order . --requires=foo/1.0", assert_error=True) assert "ERROR: --requires and --tool-requires arguments are incompatible with [path] '.' argument" in tc.out tc.run("graph build-order --requires=foo/1.0", assert_error=True) assert "Package 'foo/1.0' not resolved: No remote defined" in tc.out tc.run("graph build-order .", assert_error=True) assert "Conanfile not found" in tc.out ================================================ FILE: test/integration/command/info/test_info_folders.py ================================================ import os import textwrap import json import pytest from conan.internal.paths import CONANFILE from conan.test.utils.tools import TestClient from conan.internal.cache.conan_reference_layout import EXPORT_FOLDER conanfile_py = """ from conan import ConanFile class AConan(ConanFile): name = "package" version = "0.1.0" """ with_deps_path_file = """ from conan import ConanFile class BConan(ConanFile): name = "package2" version = "0.2.0" requires = "package/0.1.0@user/testing" """ deps_txt_file = """ [requires] package2/0.2.0@user/testing """ @pytest.fixture() def client_deps(): client = TestClient() client.save({CONANFILE: conanfile_py}) client.run(f"export . --user=user --channel=testing") client.save({CONANFILE: with_deps_path_file}, clean_first=True) client.run(f"export . --user=user --channel=testing") client.save({'conanfile.txt': deps_txt_file}, clean_first=True) return client def test_basic(): client = TestClient() client.save({CONANFILE: conanfile_py}) client.run(f"export . --user=user --channel=testing") client.run(f"graph info --requires=package/0.1.0@user/testing --format=json") nodes = json.loads(client.stdout)["graph"]["nodes"] assert client.cache_folder in nodes["1"]["recipe_folder"] assert os.path.basename(nodes["1"]["recipe_folder"]).strip() == EXPORT_FOLDER assert nodes["1"]["source_folder"] is None assert nodes["1"]["build_folder"] is None assert nodes["1"]["package_folder"] is None def test_build_id(): # https://github.com/conan-io/conan/issues/6915 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = {"myOption": [True, False]} def build_id(self): self.info_build.options.myOption = "Any" """) client.save({CONANFILE: conanfile}) client.run(f"export . --name=pkg --version=0.1 --user=user --channel=testing") client.run(f"graph info --requires=pkg/0.1@user/testing -o pkg/*:myOption=True") out = str(client.out).replace("\\", "/") assert "package_id: b868c8ab4ae6ddccfe19fabd62a5e180d4b18a2b" in out assert "build_id: d5d6fc54af6f589e338090910ac18c848a87720d" in out client.run("graph info --requires=pkg/0.1@user/testing -o pkg/*:myOption=False") out = str(client.out).replace("\\", "/") assert "package_id: 41e2e23ac9570fd23f421bcd0cf9e5cbab49e6ee" in out assert "build_id: d5d6fc54af6f589e338090910ac18c848a87720d" in out def test_deps_basic(client_deps): for ref in [f"--requires=package2/0.2.0@user/testing", "conanfile.txt"]: client_deps.run(f"graph info {ref} --format=json") nodes = json.loads(client_deps.stdout) found_ref = False assert len(nodes["graph"]["nodes"]) == 3 for _, node in nodes["graph"]["nodes"].items(): if node["ref"] == "conanfile": assert node["source_folder"] is None else: assert client_deps.cache_folder in node["recipe_folder"] assert os.path.basename(node["recipe_folder"]).strip() == EXPORT_FOLDER assert node["source_folder"] is None assert node["build_folder"] is None assert node["package_folder"] is None found_ref = found_ref or "package/0.1.0@user/testing" in node["ref"] assert found_ref def test_deps_specific_information(client_deps): client_deps.run("graph info . --package-filter package/* --format=json") nodes = json.loads(client_deps.stdout)["graph"]["nodes"] assert len(nodes) == 1 assert "package/0.1.0@user/testing" in nodes["2"]["ref"] assert nodes["2"]["source_folder"] is None assert nodes["2"]["build_folder"] is None assert nodes["2"]["package_folder"] is None client_deps.run("graph info . --package-filter package* --format=json") nodes = json.loads(client_deps.stdout)["graph"]["nodes"] assert len(nodes) == 2 assert "package2/0.2.0@user/testing" in nodes["1"]["ref"] assert nodes["1"]["source_folder"] is None assert nodes["1"]["build_folder"] is None assert nodes["1"]["package_folder"] is None assert "package/0.1.0@user/testing" in nodes["2"]["ref"] assert nodes["2"]["source_folder"] is None assert nodes["2"]["build_folder"] is None assert nodes["2"]["package_folder"] is None def test_single_field(): client = TestClient() client.save({CONANFILE: conanfile_py}) client.run(f"export . --user=user --channel=testing") client.run(f"graph info --requires package/0.1.0@user/testing --format=json") nodes = json.loads(client.stdout)["graph"]["nodes"] assert len(nodes) == 2 assert "package/0.1.0@user/testing" in nodes["1"]["ref"] assert nodes["1"]["source_folder"] is None assert nodes["1"]["build_folder"] is None assert nodes["1"]["package_folder"] is None def test_direct_conanfile(): client = TestClient() client.save({CONANFILE: conanfile_py}) client.run("graph info .") output = client.out assert "export_folder" not in output assert "source_folder: None" in output assert "build_folder: None" in output assert "package_folder: None" in output ================================================ FILE: test/integration/command/install/__init__.py ================================================ ================================================ FILE: test/integration/command/install/install_cascade_test.py ================================================ from collections import OrderedDict from conan.test.utils.tools import TestServer, GenConanfile, TestClient def test_cascade(): """ app -> E -> D -> B -> A \\-> F -> C -------/ """ server = TestServer() servers = OrderedDict([("default", server)]) c = TestClient(servers=servers) c.save({"a/conanfile.py": GenConanfile("liba", "1.0"), "b/conanfile.py": GenConanfile("libb", "1.0").with_requires("liba/1.0"), "c/conanfile.py": GenConanfile("libc", "1.0").with_requires("liba/1.0"), "d/conanfile.py": GenConanfile("libd", "1.0").with_requires("libb/1.0"), "e/conanfile.py": GenConanfile("libe", "1.0").with_requires("libd/1.0"), "f/conanfile.py": GenConanfile("libf", "1.0").with_requires("libc/1.0", "libd/1.0"), "app/conanfile.py": GenConanfile().with_requires("libe/1.0", "libf/1.0")}) for pkg in ("a", "b", "c", "d", "e", "f"): c.run(f"create {pkg}") def _assert_built(refs): for ref in refs: assert "{}: Copying sources to build folder".format(ref) in c.out for ref in ["liba/1.0", "libb/1.0", "libc/1.0", "libd/1.0", "libe/1.0", "libf/1.0"]: if ref not in refs: assert "{}: Copying sources to build folder".format(ref) not in c.out # Building A everything is built c.run("install app --build=liba* --build cascade") assert "Using build-mode 'cascade' is generally inefficient" in c.out _assert_built(["liba/1.0", "libb/1.0", "libc/1.0", "libd/1.0", "libe/1.0", "libf/1.0"]) c.run("install app --build=libd* --build cascade") _assert_built(["libd/1.0", "libe/1.0", "libf/1.0"]) c.run("install app --build=libe* --build cascade") _assert_built(["libe/1.0"]) c.run("install app --build cascade") _assert_built([]) c.run("install app --build=libc* --build cascade") _assert_built(["libc/1.0", "libf/1.0"]) ================================================ FILE: test/integration/command/install/install_missing_dep_test.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestInstallMissingDependency: def test_missing_dep(self): client = TestClient() # Create deps packages dep1_conanfile = GenConanfile("dep1") client.save({"conanfile.py": dep1_conanfile}, clean_first=True) client.run("create . --name=dep1 --version=1.0 --user=lasote --channel=testing") client.run("create . --name=dep1 --version=2.0 --user=lasote --channel=testing") dep2_conanfile = GenConanfile("dep2", "1.0").with_require("dep1/1.0@lasote/testing") client.save({"conanfile.py": dep2_conanfile}, clean_first=True) client.run("create . --user=lasote --channel=testing") # Create final package # foo -------------> dep1/1.0 # \ -> dep2/1.0---->/ conanfile = GenConanfile("foo", "1.0").with_require("dep1/1.0@lasote/testing")\ .with_require("dep2/1.0@lasote/testing") client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --user=lasote --channel=testing") # Bump version of one dependency # foo -------------> dep1/2.0 # \ -> dep2/1.0---->/ conanfile = GenConanfile("foo", "1.0").with_requirement("dep1/2.0@lasote/testing", force=True) \ .with_require("dep2/1.0@lasote/testing") client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --user=lasote --channel=testing", assert_error=True) client.assert_overrides({"dep1/1.0@lasote/testing": ['dep1/2.0@lasote/testing']}) assert "Can't find a 'dep2/1.0@lasote/testing' package" in client.out assert "dep1/2.Y.Z" in client.out def test_missing_multiple_dep(self): client = TestClient() dep1_conanfile = GenConanfile() client.save({"conanfile.py": dep1_conanfile}, clean_first=True) client.run("export . --name=dep1 --version=1.0") client.run("export . --name=dep2 --version=1.0") conanfile = GenConanfile().with_require("dep1/1.0").with_require("dep2/1.0") client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=pkg --version=1.0", assert_error=True) assert "ERROR: Missing prebuilt package for 'dep1/1.0', 'dep2/1.0'" in client.out assert "Try to build locally from sources using the '--build=dep1/1.0 --build=dep2/1.0'" in client.out ================================================ FILE: test/integration/command/install/install_parallel_test.py ================================================ from conan.test.utils.tools import GenConanfile, TestClient class TestInstallParallel: def test_basic_parallel_install(self): client = TestClient(default_server_user=True) threads = 4 counter = 8 client.save_home({"global.conf": f"core.download:parallel={threads}"}) client.save({"conanfile.py": GenConanfile()}) for i in range(counter): client.run("create . --name=pkg%s --version=0.1 --user=user --channel=testing" % i) client.run("upload * --confirm -r default") client.run("remove * -c") # Lets consume the packages conanfile_txt = ["[requires]"] for i in range(counter): conanfile_txt.append("pkg%s/0.1@user/testing" % i) conanfile_txt = "\n".join(conanfile_txt) client.save({"conanfile.txt": conanfile_txt}, clean_first=True) client.run("install .") assert "Downloading binary packages in %s parallel threads" % threads in client.out for i in range(counter): assert "pkg%s/0.1@user/testing: Package installed" % i in client.out ================================================ FILE: test/integration/command/install/install_test.py ================================================ import json import os import re import textwrap from collections import OrderedDict import pytest from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID from conan.test.utils.tools import TestClient, TestServer, GenConanfile from conan.internal.util.files import mkdir, save @pytest.fixture() def client(): c = TestClient(default_server_user=True) c.save_home({"settings.yml": "os: [Windows, Macos, Linux, FreeBSD]\nos_build: [Windows, Macos]", "profiles/default": "[settings]\nos=Windows"}) return c def test_install_reference_txt(client): # Test to check the "conan install " command argument client.save({"conanfile.txt": ""}) client.run("install .") assert "conanfile.txt" in client.out def test_install_reference_error(client): # Test to check the "conan install " command argument client.run("install --requires=pkg/0.1@myuser/testing --user=user --channel=testing", assert_error=True) assert "ERROR: Can't use --name, --version, --user or --channel arguments with --requires" in client.out client.save({"conanfile.py": GenConanfile("pkg", "1.0")}) client.run("install . --channel=testing", assert_error=True) assert "Can't specify channel 'testing' without user" in client.out def test_install_args_error(): c = TestClient() c.run("install . --requires=zlib/1.0", assert_error=True) assert "--requires and --tool-requires arguments are incompatible" in c.out def test_four_subfolder_install(client): # https://github.com/conan-io/conan/issues/3950 client.save({"path/to/sub/folder/conanfile.txt": ""}) # If this doesn't, fail, all good client.run(" install path/to/sub/folder") @pytest.mark.artifactory_ready def test_install_system_requirements(client): client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class MyPkg(ConanFile): def system_requirements(self): self.output.info("Running system requirements!!") """)}) client.run(" install .") assert "Running system requirements!!" in client.out client.run("export . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run(" install --requires=pkg/0.1@lasote/testing --build='*'") assert "Running system requirements!!" in client.out client.run("upload * --confirm -r default") client.run('remove "*" -c') client.run(" install --requires=pkg/0.1@lasote/testing") assert "Running system requirements!!" in client.out def test_install_transitive_pattern(client): # Make sure a simple conan install doesn't fire package_info() so self.package_folder breaks client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = {"shared": [True, False, "header"]} default_options = {"shared": False} def package_info(self): self.output.info("PKG OPTION: %s" % self.options.shared) """)}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o shared=True") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "pkg/0.1@user/testing" options = {"shared": [True, False, "header"]} default_options = {"shared": False} def package_info(self): self.output.info("PKG2 OPTION: %s" % self.options.shared) """)}) client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out # Priority of non-scoped options client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o shared=header -o *:shared=True") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out client.run(" install --requires=pkg2/0.1@user/testing -o shared=header -o *:shared=True") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out # Prevalence of exact named option client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg2*:shared=header") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg2*:shared=header") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out # Prevalence of exact named option reverse client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg/*:shared=header " "--build=missing") assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg/*:shared=header") assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out # Prevalence of alphabetical pattern client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg2*:shared=header") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg2*:shared=header") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out # Prevalence of last match, even first pattern match client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o pkg2*:shared=header -o *:shared=True") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out client.run(" install --requires=pkg2/0.1@user/testing -o pkg2*:shared=header -o *:shared=True") assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out # Prevalence and override of alphabetical pattern client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg*:shared=header") assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg*:shared=header") assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out def test_install_package_folder(client): # Make sure a simple conan install doesn't fire package_info() so self.package_folder breaks client.save({"conanfile.py": textwrap.dedent("""\ from conan import ConanFile import os class Pkg(ConanFile): def package_info(self): self.dummy_doesnt_exist_not_break self.output.info("Hello") self.env_info.PATH = os.path.join(self.package_folder, "bin") """)}) client.run("install .") assert "Hello" not in client.out def test_install_cwd(client): client.save({"conanfile.py": GenConanfile("hello", "0.1").with_setting("os")}) client.run("export . --user=lasote --channel=stable") client.save({"conanfile.txt": "[requires]\nhello/0.1@lasote/stable"}, clean_first=True) client.run("install . --build=missing -s os_build=Windows") assert "hello/0.1@lasote/stable#a20db3358243e96aa07f654eaada1564 - Cache" in client.out def test_install_with_profile(client): # Test for https://github.com/conan-io/conan/pull/2043 conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): settings = "os" def requirements(self): self.output.info("PKGOS=%s" % self.settings.os) """) client.save({"conanfile.py": conanfile}) save(os.path.join(client.paths.profiles_path, "myprofile"), "[settings]\nos=Linux") client.run("install . -pr=myprofile --build='*'") assert "PKGOS=Linux" in client.out mkdir(os.path.join(client.current_folder, "myprofile")) client.run("install . -pr=myprofile") save(os.path.join(client.paths.profiles_path, "myotherprofile"), "[settings]\nos=FreeBSD") client.run("install . -pr=myotherprofile") assert "PKGOS=FreeBSD" in client.out client.save({"myotherprofile": "Some garbage without sense [garbage]"}) client.run("install . -pr=myotherprofile") assert "PKGOS=FreeBSD" in client.out client.run("install . -pr=./myotherprofile", assert_error=True) assert "Error while parsing line 0" in client.out def test_install_with_path_errors(client): # Install without path param allowed, but nothing found client.run("install", assert_error=True) assert "Conanfile not found" in client.out # Install without path param allowed, but nothing found client.run("install . --requires=foo/1.0", assert_error=True) assert "--requires and --tool-requires arguments are incompatible with [path]" in client.out # Path with wrong conanfile.txt path client.run("install not_real_dir/conanfile.txt", assert_error=True) assert "Conanfile not found" in client.out def test_install_argument_order(client): # https://github.com/conan-io/conan/issues/2520 conanfile_boost = textwrap.dedent(""" from conan import ConanFile class BoostConan(ConanFile): name = "boost" version = "0.1" options = {"shared": [True, False]} default_options = {"shared": True} """) conanfile = GenConanfile().with_require("boost/0.1") client.save({"conanfile.py": conanfile, "conanfile_boost.py": conanfile_boost}) client.run("create conanfile_boost.py ") client.run("install . -o boost/*:shared=True --build=missing") output_0 = client.out client.run("install . -o boost/*:shared=True --build missing") output_1 = client.out client.run("install -o boost/*:shared=True . --build missing") output_2 = client.out client.run("install -o boost/*:shared=True --build missing .") output_3 = client.out assert "ERROR" not in output_3 assert output_0 == output_1 assert output_1 == output_2 assert output_2 == output_3 client.run("install -o boost/*:shared=True --build boost . --build missing") output_4 = client.out client.run("install -o boost/*:shared=True --build missing --build boost .") output_5 = client.out assert output_4 == output_5 def test_install_anonymous(client): # https://github.com/conan-io/conan/issues/4871 client.save({"conanfile.py": GenConanfile("pkg", "0.1")}) client.run("create . --user=lasote --channel=testing") client.run("upload * --confirm -r default") client2 = TestClient(servers=client.servers, inputs=[]) client2.run("install --requires=pkg/0.1@lasote/testing") assert "pkg/0.1@lasote/testing: Package installed" in client2.out @pytest.mark.artifactory_ready def test_install_without_ref(client): client.save({"conanfile.py": GenConanfile("lib", "1.0")}) client.run('create .') assert "lib/1.0: Package '{}' created".format(NO_SETTINGS_PACKAGE_ID) in client.out client.run('upload lib/1.0 -c -r default') assert "Uploading recipe 'lib/1.0" in client.out client.run('remove "*" -c') # This fails, Conan thinks this is a path client.run('install lib/1.0', assert_error=True) fake_path = os.path.join(client.current_folder, "lib", "1.0") assert "Conanfile not found at {}".format(fake_path) in client.out # Try this syntax to upload too client.run('install --requires=lib/1.0@') client.run('upload lib/1.0 -c -r default') @pytest.mark.artifactory_ready def test_install_disabled_remote(client): client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("upload * --confirm -r default") client.run("remote disable default") client.run("install --requires=pkg/0.1@lasote/testing -r default", assert_error=True) assert "ERROR: Remote 'default' can't be found or is disabled" in client.out client.run("remote enable default") client.run("install --requires=pkg/0.1@lasote/testing -r default") client.run("remote disable default") client.run("install --requires=pkg/0.1@lasote/testing --update -r default", assert_error=True) assert "ERROR: Remote 'default' can't be found or is disabled" in client.out def test_install_no_remotes(client): client.save({"conanfile.py": GenConanfile("pkg", "0.1")}) client.run("create .") client.run("upload * --confirm -r default") client.run("remove * -c") client.run("install --requires=pkg/0.1 -nr", assert_error=True) assert "ERROR: Package 'pkg/0.1' not resolved: No remote defined" in client.out client.run("install --requires=pkg/0.1") # this works without issue client.run("install --requires=pkg/0.1 -nr") # and now this too, pkg in cache def test_install_skip_disabled_remote(): client = TestClient(servers=OrderedDict({"default": TestServer(), "server2": TestServer(), "server3": TestServer()}), inputs=2*["admin", "password"]) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("upload * --confirm -r default") client.run("upload * --confirm -r server3") client.run("remove * -c") client.run("remote disable default") client.run("install --requires=pkg/0.1@lasote/testing", assert_error=False) assert "Trying with 'default'..." not in client.out def test_install_without_update_fail(client): # https://github.com/conan-io/conan/issues/9183 client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=zlib --version=1.0") client.run("upload * --confirm -r default") client.save({"conanfile.py": GenConanfile().with_requires("zlib/1.0")}) client.run("remote disable default") client.run("install .") assert "zlib/1.0: Already installed" in client.out def test_install_version_range_reference(client): # https://github.com/conan-io/conan/issues/5905 client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=user --channel=channel") client.run("install --requires=pkg/[*]@user/channel") assert "pkg/0.1@user/channel: Already installed!" in client.out client.run("install --requires=pkg/[>0]@user/channel") assert "pkg/0.1@user/channel: Already installed!" in client.out def test_install_error_never(client): client.save({"conanfile.py": GenConanfile("hello0", "0.1")}) client.run("create .") client.run("install . --build never --build missing", assert_error=True) assert "ERROR: --build=never not compatible with other options" in client.out client.run("install conanfile.py --build never --build Hello", assert_error=True) assert "ERROR: --build=never not compatible with other options" in client.out client.run("install ./conanfile.py --build never --build outdated", assert_error=True) assert "ERROR: --build=never not compatible with other options" in client.out def test_package_folder_available_consumer(): """ The package folder is not available when doing a consumer conan install "." We don't want to provide the package folder for the "cmake install" nor the "make install", as a consumer you could call the build system and pass the prefix PATH manually. """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class HelloConan(ConanFile): settings = "os", "arch", "build_type" def layout(self): cmake_layout(self) def generate(self): self.output.warning("Package folder is None? {}".format(self.package_folder is None)) self.output.warning("Package folder: {}".format(self.package_folder)) """) client.save({"conanfile.py": conanfile}) # Installing it with "install ." with output folder client.run("install . -of=my_build") assert "WARN: Package folder is None? True" in client.out # Installing it with "install ." without output folder client.run("install .") assert "WARN: Package folder is None? True" in client.out def test_install_multiple_requires_cli(): """ Test that it is possible to install multiple --requires=xxx --requires=yyy """ c = TestClient() c.save({"conanfile.py": GenConanfile()}) c.run("create . --name=pkg1 --version=0.1") c.run("create . --name=pkg2 --version=0.1") c.run("graph info --requires=pkg1/0.1 --requires=pkg2/0.1") assert "pkg1/0.1" in c.out assert "pkg2/0.1" in c.out c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1") assert "pkg1/0.1" in c.out assert "pkg2/0.1" in c.out c.run("lock create --requires=pkg1/0.1 --requires=pkg2/0.1 --lockfile-out=conan.lock") lock = c.load("conan.lock") assert "pkg1/0.1" in lock assert "pkg2/0.1" in lock def test_install_json_formatter(): """ Tests the ``conan install . -f json`` result """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.2" def package_info(self): self.cpp_info.libs = ["pkg"] self.cpp_info.includedirs = ["path/includes/pkg", "other/include/path/pkg"] self.cpp_info.libdirs = ["one/lib/path/pkg"] self.cpp_info.defines = ["pkg_onedefinition", "pkg_twodefinition"] self.cpp_info.cflags = ["pkg_a_c_flag"] self.cpp_info.cxxflags = ["pkg_a_cxx_flag"] self.cpp_info.sharedlinkflags = ["pkg_shared_link_flag"] self.cpp_info.exelinkflags = ["pkg_exe_link_flag"] self.cpp_info.sysroot = "/path/to/folder/pkg" self.cpp_info.frameworks = ["pkg_oneframework", "pkg_twoframework"] self.cpp_info.system_libs = ["pkg_onesystemlib", "pkg_twosystemlib"] self.cpp_info.frameworkdirs = ["one/framework/path/pkg"] self.cpp_info.set_property("pkg_config_name", "pkg_other_name") self.cpp_info.set_property("pkg_config_aliases", ["pkg_alias1", "pkg_alias2"]) self.cpp_info.components["cmp1"].libs = ["libcmp1"] self.cpp_info.components["cmp1"].set_property("pkg_config_name", "compo1") self.cpp_info.components["cmp1"].set_property("pkg_config_aliases", ["compo1_alias"]) self.cpp_info.components["cmp1"].sysroot = "/another/sysroot" """) client.save({"conanfile.py": conanfile}) client.run("create .") client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("0.1") .with_require("pkg/0.2")}, clean_first=True) client.run("install . -f json") info = json.loads(client.stdout) nodes = info["graph"]["nodes"] hello_pkg_ref = 'hello/0.1' # no revision available pkg_pkg_ref = 'pkg/0.2#926714b5fb0a994f47ec37e071eba1da' hello_cpp_info = pkg_cpp_info = None for _, n in nodes.items(): ref = n["ref"] if ref == hello_pkg_ref: assert n['binary'] is None hello_cpp_info = n['cpp_info'] elif ref == pkg_pkg_ref: assert n['binary'] == "Cache" pkg_cpp_info = n['cpp_info'] hello = nodes["0"] assert hello["ref"] == hello_pkg_ref assert hello["recipe_folder"] == client.current_folder assert hello["build_folder"] == client.current_folder assert hello["generators_folder"] == client.current_folder assert hello["package_folder"] is None assert hello_cpp_info and pkg_cpp_info # hello/0.1 cpp_info assert hello_cpp_info['root']["libs"] is None assert len(hello_cpp_info['root']["bindirs"]) == 1 assert len(hello_cpp_info['root']["libdirs"]) == 1 assert hello_cpp_info['root']["sysroot"] is None assert hello_cpp_info['root']["properties"] is None # pkg/0.2 cpp_info # root info assert pkg_cpp_info['root']["libs"] == ['pkg'] assert len(pkg_cpp_info['root']["bindirs"]) == 1 assert len(pkg_cpp_info['root']["libdirs"]) == 1 assert pkg_cpp_info['root']["sysroot"] == '/path/to/folder/pkg' assert pkg_cpp_info['root']["system_libs"] == ['pkg_onesystemlib', 'pkg_twosystemlib'] assert pkg_cpp_info['root']['cflags'] == ['pkg_a_c_flag'] assert pkg_cpp_info['root']['cxxflags'] == ['pkg_a_cxx_flag'] assert pkg_cpp_info['root']['defines'] == ['pkg_onedefinition', 'pkg_twodefinition'] assert pkg_cpp_info['root']["properties"] == { 'pkg_config_aliases': ['pkg_alias1', 'pkg_alias2'], 'pkg_config_name': 'pkg_other_name'} # component info assert pkg_cpp_info['cmp1']["libs"] == ['libcmp1'] assert pkg_cpp_info['cmp1']["bindirs"][0].endswith("bin") # Abs path /bin assert pkg_cpp_info['cmp1']["libdirs"][0].endswith("lib") # Abs path /lib assert pkg_cpp_info['cmp1']["sysroot"] == "/another/sysroot" assert pkg_cpp_info['cmp1']["properties"] == {'pkg_config_aliases': ['compo1_alias'], 'pkg_config_name': 'compo1'} def test_upload_skip_binaries_not_hit_server(): """ When upload_policy = "skip", no need to try to install from servers """ c = TestClient(servers={"default": None}) # Broken server, will raise error if used conanfile = GenConanfile("pkg", "0.1").with_class_attribute('upload_policy = "skip"') c.save({"conanfile.py": conanfile}) c.run("export .") c.run("install --requires=pkg/0.1 --build=missing") # This would crash if hits the server, but it doesnt assert "pkg/0.1: Created package" in c.out def test_upload_skip_build_missing(): c = TestClient(default_server_user=True) pkg1 = GenConanfile("pkg1", "1.0").with_class_attribute('upload_policy = "skip"') pkg2 = GenConanfile("pkg2", "1.0").with_requirement("pkg1/1.0", visible=False) pkg3 = GenConanfile("pkg3", "1.0").with_requirement("pkg2/1.0") c.save({"pkg1/conanfile.py": pkg1, "pkg2/conanfile.py": pkg2, "pkg3/conanfile.py": pkg3, }) c.run("create pkg1") c.run("create pkg2") c.run("remove pkg1/*:* -c") # remove binaries c.run("create pkg3 --build=missing") assert re.search(r"Skipped binaries(\s*)pkg1/1.0", c.out) def test_upload_skip_build_compatibles(): c = TestClient() pkg1 = textwrap.dedent("""\ from conan import ConanFile class Pkg1(ConanFile): name = "pkg1" version = "1.0" settings = "build_type" upload_policy = "skip" def compatibility(self): if self.settings.build_type == "Debug": return [{"settings": [("build_type", "Release")]}] """) pkg2 = GenConanfile("pkg2", "1.0").with_requirement("pkg1/1.0") pkg3 = GenConanfile("pkg3", "1.0").with_requirement("pkg2/1.0") c.save({"pkg1/conanfile.py": pkg1, "pkg2/conanfile.py": pkg2, "pkg3/conanfile.py": pkg3, }) c.run("create pkg1 -s build_type=Release") pkg1id = c.created_package_id("pkg1/1.0") c.run("create pkg2 -s build_type=Release") c.run("remove pkg1/*:* -c") # remove binaries c.run("create pkg3 -s build_type=Release --build=missing") c.assert_listed_binary({"pkg1/1.0": (pkg1id, "Build")}) c.run("install pkg3 -s build_type=Debug --build=missing") c.assert_listed_binary({"pkg1/1.0": (pkg1id, "Cache")}) def test_install_json_format(): # https://github.com/conan-io/conan/issues/14414 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.1" settings = "build_type" def package_info(self): self.conf_info.define("user.myteam:myconf", "myvalue") """) client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=pkg/0.1 --format=json") data = json.loads(client.stdout) conf_info = data["graph"]["nodes"]["1"]["conf_info"] assert {'user.myteam:myconf': 'myvalue'} == conf_info def test_install_json_format_not_visible(): """ The dependencies that are needed at built time, even if they are not visible, or not standard libraries (headers=False, libs=False, run=False), like for example some build assets So direct dependencies of a consumer package or a package that needs to be built cannot be skipped """ c = TestClient() dep = GenConanfile("dep", "0.1").with_package_file("somefile.txt", "contents!!!") app = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class Pkg(ConanFile): settings = "os", "arch", "build_type" name="pkg" version="0.0.1" def requirements(self): self.requires("dep/0.1", visible=False, headers=False, libs=False, run=False) def build(self): p = os.path.join(self.dependencies["dep"].package_folder, "somefile.txt") c = load(self, p) self.output.info(f"LOADED! {c}") """) c.save({"dep/conanfile.py": dep, "app/conanfile.py": app}) c.run("export-pkg dep") c.run("install app --format=json") c.assert_listed_binary({"dep/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}) data = json.loads(c.stdout) pkg_folder = data["graph"]["nodes"]["1"]["package_folder"] assert pkg_folder is not None c.run("create app") assert "pkg/0.0.1: LOADED! contents!!!" in c.out ================================================ FILE: test/integration/command/install/install_update_test.py ================================================ import os import textwrap from time import sleep from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient, GenConanfile, TestServer from conan.internal.util.files import load def test_update_binaries(): client = TestClient(default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save, load import os, random class Pkg(ConanFile): def package(self): save(self, os.path.join(self.package_folder, "file.txt"), str(random.random())) def package_info(self): content = load(self, os.path.join(self.package_folder, "file.txt")) self.output.warning("CONTENT=>{}#".format(content)) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("upload pkg/0.1@lasote/testing -r default") client2 = TestClient(servers=client.servers, inputs=["admin", "password"]) client2.run("install --requires=pkg/0.1@lasote/testing") def get_value_from_output(output): tmp = str(output).split("CONTENT=>")[1] return tmp.split("#")[0] value = get_value_from_output(client2.out) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") # Because of random, this should be NEW prev client.run("upload pkg/0.1@lasote/testing -r default") client2.run("install --requires=pkg/0.1@lasote/testing") new_value = get_value_from_output(client2.out) assert value == new_value client2.run("install --requires=pkg/0.1@lasote/testing --update") assert "Current package revision is older than the remote one" in client2.out new_value = get_value_from_output(client2.out) assert value != new_value # Now check newer local modifications are not overwritten client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("upload pkg/0.1@lasote/testing -r default") client2.save({"conanfile.py": conanfile}) client2.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client2.run("install --requires=pkg/0.1@lasote/testing") value2 = get_value_from_output(client2.out) client2.run("install --requires=pkg/0.1@lasote/testing --update -r default") assert "Current package revision is newer than the remote one" in client2.out new_value = get_value_from_output(client2.out) assert value2 == new_value def test_update_not_date(): client = TestClient(default_server_user=True) # Regression for https://github.com/conan-io/conan/issues/949 client.save({"conanfile.py": GenConanfile("hello0", "1.0")}) client.run("export . --user=lasote --channel=stable") client.save({"conanfile.py": GenConanfile("hello1", "1.0"). with_requirement("hello0/1.0@lasote/stable")}, clean_first=True) client.run("install . --build='*'") client.run("upload hello0/1.0@lasote/stable -r default") prev = client.get_latest_package_reference("hello0/1.0@lasote/stable") ref = RecipeReference.loads("hello0/1.0@lasote/stable") initial_recipe_timestamp = client.cache.get_latest_recipe_revision(ref).timestamp initial_package_timestamp = prev.timestamp # Change and rebuild package client.save({"conanfile.py": GenConanfile("hello0", "1.0").with_class_attribute("author = 'O'")}, clean_first=True) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello0/1.0@lasote/stable --build='*'") rebuild_recipe_timestamp = client.cache.get_latest_recipe_revision(ref).timestamp rebuild_package_timestamp = client.get_latest_package_reference(ref).timestamp assert rebuild_recipe_timestamp != initial_recipe_timestamp assert rebuild_package_timestamp != initial_package_timestamp # back to the consumer, try to update client.save({"conanfile.py": GenConanfile("hello1", "1.0"). with_requirement("hello0/1.0@lasote/stable")}, clean_first=True) # First assign the preference to a remote, it has been cleared when exported locally client.run("install . --update") # *1 With revisions here is removing the package because it doesn't belong to the recipe client.assert_listed_require({"hello0/1.0@lasote/stable": "Newer"}) failed_update_recipe_timestamp = client.cache.get_latest_recipe_revision(ref).timestamp failed_update_package_timestamp = client.get_latest_package_reference(ref).timestamp assert rebuild_recipe_timestamp == failed_update_recipe_timestamp assert rebuild_package_timestamp == failed_update_package_timestamp def test_reuse(): client = TestClient(default_server_user=True) conanfile = GenConanfile("hello0", "1.0")\ .with_exports_sources("*")\ .with_import("from conan.tools.files import copy")\ .with_package("copy(self, '*', self.source_folder, self.package_folder)") client.save({"conanfile.py": conanfile, "header.h": "content1"}) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello0/1.0@lasote/stable --build='*'") client.run("upload hello0/1.0@lasote/stable -r default") client2 = TestClient(servers=client.servers, inputs=["admin", "password"]) client2.run("install --requires=hello0/1.0@lasote/stable") assert "hello0/1.0@lasote/stable: Retrieving package" in client2.out client.save({"header.h": "//EMPTY!"}) sleep(1) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello0/1.0@lasote/stable --build='*'") client.run("upload hello0/1.0@lasote/stable -r default") client2.run("install --requires=hello0/1.0@lasote/stable --update") ref = RecipeReference.loads("hello0/1.0@lasote/stable") pref = client.get_latest_package_reference(ref) package_path = client2.get_latest_pkg_layout(pref).package() header = load(os.path.join(package_path, "header.h")) assert header == "//EMPTY!" def test_update_binaries_failed(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("install --requires=pkg/0.1@lasote/testing --update") assert "WARN: Can't update, there are no remotes defined" in client.out def test_install_update_repeated_tool_requires(): """ Test that requiring the same thing multiple times, like a tool-requires, only require checking the servers 1, so it is much faster https://github.com/conan-io/conan/issues/13508 """ c = TestClient(default_server_user=True) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "liba/conanfile.py": GenConanfile("liba", "0.1"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requires("liba/0.1"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requires("libb/0.1"), "profile": "[tool_requires]\ntool/0.1" }) c.run("create tool") c.run("create liba") c.run("create libb") c.run("create libc") c.run("install libc --update -pr=profile") assert 1 == str(c.out).count("tool/0.1: Checking remote") class TestUpdateOldPolicy: def test_multi_remote_update_resolution(self): c = TestClient(servers={"r1": TestServer(), "r2": TestServer(), "r3": TestServer()}, inputs=["admin", "password"] * 3, light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") rev1 = c.exported_recipe_revision() c.run("upload * -r=r1 -c") # second revision c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("auther = 'me'")}) c.run("export .") rev2 = c.exported_recipe_revision() assert rev1 != rev2 c.run("upload * -r=r2 -c") # By default uploads latest revisions only assert rev1 not in c.out assert rev2 in c.out # going back to the previous revision c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") # Makes it the latest rev3 = c.exported_recipe_revision() assert rev1 == rev3 c.run("upload * -r=r3 -c") # By default uploads latest revisions only assert rev3 in c.out assert rev2 not in c.out # now test the --update, it will pick up the latest revision, which is r3 c.run("remove * -c") c.run("graph info --requires=pkg/0.1 --update") assert f"pkg/0.1#{rev3} - Downloaded (r3)" in c.out # But if we enable order-based first found timestamp, it will pick up r2 c.run("remove * -c") c.run("graph info --requires=pkg/0.1 --update -cc core:update_policy=legacy") assert "The 'core:update_policy' conf is deprecated and will be removed" in c.out assert f"pkg/0.1#{rev2} - Downloaded (r2)" in c.out def test_multi_remote_update_resolution_2_remotes(self): c = TestClient(servers={"r1": TestServer(), "r2": TestServer()}, inputs=["admin", "password"] * 2, light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") rev1 = c.exported_recipe_revision() c.run("upload * -r=r1 -c") # second revision c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("auther = 'me'")}) c.run("export .") rev2 = c.exported_recipe_revision() assert rev1 != rev2 c.run("upload * -r=r1 -c") c.run("list *#* -r=r1") # Upload the first, old revision to the other remote c.run(f"upload pkg/0.1#{rev1} -r=r2 -c") assert rev1 in c.out assert rev2 not in c.out c.run("list *#* -r=r2") # now test the --update, it will pick up the latest revision, which is r3 c.run("remove * -c") c.run("graph info --requires=pkg/0.1 --update") assert f"pkg/0.1#{rev1} - Downloaded (r2)" in c.out # But if we enable order-based first found timestamp, it will pick up r2 c.run("remove * -c") c.run("graph info --requires=pkg/0.1 --update -cc core:update_policy=legacy") assert f"pkg/0.1#{rev2} - Downloaded (r1)" in c.out def test_lockfile(self): # https://github.com/conan-io/conan/issues/18006 c = TestClient(default_server_user=True, light=True) # needs server to fail c.save_home({"global.conf": "core:update_policy=legacy"}) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("dep/0.1")}) c.run("create dep") c.run("lock create pkg --lockfile-out base.lock --build=* --update") c.run("lock create pkg --lockfile base.lock --lockfile-out full.lock --build=* --update") # it doesn't crash assert "Generated lockfile" in c.out ================================================ FILE: test/integration/command/install/test_graph_build_mode.py ================================================ import json import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def build_all(): """ Build a simple graph to test --build option foobar <- bar <- foo <--------| All packages are built from sources to keep a cache. :return: TestClient instance """ client = TestClient() client.save({"conanfile.py": GenConanfile().with_setting("build_type")}) client.run("export . --name=foo --version=1.0 --user=user --channel=testing") client.save({"conanfile.py": GenConanfile().with_require("foo/1.0@user/testing") .with_setting("build_type")}) client.run("export . --name=bar --version=1.0 --user=user --channel=testing") client.save({"conanfile.py": GenConanfile().with_require("foo/1.0@user/testing") .with_require("bar/1.0@user/testing") .with_setting("build_type")}) client.run("export . --name=foobar --version=1.0 --user=user --channel=testing") client.run("install --requires=foobar/1.0@user/testing --build='*'") return client foo_id = "efa83b160a55b033c4ea706ddb980cd708e3ba1b" bar_id = "7d0bb2b97d4339b0d3ded1418a2593f35b9cf267" foobar_id = "af8f885f621ba7baac3f5b1d2c18cfdf5ba2550c" def check_if_build_from_sources(refs_modes, output): for ref, mode in refs_modes.items(): if mode == "Build": assert "{}/1.0@user/testing: Forced build from source".format(ref) in output else: assert "{}/1.0@user/testing: Forced build from source".format(ref) not in output def test_install_build_single(build_all): """ When only --build= is passed, only must be built """ build_all.run("install --requires=foobar/1.0@user/testing --build=foo/*") build_all.assert_listed_binary({"bar/1.0@user/testing": (bar_id, "Cache"), "foo/1.0@user/testing": (foo_id, "Build"), "foobar/1.0@user/testing": (foobar_id, "Cache"), }) assert "foo/1.0@user/testing: Forced build from source" in build_all.out assert "bar/1.0@user/testing: Forced build from source" not in build_all.out assert "foobar/1.0@user/testing: Forced build from source" not in build_all.out def test_install_build_double(build_all): """ When both --build= and --build= are passed, only both should be built """ build_all.run("install --requires=foobar/1.0@user/testing --build=foo/* --build=bar/*") build_all.assert_listed_binary({"bar/1.0@user/testing": (bar_id, "Build"), "foo/1.0@user/testing": (foo_id, "Build"), "foobar/1.0@user/testing": (foobar_id, "Cache"), }) assert "foo/1.0@user/testing: Forced build from source" in build_all.out assert "bar/1.0@user/testing: Forced build from source" in build_all.out assert "foobar/1.0@user/testing: Forced build from source" not in build_all.out @pytest.mark.parametrize("build_arg,mode", [("--build=", "Cache"), ("--build=*", "Build")]) def test_install_build_only(build_arg, mode, build_all): """ When only --build is passed wo args, that is a command arg error When only --build= is passed, it's a no-op, same as not passing any value When only --build=* is passed, all packages must be built from sources """ build_all.run("install --requires=foobar/1.0@user/testing {}".format(build_arg)) build_all.assert_listed_binary({"bar/1.0@user/testing": (bar_id, mode), "foo/1.0@user/testing": (foo_id, mode), "foobar/1.0@user/testing": (foobar_id, mode), }) if "Build" == mode: assert "foo/1.0@user/testing: Forced build from source" in build_all.out assert "bar/1.0@user/testing: Forced build from source" in build_all.out assert "foobar/1.0@user/testing: Forced build from source" in build_all.out else: assert "foo/1.0@user/testing: Forced build from source" not in build_all.out assert "bar/1.0@user/testing: Forced build from source" not in build_all.out assert "foobar/1.0@user/testing: Forced build from source" not in build_all.out @pytest.mark.parametrize("build_arg,bar,foo,foobar", [("--build=", "Cache", "Build", "Cache"), ("--build=*", "Build", "Build", "Build")]) def test_install_build_all_with_single(build_arg, bar, foo, foobar, build_all): """ When --build is passed with another package, only the package must be built from sources. When --build= is passed with another package, only the package must be built from sources. When --build=* is passed with another package, all packages must be built from sources. """ build_all.run("install --requires=foobar/1.0@user/testing --build=foo/* {}".format(build_arg)) build_all.assert_listed_binary({"bar/1.0@user/testing": (bar_id, bar), "foo/1.0@user/testing": (foo_id, foo), "foobar/1.0@user/testing": (foobar_id, foobar), }) check_if_build_from_sources({"foo": foo, "bar": bar, "foobar": foobar}, build_all.out) @pytest.mark.parametrize("build_arg,bar,foo,foobar", [("--build=", "Cache", "Cache", "Cache"), ("--build=*", "Build", "Cache", "Build")]) def test_install_build_all_with_single_skip(build_arg, bar, foo, foobar, build_all): """ When --build is passed with a skipped package, not all packages must be built from sources. When --build= is passed with another package, only the package must be built from sources. When --build=* is passed with another package, not all packages must be built from sources. The arguments order matter, that's why we need to run twice. """ for argument in ["--build=!foo/* {}".format(build_arg), "{} --build=!foo/*".format(build_arg)]: build_all.run("install --requires=foobar/1.0@user/testing {}".format(argument)) build_all.assert_listed_binary({"bar/1.0@user/testing": (bar_id, bar), "foo/1.0@user/testing": (foo_id, foo), "foobar/1.0@user/testing": (foobar_id, foobar), }) check_if_build_from_sources({"foo": foo, "bar": bar, "foobar": foobar}, build_all.out) @pytest.mark.parametrize("build_arg,bar,foo,foobar", [("--build=", "Cache", "Cache", "Cache"), ("--build=*", "Cache", "Cache", "Build")]) def test_install_build_all_with_double_skip(build_arg, bar, foo, foobar, build_all): """ When --build is passed with a skipped package, not all packages must be built from sources. When --build= is passed with another package, only the package must be built from sources. When --build=* is passed with another package, not all packages must be built from sources. The arguments order matter, that's why we need to run twice. """ for argument in ["--build=!foo/* --build=~bar/* {}".format(build_arg), "{} --build=!foo/* --build=~bar/*".format(build_arg)]: build_all.run("install --requires=foobar/1.0@user/testing {}".format(argument)) build_all.assert_listed_binary({"bar/1.0@user/testing": (bar_id, bar), "foo/1.0@user/testing": (foo_id, foo), "foobar/1.0@user/testing": (foobar_id, foobar), }) def test_build_consumer(): """ If the consumer is built from sources, the dependencies must be built too """ client = TestClient() client.save({"dep1/conanfile.py": GenConanfile("dep1", "1.0").with_build_msg("TEST: Building dep1"), "dep2/conanfile.py": GenConanfile("dep2", "1.0").with_build_msg("TEST: Building dep2"), "conanfile.py": GenConanfile("consumer", "1.0") .with_build_msg("TEST: Building consumer").with_requires("dep1/1.0", "dep2/1.0")}) client.run("create dep1") client.run("create dep2") client.run("export .") # Still makes little sense, but just to test the logic client.run("create . --build=* --build=!&", assert_error=True) assert "TEST: Building consumer" not in client.out client.run(f"graph info --requires=consumer/1.0 --build=& -f=json", redirect_stdout="graph.json") graph = json.loads(client.load("graph.json")) assert graph["graph"]["nodes"]["1"]["binary"] == "Build" # Would be missing if not built assert graph["graph"]["nodes"]["2"]["binary"] == "Cache" assert graph["graph"]["nodes"]["3"]["binary"] == "Cache" client.run(f"graph info . --build=& -f=json", redirect_stdout="graph.json") graph = json.loads(client.load("graph.json")) assert graph["graph"]["nodes"]["0"]["binary"] is None assert graph["graph"]["nodes"]["1"]["binary"] == "Cache" assert graph["graph"]["nodes"]["2"]["binary"] == "Cache" ================================================ FILE: test/integration/command/install/test_install_transitive.py ================================================ import os import pytest from conan.internal.model.info import load_binary_info from conan.api.model import RecipeReference from conan.internal.paths import CONANFILE_TXT, CONANINFO from conan.test.utils.tools import TestClient, GenConanfile from conan.internal.util.files import load @pytest.fixture() def client(): c = TestClient() c.save_home({"settings.yaml": "os: [Windows, Macos, Linux, FreeBSD]\nos_build: [Windows, Macos]\narch_build: [x86_64]", "profiles/default": "[settings]\nos=Windows"}) def base_conanfile(name): return GenConanfile(name, "0.1").with_option("language", [0, 1])\ .with_default_option("language", 0).with_settings("os") c.save({"conanfile.py": base_conanfile("hello0")}) c.run("export . --user=lasote --channel=stable") c.save({"conanfile.py": base_conanfile("hello1").with_requires("hello0/0.1@lasote/stable")}) c.run("export . --user=lasote --channel=stable") c.save({"conanfile.py": base_conanfile("hello2").with_requires("hello1/0.1@lasote/stable")}) c.run("export . --user=lasote --channel=stable") return c def test_install_combined(client): client.run("install . --build=missing") client.run("install . --build=missing --build hello1/*") assert "hello0/0.1@lasote/stable: Already installed!" in client.out assert "hello1/0.1@lasote/stable: Forced build from source" in client.out def test_install_transitive_cache(client): client.run("install --requires=hello2/0.1@lasote/stable --build=missing") assert "hello0/0.1@lasote/stable: Generating the package" in client.out assert "hello1/0.1@lasote/stable: Generating the package" in client.out assert "hello2/0.1@lasote/stable: Generating the package" in client.out def test_upper_option(client): client.run("install conanfile.py -o hello2*:language=1 -o hello1*:language=0 " "-o hello0*:language=1 --build missing") package_id = client.created_package_id("hello0/0.1@lasote/stable") package_id2 = client.created_package_id("hello1/0.1@lasote/stable") ref = RecipeReference.loads("hello0/0.1@lasote/stable") pref = client.get_latest_package_reference(ref, package_id) hello0 = client.get_latest_pkg_layout(pref).package() hello0_info = os.path.join(hello0, CONANINFO) hello0_conan_info = load_binary_info(load(hello0_info)) assert "1" == hello0_conan_info["options"]["language"] pref1 = client.get_latest_package_reference(RecipeReference.loads("hello1/0.1@lasote/stable"), package_id2) hello1 = client.get_latest_pkg_layout(pref1).package() hello1_info = os.path.join(hello1, CONANINFO) hello1_conan_info = load_binary_info(load(hello1_info)) assert "0" == hello1_conan_info["options"]["language"] def test_inverse_upper_option(client): client.run("install . -o language=0 -o hello1*:language=1 -o hello0*:language=0 --build missing") package_id = client.created_package_id("hello0/0.1@lasote/stable") package_id2 = client.created_package_id("hello1/0.1@lasote/stable") ref = RecipeReference.loads("hello0/0.1@lasote/stable") pref = client.get_latest_package_reference(ref, package_id) hello0 = client.get_latest_pkg_layout(pref).package() hello0_info = os.path.join(hello0, CONANINFO) hello0_conan_info = load_binary_info(load(hello0_info)) assert "0" == hello0_conan_info["options"]["language"] pref1 = client.get_latest_package_reference(RecipeReference.loads("hello1/0.1@lasote/stable"), package_id2) hello1 = client.get_latest_pkg_layout(pref1).package() hello1_info = os.path.join(hello1, CONANINFO) hello1_conan_info = load_binary_info(load(hello1_info)) assert "1" == hello1_conan_info["options"]["language"] def test_upper_option_txt(client): files = {CONANFILE_TXT: """[requires] hello1/0.1@lasote/stable [options] hello0*:language=1 hello1*:language=0 """} client.save(files, clean_first=True) client.run("install . --build missing") package_id = client.created_package_id("hello0/0.1@lasote/stable") package_id2 = client.created_package_id("hello1/0.1@lasote/stable") ref = RecipeReference.loads("hello0/0.1@lasote/stable") pref = client.get_latest_package_reference(ref, package_id) hello0 = client.get_latest_pkg_layout(pref).package() hello0_info = os.path.join(hello0, CONANINFO) hello0_conan_info = load_binary_info(load(hello0_info)) assert "1" == hello0_conan_info["options"]["language"] pref1 = client.get_latest_package_reference(RecipeReference.loads("hello1/0.1@lasote/stable"), package_id2) hello1 = client.get_latest_pkg_layout(pref1).package() hello1_info = os.path.join(hello1, CONANINFO) hello1_conan_info = load_binary_info(load(hello1_info)) assert "0" == hello1_conan_info["options"]["language"] ================================================ FILE: test/integration/command/list/__init__.py ================================================ ================================================ FILE: test/integration/command/list/list_test.py ================================================ import json import os import re import textwrap import time from collections import OrderedDict from unittest.mock import patch, Mock import pytest from conan.errors import ConanException from conan.internal.errors import ConanConnectionError from conan.internal.util.files import save from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.env import environment_update from conan.test.utils.tools import TestClient, TestServer, NO_SETTINGS_PACKAGE_ID class TestParamErrors: def test_default_pattern(self): c = TestClient() c.run("list") assert "Found 0 pkg/version" in c.out c.run("list -c") assert "Found 0 pkg/version" in c.out c.run('list -r="*"', assert_error=True) assert "ERROR: Remotes for pattern '*' can't be found" in c.out c.run("list --remote remote1 --cache", assert_error=True) assert "ERROR: Remote 'remote1' can't be found or is disabled" in c.out def test_query_param(self): c = TestClient() c.run("list * --graph=myjson", assert_error=True) assert "ERROR: Cannot define both the pattern and the graph json file" in c.out c.run("list * --graph-binaries=x", assert_error=True) assert "ERROR: --graph-recipes and --graph-binaries require a --graph input" in c.out c.run("list * --graph-recipes=x", assert_error=True) assert "ERROR: --graph-recipes and --graph-binaries require a --graph input" in c.out c.run("list * -p os=Linux", assert_error=True) assert "--package-query and --filter-xxx can only be done for binaries" in c.out def test_wrong_package_query(self): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run('list *:* -p "not os=Linux"') assert "ERROR: Invalid package query: not os=Linux. 'not' operator is not allowed" in c.out def test_graph_file_error(self): # This can happen when reusing the same file in input and output c = TestClient(light=True) c.run("list --graph=graph.json", assert_error=True) assert "ERROR: Graph file not found" in c.out c.save({"graph.json": ""}) c.run("list --graph=graph.json", assert_error=True) assert "ERROR: Graph file invalid JSON:" in c.out text = b'\x2b\x2f\x76\x38J\xe2nis\xa7' with open(os.path.join(c.current_folder, "graph.json"), 'wb') as handle: handle.write(text) c.run("list --graph=graph.json", assert_error=True) assert "ERROR: Graph file broken" in c.out # This can happen when using a pkg list file instead of a graph file c.save({"conanfile.py": GenConanfile("lib")}) c.run("create . --version=1.0 --format=json", redirect_stdout="graph.json") c.run("list --graph graph.json --format=json", redirect_stdout="pkglist.json") c.run("list --graph pkglist.json", assert_error=True) assert ( 'Expected a graph file but found an unexpected JSON file format. ' 'You can create a "graph" JSON file by running' in c.out ) assert ( "conan [ graph-info | create | export-pkg | install | test ] --format=json > graph.json" in c.out ) @pytest.fixture(scope="module") def client(): servers = OrderedDict([("default", TestServer()), ("other", TestServer())]) c = TestClient(servers=servers, inputs=2*["admin", "password"]) c.save({ "zlib.py": GenConanfile("zlib"), "zlib_ng.py": GenConanfile("zlib_ng", "1.0.0"), "zli.py": GenConanfile("zli", "1.0.0"), "zli_rev2.py": GenConanfile("zli", "1.0.0").with_settings("os") .with_package_file("f.txt", env_var="MYREV"), "zlix.py": GenConanfile("zlix", "1.0.0"), "test.py": GenConanfile("test", "1.0").with_requires("zlix/1.0.0") .with_python_requires("zlix/1.0.0"), "conf.py": GenConanfile("conf", "1.0") }) c.run("create zli.py") c.run("create zlib.py --version=1.0.0 --user=user --channel=channel") c.run("create zlib.py --version=2.0.0 --user=user --channel=channel") c.run("create zlix.py") c.run("create test.py") c.run('create conf.py -c tools.info.package_id:confs="[\'tools.build:cxxflags\']"' ' -c tools.build:cxxflags="[\'--flag1\']"') c.run("upload * -r=default -c") c.run("upload * -r=other -c") time.sleep(1.0) # We create and upload new revisions later, to avoid timestamp overlaps (low resolution) with environment_update({"MYREV": "0"}): c.run("create zli_rev2.py -s os=Windows") c.run("create zli_rev2.py -s os=Linux") c.run("upload * -r=default -c") with environment_update({"MYREV": "42"}): c.run("create zli_rev2.py -s os=Windows") c.run("upload * -r=default -c") return c def remove_timestamps(item): if isinstance(item, dict): if item.get("timestamp"): item["timestamp"] = "" for v in item.values(): remove_timestamps(v) return item class TestListRefs: @staticmethod def check(client, pattern, remote, expected): r = "-r=default" if remote else "" r_msg = "default" if remote else "Local Cache" client.run(f"list {pattern} {r}") expected = textwrap.indent(expected, " ") expected_output = f"{r_msg}\n" + expected expected_output = re.sub(r"\(.*\)", "", expected_output) output = re.sub(r"\(.*\)", "", str(client.out)) assert expected_output in output @staticmethod def check_json(client, pattern, remote, expected): r = "-r=default" if remote else "" r_msg = "default" if remote else "Local Cache" client.run(f"list {pattern} {r} --format=json", redirect_stdout="file.json") list_json = client.load("file.json") list_json = json.loads(list_json) assert remove_timestamps(list_json[r_msg]) == remove_timestamps(expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_recipes(self, client, remote): pattern = "z*" expected = textwrap.dedent(f"""\ zli zli/1.0.0 zlib zlib/1.0.0@user/channel zlib/2.0.0@user/channel zlix zlix/1.0.0 """) self.check(client, pattern, remote, expected) expected_json = { "zli/1.0.0": {}, "zlib/1.0.0@user/channel": {}, "zlib/2.0.0@user/channel": {}, "zlix/1.0.0": {} } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) def test_list_recipes_only_user_channel(self, client, remote): pattern = "*@user/channel" expected = textwrap.dedent(f"""\ zlib zlib/1.0.0@user/channel zlib/2.0.0@user/channel """) self.check(client, pattern, remote, expected) expected_json = { "zlib/1.0.0@user/channel": {}, "zlib/2.0.0@user/channel": {}, } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) def test_list_recipes_without_user_channel(self, client, remote): pattern = "z*@" expected = textwrap.dedent(f"""\ zli zli/1.0.0 zlix zlix/1.0.0 """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) @pytest.mark.parametrize("pattern", ["zlib", "zlib/*", "*@user/channel"]) def test_list_recipe_versions(self, client, pattern, remote): expected = textwrap.dedent(f"""\ zlib zlib/1.0.0@user/channel zlib/2.0.0@user/channel """) self.check(client, pattern, remote, expected) expected_json = { "zlib/1.0.0@user/channel": {}, "zlib/2.0.0@user/channel": {} } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) @pytest.mark.parametrize("pattern, solution", [("zlib/[*]", ("1.0.0", "2.0.0")), ("zlib*/[*]", ("1.0.0", "2.0.0")), ("zlib/[<2]", ("1.0.0",)), ("zlib/[>1]", ("2.0.0",))]) def test_list_recipe_version_ranges(self, client, pattern, solution, remote): expected_json = {f"zlib/{v}@user/channel": {} for v in solution} self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) def test_list_recipe_version_ranges_patterns(self, client, remote): pattern = "*/[>1]" expected_json = {'zlib/2.0.0@user/channel': {}} self.check_json(client, pattern, remote, expected_json) pattern = "z*/[<2]" expected_json = {'zli/1.0.0': {}, 'zlib/1.0.0@user/channel': {}, 'zlix/1.0.0': {}} self.check_json(client, pattern, remote, expected_json) def test_version_range_prerelease(self): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("foo", "1.0-pre.1")}) tc.run("export .") tc.run("list * ",) assert "foo/1.0-pre.1" in tc.out tc.run("list foo/[>=1.0]") assert "foo/1.0-pre.1" not in tc.out tc.run("list foo/[>=1.0] -cc core.version_ranges:resolve_prereleases=True") assert "foo/1.0-pre.1" in tc.out @pytest.mark.parametrize("remote", [True, False]) def test_list_recipe_versions_exact(self, client, remote): pattern = "zli/1.0.0" # by default, when a reference is complete, we show latest recipe revision expected = textwrap.dedent(f"""\ zli zli/1.0.0 """) self.check(client, pattern, remote, expected) expected_json = { "zli/1.0.0": {} } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) @pytest.mark.parametrize("pattern", ["nomatch", "nomatch*", "nomatch/*"]) def test_list_recipe_no_match(self, client, pattern, remote): if pattern == "nomatch": # EXACT IS AN ERROR expected = "ERROR: Recipe 'nomatch' not found\n" else: expected = "WARN: There are no matching recipe references\n" self.check(client, pattern, remote, expected) if pattern == "nomatch": expected_json = {"error": "Recipe 'nomatch' not found"} else: expected_json = {} self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) @pytest.mark.parametrize("pattern", ["zli/1.0.0#latest", "zli/1.0.0#b58eeddfe2fd25ac3a105f72836b3360"]) def test_list_recipe_latest_revision(self, client, remote, pattern): # by default, when a reference is complete, we show latest recipe revision expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions b58eeddfe2fd25ac3a105f72836b3360 (10-11-2023 10:13:13) """) self.check(client, pattern, remote, expected) expected_json = { "zli/1.0.0": { "revisions": { "b58eeddfe2fd25ac3a105f72836b3360": { "timestamp": "2023-01-10 00:25:32 UTC" } } } } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) def test_list_recipe_all_latest_revision(self, client, remote): # we can show the latest revision from several matches, if we add ``#latest`` pattern = "zlib/*#latest" expected = textwrap.dedent(f"""\ zlib zlib/1.0.0@user/channel revisions ffd4bc45820ddb320ab224685b9ba3fb (10-11-2023 10:13:13) zlib/2.0.0@user/channel revisions ffd4bc45820ddb320ab224685b9ba3fb (10-11-2023 10:13:13) """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_recipe_several_revision(self, client, remote): # we can show the latest revision from several matches, if we add ``#latest`` pattern = "zli/1.0.0#*" expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions f034dc90894493961d92dd32a9ee3b78 (10-11-2023 10:13:13) b58eeddfe2fd25ac3a105f72836b3360 (10-11-2023 10:13:13) """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_recipe_multiple_revision(self, client, remote): pattern = "zli*#*" expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions f034dc90894493961d92dd32a9ee3b78 (10-11-2023 10:13:13) b58eeddfe2fd25ac3a105f72836b3360 (10-11-2023 10:13:13) zlib zlib/1.0.0@user/channel revisions ffd4bc45820ddb320ab224685b9ba3fb (10-11-2023 10:13:13) zlib/2.0.0@user/channel revisions ffd4bc45820ddb320ab224685b9ba3fb (10-11-2023 10:13:13) zlix zlix/1.0.0 revisions 81f598d1d8648389bb7d0494fffb654e (10-11-2023 10:13:13) """) self.check(client, pattern, remote, expected) class TestListPrefs: @staticmethod def check(client, pattern, remote, expected): r = "-r=default" if remote else "" r_msg = "default" if remote else "Local Cache" client.run(f"list {pattern} {r}") expected = textwrap.indent(expected, " ") expected_output = f"{r_msg}\n" + expected expected_output = re.sub(r"\(.*\)", "", expected_output) output = re.sub(r"\(.*\)", "", str(client.out)) assert expected_output in output @staticmethod def check_json(client, pattern, remote, expected): r = "-r=default" if remote else "" r_msg = "default" if remote else "Local Cache" client.run(f"list {pattern} {r} --format=json", redirect_stdout="file.json") list_json = client.load("file.json") list_json = json.loads(list_json) assert remove_timestamps(list_json[r_msg]) == remove_timestamps(expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_pkg_ids(self, client, remote): pattern = "zli/1.0.0:*" expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions b58eeddfe2fd25ac3a105f72836b3360 (10-11-2023 10:13:13) packages 9a4eb3c8701508aa9458b1a73d0633783ecc2270 info settings os: Linux ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715 info settings os: Windows """) self.check(client, pattern, remote, expected) expected_json = { "zli/1.0.0": { "revisions": { "b58eeddfe2fd25ac3a105f72836b3360": { "timestamp": "2023-01-10 16:30:27 UTC", "packages": { "9a4eb3c8701508aa9458b1a73d0633783ecc2270": { "info": { "settings": { "os": "Linux" } } }, "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715": { "info": { "settings": { "os": "Windows" } } } } } } } } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) def test_list_pkg_ids_confs(self, client, remote): pattern = "conf/*:*" expected = textwrap.dedent("""\ conf conf/1.0 revisions e4e1703f72ed07c15d73a555ec3a2fa1 (10-11-2023 10:13:13) packages 78c6fa29e8164ce399087ad6067c8f9e2f1c4ad0 info conf tools.build:cxxflags: ['--flag1'] """) self.check(client, pattern, remote, expected) expected_json = { "conf/1.0": { "revisions": { "e4e1703f72ed07c15d73a555ec3a2fa1": { "timestamp": "2023-01-10 10:07:33 UTC", "packages": { "78c6fa29e8164ce399087ad6067c8f9e2f1c4ad0": { "info": { "conf": { "tools.build:cxxflags": "['--flag1']" } } } } } } } } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) def test_list_pkg_ids_requires(self, client, remote): pattern = "test/*:*" expected = textwrap.dedent("""\ test test/1.0 revisions 7df6048d3cb39b75618717987fb96453 (10-11-2023 10:13:13) packages 81d0d9a6851a0208c2bb35fdb34eb156359d939b info requires zlix/1.Y.Z python_requires zlix/1.0.Z """) self.check(client, pattern, remote, expected) expected_json = { "test/1.0": { "revisions": { "7df6048d3cb39b75618717987fb96453": { "timestamp": "2023-01-10 22:17:13 UTC", "packages": { "81d0d9a6851a0208c2bb35fdb34eb156359d939b": { "info": { "requires": [ "zlix/1.Y.Z" ], "python_requires": [ "zlix/1.0.Z" ] } } } } } } } self.check_json(client, pattern, remote, expected_json) @pytest.mark.parametrize("remote", [True, False]) def test_list_pkg_ids_all_rrevs(self, client, remote): pattern = "zli/1.0.0#*:*" expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions f034dc90894493961d92dd32a9ee3b78 (2023-01-10 22:19:58 UTC) packages da39a3ee5e6b4b0d3255bfef95601890afd80709 info b58eeddfe2fd25ac3a105f72836b3360 (2023-01-10 22:19:59 UTC) packages 9a4eb3c8701508aa9458b1a73d0633783ecc2270 info settings os: Linux ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715 info settings os: Windows """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) @pytest.mark.parametrize("version", ["1.0.0", "[>=1.0.0 <2]"]) def test_list_latest_prevs(self, client, remote, version): pattern = f'"zli/{version}:*#latest"' expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions b58eeddfe2fd25ac3a105f72836b3360 (2023-01-10 22:27:34 UTC) packages 9a4eb3c8701508aa9458b1a73d0633783ecc2270 revisions 9beff32b8c94ea0ce5a5e67dad95f525 (10-11-2023 10:13:13) info settings os: Linux ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715 revisions d9b1e9044ee265092e81db7028ae10e0 (10-11-2023 10:13:13) info settings os: Windows """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_all_prevs(self, client, remote): pattern = "zli/1.0.0:*#*" # TODO: This is doing a package_id search, but not showing info expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions b58eeddfe2fd25ac3a105f72836b3360 (2023-01-10 22:41:09 UTC) packages 9a4eb3c8701508aa9458b1a73d0633783ecc2270 revisions 9beff32b8c94ea0ce5a5e67dad95f525 (2023-01-10 22:41:09 UTC) info settings os: Linux ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715 revisions 24532a030b4fcdfed699511f6bfe35d3 (2023-01-10 22:41:09 UTC) d9b1e9044ee265092e81db7028ae10e0 (2023-01-10 22:41:10 UTC) info settings os: Windows """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_package_id_all_prevs(self, client, remote): pattern = "zli/1.0.0:ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715#*" # TODO: We might want to improve the output, grouping PREVS for the # same package_id expected_json = { "zli/1.0.0": { "revisions": { "b58eeddfe2fd25ac3a105f72836b3360": { "timestamp": "2023-01-10 22:45:49 UTC", "packages": { "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715": { "revisions": { "d9b1e9044ee265092e81db7028ae10e0": { "timestamp": "2023-01-10 22:45:49 UTC" }, "24532a030b4fcdfed699511f6bfe35d3": { "timestamp": "2023-01-10 22:45:49 UTC" } } } } } } } } self.check_json(client, pattern, remote, expected_json) expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions b58eeddfe2fd25ac3a105f72836b3360 (2023-01-10 22:41:09 UTC) packages ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715 revisions 24532a030b4fcdfed699511f6bfe35d3 (2023-01-10 22:41:09 UTC) d9b1e9044ee265092e81db7028ae10e0 (2023-01-10 22:41:10 UTC) """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_package_id_single(self, client, remote): pattern = "zli/1.0.0:ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions b58eeddfe2fd25ac3a105f72836b3360 (2023-01-10 23:13:12 UTC) packages ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715 info settings os: Windows """) self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) def test_list_missing_package_id(self, client, remote): pattern = "zli/1.0.0:nonexists_id" expected = "ERROR: Package ID 'zli/1.0.0:nonexists_id' not found\n" self.check(client, pattern, remote, expected) @pytest.mark.parametrize("remote", [True, False]) def test_query(self, client, remote): pattern = "zli/1.0.0:* -p os=Linux" expected = textwrap.dedent(f"""\ zli zli/1.0.0 revisions b58eeddfe2fd25ac3a105f72836b3360 (10-11-2023 10:13:13) packages 9a4eb3c8701508aa9458b1a73d0633783ecc2270 info settings os: Linux """) self.check(client, pattern, remote, expected) def test_list_prefs_query_custom_settings(): """ Make sure query works for custom settings # https://github.com/conan-io/conan/issues/13071 """ c = TestClient(default_server_user=True) settings = textwrap.dedent("""\ newsetting: value1: value2: subsetting: [1, 2, 3] """) save(c.paths.settings_path_user, settings) c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_settings("newsetting")}) c.run("create . -s newsetting=value1") c.run("create . -s newsetting=value2 -s newsetting.subsetting=1") c.run("create . -s newsetting=value2 -s newsetting.subsetting=2") c.run("upload * -r=default -c") c.run("list pkg/1.0:* -p newsetting=value1") assert "newsetting: value1" in c.out assert "newsetting: value2" not in c.out c.run("list pkg/1.0:* -p newsetting=value1 -r=default") assert "newsetting: value1" in c.out assert "newsetting: value2" not in c.out c.run('list pkg/1.0:* -p "newsetting=value2 AND newsetting.subsetting=1"') assert "newsetting: value2" in c.out assert "newsetting.subsetting: 1" in c.out assert "newsetting.subsetting: 2" not in c.out c.run('list pkg/1.0:* -p "newsetting=value2 AND newsetting.subsetting=1" -r=default') assert "newsetting: value2" in c.out assert "newsetting.subsetting: 1" in c.out assert "newsetting.subsetting: 2" not in c.out def test_list_query_options(): """ Make sure query works for custom settings https://github.com/conan-io/conan/issues/13617 """ c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_option("myoption", [1, 2, 3])}) c.run("create . -o myoption=1") c.run("create . -o myoption=2") c.run("create . -o myoption=3") c.run("list pkg/1.0:* -p options.myoption=1") assert "myoption: 1" in c.out assert "myoption: 2" not in c.out assert "myoption: 3" not in c.out c.run("list pkg/1.0:* -p options.myoption=2") assert "myoption: 1" not in c.out assert "myoption: 2" in c.out assert "myoption: 3" not in c.out c.run("upload * -r=default -c") c.run("list pkg/1.0:* -p options.myoption=1 -r=default") assert "myoption: 1" in c.out assert "myoption: 2" not in c.out assert "myoption: 3" not in c.out c.run("list pkg/1.0:* -p options.myoption=2 -r=default") assert "myoption: 1" not in c.out assert "myoption: 2" in c.out assert "myoption: 3" not in c.out def test_list_empty_settings(): """ If settings are empty, do not crash """ c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "1.0")}) c.run("create .") c.run("list pkg/1.0:* -p os=Windows -f=json") revisions = json.loads(c.stdout)["Local Cache"]["pkg/1.0"]["revisions"] pkgs = revisions["a69a86bbd19ae2ef7eedc64ae645c531"]["packages"] assert pkgs == {} c.run("list pkg/1.0:* -p os=None -f=json") revisions = json.loads(c.stdout)["Local Cache"]["pkg/1.0"]["revisions"] pkgs = revisions["a69a86bbd19ae2ef7eedc64ae645c531"]["packages"] assert pkgs == {NO_SETTINGS_PACKAGE_ID: {"info": {}}} class TestListNoUserChannel: def test_no_user_channel(self): c = TestClient(default_server_user=True) c.save({"zlib.py": GenConanfile("zlib"), }) c.run("create zlib.py --version=1.0.0") c.run("create zlib.py --version=1.0.0 --user=user --channel=channel") c.run("upload * -r=default -c") c.run("list zlib/1.0.0#latest") assert "user/channel" not in c.out c.run("list zlib/1.0.0#latest -r=default") assert "user/channel" not in c.out c.run("list zlib/1.0.0:*") assert "user/channel" not in c.out c.run("list zlib/1.0.0:* -r=default") assert "user/channel" not in c.out class TestListRemotes: """ advanced use case: - check multiple remotes output """ def test_search_no_matching_recipes(self, client): expected_output = textwrap.dedent("""\ Connecting to remote 'default' with user 'admin' Connecting to remote 'other' with user 'admin' Local Cache ERROR: Recipe 'whatever/0.1' not found default ERROR: Recipe 'whatever/0.1' not found other ERROR: Recipe 'whatever/0.1' not found """) client.run('list -c -r="*" whatever/0.1') assert expected_output == client.out def test_fail_if_no_configured_remotes(self): client = TestClient() client.run('list -r="*" whatever/1.0#123', assert_error=True) assert "ERROR: Remotes for pattern '*' can't be found or are disabled" in client.out @pytest.mark.parametrize("exc,output", [ (ConanConnectionError("Review your network!"), "ERROR: Review your network!"), (ConanException("Boom!"), "ERROR: Boom!") ]) def test_search_remote_errors_but_no_raising_exceptions(self, client, exc, output): with patch("conan.api.subapi.list._search_recipes", new=Mock(side_effect=exc)): client.run(f'list whatever/1.0 -r="*"') expected_output = textwrap.dedent(f"""\ default {output} other {output} """) assert expected_output == client.out class TestListHTML: def test_list_html(self): c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "1.2.3"), "pkg/conanfile.py": GenConanfile("pkg", "2.3.4").with_requires("dep/1.2.3") .with_settings("os", "arch").with_shared_option(False)}) c.run("create dep") c.run("create pkg -s os=Windows -s arch=x86") # Revision is needed explicitly! c.run("list pkg/2.3.4#latest --format=html") assert "" in c.stdout # TODO: The actual good html is missing def test_list_html_custom(self): """ test that tools.info.package_id:confs works, affecting the package_id and can be listed when we are listing packages """ c = TestClient() c.save({'lib.py': GenConanfile("lib", "0.1")}) c.run("create lib.py") template_folder = os.path.join(c.cache_folder, 'templates') c.save({"list_packages.html": '{{ base_template_path }}'}, path=template_folder) c.run("list lib/0.1#latest --format=html") assert template_folder in c.stdout class TestListCompact: def test_list_compact(self): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_settings("os", "arch") .with_shared_option(False)}) c.run("create . -s os=Windows -s arch=x86") c.run("create . -s os=Linux -s arch=armv8") c.run("create . -s os=Macos -s arch=armv8 -o shared=True") c.run("list pkg:* --format=compact") expected = textwrap.dedent("""\ pkg/1.0#03591c8b22497dd74214e08b3bf2a56f:2a67a51fbf36a4ee345b2125dd2642be60ffd3ec settings: Macos, armv8 options: shared=True pkg/1.0#03591c8b22497dd74214e08b3bf2a56f:2d46abc802bbffdf2af11591e3e452bc6149ea2b settings: Linux, armv8 options: shared=False pkg/1.0#03591c8b22497dd74214e08b3bf2a56f:d2e97769569ac0a583d72c10a37d5ca26de7c9fa settings: Windows, x86 options: shared=False """) assert textwrap.indent(expected, " ") in c.stdout def test_list_compact_no_settings_no_options(self): c = TestClient() c.save({"pkg/conanfile.py": GenConanfile("pkg", "1.0").with_settings("os", "arch"), "other/conanfile.py": GenConanfile("other", "1.0")}) c.run("create pkg -s os=Windows -s arch=x86") c.run("create other") c.run("list *:* --format=compact") expected_output = re.sub(r"%.* ", "%timestamp ", re.sub(r"\(.*\)", "(timestamp)", c.stdout)) expected = textwrap.dedent("""\ Local Cache other/1.0 other/1.0#d3c8cc5e6d23ca8c6f0eaa6285c04cbd%timestamp (timestamp) other/1.0#d3c8cc5e6d23ca8c6f0eaa6285c04cbd:da39a3ee5e6b4b0d3255bfef95601890afd80709 pkg/1.0 pkg/1.0#d24b74828b7681f08d8f5ba0e7fd791e%timestamp (timestamp) pkg/1.0#d24b74828b7681f08d8f5ba0e7fd791e:c11e463c49652ba9c5adc62573ee49f966bd8417 settings: Windows, x86 """) assert expected == expected_output @pytest.mark.parametrize("pattern", [ "pkg/*", "pkg/1.0", "pkg/1.0#*", "pkg/1.0#*:*", "pkg/1.0#*:*#*", "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:*", "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:*", "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:*#*", "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:da39a3ee5e6b4b0d3255bfef95601890afd80709#*", "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:da39a3ee5e6b4b0d3255bfef95601890afd80709#" "0ba8627bd47edc3a501e8f0eb9a79e5e" ]) def test_list_compact_patterns(self, pattern): c = TestClient(light=True) c.save({"pkg/conanfile.py": GenConanfile("pkg", "1.0")}) c.run("create pkg") c.run(f"list {pattern} --format=compact") class TestListBinaryFilter: @pytest.mark.parametrize("remote", [True, False]) def test_list_filter(self, remote): r = "-r=default" if remote else "" c = TestClient(default_server_user=remote) c.save({"pkg/conanfile.py": GenConanfile("pkg", "1.0").with_settings("os", "arch") .with_shared_option(False), "header/conanfile.py": GenConanfile("header", "1.0"), "profile_linux": "[settings]\nos=Linux", "profile_armv8": "[settings]\narch=armv8", "profile_shared": "[options]\n*:shared=True"}) c.run("create pkg -s os=Windows -s arch=x86") c.run("create pkg -s os=Linux -s arch=armv8") c.run("create pkg -s os=Macos -s arch=armv8 -o shared=True") c.run("create header") if remote: c.run("upload *:* -r=default -c") pkg_key = "default" if remote else "Local Cache" c.run(f"list *:* -fp=profile_linux --format=json {r}") result = json.loads(c.stdout) header = result[pkg_key]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} pkg = result[pkg_key]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] assert len(pkg["packages"]) == 1 settings = pkg["packages"]["2d46abc802bbffdf2af11591e3e452bc6149ea2b"]["info"]["settings"] assert settings == {"arch": "armv8", "os": "Linux"} # for linux + x86 only the header-only is a match c.run(f"list *:* -fp=profile_linux -fs=arch=x86 --format=json {r}") result = json.loads(c.stdout) header = result[pkg_key]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} pkg = result[pkg_key]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] assert pkg["packages"] == {} c.run(f"list *:* -fp=profile_armv8 --format=json {r}") result = json.loads(c.stdout) header = result[pkg_key]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} pkg = result[pkg_key]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] assert len(pkg["packages"]) == 2 settings = pkg["packages"]["2d46abc802bbffdf2af11591e3e452bc6149ea2b"]["info"]["settings"] assert settings == {"arch": "armv8", "os": "Linux"} settings = pkg["packages"]["2a67a51fbf36a4ee345b2125dd2642be60ffd3ec"]["info"]["settings"] assert settings == {"arch": "armv8", "os": "Macos"} c.run(f"list *:* -fp=profile_shared --format=json {r}") result = json.loads(c.stdout) header = result[pkg_key]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} pkg = result[pkg_key]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] assert len(pkg["packages"]) == 1 settings = pkg["packages"]["2a67a51fbf36a4ee345b2125dd2642be60ffd3ec"]["info"]["settings"] assert settings == {"arch": "armv8", "os": "Macos"} c.run(f"list *:* -fs os=Windows -fo *:shared=False --format=json {r}") result = json.loads(c.stdout) header = result[pkg_key]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} pkg = result[pkg_key]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] assert len(pkg["packages"]) == 1 settings = pkg["packages"]["d2e97769569ac0a583d72c10a37d5ca26de7c9fa"]["info"]["settings"] assert settings == {"arch": "x86", "os": "Windows"} # &: will also match every package being listed, as if it was a consumer c.run(f"list *:* -fo &:shared=False --format=json {r}") result = json.loads(c.stdout) header = result[pkg_key]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} pkg = result[pkg_key]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] assert len(pkg["packages"]) == 2 settings = pkg["packages"]["d2e97769569ac0a583d72c10a37d5ca26de7c9fa"]["info"]["settings"] assert settings == {"arch": "x86", "os": "Windows"} def test_overlapping_versions(): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("foo")}) tc.run("export . --version=1.0") tc.run("export . --version=1.0.0") tc.run("list * -c -f=json", redirect_stdout="list.json") results = json.loads(tc.load("list.json")) assert len(results["Local Cache"]) == 2 def test_list_local_recipe_index(): c = TestClient(light=True) c.run(f"new local_recipes_index -d name=pkg -d version=0.1 -d url='https://conan-fake-url.com' ") c.run(f"remote add local '{c.current_folder}'") c.run("list '*' -r=local") assert "Found 1 pkg/version recipes matching * in local" in c.out c.run("list '*/*' -r=local") assert "Found 1 pkg/version recipes matching */* in local" in c.out c.run("list '*/0.1' -r=local") assert "Found 1 pkg/version recipes matching */0.1 in local" in c.out c.run("list 'pkg/*' -r=local") assert "Found 1 pkg/version recipes matching pkg/* in local" in c.out c.run("list 'pkg/0.*' -r=local") assert "Found 1 pkg/version recipes matching pkg/0.* in local" in c.out c.run("list 'pkg' -r=local") assert "Found 1 pkg/version recipes matching pkg in local" in c.out c.run("list 'pkg/0.1' -r=local") assert "Found 1 pkg/version recipes matching pkg/0.1 in local" in c.out c.run("list 'foo' -r=local") assert "ERROR: Recipe 'foo' not found" in c.out c.run("list '/#@&%%&@#/' -r=local") assert "ERROR: Recipe '/' not found" in c.out c.run("list 'pkg/0.1@a' -r=local") assert "ERROR: Recipe 'pkg/0.1@a' not found" in c.out c.run("list 'pkg%0.1#a@b/c' -r=local") assert "ERROR: Recipe 'pkg%0.1' not found" in c.out ================================================ FILE: test/integration/command/list/search_test.py ================================================ import textwrap from collections import OrderedDict from unittest.mock import patch, Mock import pytest from conan.internal.errors import ConanConnectionError from conan.errors import ConanException from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer class TestSearch: @pytest.fixture def remotes(self): self.servers = OrderedDict() self.servers["remote1"] = TestServer(server_capabilities=[]) self.servers["remote2"] = TestServer(server_capabilities=[]) self.client = TestClient(servers=self.servers) def test_search_no_params(self): self.servers = OrderedDict() self.client = TestClient(servers=self.servers) self.client.run("search", assert_error=True) assert "error: the following arguments are required: reference" in self.client.out def test_search_no_matching_recipes(self, remotes): expected_output = ("Connecting to remote 'remote1' anonymously\n" "Connecting to remote 'remote2' anonymously\n" "remote1\n" " ERROR: Recipe 'whatever' not found\n" "remote2\n" " ERROR: Recipe 'whatever' not found\n") self.client.run("search whatever") assert expected_output == self.client.out def test_search_no_configured_remotes(self): self.servers = OrderedDict() self.client = TestClient(servers=self.servers) self.client.run("search whatever", assert_error=True) assert "There are no remotes to search from" in self.client.out def test_search_disabled_remote(self, remotes): self.client.run("remote disable remote1") self.client.run("search whatever -r remote1", assert_error=True) expected_output = "ERROR: Remote 'remote1' can't be found or is disabled" assert expected_output in self.client.out class TestRemotes: @pytest.fixture(autouse=True) def _setup(self): self.servers = OrderedDict() self.users = {} self.client = TestClient() def _add_remote(self, remote_name): self.servers[remote_name] = TestServer(users={"user": "passwd"}) self.users[remote_name] = [("user", "passwd")] self.client = TestClient(servers=self.servers, inputs=["user", "passwd"]) def _add_recipe(self, remote, reference): conanfile = textwrap.dedent(""" from conan import ConanFile class MyLib(ConanFile): pass """) self.client.save({'conanfile.py': conanfile}) reference = RecipeReference.loads(str(reference)) self.client.run(f"export . --name={reference.name} --version={reference.version} --user={reference.user} --channel={reference.channel}") self.client.run("upload --force -r {} {}".format(remote, reference)) @pytest.mark.parametrize("exc,output", [ (ConanConnectionError("Review your network!"), "ERROR: Review your network!"), (ConanException("Boom!"), "ERROR: Boom!") ]) def test_search_remote_errors_but_no_raising_exceptions(self, exc, output): self._add_remote("remote1") self._add_remote("remote2") with patch("conan.api.subapi.list._search_recipes", new=Mock(side_effect=exc)): self.client.run("search whatever") expected_output = textwrap.dedent(f"""\ remote1 {output} remote2 {output} """) assert expected_output == self.client.out def test_no_remotes(self): self.client.run("search something", assert_error=True) expected_output = "There are no remotes to search from" assert expected_output in self.client.out def test_search_by_name(self): remote_name = "remote1" recipe_name = "test_recipe/1.0.0@user/channel" self._add_remote(remote_name) self._add_recipe(remote_name, recipe_name) self.client.run("search -r {} {}".format(remote_name, "test_recipe")) expected_output = ( "remote1\n" " test_recipe\n" " {}\n".format(recipe_name) ) assert expected_output in self.client.out def test_search_in_all_remotes(self): remote1 = "remote1" remote2 = "remote2" remote1_recipe1 = "test_recipe/1.0.0@user/channel" remote1_recipe2 = "test_recipe/1.1.0@user/channel" remote2_recipe1 = "test_recipe/2.0.0@user/channel" remote2_recipe2 = "test_recipe/2.1.0@user/channel" expected_output = ( "remote1\n" " test_recipe\n" " test_recipe/1.0.0@user/channel\n" " test_recipe/1.1.0@user/channel\n" "remote2\n" " test_recipe\n" " test_recipe/2.0.0@user/channel\n" " test_recipe/2.1.0@user/channel\n" ) self._add_remote(remote1) self._add_recipe(remote1, remote1_recipe1) self._add_recipe(remote1, remote1_recipe2) self._add_remote(remote2) self._add_recipe(remote2, remote2_recipe1) self._add_recipe(remote2, remote2_recipe2) self.client.run("search test_recipe") assert expected_output in self.client.out def test_search_in_one_remote(self): remote1 = "remote1" remote2 = "remote2" remote1_recipe1 = "test_recipe/1.0.0@user/channel" remote1_recipe2 = "test_recipe/1.1.0@user/channel" remote2_recipe1 = "test_recipe/2.0.0@user/channel" remote2_recipe2 = "test_recipe/2.1.0@user/channel" expected_output = ( "remote1\n" " test_recipe\n" " test_recipe/1.0.0@user/channel\n" " test_recipe/1.1.0@user/channel\n" ) self._add_remote(remote1) self._add_recipe(remote1, remote1_recipe1) self._add_recipe(remote1, remote1_recipe2) self._add_remote(remote2) self._add_recipe(remote2, remote2_recipe1) self._add_recipe(remote2, remote2_recipe2) self.client.run("search -r remote1 test_recipe") assert expected_output in self.client.out def test_search_package_found_in_one_remote(self): remote1 = "remote1" remote2 = "remote2" remote1_recipe1 = "test_recipe/1.0.0@user/channel" remote1_recipe2 = "test_recipe/1.1.0@user/channel" remote2_recipe1 = "another_recipe/2.0.0@user/channel" remote2_recipe2 = "another_recipe/2.1.0@user/channel" expected_output = ( "remote1\n" " test_recipe\n" " test_recipe/1.0.0@user/channel\n" " test_recipe/1.1.0@user/channel\n" "remote2\n" " ERROR: Recipe 'test_recipe' not found\n" ) self._add_remote(remote1) self._add_recipe(remote1, remote1_recipe1) self._add_recipe(remote1, remote1_recipe2) self._add_remote(remote2) self._add_recipe(remote2, remote2_recipe1) self._add_recipe(remote2, remote2_recipe2) self.client.run("search test_recipe") assert expected_output in self.client.out def test_search_in_missing_remote(self): remote1 = "remote1" remote1_recipe1 = "test_recipe/1.0.0@user/channel" remote1_recipe2 = "test_recipe/1.1.0@user/channel" self._add_remote(remote1) self._add_recipe(remote1, remote1_recipe1) self._add_recipe(remote1, remote1_recipe2) self.client.run("search -r wrong_remote test_recipe", assert_error=True) expected_output = "ERROR: Remote 'wrong_remote' can't be found or is disabled" assert expected_output in self.client.out def test_search_wildcard(self): remote1 = "remote1" remote1_recipe1 = "test_recipe/1.0.0@user/channel" remote1_recipe2 = "test_recipe/1.1.0@user/channel" remote1_recipe3 = "test_another/2.1.0@user/channel" remote1_recipe4 = "test_another/4.1.0@user/channel" expected_output = ( "remote1\n" " test_another\n" " test_another/2.1.0@user/channel\n" " test_another/4.1.0@user/channel\n" " test_recipe\n" " test_recipe/1.0.0@user/channel\n" " test_recipe/1.1.0@user/channel\n" ) self._add_remote(remote1) self._add_recipe(remote1, remote1_recipe1) self._add_recipe(remote1, remote1_recipe2) self._add_recipe(remote1, remote1_recipe3) self._add_recipe(remote1, remote1_recipe4) self.client.run("search test_*") assert expected_output in self.client.out def test_no_user_channel_error(): # https://github.com/conan-io/conan/issues/13170 c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg")}) c.run("export . --version=1.0") c.run("export . --version=1.0 --user=user --channel=channel") c.run("list *") assert "pkg/1.0" in [s.strip() for s in str(c.out).splitlines()] assert "pkg/1.0@user/channel" in c.out # I want to list only those without user/channel c.run("list pkg/*@") assert "pkg/1.0" in c.out assert "user/channel" not in c.out # The same underlying logic is used in upload c.run("upload pkg/*@ -r=default -c") assert "Uploading recipe 'pkg/1.0" in c.out assert "user/channel" not in c.out c.run("search pkg/*@ -r=default") assert "pkg/1.0" in c.out assert "user/channel" not in c.out ================================================ FILE: test/integration/command/list/test_combined_pkglist_flows.py ================================================ import json from collections import OrderedDict import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.env import environment_update from conan.test.utils.tools import TestClient, TestServer class TestListUpload: refs = ["zli/1.0.0#f034dc90894493961d92dd32a9ee3b78", "zlib/1.0.0@user/channel#ffd4bc45820ddb320ab224685b9ba3fb"] @pytest.fixture() def client(self): c = TestClient(default_server_user=True, light=True) c.save({ "zlib.py": GenConanfile("zlib"), "zli.py": GenConanfile("zli", "1.0.0") }) c.run("create zli.py") c.run("create zlib.py --version=1.0.0 --user=user --channel=channel") return c def test_list_upload_recipes(self, client): pattern = "z*#latest" client.run(f"list {pattern} --format=json", redirect_stdout="pkglist.json") client.run("upload --list=pkglist.json -r=default") for r in self.refs: assert f"Uploading recipe '{r}'" in client.out assert "Uploading package" not in client.out def test_list_upload_packages(self, client): pattern = "z*#latest:*#latest" client.run(f"list {pattern} --format=json", redirect_stdout="pkglist.json") client.run("upload --list=pkglist.json -r=default") for r in self.refs: assert f"Uploading recipe '{r}'" in client.out assert str(client.out).count("Uploading package") == 2 def test_list_upload_empty_list(self, client): client.run(f"install --requires=zlib/1.0.0@user/channel -f json", redirect_stdout="install_graph.json") # Generate an empty pkglist.json client.run(f"list --format=json --graph=install_graph.json --graph-binaries=bogus", redirect_stdout="pkglist.json") # No binaries should be uploaded since the pkglist is empty, but the command # should not error client.run("upload --list=pkglist.json -r=default") assert "No packages were uploaded because the package list is empty." in client.out class TestGraphCreatedUpload: def test_create_upload(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")}) c.run("create zlib") c.run("create app --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --format=json", redirect_stdout="pkglist.json") c.run("upload --list=pkglist.json -r=default") assert "Uploading recipe 'app/1.0#0fa1ff1b90576bb782600e56df642e19'" in c.out assert "Uploading recipe 'zlib/1.0#c570d63921c5f2070567da4bf64ff261'" in c.out assert "Uploading package 'app" in c.out assert "Uploading package 'zlib" in c.out class TestExportUpload: def test_export_upload(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0")}) c.run("export zlib --format=pkglist", redirect_stdout="pkglist.json") c.run("upload --list=pkglist.json -r=default -c") assert "Uploading recipe 'zlib/1.0#c570d63921c5f2070567da4bf64ff261'" in c.out class TestCreateGraphToPkgList: def test_graph_pkg_nonexistant(self): c = TestClient(light=True) c.run("list --graph=non-existent-file.json", assert_error=True) assert "ERROR: Graph file not found" in c.out def test_graph_pkg_list_only_built(self): c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0") .with_settings("os") .with_shared_option(False)}) c.run("create zlib") c.run("create app --format=json -s os=Linux", redirect_stdout="graph.json") c.run("list --graph=graph.json --graph-binaries=build --format=json") pkglist = json.loads(c.stdout)["Local Cache"] assert len(pkglist) == 1 pkgs = pkglist["app/1.0"]["revisions"]["8263c3c32802e14a2f03a0b1fcce0d95"]["packages"] assert len(pkgs) == 1 pkg_app = pkgs["d8b3bdd894c3eb9bf2a3119ee0f8c70843ace0ac"] assert pkg_app["info"]["requires"] == ["zlib/1.0.Z"] assert pkg_app["info"]["settings"] == {'os': 'Linux'} assert pkg_app["info"]["options"] == {'shared': 'False'} def test_graph_pkg_list_all_recipes_only(self): """ --graph-recipes=* selects all the recipes in the graph """ c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")}) c.run("create zlib") c.run("create app --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --graph-recipes=* --format=json") pkglist = json.loads(c.stdout)["Local Cache"] assert len(pkglist) == 2 assert "packages" not in pkglist["app/1.0"]["revisions"]["0fa1ff1b90576bb782600e56df642e19"] assert "packages" not in pkglist["zlib/1.0"]["revisions"]["c570d63921c5f2070567da4bf64ff261"] def test_graph_pkg_list_python_requires(self): """ include python_requires too """ c = TestClient(default_server_user=True, light=True) c.save({"pytool/conanfile.py": GenConanfile("pytool", "0.1"), "zlib/conanfile.py": GenConanfile("zlib", "1.0").with_python_requires("pytool/0.1"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")}) c.run("create pytool") c.run("create zlib") c.run("upload * -c -r=default") c.run("remove * -c") c.run("create app --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --format=json") pkglist = json.loads(c.stdout)["Local Cache"] assert len(pkglist) == 3 assert "96aec08148a2392127462c800e1c8af6" in pkglist["pytool/0.1"]["revisions"] pkglist = json.loads(c.stdout)["default"] assert len(pkglist) == 2 assert "96aec08148a2392127462c800e1c8af6" in pkglist["pytool/0.1"]["revisions"] def test_graph_pkg_list_create_python_requires(self): """ include python_requires too """ c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("pytool", "0.1").with_package_type("python-require")}) c.run("create . --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --format=json") pkglist = json.loads(c.stdout)["Local Cache"] assert len(pkglist) == 1 assert "62a6a9e5347b789bfc6572948ea19f85" in pkglist["pytool/0.1"]["revisions"] class TestGraphInfoToPkgList: def test_graph_pkg_list_only_built(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")}) c.run("create zlib") c.run("create app --format=json") c.run("upload * -c -r=default") c.run("remove * -c") c.run("graph info --requires=app/1.0 --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --graph-binaries=build --format=json") pkglist = json.loads(c.stdout) assert len(pkglist["Local Cache"]) == 0 assert len(pkglist["default"]) == 2 c.run("install --requires=app/1.0 --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --graph-binaries=download --format=json") pkglist = json.loads(c.stdout) assert len(pkglist["Local Cache"]) == 2 assert len(pkglist["default"]) == 2 c.run("list --graph=graph.json --graph-binaries=build --format=json") pkglist = json.loads(c.stdout) assert len(pkglist["Local Cache"]) == 0 assert len(pkglist["default"]) == 2 class TestPkgListFindRemote: """ we can recover a list of remotes for an already installed graph, for metadata download """ def test_graph_2_pkg_list_remotes(self): servers = OrderedDict([("default", TestServer()), ("remote2", TestServer())]) c = TestClient(servers=servers, inputs=2 * ["admin", "password"], light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")}) c.run("create zlib") c.run("create app ") c.run("upload zlib* -c -r=default") c.run("upload zlib* -c -r=remote2") c.run("upload app* -c -r=remote2") # This install, packages will be in the cache c.run("install --requires=app/1.0 --format=json", redirect_stdout="graph.json") # So list, will not have remote at all c.run("list --graph=graph.json --format=json", redirect_stdout="pkglist.json") pkglist = json.loads(c.load("pkglist.json")) assert len(pkglist["Local Cache"]) == 2 assert "default" not in pkglist # The remote doesn't even exist # Lets now compute a list finding in the remotes c.run("pkglist find-remote pkglist.json --format=json", redirect_stdout="remotepkg.json") pkglist = json.loads(c.stdout) assert "Local Cache" not in pkglist assert len(pkglist["default"]) == 1 assert "zlib/1.0" in pkglist["default"] assert len(pkglist["remote2"]) == 2 assert "app/1.0" in pkglist["remote2"] assert "zlib/1.0" in pkglist["remote2"] c.run("download --list=remotepkg.json -r=default --metadata=*") assert "zlib/1.0: Retrieving recipe metadata from remote 'default'" in c.out assert "zlib/1.0: Retrieving package metadata" in c.out c.run("download --list=remotepkg.json -r=remote2 --metadata=*") assert "app/1.0: Retrieving recipe metadata from remote 'remote2'" in c.out assert "app/1.0: Retrieving package metadata" in c.out def test_input_only_name_version(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0")}) c.run("create zlib") c.run("upload zlib* -c -r=default") # Create input pkglist for find-remote c.run(f"list * --format=json", redirect_stdout="mylist.json") pkglist = json.loads(c.load("mylist.json")) expected = {"zlib/1.0": {}} assert pkglist["Local Cache"] == expected c.run("pkglist find-remote mylist.json --format=json --remote default") pkglist = json.loads(c.stdout) assert pkglist["default"] == expected def test_input_recipe_revisions(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0")}) c.run("create zlib") c.run("upload zlib* -c -r=default") # Create input pkglist for find-remote c.run(f"list *#* --format=json", redirect_stdout="mylist.json") def _check(origin): pkglist = json.loads(c.load("mylist.json")) revs = pkglist[origin]["zlib/1.0"]["revisions"] assert list(revs) == ["c570d63921c5f2070567da4bf64ff261"] assert "packages" not in revs["c570d63921c5f2070567da4bf64ff261"] _check("Local Cache") c.run("pkglist find-remote mylist.json --format=json --remote default", redirect_stdout="mylist.json") _check("default") def test_input_only_package_ids(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0").with_settings("os")}) c.run("create zlib -s os=Linux") c.run("upload zlib* -c -r=default") # Create input pkglist for find-remote c.run(f"list *:* --format=json", redirect_stdout="mylist.json") def _check(origin): pkglist = json.loads(c.load("mylist.json")) revs = pkglist[origin]["zlib/1.0"]["revisions"] assert list(revs) == ["1cb7410d0365f87510a6767c7bef804e"] info = {"info": {'settings': {'os': 'Linux'}}} expected = {"9a4eb3c8701508aa9458b1a73d0633783ecc2270": info} assert revs["1cb7410d0365f87510a6767c7bef804e"]["packages"] == expected c.run("pkglist find-remote mylist.json --format=json --remote default", redirect_stdout="mylist.json") _check("default") def test_graph_pkg_list_of_recipes_and_binaries(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0").with_settings("os")}) c.run("create zlib -s os=Linux") c.run("upload zlib* -c -r=default") # Create input pkglist for find-remote c.run(f"list *#*:*#* --format=json", redirect_stdout="mylist.json") def _check(origin): pkglist = json.loads(c.load("mylist.json")) revs = pkglist[origin]["zlib/1.0"]["revisions"] assert list(revs) == ["1cb7410d0365f87510a6767c7bef804e"] expected = {'settings': {'os': 'Linux'}} pkgs = revs["1cb7410d0365f87510a6767c7bef804e"]["packages"] assert list(pkgs) == ["9a4eb3c8701508aa9458b1a73d0633783ecc2270"] pkg = pkgs["9a4eb3c8701508aa9458b1a73d0633783ecc2270"] assert pkg["info"] == expected assert list(pkg["revisions"]) == ["1d3c57385f4133c1fbd6d13bd538496e"] _check("Local Cache") c.run("pkglist find-remote mylist.json --format=json --remote default", redirect_stdout="mylist.json") _check("default") def test_graph_pkg_list_counter_example(self): c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("zlib", "1.0").with_package_file("file.txt", env_var="MY_VAR")}) with environment_update({"MY_VAR": "1"}): c.run("create .") with environment_update({"MY_VAR": "2"}): c.run("create .") c.run("upload zlib*:*#* -c -r=default") # Create input pkglist for find-remote c.run(f"list zlib/1.0#latest:*#latest --format=json", redirect_stdout="mylist.json") def _check(origin): pkglist = json.loads(c.load("mylist.json")) prevs = pkglist[origin]["zlib/1.0"]["revisions"] input_pkgs = prevs["212b9babae6a4b8a8362703cec4257ad"]["packages"] prevs = input_pkgs["da39a3ee5e6b4b0d3255bfef95601890afd80709"]["revisions"] assert len(prevs) == 1 _check("Local Cache") c.run("pkglist find-remote mylist.json --format=json --remote default", redirect_stdout="mylist.json") _check("default") class TestPkgListMerge: """ deep merge lists """ def test_graph_2_pkg_list_remotes(self): servers = OrderedDict([("default", TestServer()), ("remote2", TestServer())]) c = TestClient(servers=servers, inputs=2 * ["admin", "password"]) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0").with_settings("build_type"), "bzip2/conanfile.py": GenConanfile("bzip2", "1.0").with_settings("build_type"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0", "bzip2/1.0") .with_settings("build_type")}) c.run("create zlib") c.run("create bzip2") c.run("create app ") c.run("list zlib:* --format=json", redirect_stdout="list1.json") c.run("list bzip2:* --format=json", redirect_stdout="list2.json") c.run("list app:* --format=json", redirect_stdout="list3.json") c.run("pkglist merge --list=list1.json --list=list2.json --list=list3.json --format=json", redirect_stdout="release.json") final = json.loads(c.stdout) assert "app/1.0" in final["Local Cache"] assert "zlib/1.0" in final["Local Cache"] assert "bzip2/1.0" in final["Local Cache"] c.run("create zlib -s build_type=Debug") c.run("create bzip2 -s build_type=Debug") c.run("create app -s build_type=Debug") c.run("list *:* -fs build_type=Debug --format=json", redirect_stdout="debug.json") c.run("pkglist merge --list=release.json --list=debug.json --format=json", redirect_stdout="release.json") final = json.loads(c.stdout) rev = final["Local Cache"]["zlib/1.0"]["revisions"]["11f74ff5f006943c6945117511ac8b64"] assert len(rev["packages"]) == 2 # Debug and Release settings = rev["packages"]["efa83b160a55b033c4ea706ddb980cd708e3ba1b"]["info"]["settings"] assert settings == {"build_type": "Release"} settings = rev["packages"]["9e186f6d94c008b544af1569d1a6368d8339efc5"]["info"]["settings"] assert settings == {"build_type": "Debug"} rev = final["Local Cache"]["bzip2/1.0"]["revisions"]["9e0352b3eb99ba4ac79bc7eeae2102c5"] assert len(rev["packages"]) == 2 # Debug and Release settings = rev["packages"]["efa83b160a55b033c4ea706ddb980cd708e3ba1b"]["info"]["settings"] assert settings == {"build_type": "Release"} settings = rev["packages"]["9e186f6d94c008b544af1569d1a6368d8339efc5"]["info"]["settings"] assert settings == {"build_type": "Debug"} def test_pkglist_file_error(self): # This can happen when reusing the same file in input and output c = TestClient(light=True) c.run("pkglist merge -l mylist.json", assert_error=True) assert "ERROR: Package list file missing or broken:" in c.out c.save({"mylist.json": ""}) c.run("pkglist merge -l mylist.json", assert_error=True) assert "ERROR: Package list file invalid JSON:" in c.out # This can happen when using a graph JSON file instead of a package list file c.save({"conanfile.py": GenConanfile("lib")}) c.run("create . --version=1.0 --format=json", redirect_stdout="out/graph.json") c.run("pkglist merge -l out/graph.json", assert_error=True) assert ( 'ERROR: Expected a package list file but found a graph file. ' 'You can create a "package list" JSON file by running:' in c.out ) assert "conan list --graph graph.json --format=json > pkglist.json" in c.out class TestDownloadUpload: @pytest.fixture() def client(self): c = TestClient(default_server_user=True, light=True) c.save({ "zlib.py": GenConanfile("zlib"), "zli.py": GenConanfile("zli", "1.0.0") }) c.run("create zli.py") c.run("create zlib.py --version=1.0.0 --user=user --channel=channel") c.run("upload * -r=default -c") c.run("remove * -c") return c @pytest.mark.parametrize("prev_list", [False, True]) def test_download_upload_all(self, client, prev_list): # We need to be consequeent with the pattern, it is not the same defaults for # download and for list pattern = "zlib/*#latest:*#latest" if prev_list: client.run(f"list {pattern} -r=default --format=json", redirect_stdout="pkglist.json") # Overwriting previous pkglist.json pattern = "--list=pkglist.json" client.run(f"download {pattern} -r=default --format=json", redirect_stdout="pkglist.json") # TODO: Discuss "origin" assert "Local Cache" in client.load("pkglist.json") client.run("remove * -r=default -c") client.run("upload --list=pkglist.json -r=default") assert f"Uploading recipe 'zlib/1.0.0" in client.out assert f"Uploading recipe 'zli/" not in client.out assert "Uploading package 'zlib/1.0.0" in client.out assert "Uploading package 'zli/" not in client.out def test_download_upload_all_no_removed(self): c = TestClient(default_server_user=True, light=True) c.save({"zlib.py": GenConanfile("zlib", "0.1")}) c.run("create zlib.py ") c.run("upload * -r=default -c --format=json", redirect_stdout="pkglist.json") c.run("remove * -c") c.run(f"download --list=pkglist.json -r=default --format=json", redirect_stdout="pkglist.json") # The original "files" and "upload-urls" fields of first upload are removed pkglist_json = c.load("pkglist.json") assert "files" not in pkglist_json assert "upload-urls" not in pkglist_json c.run("upload --list=pkglist.json -r=default --format=json") # It doesn't crash anymore @pytest.mark.parametrize("prev_list", [False, True]) def test_download_upload_only_recipes(self, client, prev_list): if prev_list: pattern = "zlib/*#latest" client.run(f"list {pattern} -r=default --format=json", redirect_stdout="pkglist.json") # Overwriting previous pkglist.json pattern = "--list=pkglist.json" else: pattern = "zlib/*#latest --only-recipe" client.run(f"download {pattern} -r=default --format=json", redirect_stdout="pkglist.json") # TODO: Discuss "origin" assert "Local Cache" in client.load("pkglist.json") # Download binary too! Just to make sure it is in the cache, but not uploaded # because it is not in the orignal list of only recipes client.run(f"download * -r=default") client.run("remove * -r=default -c") client.run("upload --list=pkglist.json -r=default") assert f"Uploading recipe 'zlib/1.0.0" in client.out assert f"Uploading recipe 'zli/" not in client.out assert "Uploading package 'zlib/1.0.0" not in client.out assert "Uploading package 'zli/" not in client.out class TestListRemove: @pytest.fixture() def client(self): c = TestClient(default_server_user=True, light=True) c.save({ "zlib.py": GenConanfile("zlib"), "zli.py": GenConanfile("zli", "1.0.0") }) c.run("create zli.py") c.run("create zlib.py --version=1.0.0 --user=user --channel=channel") c.run("upload * -r=default -c") return c def test_remove_nothing_only_refs(self, client): # It is necessary to do *#* for actually removing something client.run(f"list * --format=json", redirect_stdout="pkglist.json") client.run(f"remove --list=pkglist.json -c --format=json") assert "Nothing to remove, package list do not contain recipe revisions" in client.out result = json.loads(client.stdout) assert result["Local Cache"] == {} # Nothing was removed @pytest.mark.parametrize("remote", [False, True]) def test_remove_all(self, client, remote): # It is necessary to do *#* for actually removing something remote = "-r=default" if remote else "" client.run(f"list *#* {remote} --format=json", redirect_stdout="pkglist.json") client.run(f"remove --list=pkglist.json {remote} -c --dry-run") assert "zli/1.0.0#f034dc90894493961d92dd32a9ee3b78:" \ " Removed recipe and all binaries" in client.out assert "zlib/1.0.0@user/channel#ffd4bc45820ddb320ab224685b9ba3fb:" \ " Removed recipe and all binaries" in client.out client.run(f"remove --list=pkglist.json {remote} -c --format=json") result = json.loads(client.stdout) origin = "Local Cache" if not remote else "default" assert len(result[origin]["zli/1.0.0"]["revisions"]) == 1 assert len(result[origin]["zlib/1.0.0@user/channel"]["revisions"]) == 1 assert "packages" not in client.stdout # Packages are not listed at all client.run(f"list * {remote}") assert "There are no matching recipe references" in client.out @pytest.mark.parametrize("remote", [False, True]) def test_remove_packages_no_revisions(self, client, remote): # It is necessary to do *#*:*#* for actually removing binaries remote = "-r=default" if remote else "" client.run(f"list *#*:* {remote} --format=json", redirect_stdout="pkglist.json") client.run(f"remove --list=pkglist.json {remote} -c --format=json") assert "No binaries to remove for 'zli/1.0.0#f034dc90894493961d92dd32a9ee3b78'" in client.out assert "No binaries to remove for 'zlib/1.0.0@user/channel" \ "#ffd4bc45820ddb320ab224685b9ba3fb" in client.out result = json.loads(client.stdout) origin = "Local Cache" if not remote else "default" assert result[origin] == {} # Nothing was removed @pytest.mark.parametrize("remote", [False, True]) def test_remove_packages(self, client, remote): # It is necessary to do *#*:*#* for actually removing binaries remote = "-r=default" if remote else "" client.run(f"list *#*:*#* {remote} --format=json", redirect_stdout="pkglist.json") client.run(f"remove --list=pkglist.json {remote} -c --dry-run") assert "Removed recipe and all binaries" not in client.out assert "zli/1.0.0#f034dc90894493961d92dd32a9ee3b78: Removed binaries" in client.out assert "zlib/1.0.0@user/channel#ffd4bc45820ddb320ab224685b9ba3fb: " \ "Removed binaries" in client.out client.run(f"remove --list=pkglist.json {remote} -c --format=json") result = json.loads(client.stdout) origin = "Local Cache" if not remote else "default" zli_revs = result[origin]["zli/1.0.0"]["revisions"] zli_uc_revs = result[origin]["zlib/1.0.0@user/channel"]["revisions"] assert len(zli_revs) == 1 assert len(zli_uc_revs) == 1 assert len(zli_revs["f034dc90894493961d92dd32a9ee3b78"]["packages"]) == 1 assert len(zli_uc_revs["ffd4bc45820ddb320ab224685b9ba3fb"]["packages"]) == 1 client.run(f"list *:* {remote}") assert "zli/1.0.0" in client.out assert "zlib/1.0.0@user/channel" in client.out class TestListGraphContext: @pytest.mark.parametrize("context, refs, not_refs", [ ("build", ["cmake/1.0", "m4/1.0", "protobuf/1.0"], []), ("build-only", ["cmake/1.0", "m4/1.0"], ["protobuf/1.0"]), ("host", ["zlib/1.0", "protobuf/1.0"], []), ("host-only", ["zlib/1.0"], ["protobuf/1.0"]) ]) def test_list_graph_context(self, context, refs, not_refs): c = TestClient(default_server_user=True, light=True) c.save({ "zlib/conanfile.py": GenConanfile("zlib", "1.0").with_settings("os"), "cmake/conanfile.py": GenConanfile("cmake", "1.0").with_settings("os"), "m4/conanfile.py": GenConanfile("m4", "1.0").with_settings("os"), "protobuf/conanfile.py": GenConanfile("protobuf", "1.0").with_settings("os"), "app/conanfile.py": GenConanfile("app", "1.0").with_settings("os") .with_requires("zlib/1.0", "protobuf/1.0") .with_tool_requires("cmake/1.0", "m4/1.0", "protobuf/1.0")}) c.run("create zlib") c.run("create protobuf") c.run("create cmake") c.run("create m4") c.run("create app --format=json", redirect_stdout="graph.json") # Now, let's filter the pkgs c.run(f"list --graph=graph.json --graph-context={context} --format=json", redirect_stdout="pkglist.json") content = json.loads(c.load("pkglist.json")) for ref in refs: assert content["Local Cache"][ref]["revisions"] for ref in not_refs: assert content["Local Cache"].get(ref) is None # Let's upload all the packages c.run(f"list --graph=graph.json --format=json", redirect_stdout="pkglist.json") c.run("upload --list=pkglist.json -r=default") c.run("remove * -c") c.run("create app --format=json", redirect_stdout="graph.json") # Now, let's filter the pkgs c.run(f"list --graph=graph.json --graph-context={context} -r=default --format=json", redirect_stdout="pkglist.json") content = json.loads(c.load("pkglist.json")) for ref in refs: assert content["default"][ref]["revisions"] for ref in not_refs: assert content["default"].get(ref) is None def test_graph_list(self): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("lib", "1.0")}) tc.run("graph info . -f json --build=never --no-remote --filter=context", redirect_stdout="graph_context.json") tc.run("list --graph=graph_context.json --graph-context=build-only --format=json", assert_error=True) assert "Note that the graph file should not be filtered" in tc.out @pytest.mark.parametrize("context", ["build-only", "host-only"]) def test_context_only_binary_mismatch(self, context): tc = TestClient(light=True) tc.save({ "protobuf/conanfile.py": GenConanfile("protobuf", "1.0"), "onnx/conanfile.py": GenConanfile("onnx", "1.0") .with_requires("protobuf/1.0") .with_tool_requires("protobuf/1.0")}) tc.run("create protobuf") tc.run("create onnx") # Note that here, the protobuf from tool_requires is skipped! tc.run("graph info --requires=onnx/1.0 -f=json", redirect_stdout="graph.json") tc.run(f"list --graph=graph.json --graph-context={context} --format=json") # We removed both protobuf binaries, not even the recipe is still there assert "protobuf/1.0" not in tc.out @pytest.mark.parametrize("context", ["build-only", "host-only"]) # We're building onnx to ensure the build context protobuf binary is not skipped @pytest.mark.parametrize("build_onnx", [True, False]) def test_context_only_binary_different_pkg_id(self, context, build_onnx): tc = TestClient(light=True) tc.save({ "protobuf/conanfile.py": GenConanfile("protobuf", "1.0") .with_shared_option(), "onnx/conanfile.py": GenConanfile("onnx", "1.0") .with_requirement("protobuf/1.0", options={"shared": True}) .with_tool_requires("protobuf/1.0")}) tc.run("create protobuf -o &:shared=True") protobuf_host_shared_pkgid = tc.created_layout().reference.package_id tc.run("create protobuf -o &:shared=False") protobuf_build_static_pkgid = tc.created_layout().reference.package_id tc.run("create onnx") build_arg = "-b=&" if build_onnx else "" tc.run(f"graph info --requires=onnx/1.0 {build_arg} -f=json", redirect_stdout="graph.json") tc.run(f"list --graph=graph.json --graph-context={context} --format=json") if context == "build-only": if not build_onnx: # If we have skipped the protobuf build binary, the recipe is gone too # because we have no protobuf packages being used assert "protobuf/1.0" not in tc.out else: assert "protobuf/1.0" in tc.out assert protobuf_build_static_pkgid in tc.out assert protobuf_host_shared_pkgid not in tc.out else: assert "protobuf/1.0" in tc.out assert protobuf_build_static_pkgid not in tc.out assert protobuf_host_shared_pkgid in tc.out @pytest.mark.parametrize("context", ["build-only", "host-only"]) def test_context_only_consumer_recipe_no_named(self, context): tc = TestClient() tc.save({ "protobuf/conanfile.py": GenConanfile("protobuf", "1.0"), "consumer/conanfile.py": GenConanfile() .with_tool_requires("protobuf/1.0") .with_requires("protobuf/1.0")}) tc.run("create protobuf") tc.run(f"graph info consumer -f=json", redirect_stdout="graph.json") tc.run(f"list --graph=graph.json --graph-context={context}") assert "protobuf/1.0" not in tc.out ================================================ FILE: test/integration/command/list/test_list_lru.py ================================================ import json import time import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestLRU: def test_error_lru_remote(self): c = TestClient(default_server_user=True) c.run("list * --lru=1s -r=default", assert_error=True) assert "'--lru' cannot be used in remotes, only in cache" in c.out @pytest.mark.parametrize("method", ["list", "remove"]) def test_cache_clean_lru(self, method): c = TestClient() c.save({"conanfile.py": GenConanfile()}) c.run("create . --name=pkg --version=0.1") c.run("create . --name=dep --version=0.2") time.sleep(2) # This should update the LRU c.run("install --requires=pkg/0.1") # Removing recipes (+ its binaries) that recipes haven't been used if method == "list": c.run("list *#* --lru=1s --format=json", redirect_stdout="old.json") c.run("remove --list=old.json -c") else: c.run("remove * --lru=1s -c") # Do the checks c.run("list *:*#*") assert "pkg" in c.out assert "da39a3ee5e6b4b0d3255bfef95601890afd80709" in c.out assert "dep" not in c.out time.sleep(2) # This should update the LRU of the recipe only c.run("graph info --requires=pkg/0.1") # IMPORTANT: Note the pattern is NOT the same as the equivalent for 'conan remove' if method == "list": c.run("list *#*:*#* --lru=1s --format=json", redirect_stdout="old.json") c.run("remove --list=old.json -c") else: c.run("remove *:* --lru=1s -c") # Check the binary has been removed, but the recipe is still there c.run("list *:*") assert "pkg" in c.out assert "da39a3ee5e6b4b0d3255bfef95601890afd80709" not in c.out assert "dep" not in c.out def test_lru_just_created(self): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run("list * --lru=1d", assert_error=True) assert "'--lru' must be used with recipe revision pattern" in c.out c.run("list *#* --lru=1d") assert "pkg/0.1" not in c.out # What if no revision, but package id is passed c.run("list pkg/0.1:* --lru=1d", assert_error=True) assert "'--lru' must be used with package revision pattern" in c.out # Doesn't fail, but packages is empty c.run("list pkg/0.1:*#* --lru=1d --format=json") pkgs = json.loads(c.stdout) rev = pkgs["Local Cache"]["pkg/0.1"]["revisions"]["485dad6cb11e2fa99d9afbe44a57a164"] assert rev["packages"] == {} def test_update_lru_when_used_as_dependency(self): """Show that using a recipe as a dependency will update its LRU""" c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": GenConanfile("app", "1.0").with_require("dep/1.0")}) c.run("create dep") time.sleep(2) c.run("create .") # Dep is not removed because its lru was updated as part of the above create c.run("remove * --lru=1s -c -f=json", redirect_stdout="removed.json") removed = json.loads(c.load("removed.json")) assert len(removed["Local Cache"]) == 0 def test_lru_invalid_time_unit(self): c = TestClient(light=True) c.run(f"list *#* --lru=1x") assert "ERROR: Unrecognized time unit: 'x'" in c.out def test_lru_invalid_time_value_edge_cases(self): c = TestClient(light=True) c.run("list *#* --lru=as") assert "ERROR: invalid literal for int() with base 10: 'a'" in c.out c.run("list *#* --lru=s") assert "ERROR: invalid literal for int() with base 10: ''" in c.out def test_lru_not_in_remotes(self): c = TestClient(light=True, default_server_user=True) c.run("list *#* --lru=1s -r=default", assert_error=True) assert "'--lru' cannot be used in remotes, only in cache" in c.out ================================================ FILE: test/integration/command/remote/__init__.py ================================================ ================================================ FILE: test/integration/command/remote/remote_test.py ================================================ import json import os import pytest from collections import OrderedDict from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer from conan.internal.util.files import load class TestRemoteServer: @pytest.fixture(autouse=True) def setup(self): self.servers = OrderedDict() for i in range(3): test_server = TestServer() self.servers["remote%d" % i] = test_server self.client = TestClient(servers=self.servers, inputs=3 * ["admin", "password"], light=True) def test_list_json(self): self.client.run("remote list --format=json") data = json.loads(self.client.stdout) assert data[0]["name"] == "remote0" assert data[1]["name"] == "remote1" assert data[2]["name"] == "remote2" # TODO: Check better def test_basic(self): self.client.run("remote list") assert "remote0: http://" in self.client.out assert "remote1: http://" in self.client.out assert "remote2: http://" in self.client.out self.client.run("remote add origin https://myurl") self.client.run("remote list") lines = str(self.client.out).splitlines() assert "origin: https://myurl" in lines[3] self.client.run("remote update origin --url https://2myurl") self.client.run("remote list") assert "origin: https://2myurl" in self.client.out self.client.run("remote update remote0 --url https://remote0url") self.client.run("remote list") output = str(self.client.out) assert "remote0: https://remote0url" in output.splitlines()[0] self.client.run("remote remove remote0") self.client.run("remote list") output = str(self.client.out) assert "remote1: http://" in output.splitlines()[0] def test_remove_remote(self): self.client.run("remote list") assert "remote0: http://" in self.client.out assert "remote1: http://" in self.client.out assert "remote2: http://" in self.client.out self.client.run("remote remove remote1") self.client.run("remote list") assert "remote1" not in self.client.out assert "remote0" in self.client.out assert "remote2" in self.client.out self.client.run("remote remove remote2") self.client.run("remote list") assert "remote1" not in self.client.out assert "remote0" in self.client.out assert "remote2" not in self.client.out def test_remove_remote_all(self): self.client.run("remote list") assert "remote0: http://" in self.client.out assert "remote1: http://" in self.client.out assert "remote2: http://" in self.client.out self.client.run("remote remove *") self.client.run("remote list") assert "remote1" not in self.client.out assert "remote0" not in self.client.out assert "remote2" not in self.client.out def test_remove_remote_no_user(self): self.client.save({"conanfile.py": GenConanfile()}) self.client.run("remote remove remote0") self.client.run("remote list") assert "remote0" not in self.client.out def test_insert(self): self.client.run("remote add origin https://myurl --index", assert_error=True) self.client.run("remote add origin https://myurl --index=0") self.client.run("remote add origin2 https://myurl2 --index=0") self.client.run("remote list") lines = str(self.client.out).splitlines() assert "origin2: https://myurl2" in lines[0] assert "origin: https://myurl" in lines[1] self.client.run("remote add origin3 https://myurl3 --index=1") self.client.run("remote list") lines = str(self.client.out).splitlines() assert "origin2: https://myurl2" in lines[0] assert "origin3: https://myurl3" in lines[1] assert "origin: https://myurl" in lines[2] def test_duplicated_error(self): """ check remote name are not duplicated """ self.client.run("remote add remote1 http://otherurl", assert_error=True) assert ("ERROR: Remote 'remote1' already exists in remotes (use --force to continue)" in self.client.out) self.client.run("remote list") assert "otherurl" not in self.client.out self.client.run("remote add remote1 http://otherurl --force") assert "WARN: Remote 'remote1' already exists in remotes" in self.client.out self.client.run("remote list") assert "remote1: http://otherurl" in self.client.out def test_missing_subarguments(self): self.client.run("remote", assert_error=True) assert "ERROR: Exiting with code: 2" in self.client.out def test_invalid_url(self): self.client.run("remote add foobar foobar.com") assert ("WARN: The URL 'foobar.com' is invalid. It must contain scheme and hostname." in self.client.out) self.client.run("remote list") assert "foobar.com" in self.client.out self.client.run("remote update foobar --url pepe.org") assert ("WARN: The URL 'pepe.org' is invalid. It must contain scheme and hostname." in self.client.out) self.client.run("remote list") assert "pepe.org" in self.client.out def test_errors(self): self.client.run("remote update origin --url https://foo.com", assert_error=True) assert "ERROR: Remote 'origin' doesn't exist" in self.client.out self.client.run("remote remove origin", assert_error=True) assert "ERROR: Remote 'origin' can't be found or is disabled" in self.client.out class TestRemoteModification: def test_rename(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=hello --version=0.1 --user=user --channel=testing") client.run("remote add r1 https://r1") client.run("remote add r2 https://r2") client.run("remote add r3 https://r3") client.run("remote rename r2 mynewr2") client.run("remote list") lines = str(client.out).splitlines() assert "r1: https://r1" in lines[0] assert "mynewr2: https://r2" in lines[1] assert "r3: https://r3" in lines[2] # Rename to an existing one client.run("remote rename mynewr2 r1", assert_error=True) assert "Remote 'r1' already exists" in client.out def test_update_insert(self): client = TestClient(light=True) client.run("remote add r1 https://r1") client.run("remote add r2 https://r2") client.run("remote add r3 https://r3") client.run("remote update r2 --url https://r2new --index=0") client.run("remote list") lines = str(client.out).splitlines() assert "r2: https://r2new" in lines[0] assert "r1: https://r1" in lines[1] assert "r3: https://r3" in lines[2] client.run("remote update r2 --url https://r2new2") client.run("remote update r2 --index=2") client.run("remote list") lines = str(client.out).splitlines() assert "r1: https://r1" in lines[0] assert "r3: https://r3" in lines[1] assert "r2: https://r2new2" in lines[2] def test_update_insert_same_url(self): # https://github.com/conan-io/conan/issues/5107 client = TestClient(light=True) client.run("remote add r1 https://r1") client.run("remote add r2 https://r2") client.run("remote add r3 https://r3") client.run("remote update r2 --url https://r2") client.run("remote update r2 --index 0") client.run("remote list") assert str(client.out).find("r2") < str(client.out).find("r1") assert str(client.out).find("r1") < str(client.out).find("r3") def test_verify_ssl(self): client = TestClient(light=True) client.run("remote add my-remote http://someurl") client.run("remote add my-remote2 http://someurl2 --insecure") client.run("remote add my-remote3 http://someurl3") registry = load(client.paths.remotes_path) data = json.loads(registry) assert data["remotes"][0]["name"] == "my-remote" assert data["remotes"][0]["url"] == "http://someurl" assert data["remotes"][0]["verify_ssl"] is True assert data["remotes"][1]["name"] == "my-remote2" assert data["remotes"][1]["url"] == "http://someurl2" assert data["remotes"][1]["verify_ssl"] is False assert data["remotes"][2]["name"] == "my-remote3" assert data["remotes"][2]["url"] == "http://someurl3" assert data["remotes"][2]["verify_ssl"] is True def test_remote_disable(self): client = TestClient(light=True) client.run("remote add my-remote0 http://someurl0") client.run("remote add my-remote1 http://someurl1") client.run("remote add my-remote2 http://someurl2") client.run("remote add my-remote3 http://someurl3") client.run("remote disable my-remote0") assert "my-remote0" in client.out assert "Enabled: False" in client.out client.run("remote disable my-remote3") assert "my-remote3" in client.out assert "Enabled: False" in client.out registry = load(client.paths.remotes_path) data = json.loads(registry) assert data["remotes"][0]["name"] == "my-remote0" assert data["remotes"][0]["url"] == "http://someurl0" assert data["remotes"][0]["disabled"] is True assert data["remotes"][3]["name"] == "my-remote3" assert data["remotes"][3]["url"] == "http://someurl3" assert data["remotes"][3]["disabled"] is True # check that they are still listed, as disabled client.run("remote list") assert "my-remote0: http://someurl0 [Verify SSL: True, Enabled: False]" in client.out assert "my-remote3: http://someurl3 [Verify SSL: True, Enabled: False]" in client.out client.run("remote disable * --format=json") registry = load(client.paths.remotes_path) data = json.loads(registry) for remote in data["remotes"]: assert remote["disabled"] is True data = json.loads(client.stdout) for remote in data: assert remote["enabled"] is False client.run("remote enable *") registry = load(client.paths.remotes_path) data = json.loads(registry) for remote in data["remotes"]: assert "disabled" not in remote assert "my-remote0" in client.out assert "my-remote1" in client.out assert "my-remote2" in client.out assert "my-remote3" in client.out assert "Enabled: False" not in client.out def test_invalid_remote_disable(self): client = TestClient(light=True) client.run("remote disable invalid_remote", assert_error=True) msg = "ERROR: Remote 'invalid_remote' can't be found or is disabled" assert msg in client.out client.run("remote enable invalid_remote", assert_error=True) assert msg in client.out client.run("remote disable invalid_wildcard_*") def test_remote_disable_already_set(self): """ Check that we don't raise an error if the remote is already in the required state """ client = TestClient(light=True) client.run("remote add my-remote0 http://someurl0") client.run("remote enable my-remote0") client.run("remote enable my-remote0") client.run("remote disable my-remote0") client.run("remote disable my-remote0") def test_verify_ssl_error(self): client = TestClient(light=True) client.run("remote add my-remote http://someurl some_invalid_option=foo", assert_error=True) assert "unrecognized arguments: some_invalid_option=foo" in client.out data = json.loads(load(client.paths.remotes_path)) assert data["remotes"] == [] def test_add_duplicated_url(): """ allow duplicated URL with --force """ c = TestClient(light=True) c.run("remote add remote1 http://url") c.run("remote add remote2 http://url", assert_error=True) assert "ERROR: Remote url already existing in remote 'remote1'" in c.out c.run("remote list") assert "remote1" in c.out assert "remote2" not in c.out c.run("remote add remote2 http://url --force") assert "WARN: Remote url already existing in remote 'remote1'." in c.out c.run("remote list") assert "remote1" in c.out assert "remote2" in c.out # make sure we can remove both # https://github.com/conan-io/conan/issues/13569 c.run("remote remove *") c.run("remote list") assert "remote1" not in c.out assert "remote2" not in c.out def test_add_duplicated_name_url(): """ do not add extra remote with same name and same url # https://github.com/conan-io/conan/issues/13569 """ c = TestClient(light=True) c.run("remote add remote1 http://url") c.run("remote add remote1 http://url --force") assert "WARN: Remote 'remote1' already exists in remotes" in c.out c.run("remote list") assert 1 == str(c.out).count("remote1") def test_add_wrong_conancenter(): """ Avoid common error of adding the wrong ConanCenter URL """ c = TestClient(light=True) c.run("remote add whatever https://conan.io/center", assert_error=True) assert "Wrong ConanCenter remote URL. You are adding the web https://conan.io/center" in c.out assert "the correct remote API is https://center2.conan.io" in c.out c.run("remote add conancenter https://center2.conan.io") c.run("remote update conancenter --url=https://conan.io/center", assert_error=True) assert "Wrong ConanCenter remote URL. You are adding the web https://conan.io/center" in c.out assert "the correct remote API is https://center2.conan.io" in c.out def test_using_frozen_center(): """ If the legacy center.conan.io is in the remote list warn about it. """ c = TestClient(light=True) c.run("remote add whatever https://center.conan.io") assert ("The remote 'https://center.conan.io' is now frozen and has been " "replaced by 'https://center2.conan.io'.") in c.out assert 'conan remote update whatever --url="https://center2.conan.io"' in c.out c.run("remote list") assert ("The remote 'https://center.conan.io' is now frozen and has been " "replaced by 'https://center2.conan.io'.") in c.out c.run("remote remove whatever") c.run("remote add conancenter https://center2.conan.io") assert ("The remote 'https://center.conan.io' is now frozen and has been " "replaced by 'https://center2.conan.io'.") not in c.out c.run("remote list") assert ("The remote 'https://center.conan.io' is now frozen and has been " "replaced by 'https://center2.conan.io'.") not in c.out c.run("remote update conancenter --url=https://center.conan.io") assert ("The remote 'https://center.conan.io' is now frozen and has been " "replaced by 'https://center2.conan.io'.") in c.out c.run("remote disable conancenter") assert ("The remote 'https://center.conan.io' is now frozen and has been " "replaced by 'https://center2.conan.io'.") not in c.out def test_wrong_remotes_json_file(): c = TestClient(light=True) c.save_home({"remotes.json": ""}) c.run("remote list", assert_error=True) assert "ERROR: Error loading JSON remotes file" in c.out def test_allowed_packages_remotes(): tc = TestClient(light=True, default_server_user=True) tc.save({"conanfile.py": GenConanfile(), "app/conanfile.py": GenConanfile("app", "1.0") .with_requires("liba/1.0") .with_requires("libb/[>=1.0]")}) tc.run("create . --name=liba --version=1.0") tc.run("create . --name=libb --version=1.0") tc.run("create app") tc.run("upload * -r=default -c") tc.run("remove * -c") tc.run("install --requires=app/1.0 -r=default") assert "app/1.0: Downloaded recipe revision 04ab3bc4b945a2ee44285962c277906d" in tc.out assert "liba/1.0: Downloaded recipe revision 4d670581ccb765839f2239cc8dff8fbd" in tc.out tc.run("remove * -c") # lib/2.* pattern does not match the one being required by app, should not be found tc.run('remote update default --allowed-packages="app/*" --allowed-packages="liba/2.*"') tc.run("install --requires=app/1.0 -r=default", assert_error=True) assert "app/1.0: Downloaded recipe revision 04ab3bc4b945a2ee44285962c277906d" in tc.out assert "liba/1.0: Downloaded recipe revision 4d670581ccb765839f2239cc8dff8fbd" not in tc.out assert "ERROR: Package 'liba/1.0' not resolved: Unable to find 'liba/1.0' in remotes" in tc.out assert "Required by 'app/1.0'" in tc.out tc.run("remove * -c") tc.run('remote update default --allowed-packages="liba/*" --allowed-packages="app/*"') tc.run("remote list -f=json") assert "app/*" in tc.out tc.run("install --requires=app/1.0 -r=default", assert_error=True) assert "app/1.0: Downloaded recipe revision 04ab3bc4b945a2ee44285962c277906d" in tc.out assert "liba/1.0: Downloaded recipe revision 4d670581ccb765839f2239cc8dff8fbd" in tc.out assert "ERROR: Package 'libb/[>=1.0]' not resolved" in tc.out tc.run("remove * -c") tc.run('remote update default --allowed-packages="liba/*" --allowed-packages="app/*"') tc.run("install --requires=app/1.0 -r=default", assert_error=True) assert "app/1.0: Downloaded recipe revision 04ab3bc4b945a2ee44285962c277906d" in tc.out assert "liba/1.0: Downloaded recipe revision 4d670581ccb765839f2239cc8dff8fbd" in tc.out assert "ERROR: Package 'libb/[>=1.0]' not resolved" in tc.out tc.run("remove * -c") tc.run('remote update default --allowed-packages="*"') tc.run("install --requires=app/1.0 -r=default") assert "app/1.0: Downloaded recipe revision 04ab3bc4b945a2ee44285962c277906d" in tc.out assert "liba/1.0: Downloaded recipe revision 4d670581ccb765839f2239cc8dff8fbd" in tc.out assert "libb/1.0: Downloaded recipe revision 4d670581ccb765839f2239cc8dff8fbd" in tc.out def test_remote_allowed_packages_new(): """Test that the allowed packages are saved in the remotes.json file when adding a new remote""" tc = TestClient(light=True) tc.run("remote add foo https://foo -ap='liba/*'") remotes = json.loads(load(os.path.join(tc.cache_folder, "remotes.json"))) assert remotes["remotes"][0]["allowed_packages"] == ["liba/*"] def test_remote_allowed_negation(): tc = TestClient(light=True, default_server_user=True) tc.save({"conanfile.py": GenConanfile()}) tc.run("create . --name=config --version=1.0") tc.run("create . --name=lib --version=1.0") tc.run("upload * -c -r=default") tc.run("remove * -c") tc.run('remote update default --allowed-packages="!config/*"') tc.run("install --requires=lib/1.0 -r=default") assert "lib/1.0: Downloaded recipe revision" in tc.out tc.run("install --requires=config/1.0 -r=default", assert_error=True) assert ("ERROR: Package 'config/1.0' not resolved: Unable to find " "'config/1.0' in remotes") in tc.out ================================================ FILE: test/integration/command/remote/remote_verify_ssl_test.py ================================================ import requests from requests.models import Response from conan.test.utils.tools import TestClient resp = Response() resp._content = b'{"results": []}' resp.status_code = 200 resp.headers = {"Content-Type": "application/json", "X-Conan-Server-Capabilities": "revisions"} class RequesterMockFalse: expected = False def __init__(self, *args, **kwargs): pass def get(self, url, *args, **kwargs): # noqa assert kwargs["verify"] is self.expected return resp def Session(self): # noqa return self @property def codes(self): return requests.codes def mount(self, *args, **kwargs): pass class RequesterMockTrue(RequesterMockFalse): expected = True class TestVerifySSL: def test_verify_ssl(self): c = TestClient(requester_class=RequesterMockTrue) c.run("remote add myremote https://localhost --insecure") c.run("remote list") assert "Verify SSL: False" in c.out c.run("remote update myremote --url https://localhost --secure") c.run("remote list") assert "Verify SSL: True" in c.out c.run("remote remove myremote") c.run("remote add myremote https://localhost") c.run("remote list") assert "Verify SSL: True" in c.out # Verify that SSL is checked in requests c.run("search op* -r myremote") assert "WARN: There are no matching recipe references" in c.out # Verify that SSL is not checked in requests c = TestClient(requester_class=RequesterMockFalse) c.run("remote add myremote https://localhost --insecure") c.run("search op* -r myremote") assert "WARN: There are no matching recipe references" in c.out ================================================ FILE: test/integration/command/remote/test_remote_users.py ================================================ import json import textwrap import time from collections import OrderedDict from datetime import timedelta from unittest.mock import patch from conan.internal.api.remotes.localdb import LocalDB from conan.test.utils.tools import TestClient, TestServer from conan.test.utils.env import environment_update class TestUser: def test_command_user_no_remotes(self): """ Test that proper error is reported when no remotes are defined and conan user is executed """ client = TestClient() client.run("remote list-users", assert_error=True) assert "ERROR: No remotes defined" in client.out client.run("remote login wrong_remote foo -p bar", assert_error=True) assert "ERROR: Remote 'wrong_remote' can't be found or is disabled" in client.out def test_command_user_list(self): """ Test list of user is reported for all remotes or queried remote """ servers = OrderedDict() servers["default"] = TestServer() servers["test_remote_1"] = TestServer() client = TestClient(servers=servers) # Test with wrong remote right error is reported client.run("remote login Test_Wrong_Remote foo", assert_error=True) assert "ERROR: Remote 'Test_Wrong_Remote' can't be found or is disabled" in client.out # Test user list for all remotes is reported client.run("remote list-users") assert textwrap.dedent(""" default: No user test_remote_1: No user""") not in client.out def test_with_remote_no_connect(self): test_server = TestServer() client = TestClient(servers={"default": test_server}) client.run('remote list-users') assert textwrap.dedent(""" default: No user""") not in client.out client.run('remote set-user default john') assert "Changed user of remote 'default' from 'None' (anonymous) to 'john'" in client.out localdb = LocalDB(client.cache_folder) assert ('john', None, None) == localdb.get_login(test_server.fake_url) client.run('remote set-user default will') assert "Changed user of remote 'default' from 'john' (anonymous) to 'will'" in client.out assert ('will', None, None) == localdb.get_login(test_server.fake_url) client.run('remote logout default') assert "Changed user of remote 'default' from 'will' (anonymous) to 'None' (anonymous)" in client.out assert (None, None, None) == localdb.get_login(test_server.fake_url) def test_command_user_with_password(self): """ Checks the -p option, that obtains a token from the password. Useful for integrations as travis, that interactive password is not possible """ test_server = TestServer() servers = {"default": test_server} client = TestClient(servers=servers, inputs=["admin", "password"]) client.run('remote login default dummy -p ping_pong2', assert_error=True) assert "ERROR: Wrong user or password" in client.out client.run('remote login default admin -p password') assert "ERROR: Wrong user or password" not in client.out assert "Changed user of remote 'default' from 'None' (anonymous) to 'admin'" in client.out client.run('remote logout default') assert "Changed user of remote 'default' from 'admin' (authenticated) to 'None' (anonymous)" in client.out localdb = LocalDB(client.cache_folder) assert (None, None, None) == localdb.get_login(test_server.fake_url) client.run('remote list-users') assert 'default:\n No user' in client.out def test_command_user_with_password_spaces(self): """ Checks the -p option, that obtains a token from the password. Useful for integrations as travis, that interactive password is not possible """ test_server = TestServer(users={"lasote": 'my "password'}) servers = {"default": test_server} client = TestClient(servers=servers, inputs=["lasote", "mypass"]) client.run(r'remote login default lasote -p="my \"password"') assert "Connecting to remote" not in client.out assert "Changed user of remote 'default' from 'None' (anonymous) to 'lasote'" in client.out client.run('remote logout default') client.run(r'remote login default lasote -p "my \"password"') assert "ERROR: Wrong user or password" not in client.out assert "Changed user of remote 'default' from 'None' (anonymous) to 'lasote'" in client.out def test_clean(self): test_server = TestServer() servers = {"default": test_server} client = TestClient(servers=servers, inputs=2*["admin", "password"]) base = ''' from conan import ConanFile class ConanLib(ConanFile): name = "lib" version = "0.1" ''' files = {"conanfile.py": base} client.save(files) client.run("export . --user=lasote --channel=stable") client.run("upload lib/0.1@lasote/stable -r default --only-recipe") client.run("remote list-users") assert 'default:\n Username: admin\n authenticated: True' in client.out client.run("remote logout default") assert "Changed user of remote 'default' from 'admin' (authenticated) to 'None' (anonymous)" in client.out client.run("remote list-users") assert 'default:\n No user' in client.out # --force will force re-authentication, otherwise not necessary to auth client.run("upload lib/0.1@lasote/stable -r default --force --only-recipe") client.run("remote list-users") assert 'default:\n Username: admin\n authenticated: True' in client.out def test_command_interactive_only(self): test_server = TestServer() servers = {"default": test_server} client = TestClient(servers=servers, inputs=["password"]) client.run('remote login default admin -p') assert "Changed user of remote 'default' from 'None' (anonymous) to 'admin' (authenticated)" in client.out def test_command_user_with_interactive_password_login_prompt_disabled(self): """ Interactive password should not work. """ test_server = TestServer() servers = {"default": test_server} client = TestClient(servers=servers, inputs=[]) conan_conf = "core:non_interactive=True" client.save_home({"global.conf": conan_conf}) client.run('remote login default admin -p', assert_error=True) assert 'ERROR: Conan interactive mode disabled' in client.out assert "Please enter a password for user 'admin'" not in client.out client.run("remote list-users") assert "default:\n No user" in client.out def test_authenticated(self): test_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) servers = OrderedDict() servers["default"] = test_server servers["other_server"] = TestServer() client = TestClient(servers=servers, inputs=["lasote", "mypass", "mypass", "mypass"]) client.run("remote logout default") assert "Changed user of remote 'default' from 'None' (anonymous) to 'None' (anonymous)" in client.out assert "[authenticated]" not in client.out client.run('remote set-user default bad_user') client.run("remote list-users") assert 'default:\n Username: bad_user\n authenticated: False' in client.out client.run("remote set-user default lasote") client.run("remote list-users") assert 'default:\n Username: lasote\n authenticated: False' in client.out client.run("remote login default lasote -p mypass") client.run("remote list-users") assert 'default:\n Username: lasote\n authenticated: True' in client.out client.run("remote login default danimtb -p passpass") assert "Changed user of remote 'default' from 'lasote' (authenticated) to 'danimtb' (authenticated)" in client.out client.run("remote list-users") assert 'default:\n Username: danimtb\n authenticated: True' in client.out def test_json(self): default_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) other_server = TestServer() servers = OrderedDict() servers["default"] = default_server servers["other_server"] = other_server client = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass"]) client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ { "name": "default", "authenticated": False, "user_name": None }, { "name": "other_server", "authenticated": False, "user_name": None } ] client.run('remote set-user default bad_user') client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ { "name": "default", "authenticated": False, "user_name": "bad_user" }, { "name": "other_server", "authenticated": False, "user_name": None } ] client.run('remote set-user default lasote') client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ { "name": "default", "authenticated": False, "user_name": "lasote" }, { "name": "other_server", "authenticated": False, "user_name": None } ] client.run("remote login default lasote -p mypass") client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ { "name": "default", "authenticated": True, "user_name": "lasote" }, { "name": "other_server", "authenticated": False, "user_name": None } ] client.run("remote login default danimtb -p passpass") client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ { "name": "default", "authenticated": True, "user_name": "danimtb" }, { "name": "other_server", "authenticated": False, "user_name": None } ] client.run("remote set-user other_server lasote") client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ { "name": "default", "authenticated": True, "user_name": "danimtb" }, { "name": "other_server", "authenticated": False, "user_name": "lasote" } ] client.run("remote logout '*'") client.run("remote set-user default danimtb") client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ { "name": "default", "authenticated": False, "user_name": "danimtb" }, { "name": "other_server", "authenticated": False, "user_name": None } ] client.run("remote list-users") assert "default:\n Username: danimtb\n authenticated: False" in client.out assert "other_server:\n No user\n" in client.out def test_skip_auth(self): default_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) servers = OrderedDict() servers["default"] = default_server client = TestClient(servers=servers) # Regular auth client.run("remote login default lasote -p mypass") # Now skip the auth but keeping the same user client.run("remote set-user default lasote") assert "Changed user of remote 'default' from 'lasote' (authenticated) to 'lasote' (authenticated)" in client.out # If we change the user the credentials are removed client.run("remote set-user default flanders") assert "Changed user of remote 'default' from 'lasote' (authenticated) to 'flanders' (anonymous)" in client.out client.run("remote login default lasote -p BAD_PASS", assert_error=True) assert "Wrong user or password" in client.out # Login again correctly client.run("remote login default lasote -p mypass") def test_login_multiremote(self): servers = OrderedDict() servers["default"] = TestServer(users={"admin": "password"}) servers["other"] = TestServer(users={"admin": "password"}) c = TestClient(servers=servers, inputs=["admin", "password", "wrong", "wrong"]) # This must fail, not autthenticate in the next remote c.run("remote login *", assert_error=True) assert "ERROR: Wrong user or password. [Remote: other]" in c.out def test_user_removed_remote_removed(): # Make sure that removing a remote clears the credentials # https://github.com/conan-io/conan/issues/5562 c = TestClient(default_server_user=True) server_url = c.servers["default"].fake_url c.run("remote login default admin -p password") localdb = LocalDB(c.cache_folder) login = localdb.get_login(server_url) assert login[0] == "admin" c.run("remote remove default") login = localdb.get_login(server_url) assert login == (None, None, None) class TestRemoteAuth: def test_remote_auth(self): servers = OrderedDict() servers["default"] = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) servers["other_server"] = TestServer(users={"lasote": "mypass"}) c = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) c.run("remote auth *") text = textwrap.dedent("""\ default: user: lasote other_server: user: lasote""") assert text in c.out c.run("remote auth * --format=json") result = json.loads(c.stdout) assert result == {'default': {'user': 'lasote'}, 'other_server': {'user': 'lasote'}} def test_remote_auth_force(self): servers = OrderedDict() servers["default"] = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) servers["other_server"] = TestServer(users={"lasote": "mypass"}) c = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) with patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") as check_credentials_mock: c.run("remote auth --force *") check_credentials_mock.assert_called_with(True) def test_remote_auth_force_false(self): servers = OrderedDict() servers["default"] = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) servers["other_server"] = TestServer(users={"lasote": "mypass"}) c = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) with patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") as check_credentials_mock: c.run("remote auth *") check_credentials_mock.assert_called_with(False) def test_remote_auth_with_user(self): servers = OrderedDict() servers["default"] = TestServer(users={"lasote": "mypass"}) servers["other_server"] = TestServer() c = TestClient(servers=servers, inputs=["lasote", "mypass"]) c.run("remote set-user default lasote") c.run("remote auth * --with-user") text = textwrap.dedent("""\ default: user: lasote other_server: user: None""") assert text in c.out def test_remote_auth_with_user_env_var(self): servers = OrderedDict() servers["default"] = TestServer(users={"lasote": "mypass"}) servers["other_server"] = TestServer() c = TestClient(servers=servers) with environment_update({"CONAN_LOGIN_USERNAME_DEFAULT": "lasote", "CONAN_PASSWORD_DEFAULT": "mypass"}): c.run("remote auth * --with-user") text = textwrap.dedent("""\ default: user: lasote other_server: user: None""") assert text in c.out def test_remote_auth_error(self): servers = OrderedDict() servers["default"] = TestServer(users={"user": "password"}) c = TestClient(servers=servers, inputs=["user1", "pass", "user2", "pass", "user3", "pass"]) c.run("remote auth *") assert "error: Too many failed login attempts, bye!" in c.out def test_remote_auth_server_expire_token_secret(self): server = TestServer(users={"myuser": "password", "myotheruser": "otherpass"}) c = TestClient(servers={"default": server}, inputs=["myuser", "password", "myotheruser", "otherpass", "user", "pass", "user", "pass", "user", "pass"]) c.run("remote auth *") assert "Remote 'default' needs authentication, obtaining credentials" in c.out assert "user: myuser" in c.out c.run("remote auth *") assert "Remote 'default' needs authentication, obtaining credentials" not in c.out assert "user: myuser" in c.out # Invalidate server secret server.test_server.ra.api_v2.credentials_manager.secret = "potato" * 6 c.run("remote auth *") assert "user: myotheruser" in c.out # Invalidate server secret again server.test_server.ra.api_v2.credentials_manager.secret = "potato2" * 6 c.run("remote auth *") assert "error: Too many failed login attempts, bye!" in c.out def test_remote_auth_server_expire_token(self): server = TestServer(users={"myuser": "password", "myotheruser": "otherpass"}) server.test_server.ra.api_v2.credentials_manager.expire_time = timedelta(seconds=2) c = TestClient(servers={"default": server}, inputs=["myuser", "password", "myotheruser", "otherpass", "user", "pass", "user", "pass", "user", "pass"]) c.run("remote auth *") assert "user: myuser" in c.out # token not expired yet, should work c.run("remote auth *") assert "user: myuser" in c.out # Token should expire time.sleep(3) c.run("remote auth *") assert "user: myotheruser" in c.out # Token should expire time.sleep(3) c.run("remote auth *") assert "error: Too many failed login attempts, bye!" in c.out def test_auth_after_logout(self): server = TestServer(users={"myuser": "password"}) c = TestClient(servers={"default": server}, inputs=["myuser", "password"]*2) c.run("remote auth *") assert "Remote 'default' needs authentication, obtaining credentials" in c.out assert "user: myuser" in c.out c.run("remote logout *") assert "Changed user of remote 'default' from 'myuser' (authenticated) to 'None'" in c.out c.run("remote auth *") assert "Remote 'default' needs authentication, obtaining credentials" in c.out assert "user: myuser" in c.out ================================================ FILE: test/integration/command/remove_empty_dirs_test.py ================================================ import os from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestRemoveEmptyDirs: def test_basic(self): client = TestClient() client.save({"conanfile.py": GenConanfile("hello", "0.1")}) client.run("export . --user=lasote --channel=stable") ref_layout = client.exported_layout() assert os.path.exists(ref_layout.base_folder) client.run("remove hello* -c") assert not os.path.exists(ref_layout.base_folder) def test_shared_folder(self): client = TestClient() client.save({"conanfile.py": GenConanfile("hello", "0.1")}) client.run("export . --user=lasote --channel=stable") ref_layout = client.exported_layout() assert os.path.exists(ref_layout.base_folder) client.run("export . --user=lasote2 --channel=stable") ref_layout2 = client.exported_layout() assert os.path.exists(ref_layout2.base_folder) client.run("remove hello/0.1@lasote/stable -c") assert not os.path.exists(ref_layout.base_folder) assert os.path.exists(ref_layout2.base_folder) ================================================ FILE: test/integration/command/remove_test.py ================================================ import json import os import shutil import pytest from conan.api.conan_api import ConanAPI from conan.internal.errors import NotFoundException from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, GenConanfile from conan.test.utils.env import environment_update class TestRemoveWithoutUserChannel: @pytest.mark.parametrize("dry_run", [True, False]) def test_local(self, dry_run): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0") ref_layout = client.exported_layout() pkg_layout = client.created_layout() extra = " --dry-run" if dry_run else "" client.run("remove lib/1.0 -c" + extra) assert os.path.exists(ref_layout.base_folder) == dry_run assert os.path.exists(pkg_layout.base_folder) == dry_run @pytest.mark.parametrize("confirm", [True, False]) def test_local_dryrun_output(self, confirm): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0") client.run("remove lib/1.0 --dry-run", inputs=["yes" if confirm else "no"]) assert "(yes/no)" in client.out # Users are asked even when dry-run is set if confirm: assert "Removed recipe and all binaries" in client.out # Output it printed for dry-run else: assert "Removed recipe and all binaries" not in client.out @pytest.mark.artifactory_ready def test_remote(self): client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0") client.run("upload lib/1.0 -r default -c") client.run("remove lib/1.0 -c") # we can still install it client.run("install --requires=lib/1.0@") assert "lib/1.0: Retrieving package" in client.out client.run("remove lib/1.0 -c") # Now remove remotely, dry run first client.run("remove lib/1.0 -c -r default --dry-run") # we can still install it client.run("install --requires=lib/1.0@") assert "lib/1.0: Retrieving package" in client.out client.run("remove lib/1.0 -c") # Now remove remotely, for real this time client.run("remove lib/1.0 -c -r default") client.run("install --requires=lib/1.0@", assert_error=True) assert "Unable to find 'lib/1.0' in remotes" in client.out class TestRemovePackageRevisions: @pytest.mark.artifactory_ready def test_remove_package_revision(self): """ Remove package ID based on recipe revision. The package must be deleted, but the recipe must be preserved """ ref = RecipeReference.loads("foobar/0.1@user/testing#4d670581ccb765839f2239cc8dff8fbd") pref_arg = PkgReference(ref, NO_SETTINGS_PACKAGE_ID) pref = PkgReference(ref, NO_SETTINGS_PACKAGE_ID, "0ba8627bd47edc3a501e8f0eb9a79e5e") c = TestClient(light=True, default_server_user=True) c.save({"conanfile.py": GenConanfile()}) c.run("create . --name=foobar --version=0.1 --user=user --channel=testing") assert c.package_exists(pref) c.run("upload * -r default -c") c.run(f"list {ref}:*") assert NO_SETTINGS_PACKAGE_ID in c.out # remove from cache c.run(f"remove -c {pref_arg.repr_notime()}") assert not c.package_exists(pref) # remove From server c.run(f"remove -c {pref_arg.repr_notime()} -r=default") c.run(f"list {ref}:*") assert NO_SETTINGS_PACKAGE_ID not in c.out @pytest.mark.artifactory_ready def test_remove_all_packages_but_the_recipe_at_remote(self): """ Remove all the packages but not the recipe in a remote """ c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("foobar", "0.1").with_settings("arch")}) c.run("create . --user=user --channel=testing -s arch=x86_64") c.run("create . --user=user --channel=testing -s arch=x86") c.run("upload * -r default -c") c.run("list *:* -r default") assert "foobar/0.1@user/testing" in c.out assert "arch: x86_64" in c.out assert "arch: x86" in c.out c.run("remove -c foobar/0.1@user/testing:* -r default") c.run("list *:* -r default") assert "foobar/0.1@user/testing" in c.out assert "arch: x86_64" not in c.out assert "arch: x86" not in c.out # populated packages of bar bar_rrev = "bar/1.1#7db54b020cc95b8bdce49cd6aa5623c0" bar_rrev2 = "bar/1.1#78b42a981b29d2cb00fda10b72f1e72a" pkg_id_debug = "9e186f6d94c008b544af1569d1a6368d8339efc5" pkg_id_release = "efa83b160a55b033c4ea706ddb980cd708e3ba1b" bar_rrev2_debug = '{}:{}'.format(bar_rrev2, pkg_id_debug) bar_rrev2_release = '{}:{}'.format(bar_rrev2, pkg_id_release) bar_prev1 = "ee1ca5821b390a75d7168f4567f9ba75" bar_prev2 = "f523273047249d5a136fe48d33f645bb" bar_rrev2_release_prev1 = "{}#{}".format(bar_rrev2_release, bar_prev1) bar_rrev2_release_prev2 = "{}#{}".format(bar_rrev2_release, bar_prev2) @pytest.fixture(scope="module") def _populated_client_base(): """ foo/1.0@ (one revision) no packages foo/1.0@user/channel (one revision) no packages fbar/1.1@ (one revision) no packages bar/1.0@ (two revision) => Debug, Release => (two package revision each) """ # To generate different package revisions package_lines = 'save(self, os.path.join(self.package_folder, "foo.txt"), ' \ 'os.getenv("foo_test", "Na"))' client = TestClient(default_server_user=True) conanfile = str(GenConanfile().with_settings("build_type") .with_package(package_lines) .with_import("from conan.tools.files import save") .with_import("import os") .with_import("import time")) client.save({"conanfile.py": conanfile}) client.run("export . --name foo --version 1.0") client.run("export . --name foo --version 1.0 --user user --channel channel") client.run("export . --name fbar --version 1.1") # Two package revisions for bar/1.1 (Release) for _i in range(2): with environment_update({'foo_test': str(_i)}): client.run("create . --name=bar --version=1.1 -s build_type=Release") client.run("create . --name=bar --version=1.1 -s build_type=Debug") prefs = _get_revisions_packages(client, bar_rrev2_release, False) assert set(prefs) == {bar_rrev2_release_prev1, bar_rrev2_release_prev2} # Two recipe revisions for bar/1.1 client.save({"conanfile.py": conanfile + "\n # THIS IS ANOTHER RECIPE REVISION"}) client.run("create . --name=bar --version=1.1 -s build_type=Debug") client.run("upload '*#*:*#*' -c -r default") return client @pytest.fixture() def populated_client(_populated_client_base): """ this is much faster than creating and uploading everythin """ client = TestClient(default_server_user=True) shutil.rmtree(client.cache_folder) shutil.copytree(_populated_client_base.cache_folder, client.cache_folder) shutil.rmtree(client.servers["default"].test_server._base_path) shutil.copytree(_populated_client_base.servers["default"].test_server._base_path, client.servers["default"].test_server._base_path) client.update_servers() return client @pytest.mark.parametrize("with_remote", [True, False]) @pytest.mark.parametrize("data", [ {"remove": "foo*", "recipes": ['bar/1.1', 'fbar/1.1']}, {"remove": "foo/*", "recipes": ['bar/1.1', 'fbar/1.1']}, {"remove": "*", "recipes": []}, {"remove": "*/*", "recipes": []}, {"remove": "*/*#*", "recipes": []}, {"remove": "*/*#z*", "recipes": ['foo/1.0@user/channel', 'foo/1.0', 'bar/1.1', 'fbar/1.1']}, {"remove": "f*", "recipes": ["bar/1.1"]}, {"remove": "*/1.1", "recipes": ["foo/1.0", "foo/1.0@user/channel"]}, {"remove": "*/*@user/*", "recipes": ["foo/1.0", "fbar/1.1", "bar/1.1"]}, {"remove": "*/*@*", "recipes": ['foo/1.0', 'fbar/1.1', 'bar/1.1']}, {"remove": "*/*#*:*", "recipes": ['bar/1.1', 'foo/1.0@user/channel', 'foo/1.0', 'fbar/1.1']}, {"remove": "foo/1.0@user/channel:*", "recipes": ['bar/1.1', 'foo/1.0@user/channel', 'foo/1.0', 'fbar/1.1']}, {"remove": "foo/*@", "recipes": ['foo/1.0@user/channel', 'bar/1.1', 'fbar/1.1']}, {"remove": "foo", "recipes": ['bar/1.1', 'fbar/1.1']}, {"remove": "*#", "recipes": []}, {"remove": "*/*#", "recipes": []}, ]) def test_new_remove_recipes_expressions(populated_client, with_remote, data): r = "-r default" if with_remote else "" assert "error" not in data populated_client.run("remove {} -c {}".format(data["remove"], r)) populated_client.run(f"list {r} --format=json") origin = "default" if with_remote else "Local Cache" assert set(json.loads(populated_client.stdout)[origin]) == set(data["recipes"]) @pytest.mark.parametrize("with_remote", [True, False]) @pytest.mark.parametrize("data", [ {"remove": "bar/*#*", "rrevs": []}, {"remove": "bar/1.1#z*", "rrevs": [bar_rrev, bar_rrev2]}, {"remove": "bar/1.1#*9*", "rrevs": []}, {"remove": "bar/1.1#*2a", "rrevs": [bar_rrev]}, {"remove": "bar*#*50", "rrevs": [bar_rrev, bar_rrev2]}, {"remove": "bar*#latest", "rrevs": [bar_rrev2]}, # latest is bar_rrev {"remove": "bar*#!latest", "rrevs": [bar_rrev]}, # latest is bar_rrev {"remove": "bar*#~latest", "rrevs": [bar_rrev]}, # latest is bar_rrev ]) def test_new_remove_recipe_revisions_expressions(populated_client, with_remote, data): r = "-r default" if with_remote else "" error = data.get("error", False) populated_client.run("remove {} -c {}".format(data["remove"], r), assert_error=error) if not error: rrevs = _get_revisions_recipes(populated_client, "bar/1.1", with_remote) assert rrevs == set(data["rrevs"]) @pytest.mark.parametrize("with_remote", [True, False]) @pytest.mark.parametrize("data", [ {"remove": "bar/1.1#*:*", "prefs": []}, {"remove": "bar/1.1#*:*#*", "prefs": []}, {"remove": "bar/1.1#z*:*", "prefs": [bar_rrev2_debug, bar_rrev2_release]}, {"remove": "bar/1.1#*:*#kk*", "prefs": [bar_rrev2_debug, bar_rrev2_release]}, {"remove": "bar/1.1#*:{}*".format(pkg_id_release), "prefs": [bar_rrev2_debug]}, {"remove": "bar/1.1#*:*{}".format(pkg_id_release[-12:]), "prefs": [bar_rrev2_debug]}, {"remove": "{}:*{}*".format(bar_rrev2, pkg_id_debug[5:15]), "prefs": [bar_rrev2_release]}, {"remove": "*/*#*:*{}*".format(pkg_id_debug[5:15]), "prefs": [bar_rrev2_release]}, {"remove": '*/*#*:* -p build_type="fake"', "prefs": [bar_rrev2_release, bar_rrev2_debug]}, {"remove": '*/*#*:* -p build_type="Release"', "prefs": [bar_rrev2_debug]}, {"remove": '*/*#*:* -p build_type="Debug"', "prefs": [bar_rrev2_release]}, # Errors {"remove": '*/*#*:*#* -p', "error": True, "error_msg": "argument -p/--package-query: expected one argument"}, {"remove": "bar/1.1#*:", "prefs": []}, {"remove": "bar/1.1#*:234234*#", "prefs": [bar_rrev2_debug, bar_rrev2_release]}, {"remove": "bar/1.1:234234*", "prefs": [bar_rrev2_debug, bar_rrev2_release]}, {"remove": "bar/1.1:* -p os=Windows", "prefs": [bar_rrev2_debug, bar_rrev2_release]}, ]) def test_new_remove_package_expressions(populated_client, with_remote, data): # Remove the ones we are not testing here r = "-r default" if with_remote else "" pids = _get_all_packages(populated_client, bar_rrev2, with_remote) assert pids == {bar_rrev2_debug, bar_rrev2_release} error = data.get("error", False) populated_client.run("remove {} -c {}".format(data["remove"], r), assert_error=error) if not error: pids = _get_all_packages(populated_client, bar_rrev2, with_remote) assert pids == set(data["prefs"]) elif data.get("error_msg"): assert data.get("error_msg") in populated_client.out @pytest.mark.parametrize("with_remote", [True, False]) @pytest.mark.parametrize("data", [ {"remove": '{}#*kk*'.format(bar_rrev2_release), "prevs": [bar_rrev2_release_prev1, bar_rrev2_release_prev2]}, {"remove": '{}#*'.format(bar_rrev2_release), "prevs": []}, {"remove": '{}*#{}* -p "build_type=Debug"'.format(bar_rrev2_release, bar_prev2[:3]), "prevs": [bar_rrev2_release_prev1, bar_rrev2_release_prev2]}, {"remove": '{}*#{}* -p "build_type=Release"'.format(bar_rrev2_release, bar_prev2[:3]), "prevs": [bar_rrev2_release_prev1]}, {"remove": '{}*#* -p "build_type=Release"'.format(bar_rrev2_release), "prevs": []}, {"remove": '{}*#* -p "build_type=Debug"'.format(bar_rrev2_release), "prevs": [bar_rrev2_release_prev1, bar_rrev2_release_prev2]}, # Errors {"remove": '{}#'.format(bar_rrev2_release), "prevs": []}, {"remove": '{}#latest'.format(bar_rrev2_release), "prevs": [bar_rrev2_release_prev1]}, {"remove": '{}#!latest'.format(bar_rrev2_release), "prevs": [bar_rrev2_release_prev2]}, {"remove": '{}#~latest'.format(bar_rrev2_release), "prevs": [bar_rrev2_release_prev2]}, ]) def test_new_remove_package_revisions_expressions(populated_client, with_remote, data): # Remove the ones we are not testing here r = "-r default" if with_remote else "" populated_client.run("remove f/* -c {}".format(r)) prefs = _get_revisions_packages(populated_client, bar_rrev2_release, with_remote) assert set(prefs) == {bar_rrev2_release_prev1, bar_rrev2_release_prev2} assert "error" not in data populated_client.run("remove {} -c {}".format(data["remove"], r)) prefs = _get_revisions_packages(populated_client, bar_rrev2_release, with_remote) assert set(prefs) == set(data["prevs"]) def test_package_query_no_package_ref(populated_client): populated_client.run("remove * -p 'compiler=clang'", assert_error=True) assert "--package-query supplied but the pattern does not match packages" in populated_client.out def _get_all_packages(client, ref, with_remote): ref = RecipeReference.loads(ref) with client.mocked_servers(): api = ConanAPI(client.cache_folder) remote = api.remotes.get("default") if with_remote else None try: return set([r.repr_notime() for r in api.list._packages_configurations(ref, remote=remote)]) # noqa except NotFoundException: return set() def _get_revisions_recipes(client, ref, with_remote): ref = RecipeReference.loads(ref) with client.mocked_servers(): api = ConanAPI(client.cache_folder) remote = api.remotes.get("default") if with_remote else None try: return set([r.repr_notime() for r in api.list.recipe_revisions(ref, remote=remote)]) except NotFoundException: return set() def _get_revisions_packages(client, pref, with_remote): pref = PkgReference.loads(pref) with client.mocked_servers(): api = ConanAPI(client.cache_folder) remote = api.remotes.get("default") if with_remote else None try: return set([r.repr_notime() for r in api.list.package_revisions(pref, remote=remote)]) except NotFoundException: return set() ================================================ FILE: test/integration/command/require/__init__.py ================================================ ================================================ FILE: test/integration/command/require/test_command_require.py ================================================ import textwrap from conan.test.utils.tools import GenConanfile, TestClient def test_add_require(): """ Testing the "conan dep add" command which should always use the highest version found in the first remote server or the local cache """ client = TestClient(default_server_user=True, light=True) # No conanfile is present - error client.run("require add hello", assert_error=True) assert "ERROR: Conanfile not found at" in client.out client.save({"conanfile.py": GenConanfile(name="app")}) # No requirements define client.run("require add", assert_error=True) assert "ERROR: You need to add any requires, tool_requires or test_requires." in client.out # No remote recipe "hello" exists client.run("require add hello") assert "ERROR: Recipe hello not found." in client.out hello_lib = GenConanfile(name="hello") client.save({"hello/conanfile.py": hello_lib}) client.run("create hello --version=1.0") client.run("create hello --version=2.0") client.run("create hello --version=3.0") client.run("upload * --confirm -r default") # Save a normal requires "hello" client.run("require add hello") assert "Added 'hello/[>=3.0 <4]' as a new requires." in client.out content = client.load("conanfile.py") assert 'self.requires("hello/[>=3.0 <4]")' in content # Checking that it works client.run("install .") expected = textwrap.dedent("""\ Resolved version ranges hello/[>=3.0 <4]: hello/3.0 """) assert expected in client.out # Let's add the same "hello" but now as tool_requires and test_requires client.run("require add --tool=hello --test=hello") # [tool|test]_requires assert "Added 'hello/[>=3.0 <4]' as a new tool_requires." in client.out assert "Added 'hello/[>=3.0 <4]' as a new test_requires." in client.out # Try to add them again - does nothing and shows a warning client.run("require add hello --tool=hello --test=hello") assert "The requires hello is already in use." in client.out assert "The tool_requires hello is already in use." in client.out assert "The test_requires hello is already in use." in client.out # Using only the local cache bye_lib = GenConanfile(name="bye") client.save({"bye/conanfile.py": bye_lib}) client.run("create bye --version=1.0") client.run("create bye --version=2.0") client.run("require add bye --no-remote") # from cache assert "Added 'bye/[>=2.0 <3]' as a new requires." in client.out # Using a specific version (it does not look for it neither locally nor remotely) client.run("require add mylib/1.2") assert "Added 'mylib/[>=1.2 <2]' as a new requires." in client.out # Using commit as a version client.run("require add other/cci.20203034") # can not bump the version, won't use vrange assert "Added 'other/cci.20203034' as a new requires." in client.out def test_remove_require(): client = TestClient(light=True) # No conanfile is present - error client.run("require remove hello", assert_error=True) assert "ERROR: Conanfile not found at" in client.out client.save({"conanfile.py": GenConanfile(name="app")}) # No requirement "hello" declared client.run("require remove hello") assert "WARN: The requires hello is not declared in your conanfile." in client.out client.save({"conanfile.py": GenConanfile(name="app") .with_requirement("hello/1.2") .with_tool_requirement("hello/1.2") .with_test_requirement("hello/1.2")}) client.run("require remove hello --tool=hello --test=hello") assert "Removed hello dependency as requires." in client.out assert "Removed hello dependency as tool_requires." in client.out assert "Removed hello dependency as test_requires." in client.out content = client.load("conanfile.py") assert 'self.requires("hello/1.2"' not in content assert 'self.tool_requires("hello/1.2"' not in content assert 'self.test_requires("hello/1.2"' not in content client.run("require remove hello --tool=hello --test=hello") assert "WARN: The requires hello is not declared in your conanfile." in client.out assert "WARN: The tool_requires hello is not declared in your conanfile." in client.out assert "WARN: The test_requires hello is not declared in your conanfile." in client.out ================================================ FILE: test/integration/command/source_test.py ================================================ import os import re import textwrap from collections import OrderedDict import pytest from conan.internal.paths import CONANFILE from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer class TestSource: def test_local_flow_patch(self): # https://github.com/conan-io/conan/issues/2327 conanfile = """from conan import ConanFile from conan.tools.files import save, load import os class TestexportConan(ConanFile): name = "test" version = "0.1" exports = "mypython.py" exports_sources = "patch.patch" def source(self): save(self, "hello/hello.h", "my hello header!") patch = os.path.join(self.source_folder, "patch.patch") self.output.info("PATCH: %s" % load(self, patch)) header = os.path.join(self.source_folder, "hello/hello.h") self.output.info("HEADER: %s" % load(self, header)) python = os.path.join(self.recipe_folder, "mypython.py") self.output.info("PYTHON: %s" % load(self, python)) """ client = TestClient(light=True) client.save({"conanfile.py": conanfile, "patch.patch": "mypatch", "mypython.py": "mypython"}) client.run("source .") assert "conanfile.py (test/0.1): PATCH: mypatch" in client.out assert "conanfile.py (test/0.1): HEADER: my hello header!" in client.out assert "conanfile.py (test/0.1): PYTHON: mypython" in client.out client.run("create . ") assert "test/0.1: PATCH: mypatch" in client.out assert "test/0.1: HEADER: my hello header!" in client.out assert "test/0.1: PYTHON: mypython" in client.out def test_apply_patch(self): # https://github.com/conan-io/conan/issues/2327 # Test if a patch can be applied in source() both in create # and local flow client = TestClient(light=True) conanfile = """from conan import ConanFile from conan.tools.files import load import os class Pkg(ConanFile): exports_sources = "*" def source(self): patch = os.path.join(self.source_folder, "mypatch") self.output.info("PATCH: %s" % load(self, patch)) """ client.save({"conanfile.py": conanfile, "mypatch": "this is my patch"}) client.run("source .") assert "PATCH: this is my patch" in client.out client.run("source .") assert "PATCH: this is my patch" in client.out client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "PATCH: this is my patch" in client.out def test_source_warning_os_build(self): # https://github.com/conan-io/conan/issues/2368 conanfile = '''from conan import ConanFile class ConanLib(ConanFile): pass ''' client = TestClient(light=True) client.save({CONANFILE: conanfile}) client.run("source .") assert "This package defines both 'os' and 'os_build'" not in client.out def test_source_with_path_errors(self): client = TestClient(light=True) client.save({"conanfile.txt": "contents"}, clean_first=True) # Path with conanfile.txt client.run("source conanfile.txt", assert_error=True) assert "A conanfile.py is needed, %s is not acceptable" \ % os.path.join(client.current_folder, "conanfile.txt") in client.out def test_source_local_cwd(self): conanfile = ''' import os from conan import ConanFile class ConanLib(ConanFile): name = "hello" version = "0.1" def source(self): self.output.info("Running source!") self.output.info("cwd=>%s" % os.getcwd()) ''' client = TestClient(light=True) client.save({CONANFILE: conanfile}) client.run("install .") client.run("source .") assert "conanfile.py (hello/0.1): Calling source()" in client.out assert "conanfile.py (hello/0.1): cwd=>%s" % client.current_folder in client.out def test_local_source(self): conanfile = ''' from conan import ConanFile from conan.tools.files import save class ConanLib(ConanFile): def source(self): self.output.info("Running source!") err save(self, "file1.txt", "Hello World") ''' # First, failing source() client = TestClient(light=True) client.save({CONANFILE: conanfile}) client.run("source .", assert_error=True) assert "conanfile.py: Running source!" in client.out assert "ERROR: conanfile.py: Error in source() method, line 9" in client.out # Fix the error and repeat client.save({CONANFILE: conanfile.replace("err", "")}) client.run("source") assert "conanfile.py: Calling source() in" in client.out assert "conanfile.py: Running source!" in client.out assert "Hello World" == client.load("file1.txt") def test_local_source_layout_root(self): # Ensure that if a root folder is specified Conan source method # uses it. This is especially important when running with # no_copy_source where both the build() and source() methods # need to reference source_folder (so the value must be the # same in both methods) conanfile = textwrap.dedent(''' from conan import ConanFile from conan.tools.files import save, load class ConanLib(ConanFile): def layout(self): self.folders.root = ".." self.folders.source = "src" def source(self): self.output.info(f"In the source() method the Source folder is: {self.source_folder}") save(self, "source_file.c", "conent") def build(self): self.output.info(f"In the build() method the Source folder is: {self.source_folder}") load(self, f"{self.source_folder}/source_file.c") ''') client = TestClient(light=True) client.save({CONANFILE: conanfile}) client.run("source .") # The build method will fail if source() and build() don't # both use the correct source_folder client.run("build .") assert "conanfile.py: Calling build()" in client.out # doesn't fail def test_retrieve_exports_sources(self): # For Conan 2.0 if we install a package from a remote and we want to upload to other # remote we need to download the sources, as we consider revisions immutable, let's # iterate through the remotes to get the sources from the first match servers = OrderedDict() for index in range(2): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) client = TestClient(servers=servers, inputs=3*["user", "password"]) client.save({"conanfile.py": GenConanfile().with_exports_sources("*"), "sources.cpp": "sources"}) client.run("create . --name=hello --version=0.1") rrev = client.exported_recipe_revision() client.run("upload hello/0.1 -r server0") # Ensure we uploaded it assert re.search(r"Uploading recipe 'hello/0.1#.*' \(.*\)", client.out) assert re.search(r"Uploading package 'hello/0.1#.*' \(.*\)", client.out) client.run("remove * -c") # install from server0 that has the sources, upload to server1 (does not have the package) # download the sources from server0 client.run("install --requires=hello/0.1@ -r server0") client.run("upload hello/0.1 -r server1") assert "Sources downloaded from 'server0'" in client.out # install from server1 that has the sources, upload to server1 # Will not download sources, revision already in server client.run("remove * -c") client.run("install --requires=hello/0.1@ -r server1") client.run("upload hello/0.1 -r server1") assert f"'hello/0.1#{rrev}' already in server, skipping upload" in client.out assert "Sources downloaded from 'server0'" not in client.out # install from server0 and build # download sources from server0 client.run("remove * -c") client.run("install --requires=hello/0.1@ -r server0 --build='*'") assert "Sources downloaded from 'server0'" in client.out def test_source_method_called_once(self): """ Test that the source() method will be executed just once, and the source code will be shared for all the package builds. """ conanfile = textwrap.dedent(''' import os from conan import ConanFile from conan.tools.files import save class ConanLib(ConanFile): def source(self): save(self, os.path.join(self.source_folder, "main.cpp"), "void main() {}") self.output.info("Running source!") ''') client = TestClient() client.save({CONANFILE: conanfile}) client.run("create . --name=lib --version=1.0") assert "Running source!" in client.out client.run("create . --name=lib --version=1.0") assert "Running source!" not in client.out client.run("create . --name=lib --version=1.0 -s build_type=Debug") assert "Running source!" not in client.out def test_source_method_called_again_if_left_dirty(self): """ If we fail in retreiving sources make sure the source() method will be called next time we create """ conanfile = textwrap.dedent(''' import os from conan import ConanFile class ConanLib(ConanFile): def source(self): self.output.info("Running source!") assert False ''') client = TestClient(light=True) client.save({CONANFILE: conanfile}) client.run("create . --name=lib --version=1.0", assert_error=True) assert "Running source!" in client.out client.run("create . --name=lib --version=1.0", assert_error=True) assert "Running source!" in client.out assert "Source folder is corrupted, forcing removal" in client.out class TestSourceWithoutDefaultProfile: # https://github.com/conan-io/conan/issues/12371 @pytest.fixture() def client(self): c = TestClient() # The ``source()`` should still receive necessary configuration c.save_home({"global.conf": "tools.files.download:retry=MYCACHE"}) # Make sure we don't have default profile os.remove(os.path.join(c.cache_folder, "profiles", "default")) return c def test_source_without_default_profile(self, client): conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def source(self): c = self.conf.get("tools.files.download:retry") self.output.info("CACHE:{}!!".format(c)) """) client.save({"conanfile.py": conanfile}) client.run("source .") assert "conanfile.py: Calling source()" in client.out assert "CACHE:MYCACHE!!" in client.out def test_source_with_layout(self, client): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def layout(self): cmake_layout(self) def source(self): c = self.conf.get("tools.files.download:retry") self.output.info("CACHE:{}!!".format(c)) """) client.save({"conanfile.py": conanfile}) client.run("source .") assert "conanfile.py: Calling source()" in client.out assert "CACHE:MYCACHE!!" in client.out def test_source_python_requires(): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pytool", "0.1")}) c.run("export . ") c.run("upload * -r=default -c") c.run("remove * -c") c.save({"conanfile.py": GenConanfile().with_python_requires("pytool/0.1")}, clean_first=True) c.run("source . ") assert "pytool/0.1: Not found in local cache, looking in remotes" in c.out assert "pytool/0.1: Downloaded recipe" in c.out @pytest.mark.parametrize("attribute", ["info", "settings", "options"]) def test_source_method_forbidden_attributes(attribute): conanfile = textwrap.dedent(f""" from conan import ConanFile class Package(ConanFile): def source(self): self.output.info(self.{attribute}) """) client = TestClient(light=True) client.save({"conanfile.py": conanfile}) client.run("source .", assert_error=True) assert f"'self.{attribute}' access in 'source()' method is forbidden" in client.out ================================================ FILE: test/integration/command/test_audit.py ================================================ import json import os import platform import sys from contextlib import contextmanager import pytest from unittest.mock import patch, MagicMock from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.env import environment_update from conan.test.utils.tools import TestClient _sbom_zlib_1_2_11 = """ { "components" : [ { "author" : " ", "bom-ref" : "pkg:conan/zlib@1.2.11?rref=6754320047c5dd54830baaaf9fc733c4", "description" : "", "licenses" : [ { "license" : { "name" : "" } } ], "name" : "zlib", "purl" : "pkg:conan/zlib@1.2.11", "type" : "library", "version" : "1.2.11" } ], "dependencies" : [ { "ref" : "pkg:conan/zlib@1.2.11?rref=6754320047c5dd54830baaaf9fc733c4" } ], "metadata" : { "component" : { "author" : " ", "bom-ref" : "pkg:conan/zlib@1.2.11?rref=6754320047c5dd54830baaaf9fc733c4", "name" : "zlib/1.2.11", "type" : "library" }, "timestamp" : "2025-06-10T08:13:11Z", "tools" : [ { "externalReferences" : [ { "type" : "website", "url" : "https://github.com/conan-io/conan" } ], "name" : "Conan-io" } ] }, "serialNumber" : "urn:uuid:4f7ce240-4c6d-4a87-bfb6-78d0fbc839e3", "bomFormat" : "CycloneDX", "specVersion" : "1.4", "version" : 1 } """ @contextmanager def proxy_response(status, data, retry_after=60): with patch("conan.api.conan_api.ConanAPI._ApiHelpers.requester") as conanRequesterMock: return_status = MagicMock() return_status.status_code = status return_status.json = MagicMock(return_value=data) return_status.headers = {"retry-after": retry_after} conanRequesterMock.post = MagicMock(return_value=return_status) yield conanRequesterMock, return_status def test_conan_audit_proxy(): successful_response = { "data": { "query": { "vulnerabilities": { "totalCount": 1, "edges": [ { "node": { "name": "CVE-2023-45853", "description": "Zip vulnerability" + "a" * 90, # Force wrapping "severity": "Critical", "cvss": { "preferredBaseScore": 8.9 }, "aliases": [ "CVE-2023-45853", "JFSA-2023-000272529" ], "advisories": [ { "name": "CVE-2023-45853" }, { "name": "JFSA-2023-000272529" } ], "references": [ "https://pypi.org/project/pyminizip/#history", ] } } ] } } }, "error": None } tc = TestClient(light=True, default_server_user=True) tc.save({"conanfile.py": GenConanfile("zlib", "1.2.11"), "sbom.cdx.json": _sbom_zlib_1_2_11}) tc.run("create . --lockfile-out=conan.lock") tc.run("upload * -c -r=default") tc.run("list * -r=default -f=json", redirect_stdout="pkglist_remote.json") tc.run("list '*' -f=json", redirect_stdout="pkglist.json") tc.run("audit list zlib/1.2.11", assert_error=True) assert "Authentication required for the CVE provider: 'conancenter" in tc.out tc.run("audit provider auth conancenter --token=valid_token") with proxy_response(200, successful_response): tc.run("audit list zlib/1.2.11") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list -l=pkglist.json") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list -l=pkglist_remote.json -r=default") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list --lockfile=conan.lock") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list --sbom=sbom.cdx.json") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit scan --requires=zlib/1.2.11") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.save({"conanfile.txt": "[requires]\nzlib/1.2.11\n"}, clean_first=True) tc.run("audit scan") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit scan . --requires=zlib/1.2.11", assert_error=True) assert "--requires and --tool-requires arguments are incompatible with [path] '.' argument" in tc.out tc.run("audit list zlib/1.2.11 -f=html") assert "CVE-2023-45853" in tc.out # Now some common errors, like rate limited or missing lib, but it should not fail! with proxy_response(429, {"error": "Rate limit exceeded"}): tc.run("audit list zlib/1.2.11", assert_error=True) assert "You have exceeded the number of allowed requests" in tc.out assert "The limit will reset in 1 minute" in tc.out with proxy_response(429, {"error": "Rate limit exceeded"}, retry_after=2 * 3600): tc.run("audit list zlib/1.2.11", assert_error=True) assert "You have exceeded the number of allowed requests" in tc.out assert "The limit will reset in 2 hours and 0 minute" in tc.out with proxy_response(400, {"error": "Not found"}): # Not finding a package should not be an error tc.run("audit list zlib/1.2.11") assert "Package 'zlib/1.2.11' not scanned: Not found." in tc.stdout with proxy_response(403, {"error": "Error not shown"}): tc.run("audit list zlib/1.2.11", assert_error=True) assert "ERROR: Authentication error (403)" in tc.out assert "Error not shown" not in tc.out with proxy_response(500, {"error": "Internal error"}): tc.run("audit list zlib/1.2.11", assert_error=True) assert "Internal server error (500)" in tc.out with proxy_response(405, {"error": "Method not allowed"}): tc.run("audit list zlib/1.2.11", assert_error=True) assert "Error in zlib/1.2.11 (405)" in tc.out tc.run("audit provider add myprivate --url=foo --type=private --token=valid_token") tc.run("audit provider list") assert "(type: conan-center-proxy)" in tc.out assert "(type: private)" in tc.out tc.run("audit provider remove conancenter") tc.run("audit list zlib/1.2.11", assert_error=True) assert ("ERROR: Provider 'conancenter' not found. Please specify a valid provider name or add " "it using: 'conan audit provider add conancenter --url=https://audit.conan.io/ " "--type=conan-center-proxy --token='") in tc.out assert "If you don't have a valid token, register at: https://audit.conan.io/register." in tc.out if platform.system() != "Windows": providers_stat = os.stat(os.path.join(tc.cache_folder, "audit_providers.json")) # Assert that only the current user can read/write the file assert providers_stat.st_uid == os.getuid() assert providers_stat.st_gid == os.getgid() assert providers_stat.st_mode & 0o777 == 0o600 def test_conan_audit_private(): successful_response = { "data": { "query": { "vulnerabilities": { "totalCount": 1, "edges": [ { "node": { "name": "CVE-2023-45853", "description": "Zip vulnerability", "severity": "Critical", "cvss": { "preferredBaseScore": 8.9 }, "aliases": [ "CVE-2023-45853", "JFSA-2023-000272529" ], "withdrawn": True, "publishedAt": "Yesterday", "advisories": [ { "name": "CVE-2023-45853", "shortDescription": "Zip vulnerability (CVE)", "severity": "Critical" }, { "name": "JFSA-2023-000272529", "shortDescription": "Zip vulnerability (JFSA)", "severity": "Moderate", "impactReasons": [ {"name": "Reason 1", "isPositive": True}, {"name": "Reason 2", "isPositive": False} ] } ], "references": [ "https://pypi.org/project/pyminizip/#history", ], "vulnerablePackages": { "totalCount": 1, "edges": [{ "node": {"fixVersions": [{"version": "1.2.12"}]} }] } } } ] } } } } tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("zlib", "1.2.11"), "sbom.cdx.json": _sbom_zlib_1_2_11}) tc.run("create . --lockfile-out=conan.lock") tc.run("list '*' -f=json", redirect_stdout="pkglist.json") # TODO: If the CLI does not allow tokenless provider, should this case not be handled? tc.run("audit provider add myprivate --url=foo --type=private --token=f") # Now, remove the token as if the user didn't set it manually in the json providers = json.loads(tc.load_home("audit_providers.json")) providers["myprivate"].pop("token", None) tc.save_home({"audit_providers.json": json.dumps(providers)}) tc.run("audit list zlib/1.2.11 -p=myprivate", assert_error=True) assert "Missing authentication token for 'myprivate' provider" in tc.out tc.run("audit provider auth myprivate --token=valid_token") with proxy_response(200, successful_response): tc.run("audit list zlib/1.2.11 -p=myprivate") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list -l=pkglist.json -p=myprivate") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list --lockfile=conan.lock -p=myprivate") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list --sbom=sbom.cdx.json -p=myprivate") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit scan --requires=zlib/1.2.11 -p=myprivate") assert "zlib/1.2.11 1 vulnerability found" in tc.out tc.run("audit list zlib/1.2.11 -p=myprivate -f=html") assert "CVE-2023-45853" in tc.out assert "Yesterday" in tc.out assert "[WITHDRAWN]" in tc.out # Fixed version assert "1.2.12" in tc.out assert "Zip vulnerability (JFSA)" in tc.out assert 'inherit;">Reason 1' in tc.out # Positive impact assert 'red;">Reason 2' in tc.out # Negative impact # Now some common errors, like rate limited or missing lib, but it should not fail! with proxy_response(400, {"errors": [{"message": "Ref not found"}]}): # Not finding a package should not be an error tc.run("audit list zlib/1.2.11 -p=myprivate") assert "Package 'zlib/1.2.11' not scanned: Not found." in tc.stdout with proxy_response(403, {"errors": [{"message": "Authentication error"}]}): tc.run("audit list zlib/1.2.11 -p=myprivate") assert "Unknown error" in tc.out with proxy_response(500, {"errors": [{"message": "Internal error"}]}): tc.run("audit list zlib/1.2.11 -p=myprivate") assert "Unknown error" in tc.out with proxy_response(405, {"errors": [{"message": "Method not allowed"}]}): tc.run("audit list zlib/1.2.11 -p=myprivate") assert "Unknown error" in tc.out with proxy_response(404, {"errors": [{"message": "Not found"}]}): tc.run("audit list zlib/1.2.11 -p=myprivate") assert "An error occurred while connecting to the 'myprivate' provider" in tc.out @pytest.mark.skipif(sys.version_info < (3, 10), reason="Strict Base64 validation introduced in Python 3.10") def test_conan_audit_corrupted_token(): tc = TestClient(light=True) json_path = os.path.join(tc.cache_folder, "audit_providers.json") with open(json_path, "r") as f: data = json.load(f) # this is not a valid base64 string and will raise an exception data["conancenter"]["token"] = "corrupted_token" with open(json_path, "w") as f: json.dump(data, f) tc.run("audit list zlib/1.2.11", assert_error=True) assert "Invalid token format for provider 'conancenter'. The token might be corrupt." in tc.out def test_audit_list_conflicting_args(): tc = TestClient(light=True) tc.save({"pkglist.json": '{"Local Cache": {"zlib/1.2.11": {}}}'}) tc.run("audit list zlib/1.2.11 -l=pkglist.json", assert_error=True) assert "argument -l/--list: not allowed with argument reference" in tc.out def test_audit_provider_add_missing_url(): tc = TestClient(light=True) tc.run("audit provider add myprivate --type=private --token=valid_token", assert_error=True) assert "Name, URL and type are required to add a provider" in tc.out def test_audit_provider_remove_nonexistent(): tc = TestClient(light=True) tc.run("audit provider remove nonexistingprovider", assert_error=True) assert "Provider 'nonexistingprovider' not found" in tc.out def test_audit_list_missing_arguments(): tc = TestClient(light=True) tc.run("audit list", assert_error=True) assert "one of the arguments reference" in tc.out def test_audit_provider_env_credentials_with_proxy(monkeypatch): tc = TestClient(light=True) # Authenticate the provider with an old token to verify that the env variable overrides it tc.run("audit provider auth conancenter --token=old_token") captured_headers = {} def fake_post(url, headers, json): # noqa # Capture the headers used in the request captured_headers.update(headers) response = MagicMock() response.status_code = 200 response.json.return_value = { "data": {"query": {"vulnerabilities": {"totalCount": 0, "edges": []}}} } response.headers = {"retry-after": 60} return response with environment_update({"CONAN_AUDIT_PROVIDER_TOKEN_CONANCENTER": "env_token_value"}): with patch("conan.api.conan_api.ConanAPI._ApiHelpers.requester", new_callable=MagicMock) as requester_mock: requester_mock.post = fake_post tc.run("audit list zlib/1.2.11") # Verify that the Authorization header uses the token from the environment variable assert captured_headers.get("Authorization") == "Bearer env_token_value" def test_audit_global_error_exception(): """ Test that a global error returned by the provider results in ConanException raised by the formatter, using the 'details' field. """ tc = TestClient(light=True) tc.run("audit provider auth conancenter --token=valid_token") mock_provider_result = { "data": {}, "conan_error": "Fatal error." } with patch("conan.api.conan_api.AuditAPI.list", return_value=mock_provider_result): tc.run("audit list zlib/1.2.11", assert_error=True) assert "ERROR: Fatal error." in tc.out tc.run("audit list zlib/1.2.11 -f json", assert_error=True) assert "ERROR: Fatal error." in tc.out tc.run("audit list zlib/1.2.11 -f html", assert_error=True) assert "ERROR: Fatal error." in tc.out @pytest.mark.parametrize("severity_level, threshold, should_fail", [ (1.0, None, False), (8.9, None, False), (9.0, None, True), (9.1, None, True), (5.0, 5.1, False), (5.1, 5.1, True), (5.2, 5.1, True), (9.0, 11.0, False), ]) def test_audit_scan_threshold_error(severity_level, threshold, should_fail): """In case the severity level is equal or higher than the found for a CVE, the command should output the information as usual, and exit with non-success code error. """ successful_response = { "data": { "query": { "vulnerabilities": { "totalCount": 1, "edges": [ { "node": { "name": "CVE-2023-45853", "description": "Zip vulnerability", "severity": "Critical", "cvss": { "preferredBaseScore": severity_level }, "aliases": [ "CVE-2023-45853", "JFSA-2023-000272529" ], "advisories": [ { "name": "CVE-2023-45853" }, { "name": "JFSA-2023-000272529" } ], "references": [ "https://pypi.org/project/pyminizip/#history", ] } } ] } } } } tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("foobar", "0.1.0")}) tc.run("export .") tc.run("audit provider auth conancenter --token=valid_token") with proxy_response(200, successful_response): severity_param = "" if threshold is None else f"-sl {threshold}" tc.run(f"audit scan --requires=foobar/0.1.0 {severity_param}", assert_error=should_fail) assert "foobar/0.1.0 1 vulnerability found" in tc.out assert f"CVSS: {severity_level}" in tc.out if should_fail: if threshold is None: threshold = "9.0" assert f"ERROR: The package foobar/0.1.0 has a CVSS score {severity_level} and exceeded the threshold severity level {threshold}" in tc.out def test_parse_error_crash_when_no_edges(): from conan.cli.commands.audit import _parse_error_threshold scan_result = { "data": { # this used to crash because dav1d not having vulnerabilities field "dav1d/1.4.3": {"error": {"details": "Package 'dav1d/1.4.3' not scanned: Not found."}}, "zlib/1.2.11": { "vulnerabilities": { "totalCount": 1, "edges": [ {"node": {"cvss": {"preferredBaseScore": 7.0}}} ] } } } } _parse_error_threshold(scan_result, error_level=5.0) assert "conan_error" in scan_result assert "zlib/1.2.11" in scan_result["conan_error"] assert "7.0" in scan_result["conan_error"] @pytest.mark.parametrize("package_context", ["build", "host"]) @pytest.mark.parametrize("filter_context", ["build", "host", None]) def test_audit_scan_context_filter(package_context, filter_context): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("zlib", "1.2.11")}) tc.run("export .") tc.run("audit provider auth conancenter --token=valid_token") requires = "requires" if package_context == "host" else "tool-requires" context = "" if filter_context is None else f"--context={filter_context}" with proxy_response(200, {}): tc.run(f"audit scan --{requires}=zlib/1.2.11 {context}") if filter_context is None or filter_context == package_context: assert "Requesting vulnerability info for: zlib/1.2.11" in tc.out else: assert "Requesting vulnerability info for: zlib/1.2.11" not in tc.out class TestAuditApiBranchouts: def test_audit_load_provider_default(self): tc = TestClient(light=True) tc.run("audit provider list -f=json", redirect_stdout="before.json") os.unlink(os.path.join(tc.cache_folder, "audit_providers.json")) tc.run("audit provider list -f=json", redirect_stdout="after.json") before = json.loads(tc.load("before.json")) after = json.loads(tc.load("after.json")) assert after[0]["url"] == "https://audit.conan.io/" after[0]["url"] = before[0]["url"] # And the rest is the same assert before == after def test_audit_provider_add_duplicate(self): tc = TestClient(light=True) tc.run("audit provider add conancenter --url=foo --type=conan-center-proxy --token=valid_token", assert_error=True) assert "Provider 'conancenter' already exists" in tc.out class TestAuditScanBranchouts: def test_audit_scan_graph_error(self): tc = TestClient(light=True) tc.save({"bar/conanfile.py": GenConanfile("bar", "1.0"), "foo/conanfile.py": GenConanfile("foo", "1.0").with_provides("bar")}) tc.run("export bar") tc.run("export foo") tc.run("audit scan --requires=foo/1.0 --requires=bar/1.0", assert_error=True) assert "Provide Conflict" in tc.out class TestAuditListBranchouts: def test_audit_list_pkglist_empty(self): tc = TestClient(light=True) tc.save({"pkglist.json": '{"Local Cache": {}}'}) tc.run("audit provider auth conancenter --token=valid_token") tc.run("audit list -l=pkglist.json") assert "Nothing to list" in tc.out assert "Total vulnerabilities found: 0" in tc.out def test_audit_list_sbom_non_cyclone(self): tc = TestClient(light=True) tc.save({"sbom.json": '{"bomFormat": "SPDX"}'}) tc.run("audit list --sbom=sbom.json", assert_error=True) assert "Unsupported SBOM format, only CycloneDX is supported" in tc.out class TestAuditProviderBranchouts: def test_provider_json_format(self): tc = TestClient(light=True) tc.run("audit provider list -f=json", redirect_stdout="out.json") out = json.loads(tc.load("out.json")) assert len(out) == 1 def test_provider_add_spaces_in_name(self): tc = TestClient(light=True) tc.run('audit provider add "my private" --url=foo --type=private --token=valid_token', assert_error=True) assert "Name cannot contain spaces" in tc.out def test_provider_add_user_input_token(self): tc = TestClient(light=True, inputs=["valid_token"]) tc.run('audit provider add private --url=foo --type=private') providers = json.loads(tc.load_home("audit_providers.json")) assert providers["private"]["token"] == 'Z1RWYEZUWmBeT2U=' def test_provider_remove_no_name(self): tc = TestClient(light=True) tc.run('audit provider remove ""', assert_error=True) assert "Name required to remove a provider" in tc.out def test_provider_auth_no_name(self): tc = TestClient(light=True) tc.run('audit provider auth ""', assert_error=True) assert "Name is required to authenticate on a provider" in tc.out def test_provider_auth_user_input_token(self): tc = TestClient(light=True, inputs=["valid_token"]) tc.run('audit provider auth conancenter') providers = json.loads(tc.load_home("audit_providers.json")) assert providers["conancenter"]["token"] == 'Z1RWYEZUWmBeT2U=' ================================================ FILE: test/integration/command/test_build.py ================================================ import json from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_build_cmd_deploy_generators(): """ Test that "conan build --deployer/--generators" args work """ c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_settings("build_type") .with_requires("dep/1.0")}) c.run("create dep") c.run("build pkg --deployer=full_deploy --deployer-folder=./myfolder -g=CMakeDeps") cmake = c.load("pkg/dep-release-data.cmake") current_folder = c.current_folder.replace("\\", "/") path = f'{current_folder}/myfolder/full_deploy/host/dep/1.0' assert f'set(dep_PACKAGE_FOLDER_RELEASE "{path}")' in cmake def test_build_output_json(): """ The build command should have the --format=json option. """ _OUTPUT_FILE = "output.json" client = TestClient() conanfile = GenConanfile() client.save({"conanfile.py": conanfile}) client.run("build --format=json", redirect_stdout=_OUTPUT_FILE) output = json.loads(client.load(_OUTPUT_FILE)) assert "graph" in output assert "nodes" in output["graph"] ================================================ FILE: test/integration/command/test_forced_download_source.py ================================================ import json import os import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.fixture() def client(): c = TestClient(default_server_user=True) dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "0.1" def source(self): self.output.info("RUNNING SOURCE!!") """) c.save({"dep/conanfile.py": dep}) c.run("create dep") c.run("upload * -c -r=default") c.run("remove * -c") return c def test_install(client): client.run("install --requires=dep/0.1") assert "RUNNING SOURCE" not in client.out client.run("install --requires=dep/0.1 -c tools.build:download_source=True --format=json") assert "RUNNING SOURCE" in client.out graph = json.loads(client.stdout) zlib = graph["graph"]["nodes"]["1"] assert os.path.exists(zlib["source_folder"]) client.run("install --requires=dep/0.1 -c tools.build:download_source=True --format=json") assert "RUNNING SOURCE" not in client.out graph = json.loads(client.stdout) zlib = graph["graph"]["nodes"]["1"] assert os.path.exists(zlib["source_folder"]) def test_info(client): client.run("graph info --requires=dep/0.1") assert "RUNNING SOURCE" not in client.out # Even if the package is to be built, it shouldn't download sources unless the conf is defined client.run("graph info --requires=dep/0.1 --build=dep*") assert "RUNNING SOURCE" not in client.out client.run("graph info --requires=dep/0.1 -c tools.build:download_source=True") assert "RUNNING SOURCE" in client.out client.run("graph info --requires=dep/0.1 -c tools.build:download_source=True") assert "RUNNING SOURCE" not in client.out def test_info_editable(): """ graph info for editable shouldn't crash, but it also shoudn't do anythin # https://github.com/conan-io/conan/issues/15003 """ c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "0.1" def source(self): self.output.info("RUNNING SOURCE!!") """) c.save({"conanfile.py": dep}) c.run("editable add .") c.run("graph info --requires=dep/0.1") assert "RUNNING SOURCE" not in c.out c.run("graph info --requires=dep/0.1 -c tools.build:download_source=True") assert "RUNNING SOURCE" not in c.out # BUT it doesn't crash, it used to crash def test_build_editable_with_download_source(): """ conan build with -b=editable and tools.build:download_source=True should not crash # https://github.com/conan-io/conan/issues/19757 """ c = TestClient() liba = textwrap.dedent(""" from conan import ConanFile class LibA(ConanFile): name = "liba" version = "0.1" def source(self): self.output.info("RUNNING SOURCE!!") """) consumer = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): requires = "liba/0.1" """) c.save({"liba/conanfile.py": liba, "consumer/conanfile.py": consumer}) c.run("editable add liba") c.run("build consumer -b=editable -c tools.build:download_source=True") assert "RUNNING SOURCE" not in c.out ================================================ FILE: test/integration/command/test_graph_find_binaries.py ================================================ import json import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestFilterProfile: @pytest.fixture() def client(self): c = TestClient() c.save({"lib/conanfile.py": GenConanfile("lib", "1.0").with_settings("os", "build_type") .with_shared_option()}) c.run("create lib -s os=Linux") c.run("create lib -s os=Windows") c.run("create lib -s os=Windows -o *:shared=True") return c def test_exact_match(self, client): c = client c.run("graph explain --requires=lib/1.0 --missing=lib/1.0 -s os=Windows") expected = textwrap.dedent("""\ settings: Windows, Release options: shared=False diff explanation: This binary is an exact match for the defined inputs """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 --missing=lib/1.0 -s os=Windows --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["3d714b452400b3c3d6a964f42d5ec5004a6f22dc"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary is an exact match for the defined inputs" def test_settings_incomplete(self, client): c = client c.save({"windows": "[settings]\nos=Windows"}) c.run("graph explain --requires=lib/1.0 -pr windows") expected = textwrap.dedent("""\ settings: Windows, Release options: shared=False diff settings expected: build_type=None existing: build_type=Release explanation: This binary was built with different settings. """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -pr windows --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["3d714b452400b3c3d6a964f42d5ec5004a6f22dc"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {'existing': ['build_type=Release'], 'expected': ['build_type=None']} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary was built with different settings." def test_settings_with_option(self, client): c = client c.save({"windows": "[settings]\nos=Windows\n[options]\n*:shared=True"}) c.run("graph explain --requires=lib/1.0 -pr windows") expected = textwrap.dedent("""\ settings: Windows, Release options: shared=True diff settings expected: build_type=None existing: build_type=Release explanation: This binary was built with different settings. """) assert textwrap.indent(expected, " ") in c.out def test_different_option(self, client): # We find 1 closest match in Linux static c = client c.save({"linux": "[settings]\nos=Linux\nbuild_type=Release\n[options]\n*:shared=True"}) c.run("graph explain --requires=lib/1.0 -pr linux") expected = textwrap.dedent("""\ remote: Local Cache settings: Linux, Release options: shared=False diff options expected: shared=True existing: shared=False explanation: This binary was built with the same settings, but different options """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -pr linux --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["499989797d9192081b8f16f7d797b107a2edd8da"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {'existing': ['shared=False'], 'expected': ['shared=True']} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary was built with the same settings, " \ "but different options" def test_different_option_none(self): # We find 1 closest match in Linux static c = TestClient() c.save({"conanfile.py": GenConanfile("lib", "1.0").with_option("opt", [None, 1])}) c.run("create .") c.run("graph explain --requires=lib/1.0 -o *:opt=1") expected = textwrap.dedent("""\ remote: Local Cache diff options expected: opt=1 existing: opt=None explanation: This binary was built with the same settings, but different options """) assert textwrap.indent(expected, " ") in c.out c.run("remove * -c") c.run("create . -o *:opt=1") c.run("graph explain --requires=lib/1.0") expected = textwrap.dedent("""\ remote: Local Cache options: opt=1 diff options expected: opt=None existing: opt=1 explanation: This binary was built with the same settings, but different options """) assert textwrap.indent(expected, " ") in c.out def test_different_setting(self, client): # We find 1 closest match in Linux static c = client c.save({"windows": "[settings]\nos=Windows\nbuild_type=Debug\n[options]\n*:shared=True"}) c.run("graph explain --requires=lib/1.0 -pr windows") expected = textwrap.dedent("""\ remote: Local Cache settings: Windows, Release options: shared=True diff settings expected: build_type=Debug existing: build_type=Release explanation: This binary was built with different settings. """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -pr windows --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["c2dd2d51b5074bdb5b7d717929372de09830017b"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {'existing': ['build_type=Release'], 'expected': ['build_type=Debug']} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary was built with different settings." def test_different_settings_target(self): c = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "tool" version = "1.0" def package_id(self): self.info.settings_target = self.settings_target.copy() self.info.settings_target.constrained(["os"]) """) c.save({"conanfile.py": conanfile}) c.run("create . --build-require -s:b os=Windows -s:h os=Linux") c.run("graph explain --tool-requires=tool/1.0 -s:b os=Windows -s:h os=Macos") expected = textwrap.dedent("""\ remote: Local Cache settings_target: os=Linux diff settings_target expected: os=Macos existing: os=Linux explanation: This binary was built with different settings_target. """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --tool-requires=tool/1.0 -s:b os=Windows -s:h os=Macos --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["tool/1.0"]["revisions"] pkgs = revisions["4cc4b286a46dc2ed188d8c417eadb4e6"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["d66135125c07cc240b8d6adda090b76d60341205"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings_target"] == {'existing': ['os=Linux'], 'expected': ['os=Macos']} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary was built with different settings_target." def test_different_platform(self, client): # We find closest match in other platforms c = client c.save({"macos": "[settings]\nos=Macos\nbuild_type=Release\n[options]\n*:shared=True"}) c.run("graph explain --requires=lib/1.0 -pr macos") expected = textwrap.dedent("""\ remote: Local Cache settings: Windows, Release options: shared=True diff platform expected: os=Macos existing: os=Windows explanation: This binary belongs to another OS or Architecture, highly incompatible. """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -pr macos --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["c2dd2d51b5074bdb5b7d717929372de09830017b"] assert pkg1["diff"]["platform"] == {'existing': ['os=Windows'], 'expected': ['os=Macos']} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary belongs to another OS or Architecture, " \ "highly incompatible." def test_different_conf(self, client): # We find closest match in other platforms c = client c.save_home({"global.conf": "tools.info.package_id:confs=['user.foo:bar']"}) c.run("graph explain --requires=lib/1.0 -c user.foo:bar=42 -s os=Linux") expected = textwrap.dedent("""\ remote: Local Cache settings: Linux, Release options: shared=False diff confs expected: user.foo:bar=42 existing: user.foo:bar=None explanation: This binary has same settings, options and dependencies, but different confs """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -c user.foo:bar=42 -s os=Linux -f=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["499989797d9192081b8f16f7d797b107a2edd8da"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["confs"] == {"expected": ["user.foo:bar=42"], "existing": ["user.foo:bar=None"]} assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ "dependencies, but different confs" class TestMissingBinaryDeps: @pytest.fixture() def client(self): c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep").with_settings("os"), "lib/conanfile.py": GenConanfile("lib", "1.0").with_settings("os") .with_requires("dep/[>=1.0]")}) c.run("create dep --version=1.0 -s os=Linux") c.run("create lib -s os=Linux") c.run("create dep --version=2.0 -s os=Linux") return c def test_other_platform(self, client): c = client c.run("install --requires=lib/1.0 -s os=Windows", assert_error=True) assert "ERROR: Missing prebuilt package for 'dep/2.0', 'lib/1.0'" in c.out # We use the --missing=lib/1.0 to specify we want this binary and not dep/2.0 c.run("graph explain --requires=lib/1.0 --missing=lib/1.0 -s os=Windows") expected = textwrap.dedent("""\ settings: Linux requires: dep/1.Y.Z diff platform expected: os=Windows existing: os=Linux dependencies expected: dep/2.Y.Z existing: dep/1.Y.Z explanation: This binary belongs to another OS or Architecture, highly incompatible. """) assert textwrap.indent(expected, " ") in c.out def test_other_dependencies(self, client): c = client c.run("install --requires=lib/1.0 -s os=Linux", assert_error=True) assert "ERROR: Missing prebuilt package for 'lib/1.0'" in c.out c.run("graph explain --requires=lib/1.0 -s os=Linux") expected = textwrap.dedent("""\ settings: Linux requires: dep/1.Y.Z diff dependencies expected: dep/2.Y.Z existing: dep/1.Y.Z explanation: This binary has same settings and options, but different dependencies """) assert textwrap.indent(expected, " ") in c.out def test_different_python_requires(self): c = TestClient(light=True) c.save({"tool/conanfile.py": GenConanfile("tool"), "lib/conanfile.py": GenConanfile("lib", "1.0").with_python_requires("tool/[>=1.0]")}) c.run("create tool --version=1.0") c.run("create lib") c.run("create tool --version=2.0") c.run("graph explain --requires=lib/1.0") expected = textwrap.dedent("""\ remote: Local Cache python_requires: tool/1.0.Z diff python_requires expected: tool/2.0.Z existing: tool/1.0.Z explanation: This binary has same settings, options and dependencies, but different python_requires """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -s os=Linux --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["7bf17caa5bf9d2ed1dd8b337e9623fc0"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["5ccdb706197ca94edc0ecee9ef0d0b11b887d937"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["python_requires"] == {"expected": ["tool/2.0.Z"], "existing": ["tool/1.0.Z"]} assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ "dependencies, but different python_requires" def test_build_requires(self): c = TestClient(light=True) c.save_home({"global.conf": "core.package_id:default_build_mode=minor_mode"}) c.save({"tool/conanfile.py": GenConanfile("tool"), "lib/conanfile.py": GenConanfile("lib", "1.0").with_tool_requires("tool/[>=1.0]")}) c.run("create tool --version=1.0") c.run("create lib") c.run("create tool --version=2.0") c.run("graph explain --requires=lib/1.0") expected = textwrap.dedent("""\ remote: Local Cache build_requires: tool/1.0.Z diff build_requires expected: tool/2.0.Z existing: tool/1.0.Z explanation: This binary has same settings, options and dependencies, but different build_requires """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -s os=Linux --format=json") closest = json.loads(c.stdout)["closest_binaries"] revisions = closest["lib/1.0"]["revisions"] pkgs = revisions["4790f7f1561b52be5d39f0bc8e9acbed"]["packages"] assert len(pkgs) == 1 pkg1 = pkgs["c9d96a611b8c819f35728d58d743f6a78a1b5942"] assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["build_requires"] == {"expected": ["tool/2.0.Z"], "existing": ["tool/1.0.Z"]} assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ "dependencies, but different build_requires" def test_change_in_package_type(): tc = TestClient(light=True) tc.save({ "libc/conanfile.py": GenConanfile("libc", "1.0"), "libb/conanfile.py": GenConanfile("libb", "1.0") .with_requires("libc/1.0"), "liba/conanfile.py": GenConanfile("liba", "1.0") .with_requires("libb/1.0") }) tc.run("create libc") tc.run("create libb") tc.run("create liba") tc.save({ "libc/conanfile.py": GenConanfile("libc", "1.0") .with_package_type("application") }) tc.run("create libc") tc.run("create liba", assert_error=True) assert "Missing binary: libb/1.0" in tc.out tc.run("graph explain --requires=liba/1.0") # This fails, graph explain thinks everything is ok assert "explanation: This binary is an exact match for the defined inputs" not in tc.out def test_conf_difference_shown(): tc = TestClient(light=True) tc.save({ "libc/conanfile.py": GenConanfile("libc", "1.0"), "libb/conanfile.py": GenConanfile("libb", "1.0").with_requires("libc/1.0"), "liba/conanfile.py": GenConanfile("liba", "1.0").with_requires("libb/1.0") }) tc.save_home({"global.conf": "tools.info.package_id:confs=['user.foo:bar']"}) tc.run("create libc") tc.run("create libb") tc.run("create liba") tc.run("remove libc/*:* -c") tc.run("create libc -c user.foo:bar=42") tc.run("create liba", assert_error=True) assert "Missing prebuilt package for 'libc/1.0'" in tc.out tc.run("graph explain --requires=liba/1.0") assert "conf: user.foo:bar=42" in tc.out class TestDistance: def test_multiple_distance_ordering(self): tc = TestClient() tc.save({ "conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "dep/conanfile.py": GenConanfile("dep", "1.0") .with_option("shared", [True, False]) .with_option("fPIC", [True, False])}) tc.run("create dep -o shared=True -o fPIC=True") tc.run("create dep -o shared=True -o fPIC=False") tc.run('graph explain . -o *:shared=False -o *:fPIC=False') # We don't expect the further binary to show assert "a657a8fc96dd855e2a1c90a9fe80125f0c4635a0" not in tc.out # We expect the closer binary to show assert "a6923b987deb1469815dda84aab36ac34f370c48" in tc.out def test_no_binaries(): # https://github.com/conan-io/conan/issues/15819 c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") c.run("graph explain --requires=pkg/0.1") assert "ERROR: No package binaries exist" in c.out # The json is not an issue, it won't have anything as contents ================================================ FILE: test/integration/command/test_inspect.py ================================================ import json import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_basic_inspect(): t = TestClient() t.save({"foo/conanfile.py": GenConanfile().with_name("foo").with_shared_option()}) t.run("inspect foo/conanfile.py") lines = t.out.splitlines() assert lines == ['default_options:', ' shared: False', 'generators: []', 'label: ', 'languages: []', 'name: foo', 'options:', ' shared: False', 'options_definitions:', " shared: ['True', 'False']", 'package_type: None', 'requires: []', 'revision_mode: hash', 'vendor: False' ] def test_options_description(): t = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): options = {"shared": [True, False, None], "fpic": [True, False, None]} options_description = {"shared": "Some long explanation about shared option", "fpic": "Yet another long explanation of fpic"} """) t.save({"foo/conanfile.py": conanfile}) t.run("inspect foo/conanfile.py") assert "shared: Some long explanation about shared option" in t.out assert "fpic: Yet another long explanation of fpic" in t.out def test_missing_conanfile(): t = TestClient() t.run("inspect missing/conanfile.py", assert_error=True) assert "Conanfile not found at" in t.out def test_dot_and_folder_conanfile(): t = TestClient() t.save({"conanfile.py": GenConanfile().with_name("foo")}) t.run("inspect") assert 'name: foo' in t.out t.save({"foo/conanfile.py": GenConanfile().with_name("foo")}, clean_first=True) t.run("inspect foo") assert 'name: foo' in t.out def test_inspect_understands_setname(): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "arch" def set_name(self): self.name = "foo" def set_version(self): self.version = "1.0" """) tc.save({"conanfile.py": conanfile}) tc.run("inspect .") assert "foo" in tc.out assert "1.0" in tc.out def test_normal_inspect(): tc = TestClient() tc.run("new basic -d name=pkg -d version=1.0") tc.run("inspect .") assert tc.out.splitlines() == ['description: A basic recipe', 'generators: []', 'homepage: ', 'label: ', 'languages: []', 'license: ', 'name: pkg', 'options:', 'options_definitions:', 'package_type: None', 'requires: []', 'revision_mode: hash', 'vendor: False', 'version: 1.0'] def test_empty_inspect(): conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): pass""") tc = TestClient() tc.save({"conanfile.py": conanfile}) tc.run("inspect . -f json") def test_basic_new_inspect(): tc = TestClient() tc.run("new basic") tc.run("inspect . -f json") tc.run("new cmake_lib -d name=pkg -d version=1.0 -f") tc.run("inspect . -f json") def test_requiremens_inspect(): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "zlib/1.2.13" license = "MIT", "Apache" """) tc.save({"conanfile.py": conanfile}) tc.run("inspect .") assert ['generators: []', 'label: ', 'languages: []', "license: ['MIT', 'Apache']", 'options:', 'options_definitions:', 'package_type: None', "requires: [{'ref': 'zlib/1.2.13', 'require': 'zlib/1.2.13', 'run': False, " "'libs': True, 'skip': False, 'test': False, 'force': False, 'direct': True, 'build': " "False, 'transitive_headers': None, 'transitive_libs': None, 'headers': " "True, 'package_id_mode': None, 'visible': True}]", 'revision_mode: hash', 'vendor: False'] == tc.out.splitlines() def test_pythonrequires_remote(): tc = TestClient(default_server_user=True) pyrequires = textwrap.dedent(""" from conan import ConanFile class MyBase: def set_name(self): self.name = "my_company_package" class PyReq(ConanFile): name = "pyreq" version = "1.0" package_type = "python-require" """) tc.save({"pyreq/conanfile.py": pyrequires}) tc.run("create pyreq/") tc.run("upload pyreq/1.0 -r default") tc.run("search * -r default") assert "pyreq/1.0" in tc.out tc.run("remove * -c") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): python_requires = "pyreq/1.0" python_requires_extend = "pyreq.MyBase" def set_version(self): self.version = "1.0" """) tc.save({"conanfile.py": conanfile}) # Not specifying the remote also works tc.run("inspect .") assert "pyreq/1.0: Downloaded recipe revision 0ca726ab0febe1100901fffb27dc421f" in tc.out assert "name: my_company_package" in tc.out assert "version: 1.0" in tc.out # It now finds it on the cache, because it was downloaded tc.run("inspect . -nr") assert "name: my_company_package" in tc.out assert "version: 1.0" in tc.out assert "'recipe': 'Cache'" in tc.out tc.run("remove pyreq/* -c") # And now no remotes fails tc.run("inspect . -nr", assert_error=True) assert "Cannot resolve python_requires 'pyreq/1.0': No remote defined" in tc.out def test_serializable_inspect(): tc = TestClient() tc.save({"conanfile.py": GenConanfile("a", "1.0") .with_requires("b/2.0") .with_setting("os") .with_option("shared", [True, False]) .with_generator("CMakeDeps")}) tc.run("inspect . --format=json") assert json.loads(tc.out)["name"] == "a" ================================================ FILE: test/integration/command/test_new.py ================================================ import os import textwrap import pytest from conan import __version__ from conan.internal.util.files import save from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient class TestNewCommand: def test_new_cmake_lib(self): client = TestClient(light=True) client.run("new cmake_lib -d name=pkg -d version=1.3") conanfile = client.load("conanfile.py") assert "CMakeToolchain" in conanfile conanfile = client.load("test_package/conanfile.py") assert "CMakeToolchain" in conanfile cmake = client.load("test_package/CMakeLists.txt") assert "find_package" in cmake # Test at least it exports correctly client.run("export . --user=myuser --channel=testing") assert "pkg/1.3@myuser/testing" in client.out def test_new_cmake_exe(self): client = TestClient(light=True) client.run("new cmake_exe -d name=pkg -d version=1.3") conanfile = client.load("conanfile.py") assert "CMakeToolchain" in conanfile conanfile = client.load("test_package/conanfile.py") assert "def test(self):" in conanfile # Test at least it exports correctly client.run("export . --user=myuser --channel=testing") assert "pkg/1.3@myuser/testing" in client.out def test_new_basic_template(self): tc = TestClient(light=True) tc.run("new basic") assert '# self.requires("zlib/1.2.13")' in tc.load("conanfile.py") tc.run("new basic -d name=mygame -d requires=math/1.0 -d requires=ai/1.0 -f") conanfile = tc.load("conanfile.py") assert 'self.requires("math/1.0")' in conanfile assert 'self.requires("ai/1.0")' in conanfile assert 'name = "mygame"' in conanfile def test_new_basic_template_multi_arg(self): tc = TestClient(light=True) tc.run("new basic -d name=mygame -d requires=d1/1.0 -d requires=d2/1.0 -d requires=d3/1.0") conanfile = tc.load("conanfile.py") assert 'self.requires("d1/1.0")' in conanfile assert 'self.requires("d2/1.0")' in conanfile assert 'self.requires("d3/1.0")' in conanfile def test_new_defaults(self): c = TestClient(light=True) for t in ("cmake_lib", "cmake_exe", "meson_lib", "meson_exe", "msbuild_lib", "msbuild_exe", "bazel_lib", "bazel_exe", "autotools_lib", "autotools_exe"): c.save({}, clean_first=True) c.run(f"new {t}") conanfile = c.load("conanfile.py") assert 'name = "mypkg"' in conanfile assert 'version = "0.1"' in conanfile if t == "cmake_lib": cmake = c.load("CMakeLists.txt") assert "src/mypkg.cpp" in cmake cpp = c.load("src/mypkg.cpp") assert "mypkg()" in cpp hpp = c.load("include/mypkg.h") assert "void mypkg();" in hpp c.run(f"new {t} -d name=mylib -f") conanfile = c.load("conanfile.py") assert 'name = "mylib"' in conanfile assert 'version = "0.1"' in conanfile class TestNewCommandUserTemplate: @pytest.mark.parametrize("folder", ("mytemplate", "sub/mytemplate")) def test_user_template(self, folder): client = TestClient(light=True) template1 = textwrap.dedent(""" class Conan(ConanFile): name = "{{name}}" version = "{{version}}" conan_version = "{{conan_version}}" """) save(os.path.join(client.cache_folder, f"templates/command/new/{folder}/conanfile.py"), template1) client.run(f"new {folder} -d name=hello -d version=0.1") conanfile = client.load("conanfile.py") assert 'name = "hello"' in conanfile assert 'version = "0.1"' in conanfile assert 'conan_version = "{}"'.format(__version__) in conanfile def test_user_template_abs(self): tmp_folder = temp_folder() client = TestClient(light=True) template1 = textwrap.dedent(""" class Conan(ConanFile): name = "{{name}}" """) save(os.path.join(tmp_folder, "conanfile.py"), template1) client.run(f'new "{tmp_folder}" -d name=hello -d version=0.1') conanfile = client.load("conanfile.py") assert 'name = "hello"' in conanfile def test_user_template_filenames(self): client = TestClient(light=True) save(os.path.join(client.cache_folder, "templates/command/new/mytemplate/{{name}}"), "Hi!") client.run(f"new mytemplate -d name=pkg.txt") assert "Hi!" == client.load("pkg.txt") def test_skip_files(self): client = TestClient(light=True) template1 = textwrap.dedent(""" class Conan(ConanFile): name = "{{name}}" version = "{{version}}" conan_version = "{{conan_version}}" """) path = os.path.join(client.cache_folder, f"templates/command/new/mytemplate") save(os.path.join(path, "conanfile.py"), template1) save(os.path.join(path, "file.h"), "{{header}}") client.run(f"new mytemplate -d name=hello -d version=0.1 -d header=") assert not os.path.exists(os.path.join(client.current_folder, "file.h")) assert not os.path.exists(os.path.join(client.current_folder, "file.cpp")) client.run(f"new mytemplate -d name=hello -d version=0.1 -d header=xxx -f") assert os.path.exists(os.path.join(client.current_folder, "file.h")) assert not os.path.exists(os.path.join(client.current_folder, "file.cpp")) def test_template_image_files(self): """ problematic files that we dont want to render with Jinja, like PNG or other binaries, have to be explicitly excluded from render""" client = TestClient(light=True) template_dir = "templates/command/new/t_dir" png = "$&(){}{}{{}{}" save(os.path.join(client.cache_folder, template_dir, "myimage.png"), png) client.run("new t_dir -d name=hello -d version=0.1", assert_error=True) assert "TemplateSyntaxError" in client.out save(os.path.join(client.cache_folder, template_dir, "not_templates"), "*.png") client.run("new t_dir -d name=hello -d version=0.1") myimage = client.load("myimage.png") assert myimage == png assert not os.path.exists(os.path.join(client.current_folder, "not_templates")) client.run("new t_dir -d name=hello -d version=0.1", assert_error=True) assert "File 'myimage.png' already exists, and --force not defined, aborting" in client.out def test_wrong_argument(self): c = TestClient(light=True) save(os.path.join(c.cache_folder, f"templates/command/new/mytpl/conanfile.py"), "") c.run(f"new mytpl -d version", assert_error=True) assert "ERROR: Template definitions must be 'key=value', received 'version'" in c.out def test_missing_argument(self): c = TestClient(light=True) save(os.path.join(c.cache_folder, f"templates/command/new/mytpl/file.txt"), "{{myarg}}") c.run(f"new mytpl", assert_error=True) assert ("ERROR: Missing definitions for the template. " "Required definitions are: 'myarg'") in c.out class TestNewErrors: def test_template_errors(self): client = TestClient(light=True) client.run("new mytemplate", assert_error=True) assert "ERROR: Template doesn't exist" in client.out def test_forced(self): client = TestClient(light=True) client.run("new cmake_lib -d name=hello -d version=0.1") client.run("new cmake_lib -d name=hello -d version=0.1", assert_error=True) assert "ERROR: File 'CMakeLists.txt' already exists, and --force not defined" in client.out client.run("new cmake_lib -d name=bye -d version=0.2 --force") conanfile = client.load("conanfile.py") assert 'name = "bye"' in conanfile assert 'version = "0.2"' in conanfile def test_duplicated(self): client = TestClient(light=True) client.run("new cmake_lib -d name=hello -d name=0.1", assert_error=True) assert "ERROR: name argument can't be multiple: ['hello', '0.1']" in client.out # It will create a list and assign to it, but it will not fail ugly client.run("new cmake_lib -d name=pkg -d version=0.1 -d version=0.2", assert_error=True) assert "ERROR: version argument can't be multiple: ['0.1', '0.2']" in client.out def test_name_uppercase(self): client = TestClient(light=True) client.run("new cmake_lib -d name=Hello", assert_error=True) assert "ERROR: name argument must be lowercase: Hello" in client.out def test_new_change_folder(self): client = TestClient(light=True) client.run("new cmake_lib -d name=hello -d version=0.1 -o=myfolder") assert os.path.exists(os.path.join(client.current_folder, "myfolder", "conanfile.py")) ================================================ FILE: test/integration/command/test_outdated.py ================================================ import json from collections import OrderedDict import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer @pytest.fixture def create_libs(): tc = TestClient(default_server_user=True, light=True) tc.save({"conanfile.py": GenConanfile()}) tc.run("create . --name=zlib --version=1.0") tc.run("create . --name=zlib --version=2.0") tc.run("create . --name=foo --version=1.0") tc.run("create . --name=foo --version=2.0") tc.save({"conanfile.py": GenConanfile().with_requires("foo/[>=1.0]")}) tc.run("create . --name=libcurl --version=1.0") # Upload all created libs to the remote tc.run("upload * -c -r=default") tc.save({"conanfile.py": GenConanfile("app", "1.0") .with_requires("zlib/1.0", "libcurl/[>=1.0]")}) return tc def test_outdated_command(create_libs): tc = create_libs # Remove versions from cache so they are found as outdated tc.run("remove foo/2.0 -c") tc.run("remove zlib/2.0 -c") tc.run("graph outdated . --format=json") output = json.loads(tc.stdout) assert "zlib" in output assert "foo" in output assert "libcurl" not in output assert output["zlib"]["current_versions"] == ["zlib/1.0"] assert output["zlib"]["version_ranges"] == [] assert output["zlib"]["latest_remote"]["ref"] == "zlib/2.0" assert output["zlib"]["latest_remote"]["remote"].startswith("default") assert output["foo"]["current_versions"] == ["foo/1.0"] assert output["foo"]["version_ranges"] == ["foo/[>=1.0]"] assert output["foo"]["latest_remote"]["ref"] == "foo/2.0" assert output["foo"]["latest_remote"]["remote"].startswith("default") def test_recipe_with_lockfile(create_libs): tc = create_libs # Remove versions from cache so they are found as outdated tc.run("remove foo/2.0 -c") tc.run("remove zlib/2.0 -c") # Creating the lockfile sets foo/1.0 as only valid version for the recipe tc.run("lock create .") tc.run("graph outdated . --format=json") output = json.loads(tc.stdout) assert "foo" in output assert output["foo"]["current_versions"] == ["foo/1.0"] # Creating the lockfile makes the previous range obsolete assert output["foo"]["version_ranges"] == [] # Adding foo/2.0 to the lockfile forces the download so foo is no longer outdated tc.run("lock add --requires=foo/2.0") tc.run("graph outdated . --format=json") output = json.loads(tc.stdout) assert "foo" not in output def test_recipe_with_no_remote_ref(create_libs): tc = create_libs # Remove versions from cache so they are found as outdated tc.run("remove foo/2.0 -c") tc.run("remove zlib/2.0 -c") tc.run("remove libcurl -c") tc.run("graph outdated . --format=json") output = json.loads(tc.stdout) # Check that libcurl doesn't appear as there is no version in the remotes assert "zlib" in output assert "foo" in output assert "libcurl" not in output def test_cache_ref_newer_than_latest_in_remote(create_libs): tc = create_libs tc.run("remove foo/2.0 -c -r=default") tc.run("graph outdated . --format=json") output = json.loads(tc.stdout) # Check that foo doesn't appear because the version in cache is higher than the latest version # in remote assert "zlib" in output assert "libcurl" not in output assert "foo" not in output def test_two_remotes(): # remote1: zlib/1.0, libcurl/2.0, foo/1.0 # remote2: zlib/[1.0, 2.0], libcurl/1.0, foo/1.0 # local cache: zlib/1.0, libcurl/1.0, foo/1.0 servers = OrderedDict() for i in [1, 2]: test_server = TestServer() servers["remote%d" % i] = test_server tc = TestClient(servers=servers, inputs=2 * ["admin", "password"], light=True) tc.save({"conanfile.py": GenConanfile()}) tc.run("create . --name=zlib --version=1.0") tc.run("create . --name=foo --version=1.0") tc.run("create . --name=zlib --version=2.0") tc.save({"conanfile.py": GenConanfile().with_requires("foo/[>=1.0]")}) tc.run("create . --name=libcurl --version=1.0") tc.run("create . --name=libcurl --version=2.0") # Upload the created libraries 1.0 to remotes tc.run("upload zlib/1.0 -c -r=remote1") tc.run("upload libcurl/2.0 -c -r=remote1") tc.run("upload foo/1.0 -c -r=remote1") tc.run("upload zlib/* -c -r=remote2") tc.run("upload libcurl/1.0 -c -r=remote2") tc.run("upload foo/1.0 -c -r=remote2") # Remove from cache the 2.0 libraries tc.run("remove libcurl/2.0 -c") tc.run("remove zlib/2.0 -c") tc.save({"conanfile.py": GenConanfile("app", "1.0") .with_requires("zlib/1.0", "libcurl/[>=1.0]")}) tc.run("graph outdated . --format=json") output = json.loads(tc.stdout) assert "zlib" in output assert "libcurl" in output assert "foo" not in output assert output["libcurl"]["latest_remote"]["ref"].startswith("libcurl/2.0") assert output["libcurl"]["latest_remote"]["remote"].startswith("remote1") assert output["zlib"]["latest_remote"]["ref"].startswith("zlib/2.0") assert output["zlib"]["latest_remote"]["remote"].startswith("remote2") def test_duplicated_tool_requires(): tc = TestClient(default_server_user=True) # Create libraries needed to generate the dependency graph tc.save({"conanfile.py": GenConanfile()}) tc.run("create . --name=cmake --version=1.0") tc.run("create . --name=cmake --version=2.0") tc.run("create . --name=cmake --version=3.0") tc.save({"conanfile.py": GenConanfile().with_tool_requires("cmake/1.0")}) tc.run("create . --name=foo --version=1.0") tc.save({"conanfile.py": GenConanfile().with_tool_requires("cmake/[>=1.0]")}) tc.run("create . --name=bar --version=1.0") # Upload the created libraries to remote tc.run("upload * -c -r=default") tc.save({"conanfile.py": GenConanfile("app", "1.0") .with_requires("foo/1.0", "bar/1.0").with_tool_requires("cmake/[<=2.0]")}) tc.run("graph outdated . --format=json") output = json.loads(tc.stdout) assert sorted(output["cmake"]["current_versions"]) == ["cmake/1.0", "cmake/2.0", "cmake/3.0"] assert sorted(output["cmake"]["version_ranges"]) == ["cmake/[<=2.0]", "cmake/[>=1.0]"] assert output["cmake"]["latest_remote"]["ref"] == "cmake/3.0" def test_no_outdated_dependencies(): tc = TestClient(default_server_user=True) tc.save({"conanfile.py": GenConanfile()}) tc.run("create . --name=foo --version=1.0") tc.run("upload * -c -r=default") tc.run("graph outdated .") assert "No outdated dependencies in graph" in tc.stdout ================================================ FILE: test/integration/command/test_output.py ================================================ import json import os from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update class TestOutputLevel: def test_invalid_output_level(self): t = TestClient(light=True) t.save({"conanfile.py": GenConanfile("foo", "1.0")}) t.run("create . -vfooling", assert_error=True) assert "argument -v: invalid choice: 'fooling'" in t.out with environment_update({"CONAN_LOG_LEVEL": "fail"}): t.run("create .", assert_error=True) assert "Invalid argument '-vfail' defined in CONAN_LOG_LEVEL " \ "environment variable" in t.out def test_output_level(self): lines = ("self.output.trace('This is a trace')", "self.output.debug('This is a debug')", "self.output.verbose('This is a verbose')", "self.output.info('This is a info')", "self.output.highlight('This is a highlight')", "self.output.success('This is a success')", "self.output.warning('This is a warning')", "self.output.error('This is a error')", ) t = TestClient(light=True) t.save({"conanfile.py": GenConanfile("foo", "1.0").with_package(*lines)}) # By default, it prints > info t.run("create .") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" not in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # Check if -v argument is equal to VERBOSE level t.run("create . -v") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # Print also verbose traces t.run("create . -vverbose") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # Print also debug traces t.run("create . -vv") assert "This is a trace" not in t.out assert "This is a debug" in t.out assert "This is a verbose" in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out t.run("create . -vdebug") assert "This is a trace" not in t.out assert "This is a debug" in t.out assert "This is a verbose" in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # Print also "trace" traces t.run("create . -vvv") assert "This is a trace" in t.out assert "This is a debug" in t.out assert "This is a verbose" in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out t.run("create . -vtrace") assert "This is a trace" in t.out assert "This is a debug" in t.out assert "This is a verbose" in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # With notice t.run("create . -vstatus") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" not in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # With notice t.run("create . -vnotice") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" not in t.out assert "This is a info" not in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # With warnings t.run("create . -vwarning") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" not in t.out assert "This is a info" not in t.out assert "This is a highlight" not in t.out assert "This is a success" not in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # With errors t.run("create . -verror") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" not in t.out assert "This is a info" not in t.out assert "This is a highlight" not in t.out assert "This is a success" not in t.out assert "This is a warning" not in t.out assert "This is a error" in t.out def test_output_level_envvar(): lines = ("self.output.trace('This is a trace')", "self.output.debug('This is a debug')", "self.output.verbose('This is a verbose')", "self.output.info('This is a info')", "self.output.highlight('This is a highlight')", "self.output.success('This is a success')", "self.output.warning('This is a warning')", "self.output.error('This is a error')", ) t = TestClient(light=True) t.save({"conanfile.py": GenConanfile().with_package(*lines)}) # Check if -v argument is equal to VERBOSE level with environment_update({"CONAN_LOG_LEVEL": "verbose"}): t.run("create . --name foo --version 1.0") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" in t.out assert "This is a info" in t.out assert "This is a highlight" in t.out assert "This is a success" in t.out assert "This is a warning" in t.out assert "This is a error" in t.out # Check if -v argument is equal to VERBOSE level with environment_update({"CONAN_LOG_LEVEL": "error"}): t.run("create . --name foo --version 1.0") assert "This is a trace" not in t.out assert "This is a debug" not in t.out assert "This is a verbose" not in t.out assert "This is a info" not in t.out assert "This is a highlight" not in t.out assert "This is a success" not in t.out assert "This is a warning" not in t.out assert "This is a error" in t.out class TestWarningHandling: warning_lines = ("self.output.warning('Tagged warning', warn_tag='tag')", "self.output.warning('Untagged warning')") error_lines = ("self.output.error('Tagged error', error_type='exception')", "self.output.error('Untagged error')") def test_warning_as_error_deprecated_syntax(self): t = TestClient(light=True) t.save({"conanfile.py": GenConanfile("foo", "1.0").with_package(*self.warning_lines)}) t.save_home({"global.conf": "core:warnings_as_errors=[]"}) t.run("create . -vwarning") assert "WARN: Untagged warning" in t.out assert "WARN: tag: Tagged warning" in t.out t.save_home({"global.conf": "core:warnings_as_errors=['*']"}) t.run("create . -vwarning", assert_error=True) assert "ConanException: tag: Tagged warning" in t.out # We bailed early, didn't get a chance to print this one assert "Untagged warning" not in t.out t.save_home({"global.conf": """core:warnings_as_errors=['*']\ncore:skip_warnings=["tag"]"""}) t.run("create . -verror", assert_error=True) assert "ConanException: Untagged warning" in t.out assert "Tagged warning" not in t.out t.save_home({"global.conf": "core:warnings_as_errors=[]"}) t.run("create . -verror") assert "ERROR: Untagged warning" not in t.out assert "ERROR: tag: Tagged warning" not in t.out def test_skip_warnings(self): t = TestClient(light=True) t.save({"conanfile.py": GenConanfile("foo", "1.0").with_package(*self.warning_lines)}) t.save_home({"global.conf": "core:skip_warnings=[]"}) t.run("create . -vwarning") assert "WARN: Untagged warning" in t.out assert "WARN: tag: Tagged warning" in t.out t.save_home({"global.conf": "core:skip_warnings=['tag']"}) t.run("create . -vwarning") assert "WARN: Untagged warning" in t.out assert "WARN: tag: Tagged warning" not in t.out t.save_home({"global.conf": "core:skip_warnings=['unknown']"}) t.run("create . -vwarning") assert "WARN: Untagged warning" not in t.out assert "WARN: tag: Tagged warning" in t.out t.save_home({"global.conf": "core:skip_warnings=['unknown', 'tag']"}) t.run("create . -vwarning") assert "WARN: Untagged warning" not in t.out assert "WARN: tag: Tagged warning" not in t.out def test_exception_errors(self): t = TestClient(light=True) t.save({"conanfile.py": GenConanfile("foo", "1.0").with_package(*self.error_lines)}) t.save_home({"global.conf": "core:warnings_as_errors=[]"}) t.run("create .") assert "ERROR: Tagged error" in t.out assert "ERROR: Untagged error" in t.out t.save_home({"global.conf": "core:warnings_as_errors=['*']"}) t.run("create .", assert_error=True) assert "ERROR: Tagged error" in t.out assert "ConanException: Untagged error" in t.out t.run("create . -vquiet", assert_error=True) assert "ERROR: Tagged error" not in t.out assert "ConanException: Untagged error" not in t.out def test_formatter_redirection_to_file(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("config home --out-file=cmd_out.txt") assert "Formatted output saved to 'cmd_out.txt'" in c.out cmd_out = c.load("cmd_out.txt") assert f"{c.cache_folder}" in cmd_out assert not f"{c.cache_folder}" in c.stdout c.run("graph info . --format=json --out-file=graph.json") assert "Formatted output saved to 'graph.json'" in c.out graph = json.loads(c.load("graph.json")) assert len(graph["graph"]["nodes"]) == 1 assert "nodes" not in c.stdout c.run("graph info . --format=html --out-file=graph.html") assert "Formatted output saved to 'graph.html'" in c.out html = c.load("graph.html") assert "" in html assert "" not in c.stdout c.run("install . --format=json --out-file=graph.json") assert "Formatted output saved to 'graph.json'" in c.out graph = json.loads(c.load("graph.json")) assert len(graph["graph"]["nodes"]) == 1 assert "nodes" not in c.stdout def test_redirect_to_file_create_dir(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("config home --out-file=subdir/cmd_out.txt") cmd_out = c.load("subdir/cmd_out.txt") assert f"{c.cache_folder}" in cmd_out base = temp_folder() c = TestClient(light=True, current_folder=os.path.join(base, "sub1")) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("config home --out-file=../subdir/cmd_out.txt") cmd_out = c.load("../subdir/cmd_out.txt") assert f"{c.cache_folder}" in cmd_out ================================================ FILE: test/integration/command/test_package_test.py ================================================ import json import os import textwrap from conan.api.model import RecipeReference from conan.internal.paths import CONANFILE from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, GenConanfile from conan.internal.util.files import load class TestPackageTest: def test_basic(self): client = TestClient() test_package = textwrap.dedent(""" from conan import ConanFile class TestPackage(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def test(self): self.output.info("TESTING") """) client.save({CONANFILE: GenConanfile("hello", "0.1"), "test_package/conanfile.py": test_package}) client.run("create . --user=lasote --channel=stable") assert "hello/0.1@lasote/stable: Created package" in client.out client.run("test test_package hello/0.1@lasote/stable") assert "hello/0.1@lasote/stable (test package): TESTING" in client.out client.run("test hello/0.1@lasote/stable") assert "hello/0.1@lasote/stable (test package): TESTING" in client.out def test_basic_json(self): client = TestClient() client.save({CONANFILE: GenConanfile("hello", "0.1"), "test_package/conanfile.py": GenConanfile().with_test("pass")}) client.run("create . --format=json") graph = json.loads(client.stdout) assert graph["graph"]["nodes"]["1"]["ref"] == "hello/0.1#a90ba236e5310a473dae9f767a41db91" def test_test_only(self): test_conanfile = GenConanfile().with_test("pass") client = TestClient() client.save({CONANFILE: GenConanfile().with_name("hello").with_version("0.1"), "test_package/conanfile.py": test_conanfile}) client.run("create . --user=lasote --channel=stable") client.run("test test_package hello/0.1@lasote/stable") assert "Exporting package recipe" not in client.out assert "Forced build from source" not in client.out assert "Package '%s' created" % NO_SETTINGS_PACKAGE_ID not in client.out assert "Forced build from source" not in client.out assert "hello/0.1@lasote/stable: Already installed!" in client.out client.save({"test_package/conanfile.py": test_conanfile}, clean_first=True) client.run("test test_package hello/0.1@lasote/stable") assert "hello/0.1@lasote/stable: Configuring sources" not in client.out assert "hello/0.1@lasote/stable: Created package" not in client.out assert "hello/0.1@lasote/stable: Already installed!" in client.out assert "hello/0.1@lasote/stable (test package): Running test()" in client.out def test_wrong_version(self): test_conanfile = GenConanfile().with_test("pass").with_require("hello/0.2@user/cc") client = TestClient() client.save({CONANFILE: GenConanfile().with_name("hello").with_version("0.1"), "test_package/conanfile.py": test_conanfile}) client.run("create . --user=user --channel=channel", assert_error=True) assert "Duplicated requirement: hello/0.1@user/channel" in client.out def test_other_requirements(self): test_conanfile = (GenConanfile().with_require("other/0.2@user2/channel2") .with_test("pass")) client = TestClient() other_conanfile = GenConanfile().with_name("other").with_version("0.2") client.save({CONANFILE: other_conanfile}) client.run("export . --user=user2 --channel=channel2") client.run("install --requires=other/0.2@user2/channel2 --build='*'") client.save({CONANFILE: GenConanfile().with_name("hello").with_version("0.1"), "test_package/conanfile.py": test_conanfile}) client.run("create . --user=user --channel=channel") assert "hello/0.1@user/channel: Created package" in client.out # explicit override of user/channel works client.run("create . --user=lasote --channel=stable") assert "hello/0.1@lasote/stable: Created package" in client.out def test_test_with_path_errors(self): client = TestClient() client.save({"conanfile.txt": "contents"}, clean_first=True) # Path with conanfile.txt client.run("test conanfile.txt other/0.2@user2/channel2", assert_error=True) assert ("A conanfile.py is needed, %s is not acceptable" % os.path.join(client.current_folder, "conanfile.txt") in client.out) # Path with wrong conanfile path client.run("test not_real_dir/conanfile.py other/0.2@user2/channel2", assert_error=True) assert ("Conanfile not found at %s" % os.path.join(client.current_folder, "not_real_dir", "conanfile.py") in client.out) def test_check_version(self): client = TestClient() client.save({CONANFILE: GenConanfile()}) client.run("create . --name=dep --version=1.1") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/1.1" def build(self): ref = self.dependencies["dep"].ref self.output.info("BUILD Dep VERSION %s" % ref.version) """) test_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def build(self): ref = self.dependencies["hello"].ref self.output.info("BUILD HELLO VERSION %s" % ref.version) def test(self): ref = self.dependencies["hello"].ref self.output.info("TEST HELLO VERSION %s" % ref.version) """) client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) client.run("create . --name=hello --version=0.1") assert "hello/0.1: BUILD Dep VERSION 1.1" in client.out assert "hello/0.1 (test package): BUILD HELLO VERSION 0.1" in client.out assert "hello/0.1 (test package): TEST HELLO VERSION 0.1" in client.out class TestPackageBuild: def test_build_all(self): c = TestClient() c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1"), "pkg/test_package/conanfile.py": GenConanfile().with_tool_requires("tool/0.1") .with_test("pass")}) c.run("export tool") c.run("export dep") c.run("create pkg --build=*") c.assert_listed_binary({"dep/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build"), "pkg/0.1": ("59205ba5b14b8f4ebc216a6c51a89553021e82c1", "Build")}) c.assert_listed_require({"tool/0.1": "Cache"}, build=True, test_package=True) c.assert_listed_binary({"tool/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}, build=True, test_package=True) # Note we do NOT rebuild the already built binaries c.assert_listed_binary({"dep/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "pkg/0.1": ("59205ba5b14b8f4ebc216a6c51a89553021e82c1", "Cache")}, test_package=True) def test_build_missing(self): c = TestClient() c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1"), "pkg/test_package/conanfile.py": GenConanfile().with_tool_requires("tool/0.1") .with_test("pass")}) c.run("export tool") c.run("create dep") c.run("create pkg --build=missing") c.assert_listed_binary({"dep/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "pkg/0.1": ("59205ba5b14b8f4ebc216a6c51a89553021e82c1", "Build")}) c.assert_listed_require({"tool/0.1": "Cache"}, build=True, test_package=True) c.assert_listed_binary({"tool/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}, build=True, test_package=True) c.assert_listed_binary({"dep/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "pkg/0.1": ("59205ba5b14b8f4ebc216a6c51a89553021e82c1", "Cache")}, test_package=True) def test_build_test_package_dep(self): c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1"), "pkg/test_package/conanfile.py": GenConanfile().with_requires("dep/0.1") .with_test("pass")}) c.run("export dep") c.run('create pkg --build=missing --build-test=""', assert_error=True) c.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) c.assert_listed_binary({"dep/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Missing"), "pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}, test_package=True) c.run("create pkg --build-test=missing") c.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) c.assert_listed_binary({"dep/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build"), "pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}, test_package=True) class TestConanTestTest: def test_partial_reference(self): # Create two packages to test with the same test conanfile = ''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" ''' client = TestClient() client.save({CONANFILE: conanfile}) client.run("create . --user=conan --channel=stable") client.run("create . --user=conan --channel=testing") client.run("create . --user=conan --channel=foo") def test(conanfile_test, test_reference, path=None): path = path or "." client.save({os.path.join(path, CONANFILE): conanfile_test}, clean_first=True) client.run("test %s %s" % (path, test_reference)) # Specify a valid name test(''' from conan import ConanFile class HelloTestConan(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def test(self): self.output.warning("Tested ok!") ''', "hello/0.1@conan/stable") assert "Tested ok!" in client.out def test_test_package_env(self): client = TestClient() conanfile = ''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" def package_info(self): self.buildenv_info.define("MYVAR", "new/pythonpath/value") ''' test_package = ''' import os, platform from conan import ConanFile from conan.tools.env import VirtualBuildEnv class HelloTestConan(ConanFile): generators = "VirtualBuildEnv" def requirements(self): self.build_requires(self.tested_reference_str) def build(self): build_env = VirtualBuildEnv(self).vars() with build_env.apply(): assert("new/pythonpath/value" in os.environ["MYVAR"]) def test(self): build_env = VirtualBuildEnv(self).vars() with build_env.apply(): assert("new/pythonpath/value" in os.environ["MYVAR"]) ''' client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_package}) client.run("create . --user=lasote --channel=testing") client.run("test test_package hello/0.1@lasote/testing") def test_fail_test_package(self): client = TestClient() conanfile = """ from conan import ConanFile from conan.tools.files import copy class HelloConan(ConanFile): name = "hello" version = "0.1" exports_sources = "*" def package(self): copy(self, "*", self.source_folder, self.package_folder) """ test_conanfile = """ from conan import ConanFile class HelloReuseConan(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def test(self): pass """ client.save({"conanfile.py": conanfile, "FindXXX.cmake": "Hello FindCmake", "test/conanfile.py": test_conanfile}) client.run("create . --user=lasote --channel=stable") ref = RecipeReference.loads("hello/0.1@lasote/stable") client.run(f"test test {str(ref)}") pref = client.get_latest_package_reference(ref, NO_SETTINGS_PACKAGE_ID) assert "Hello FindCmake" == load(os.path.join(client.get_latest_pkg_layout(pref).package(), "FindXXX.cmake")) client.save({"FindXXX.cmake": "Bye FindCmake"}) client.run(f"test test {str(ref)}") # Test do not rebuild the package pref = client.get_latest_package_reference(ref, NO_SETTINGS_PACKAGE_ID) assert "Hello FindCmake" == load(os.path.join(client.get_latest_pkg_layout(pref).package(), "FindXXX.cmake")) client.run("create . --user=lasote --channel=stable") # create rebuild the package pref = client.get_latest_package_reference(ref, NO_SETTINGS_PACKAGE_ID) assert "Bye FindCmake" == load(os.path.join(client.get_latest_pkg_layout(pref).package(), "FindXXX.cmake")) def test_no_reference_in_test_package(): client = TestClient() test_conanfile = textwrap.dedent(""" from conan import ConanFile import os class HelloReuseConan(ConanFile): def test(self): self.output.warning("At test: {}".format(self.tested_reference_str)) """) client.save({"conanfile.py": GenConanfile(), "test_package/conanfile.py": test_conanfile}) client.run("create . --name=foo --version=1.0", assert_error=True) assert "doesn't declare any requirement, use `self.tested_reference_str` to require the " \ "package being created" in client.out def test_tested_reference_str(): """ At the test_package/conanfile the variable `self.tested_reference_str` is injected with the str of the reference being tested. It is available in all the methods. """ client = TestClient() test_conanfile = textwrap.dedent(""" from conan import ConanFile import os class HelloReuseConan(ConanFile): def generate(self): self.output.warning("At generate: {}".format(self.tested_reference_str)) assert len(self.dependencies.values()) == 1 assert len(self.dependencies.build.values()) == 1 def build(self): self.output.warning("At build: {}".format(self.tested_reference_str)) def build_requirements(self): self.output.warning("At build_requirements: {}".format(self.tested_reference_str)) self.build_requires(self.tested_reference_str) def test(self): self.output.warning("At test: {}".format(self.tested_reference_str)) """) client.save({"conanfile.py": GenConanfile(), "test_package/conanfile.py": test_conanfile}) client.run("create . --name=foo --version=1.0") for method in ("generate", "build", "build_requirements", "test"): assert "At {}: foo/1.0".format(method) in client.out def test_folder_output(): """ the "conan test" command should also follow the test_output layout folder """ c = TestClient() c.save({"conanfile.py": GenConanfile("hello", "0.1")}) c.run("create .") c.save({"test_package/conanfile.py": GenConanfile().with_test("pass").with_settings("build_type") .with_generator("CMakeDeps")}) # c.run("create .") c.run("test test_package hello/0.1@") assert os.path.exists(os.path.join(c.current_folder, "test_package/hello-config.cmake")) def test_removing_test_package_build_folder(): """ The test_package could crash if not cleaning correctly the test_package output folder. This will still crassh if the layout is not creating different build folders """ client = TestClient() test_package = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Test(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def generate(self): assert not os.path.exists("myfile.txt") save(self, "myfile.txt", "") def layout(self): self.folders.build = "mybuild" self.folders.generators = "mybuild" def test(self): pass """) client.save({"conanfile.py": GenConanfile("pkg", "1.0"), "test_package/conanfile.py": test_package}) client.run("create .") # This was crashing because not cleaned client.run("create .") assert "Removing previously existing 'test_package' build folder" in client.out def test_test_package_lockfile_location(): """ the lockfile should be in the caller cwd https://github.com/conan-io/conan/issues/13850 """ c = TestClient() c.save({"conanfile.py": GenConanfile("dep", "0.1"), "test_package/conanfile.py": GenConanfile().with_test("pass")}) c.run("create . --lockfile-out=myconan.lock") assert os.path.exists(os.path.join(c.current_folder, "myconan.lock")) c.run("test test_package dep/0.1 --lockfile=myconan.lock --lockfile-out=myconan2.lock") assert os.path.exists(os.path.join(c.current_folder, "myconan2.lock")) def test_package_missing_binary_msg(): # https://github.com/conan-io/conan/issues/13904 c = TestClient() c.save({"conanfile.py": GenConanfile("dep", "0.1"), "test_package/conanfile.py": GenConanfile().with_test("pass")}) c.run("export .") c.run("test test_package dep/0.1", assert_error=True) assert "ERROR: Missing binary: dep/0.1" in c.out assert "This is a **test_package** missing binary." in c.out c.run("test test_package dep/0.1 --build=dep/0.1") c.assert_listed_binary({"dep/0.1": (NO_SETTINGS_PACKAGE_ID, "Build")}) def test_test_binary_missing(): # Trying to reproduce https://github.com/conan-io/conan/issues/14352, # without success so far c = TestClient() c.save({"zlib/conanfile.py": GenConanfile("zlib", "0.1"), "openssl/conanfile.py": GenConanfile("openssl", "0.1").with_requires("zlib/0.1"), "cmake/conanfile.py": GenConanfile("cmake", "0.1").with_requires("openssl/0.1"), "dep/conanfile.py": GenConanfile("dep", "0.1").with_requires("openssl/0.1") .with_tool_requires("cmake/0.1"), "dep/test_package/conanfile.py": GenConanfile().with_test("pass")}) c.run("export zlib") c.run("export openssl") c.run("export cmake") c.run("export dep") c.run("test dep/test_package dep/0.1 --build=missing") ================================================ FILE: test/integration/command/test_profile.py ================================================ import json import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_profile_path(): c = TestClient() c.run("profile path default") assert "default" in c.out def test_profile_path_missing(): c = TestClient() c.run("profile path notexisting", assert_error=True) assert "ERROR: Profile not found: notexisting" in c.out def test_ignore_paths_when_listing_profiles(): c = TestClient() ignore_path = '.DS_Store' # just in case os.makedirs(c.paths.profiles_path, exist_ok=True) # This a "touch" equivalent open(os.path.join(c.paths.profiles_path, '.DS_Store'), 'w').close() os.utime(os.path.join(c.paths.profiles_path, ".DS_Store")) c.run("profile list") assert ignore_path not in c.out def test_shorthand_syntax(): tc = TestClient() tc.save({"profile": "[conf]\nuser:profile=True"}) tc.run("profile show -h") assert "[-pr:b" in tc.out assert "[-pr:h" in tc.out assert "[-pr:a" in tc.out tc.run( "profile show -o:a=both_options=True -pr:a=profile -s:a=os=WindowsCE -s:a=os.platform=conan -c:a=user.conf:cli=True -f=json") out = json.loads(tc.stdout) assert out == {'build': {'build_env': '', 'conf': {'user.conf:cli': True, 'user:profile': True}, 'options': {'both_options': 'True'}, 'package_settings': {}, 'settings': {'os': 'WindowsCE', 'os.platform': 'conan'}, 'tool_requires': {}}, 'host': {'build_env': '', 'conf': {'user.conf:cli': True, 'user:profile': True}, 'options': {'both_options': 'True'}, 'package_settings': {}, 'settings': {'os': 'WindowsCE', 'os.platform': 'conan'}, 'tool_requires': {}}} tc.save({"pre": textwrap.dedent(""" [settings] os=Linux compiler=gcc compiler.version=11 """), "mid": textwrap.dedent(""" [settings] compiler=clang compiler.version=14 """), "post": textwrap.dedent(""" [settings] compiler.version=13 """)}) tc.run("profile show -pr:a=pre -pr:a=mid -pr:a=post -f=json") out = json.loads(tc.stdout) assert out == {'build': {'build_env': '', 'conf': {}, 'options': {}, 'package_settings': {}, 'settings': {'compiler': 'clang', 'compiler.version': '13', 'os': 'Linux'}, 'tool_requires': {}}, 'host': {'build_env': '', 'conf': {}, 'options': {}, 'package_settings': {}, 'settings': {'compiler': 'clang', 'compiler.version': '13', 'os': 'Linux'}, 'tool_requires': {}}} tc.run("profile show -pr:a=pre -pr:h=post -f=json") out = json.loads(tc.stdout) assert out == {'build': {'build_env': '', 'conf': {}, 'options': {}, 'package_settings': {}, 'settings': {'compiler': 'gcc', 'compiler.version': '11', 'os': 'Linux'}, 'tool_requires': {}}, 'host': {'build_env': '', 'conf': {}, 'options': {}, 'package_settings': {}, 'settings': {'compiler': 'gcc', 'compiler.version': '13', 'os': 'Linux'}, 'tool_requires': {}}} tc.run("profile show -pr:a=pre -o:b foo=False -o:a foo=True -o:h foo=False -f=json") out = json.loads(tc.stdout) assert out == {'build': {'build_env': '', 'conf': {}, 'options': {'foo': 'True'}, 'package_settings': {}, 'settings': {'compiler': 'gcc', 'compiler.version': '11', 'os': 'Linux'}, 'tool_requires': {}}, 'host': {'build_env': '', 'conf': {}, 'options': {'foo': 'False'}, 'package_settings': {}, 'settings': {'compiler': 'gcc', 'compiler.version': '11', 'os': 'Linux'}, 'tool_requires': {}}} tc.run("profile show -o:shared=True", assert_error=True) assert 'Invalid empty package name in options. Use a pattern like `mypkg/*:shared`' in tc.out def test_profile_show_json(): c = TestClient() c.save({"myprofilewin": "[settings]\nos=Windows\n" "[tool_requires]\nmytool/*:mytool/1.0\n" "[conf]\nuser.conf:value=42\nlibiconv/*:tools.env.virtualenv:powershell=False\n" "[options]\n*:myoption=True\n" "[replace_requires]\ncmake/*: cmake/3.29.0\n" "[platform_requires]\ncmake/3.29.0\n", "myprofilelinux": "[settings]\nos=Linux"}) c.run("profile show -pr:b=myprofilewin -pr:h=myprofilelinux --format=json") profile = json.loads(c.stdout) assert profile["host"]["settings"] == {"os": "Linux"} assert profile["build"]["settings"] == {"os": "Windows"} # Check that tool_requires are properly serialized in json format # https://github.com/conan-io/conan/issues/15183 assert profile["build"]["tool_requires"] == {'mytool/*': ["mytool/1.0"]} @pytest.mark.parametrize("new_global", [None, 0, 1, True, False, ""]) def test_settings_serialize(new_global): tc = TestClient(light=True) settings_user = "new_global: [null, 0, 1, True, False, '']" tc.save_home({"settings_user.yml": settings_user}) tc.save({"conanfile.py": GenConanfile("foo", "1.0").with_settings("new_global")}) s = f"-s new_global={new_global}" if new_global is not None else "" tc.run(f"create . {s}") if new_global is None: assert "settings: new_global=" not in tc.out else: assert f"settings: new_global={new_global}" in tc.out ================================================ FILE: test/integration/command/test_run.py ================================================ import textwrap import pytest import platform from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient executable = "myapp.bat" if platform.system() == "Windows" else "myapp.sh" @pytest.fixture(scope="module") def client(): tc = TestClient(default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class Pkg(ConanFile): name = "pkg" version = "0.1" # So that the requirement is run=True even for --requires package_type = "application" options = {"foo": [True, False, "bar"]} default_options = {"foo": True} settings = "os" def package(self): executable = os.path.join(self.package_folder, "bin", '""" + executable + """') save(self, executable, f"echo Hello World! foo={self.options.foo}") # Make it executable os.chmod(executable, 0o755) """) tc.save({"pkg/conanfile.py": conanfile}) tc.run("create pkg") return tc @pytest.mark.parametrize("context_flag", ["host", "build", None]) @pytest.mark.parametrize("requires_context", ["host", "build",]) @pytest.mark.parametrize("use_conanfile", [True, False]) def test_run(client, context_flag, requires_context, use_conanfile): context_arg = { "host": "--context=host", "build": "--context=build", None: "", }.get(context_flag) should_find_binary = (context_flag == requires_context) or (context_flag is None) if use_conanfile: conanfile_consumer = GenConanfile("consumer", "1.0").with_settings("os") if requires_context == "host": conanfile_consumer.with_requires("pkg/0.1") else: conanfile_consumer.with_tool_requires("pkg/0.1") client.save({"conanfile.py": conanfile_consumer}) client.run(f"run {executable} {context_arg}", assert_error=not should_find_binary) else: requires = "requires" if requires_context == "host" else "tool-requires" client.run(f"run {executable} --{requires}=pkg/0.1 {context_arg}", assert_error=not should_find_binary) if should_find_binary: assert "Hello World!" in client.out else: if platform.system() == "Windows": assert "not recognized as an internal or external command" in client.out else: assert "Error 127 while executing" in client.out def test_run_context_priority(client): client.run("create pkg -o=pkg/*:foo=False") client.run(f"run {executable} --requires=pkg/0.1 --tool-requires=pkg/0.1 -o:b=pkg/*:foo=False") # True is host, False is build, run gives priority to host assert "Hello World! foo=True" in client.out def test_run_missing_executable(client): client.run(f"run a-binary-name-that-does-not-exist --requires=pkg/0.1", assert_error=True) if platform.system() == "Windows": assert "not recognized as an internal or external command" in client.out else: assert "Error 127 while executing" in client.out def test_run_missing_binary(client): client.run("run foo --requires=pkg/0.1 -o=pkg/*:foo=bar", assert_error=True) assert "Error installing the dependencies" in client.out assert "Missing prebuilt package for 'pkg/0.1'" in client.out def test_run_missing_package(client): client.run("run foo --requires=pkg/2.1", assert_error=True) assert "Error installing the dependencies" in client.out assert "Package 'pkg/2.1' not resolved" in client.out @pytest.mark.skipif(platform.system() == "Windows", reason="Unix only") def test_run_status_is_propagated(client): client.run("run false --requires=pkg/0.1", assert_error=True) assert "Error installing the dependencies" not in client.out assert "ERROR: Error 1 while executing" in client.out ================================================ FILE: test/integration/command/test_version.py ================================================ from conan.test.utils.tools import TestClient from conan import __version__ import json import platform import sys def _python_version(): return platform.python_version().replace("\n", "") def _sys_version(): return sys.version.replace("\n", "") def test_version_json(): """ Conan version command should be able to output a json with the version and python version. """ t = TestClient() t.run("version --format=json") js = json.loads(t.stdout) assert js["version"] == __version__ assert js["conan_path"] == sys.argv[0] assert js["python"]["version"] == _python_version() assert js["python"]["sys_version"] == _sys_version() assert js["python"]["sys_executable"] == sys.executable assert js["python"]["is_frozen"] == getattr(sys, 'frozen', False) assert js["python"]["architecture"] == platform.machine() assert js["system"]["version"] == platform.version() assert js["system"]["platform"] == platform.platform() assert js["system"]["system"] == platform.system() assert js["system"]["release"] == platform.release() assert js["system"]["cpu"] == platform.processor() def test_version_text(): """ Conan version command should be able to output a raw text with the version and python version. """ t = TestClient() t.run("version --format=text") _validate_text_output(t.out) def test_version_raw(): """ Conan version command should be able to output a raw text with the version and python version, when no format is specified. """ t = TestClient() t.run("version") _validate_text_output(t.out) def _validate_text_output(output): lines = output.splitlines() assert f'version: {__version__}' in lines assert f'conan_path: {sys.argv[0]}' in lines assert 'python' in lines assert f' version: {_python_version()}' in lines assert f' sys_version: {_sys_version()}' in lines assert f' sys_executable: {sys.executable}' in lines assert f' is_frozen: {getattr(sys, "frozen", False)}' in lines assert f' architecture: {platform.machine()}' in lines assert 'system' in lines assert f' version: {platform.version()}' in lines assert f' platform: {platform.platform()}' in lines assert f' system: {platform.system()}' in lines assert f' release: {platform.release()}' in lines assert f' cpu: {platform.processor()}' in lines ================================================ FILE: test/integration/command/upload/__init__.py ================================================ ================================================ FILE: test/integration/command/upload/test_upload_bundle.py ================================================ import json import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_upload_pkg_list(): """ Test how a custom command can create an upload pkglist and print it """ c = TestClient(default_server_user=True, light=True) mycommand = textwrap.dedent(""" import json import os from conan.cli.command import conan_command, OnceArgument from conan.api.model import ListPattern from conan.api.output import cli_out_write @conan_command(group="custom commands") def upload_pkglist(conan_api, parser, *args, **kwargs): \""" create an upload pkglist \""" parser.add_argument('reference', help="Recipe reference or package reference, can contain * as " "wildcard at any reference field.") # using required, we may want to pass this as a positional argument? parser.add_argument("-r", "--remote", action=OnceArgument, required=True, help='Upload to this specific remote') args = parser.parse_args(*args) remote = conan_api.remotes.get(args.remote) enabled_remotes = conan_api.remotes.list() ref_pattern = ListPattern(args.reference, package_id="*") package_list = conan_api.list.select(ref_pattern) if not package_list: raise ConanException("No recipes found matching pattern '{}'".format(args.reference)) # Check if the recipes/packages are in the remote conan_api.upload.check_upstream(package_list, remote, enabled_remotes) conan_api.upload.prepare(package_list, enabled_remotes) cli_out_write(json.dumps(package_list.serialize(), indent=4)) """) command_file_path = os.path.join(c.cache_folder, 'extensions', 'commands', 'cmd_upload_pkglist.py') c.save({command_file_path: mycommand}) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run('upload-pkglist "*" -r=default', redirect_stdout="mypkglist.json") pkglist = c.load("mypkglist.json") pkglist = json.loads(pkglist) assert pkglist["pkg/0.1"]["revisions"]["485dad6cb11e2fa99d9afbe44a57a164"]["upload"] is True ================================================ FILE: test/integration/command/upload/test_upload_parallel.py ================================================ from requests import ConnectionError from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestRequester def test_upload_parallel_error(): """Cause an error in the parallel transfer and see some message""" class FailOnReferencesUploader(TestRequester): fail_on = ["lib1", "lib3"] def put(self, *args, **kwargs): if any(ref in args[0] for ref in self.fail_on): raise ConnectionError("Connection fails with lib2 and lib4 references!") else: return super(FailOnReferencesUploader, self).put(*args, **kwargs) client = TestClient(requester_class=FailOnReferencesUploader, default_server_user=True) client.save_home({"global.conf": f"core.upload:parallel=2\ncore.upload:retry_wait=0"}) client.save({"conanfile.py": GenConanfile()}) client.run('remote login default admin -p password') for index in range(4): client.run('create . --name=lib{} --version=1.0 --user=user --channel=channel'.format(index)) client.run('upload lib* -c -r default', assert_error=True) assert "Connection fails with lib2 and lib4 references!" in client.out assert "Execute upload again to retry upload the failed files" in client.out def test_upload_parallel_success(): """Upload 2 packages in parallel with success""" client = TestClient(default_server_user=True) client.save_home({"global.conf": f"core.upload:parallel=2"}) client.save({"conanfile.py": GenConanfile()}) client.run('create . --name=lib0 --version=1.0 --user=user --channel=channel') client.run('create . --name=lib1 --version=1.0 --user=user --channel=channel') client.run('remote login default admin -p password') client.run('upload lib* -c -r default') assert "Uploading recipe 'lib0/1.0@user/channel#4d670581ccb765839f2239cc8dff8fbd'" in client.out assert "Uploading recipe 'lib1/1.0@user/channel#4d670581ccb765839f2239cc8dff8fbd'" in client.out client.run('search lib0/1.0@user/channel -r default') assert "lib0/1.0@user/channel" in client.out client.run('search lib1/1.0@user/channel -r default') assert "lib1/1.0@user/channel" in client.out def test_upload_parallel_fail_on_interaction(): """Upload 2 packages in parallel and fail because non_interactive forced""" client = TestClient(default_server_user=True) client.save_home({"global.conf": f"core.upload:parallel=2\ncore:non_interactive=True"}) client.save({"conanfile.py": GenConanfile()}) num_references = 2 for index in range(num_references): client.run('create . --name=lib{} --version=1.0 --user=user --channel=channel'.format(index)) client.run('remote logout default') client.run('upload lib* -c -r default', assert_error=True) assert "ERROR: Conan interactive mode disabled. [Remote: default]" in client.out ================================================ FILE: test/integration/command/upload/test_upload_patterns.py ================================================ import re import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update class TestUploadPatterns: @pytest.fixture(scope="class") # Takes 6 seconds, reuse it def client(self): """ create a few packages, with several recipe revisions, several pids, several prevs """ client = TestClient(default_server_user=True) for pkg in ("pkga", "pkgb"): for version in ("1.0", "1.1"): for rrev in ("rev1", "rev2"): client.save({"conanfile.py": GenConanfile().with_settings("os") .with_class_attribute(f"potato='{rrev}'") .with_package_file("file", env_var="MYVAR")}) for the_os in ("Windows", "Linux"): for prev in ("prev1", "prev2"): with environment_update({"MYVAR": prev}): client.run(f"create . --name={pkg} --version={version} " f"-s os={the_os}") return client @staticmethod def assert_uploaded(pattern, result, client, only_recipe=False, query=None): def ref_map(r): rev1 = "ad55a66b62acb63ffa99ea9b75c16b99" rev2 = "127fb537a658ad6a57153a038960dc53" pid1 = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" pid2 = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" if "Linux" in r: prev1 = "7ce684c6109943482b9174dd089e717b" prev2 = "772234192e8e4ba71b018d2e7c02423e" else: prev1 = "45f88f3c318bd43d1bc48a5d408a57ef" prev2 = "c5375d1f517ecb1ed6c9532b0f4d86aa" r = r.replace("prev1", prev1).replace("prev2", prev2).replace("Windows", pid1) r = r.replace("Linux", pid2).replace("rev1", rev1).replace("rev2", rev2) return r pattern = ref_map(pattern) only_recipe = "" if not only_recipe else "--only-recipe" query = "" if not query else f"-p={query}" client.run(f"upload {pattern} -r=default -c {only_recipe} {query}") out = str(client.out) uploaded_recipes = [f"{p}/{v}#{rr}" for p in result[0] for v in result[1] for rr in result[2]] uploaded_packages = [f"{r}:{pid}#{pr}" for r in uploaded_recipes for pid in result[3] for pr in result[4]] # Checks upload_recipe_count = out.count("Uploading recipe") skipped_recipe_count = len(re.findall("Recipe '.+' already in server, skipping", out)) assert upload_recipe_count + skipped_recipe_count == len(uploaded_recipes) for recipe in uploaded_recipes: recipe = ref_map(recipe) upload = f"Uploading recipe '{recipe}" in out existing = f"Recipe '{recipe}' already in server" in out assert upload or existing upload_pkg_count = out.count("Uploading package") skipped_pkg_count = len(re.findall("Package '.+' already in server, skipping", out)) assert upload_pkg_count + skipped_pkg_count == len(uploaded_packages) for pkg in uploaded_packages: pkg = ref_map(pkg) upload = f"Uploading package '{pkg}" in out existing = f"Package '{pkg}' already in server" in out assert upload or existing def test_all_latest(self, client): result = ("pkga", "pkgb"), ("1.0", "1.1"), ("rev2",), ("Windows", "Linux"), ("prev2",) self.assert_uploaded("*", result, client) def test_all(self, client): result = ("pkga", "pkgb"), ("1.0", "1.1"), ("rev1", "rev2",), ("Windows", "Linux"), \ ("prev1", "prev2") self.assert_uploaded("*#*:*#*", result, client) def test_pkg(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), ("Windows", "Linux"), ("prev2",) self.assert_uploaded("pkga", result, client) # equivalent to using explicitly latest self.assert_uploaded("pkga#latest", result, client) def test_pkg_rrev(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), ("Windows", "Linux"), ("prev2",) self.assert_uploaded("pkga#rev1", result, client) # equivalent to using explicitly latest self.assert_uploaded("pkga#rev1:*#latest", result, client) def test_pkg_rrevs(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1", "rev2"), ("Windows", "Linux"), ("prev2",) self.assert_uploaded("pkga#*", result, client) def test_pkg_pid(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), ("Windows",), ("prev2",) self.assert_uploaded("pkga:Windows", result, client) self.assert_uploaded("pkga:Windows#latest", result, client) def test_pkg_rrev_pid(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), ("Windows",), ("prev2",) self.assert_uploaded("pkga#rev1:Windows", result, client) def test_pkg_rrevs_pid(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1", "rev2"), ("Windows",), ("prev2",) self.assert_uploaded("pkga#*:Windows", result, client) def test_pkg_rrev_pid_prev(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), ("Windows",), ("prev1",) self.assert_uploaded("pkga#rev1:Windows#prev1", result, client) # Only recipes def test_all_latest_only_recipe(self, client): result = ("pkga", "pkgb"), ("1.0", "1.1"), ("rev2",), (), () self.assert_uploaded("*", result, client, only_recipe=True) def test_pkg_only_recipe(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), (), () self.assert_uploaded("pkga", result, client, only_recipe=True) def test_pkg_rrev_only_recipe(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1",), (), () self.assert_uploaded("pkga#rev1", result, client, only_recipe=True) def test_pkg_rrevs_only_recipe(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev1", "rev2"), (), () self.assert_uploaded("pkga#*", result, client, only_recipe=True) # Package query def test_pkg_query(self, client): result = ("pkga",), ("1.0", "1.1"), ("rev2",), ("Windows",), ("prev2",) self.assert_uploaded("pkga", result, client, query="os=Windows") class TestUploadPatternErrors: @pytest.fixture(scope="class") def client(self): client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile("pkg", "0.1")}) client.run(f"create .") return client @staticmethod def assert_error(pattern, error, client, only_recipe=False, query=None): only_recipe = "" if not only_recipe else "--only-recipe" query = "" if not query else f"-p={query}" client.run(f"upload {pattern} -r=default {only_recipe} {query}", assert_error=True) assert error in client.out def test_recipe_not_found(self, client): error = "ERROR: Recipe 'zlib/1.2.11' not found" self.assert_error("zlib/1.2.11", error, client) def test_rrev_not_found(self, client): error = "ERROR: Recipe revision 'pkg/0.1#rev1' not found" self.assert_error("pkg/0.1#rev1", error, client) def test_pid_not_found(self, client): rrev = "485dad6cb11e2fa99d9afbe44a57a164" error = "ERROR: Binary package not found: 'pkg/0.1:pid1" self.assert_error(f"pkg/0.1#{rrev}:pid1", error, client) def test_prev_not_found(self, client): rrev = "485dad6cb11e2fa99d9afbe44a57a164" pid = "da39a3ee5e6b4b0d3255bfef95601890afd80709" error = f"ERROR: Package revision 'pkg/0.1#{rrev}:{pid}#prev' not found" self.assert_error(f"pkg/0.1#{rrev}:{pid}#prev", error, client) def test_bad_package_query(self, client): error = "Invalid package query: blah. Invalid expression: blah" self.assert_error("* -p blah", error, client) ================================================ FILE: test/integration/command/upload/upload_complete_test.py ================================================ import os from requests import ConnectionError from conan.internal.paths import CONAN_MANIFEST from conan.test.utils.tools import TestClient, TestRequester, TestServer, GenConanfile from conan.test.utils.env import environment_update class BadConnectionUploader(TestRequester): fail_on = 1 def __init__(self, *args, **kwargs): super(BadConnectionUploader, self).__init__(*args, **kwargs) self.counter_fail = 0 def put(self, *args, **kwargs): self.counter_fail += 1 if self.counter_fail == self.fail_on: raise ConnectionError("Can't connect because of the evil mock") else: return super(BadConnectionUploader, self).put(*args, **kwargs) class TerribleConnectionUploader(BadConnectionUploader): def put(self, *args, **kwargs): raise ConnectionError("Can't connect because of the evil mock") class FailPairFilesUploader(BadConnectionUploader): def put(self, *args, **kwargs): self.counter_fail += 1 if self.counter_fail % 2 == 1: raise ConnectionError("Pair file, error!") else: return super(BadConnectionUploader, self).put(*args, **kwargs) def test_try_upload_bad_recipe(): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": GenConanfile("hello0", "1.2.1")}) client.run("export . --user=frodo --channel=stable") layout = client.exported_layout() os.unlink(os.path.join(layout.export(), CONAN_MANIFEST)) client.run("upload %s -r default" % str(layout.reference), assert_error=True) assert "Cannot upload corrupted recipe" in client.out def test_upload_with_pattern(): client = TestClient(default_server_user=True, light=True) for num in range(3): client.save({"conanfile.py": GenConanfile("hello{}".format(num), "1.2.1")}) client.run("export . --user=frodo --channel=stable") client.run("upload hello* --confirm -r default") for num in range(3): assert "Uploading recipe 'hello%s/1.2.1@frodo/stable" % num in client.out client.run("upload hello0* --confirm -r default") assert f"'hello0/1.2.1@frodo/stable#761f54e34d59deb172d6078add7050a7' "\ "already in server, skipping upload" in client.out assert "hello1" not in client.out assert "hello2" not in client.out def test_check_upload_confirm_question(): server = TestServer() client = TestClient(servers={"default": server}, inputs=["yes", "admin", "password", "n", "n", "n"], light=True) client.save({"conanfile.py": GenConanfile("hello1", "1.2.1")}) client.run("export . --user=frodo --channel=stable") client.run("upload hello* -r default") assert "Uploading recipe 'hello1/1.2.1@frodo/stable" in client.out client.save({"conanfile.py": GenConanfile("hello2", "1.2.1")}) client.run("create . --user=frodo --channel=stable") client.run("upload hello* -r default") assert "Uploading recipe 'hello2/1.2.1@frodo/stable" not in client.out def test_check_upload_confirm_question_yes(): server = TestServer() client = TestClient(servers={"default": server}, inputs=["yes", "yes", "yes", "yes", "yes", "admin", "password"], light=True) client.save({"conanfile.py": GenConanfile("hello1", "1.2.1")}) client.run("create . ") client.save({"conanfile.py": GenConanfile("hello1", "1.2.1").with_package_file("file.txt", env_var="MYVAR")}) with environment_update({"MYVAR": "0"}): client.run("create . ") with environment_update({"MYVAR": "1"}): client.run("create . ") client.run("upload hello*#*:*#* -r default") assert str(client.out).count("(Uploaded)") == 5 class TestUpload: def _get_client(self, requester=None): servers = {} # All can write (for avoid authentication until we mock user_io) self.test_server = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"lasote": "mypass"}) servers["default"] = self.test_server test_client = TestClient(servers=servers, inputs=["lasote", "mypass"], requester_class=requester, light=True) return test_client def test_upload_error(self): """Cause an error in the transfer and see some message""" # Check for the default behaviour client = self._get_client(BadConnectionUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*")} client.save(files) client.run("export . --user=frodo --channel=stable") client.run("upload hello* --confirm -r default") assert "Can't connect because of the evil mock" in client.out assert "Waiting 5 seconds to retry..." in client.out # This will fail in the first put file, so, as we need to # upload 3 files (conanmanifest, conanfile and tgz) will do it with 2 retries client = self._get_client(BadConnectionUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*")} client.save(files) client.run("export . --user=frodo --channel=stable") self._set_global_conf(client, retry_wait=0) client.run("upload hello* --confirm -r default") assert "Can't connect because of the evil mock" in client.out assert "Waiting 0 seconds to retry..." in client.out # but not with 0 client = self._get_client(BadConnectionUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*"), "somefile.txt": ""} client.save(files) client.run("export . --user=frodo --channel=stable") self._set_global_conf(client, retry=0, retry_wait=1) client.run("upload hello* --confirm -r default", assert_error=True) assert "Waiting 1 seconds to retry..." not in client.out assert ("Execute upload again to retry upload the failed files: " "conan_export.tgz. [Remote: default]") in client.out # Try with broken connection even with 10 retries client = self._get_client(TerribleConnectionUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*")} client.save(files) client.run("export . --user=frodo --channel=stable") self._set_global_conf(client, retry=10, retry_wait=0) client.run("upload hello* --confirm -r default", assert_error=True) assert "Waiting 0 seconds to retry..." in client.out assert "Execute upload again to retry upload the failed files" in client.out # For each file will fail the first time and will success in the second one client = self._get_client(FailPairFilesUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*")} client.save(files) client.run("export . --user=frodo --channel=stable") client.run("install --requires=hello0/1.2.1@frodo/stable --build='*' -r default") self._set_global_conf(client, retry=3, retry_wait=0) client.run("upload hello* --confirm -r default") assert str(client.out).count("WARN: network: Pair file, error!") == 5 @staticmethod def _set_global_conf(client, retry=None, retry_wait=None): lines = [] if retry is not None: lines.append("core.upload:retry={}".format(retry)) if retry_wait is not None: lines.append("core.upload:retry_wait={}".format(retry_wait)) client.save_home({"global.conf": "\n".join(lines)}) def test_upload_error_with_config(self): """Cause an error in the transfer and see some message""" # This will fail in the first put file, so, as we need to # upload 3 files (conanmanifest, conanfile and tgz) will do it with 2 retries client = self._get_client(BadConnectionUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*")} client.save(files) client.run("export . --user=frodo --channel=stable") self._set_global_conf(client, retry_wait=0) client.run("upload hello* --confirm -r default") assert "Can't connect because of the evil mock" in client.out assert "Waiting 0 seconds to retry..." in client.out # but not with 0 client = self._get_client(BadConnectionUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*"), "somefile.txt": ""} client.save(files) client.run("export . --user=frodo --channel=stable") self._set_global_conf(client, retry=0, retry_wait=1) client.run("upload hello* --confirm -r default", assert_error=True) assert "Waiting 1 seconds to retry..." not in client.out assert ("Execute upload again to retry upload the failed files: " "conan_export.tgz. [Remote: default]") in client.out # Try with broken connection even with 10 retries client = self._get_client(TerribleConnectionUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*")} client.save(files) client.run("export . --user=frodo --channel=stable") self._set_global_conf(client, retry=10, retry_wait=0) client.run("upload hello* --confirm -r default", assert_error=True) assert "Waiting 0 seconds to retry..." in client.out assert "Execute upload again to retry upload the failed files" in client.out # For each file will fail the first time and will success in the second one client = self._get_client(FailPairFilesUploader) files = {"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*")} client.save(files) client.run("export . --user=frodo --channel=stable") client.run("install --requires=hello0/1.2.1@frodo/stable --build='*'") self._set_global_conf(client, retry=3, retry_wait=0) client.run("upload hello* --confirm -r default") assert str(client.out).count("WARN: network: Pair file, error!") == 5 def test_upload_same_package_dont_compress(self): client = self._get_client() client.save({"conanfile.py": GenConanfile().with_exports_sources("*"), "content.txt": "foo"}) client.run("create . --name foo --version 1.0") client.run("upload foo/1.0 -r default") assert "foo/1.0: Compressing conan_sources.tgz" in client.out assert ("foo/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709: " "Compressing conan_package.tgz") in client.out client.run("upload foo/1.0 -r default") assert "Compressing" not in client.out assert "already in server, skipping upload" in client.out ================================================ FILE: test/integration/command/upload/upload_compression_test.py ================================================ import os from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import uncompress_packaged_files from conan.test.utils.tools import TestClient def test_reuse_uploaded_tgz(): client = TestClient(default_server_user=True) # Download packages from a remote, then copy to another channel # and reupload them. Because they have not changed, the tgz is not created again # UPLOAD A PACKAGE ref = RecipeReference.loads("hello0/0.1@user/stable") files = {"conanfile.py": GenConanfile("hello0", "0.1").with_exports("*"), "another_export_file.lib": "to compress"} client.save(files) client.run("create . --user=user --channel=stable") client.run("upload %s -r default" % str(ref)) assert "Compressing conan_export.tgz" in client.out assert "Compressing conan_package.tgz" in client.out def test_reuse_downloaded_tgz(): # Download packages from a remote, then copy to another channel # and reupload them. It needs to compress it again, not tgz is kept client = TestClient(default_server_user=True) # UPLOAD A PACKAGE files = {"conanfile.py": GenConanfile("hello0", "0.1").with_exports("*"), "another_export_file.lib": "to compress"} client.save(files) client.run("create . --user=user --channel=stable") client.run("upload hello0/0.1@user/stable -r default") assert "Compressing conan_export.tgz" in client.out assert "Compressing conan_package.tgz" in client.out # Other user downloads the package # THEN A NEW USER DOWNLOADS THE PACKAGES AND UPLOADS COMPRESSING AGAIN # BECAUSE ONLY TGZ IS KEPT WHEN UPLOADING other_client = TestClient(servers=client.servers, inputs=["admin", "password"]) other_client.run("download hello0/0.1@user/stable -r default") other_client.run("upload hello0/0.1@user/stable -r default") assert "Compressing conan_export.tgz" in client.out assert "Compressing conan_package.tgz" in client.out def test_upload_only_tgz_if_needed(): client = TestClient(default_server_user=True) ref = RecipeReference.loads("hello0/0.1@user/stable") conanfile = GenConanfile("hello0", "0.1").with_exports("*").with_package_file("lib/file.lib", "File") client.save({"conanfile.py": conanfile, "file.txt": "contents"}) client.run("create . --user=user --channel=stable") # Upload conans client.run("upload %s -r default --only-recipe" % str(ref)) assert "Compressing conan_export.tgz" in client.out # Not needed to tgz again client.run("upload %s -r default --only-recipe" % str(ref)) assert "Compressing" not in client.out # Check that conans exists on server server_paths = client.servers["default"].server_store conan_path = server_paths.conan_revisions_root(ref) assert os.path.exists(conan_path) latest_rrev = client.cache.get_latest_recipe_revision(ref) package_ids = client.cache.get_package_references(latest_rrev) pref = package_ids[0] # Upload package client.run("upload %s#*:%s -r default -c" % (str(ref), str(pref.package_id))) assert "Compressing conan_package.tgz" in client.out # Not needed to tgz again client.run("upload %s#*:%s -r default -c" % (str(ref), str(pref.package_id))) assert "Compressing" not in client.out # If we install the package again will be removed and re tgz client.run("install --requires=%s --build missing" % str(ref)) # Upload package client.run("upload %s#*:%s -r default -c" % (str(ref), str(pref.package_id))) assert "Compressing" not in client.out # Check library on server folder = uncompress_packaged_files(server_paths, pref) libraries = os.listdir(os.path.join(folder, "lib")) assert len(libraries) == 1 ================================================ FILE: test/integration/command/upload/upload_test.py ================================================ import json import os import platform import stat import textwrap from collections import OrderedDict import pytest from unittest.mock import patch from requests import Response from conan.errors import ConanException from conan.api.model import PkgReference from conan.internal.api.uploader import gzopen_without_timestamps from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer, \ GenConanfile, TestRequester, TestingResponse from conan.internal.util.files import is_dirty, save, set_dirty, sha1sum conanfile = """from conan import ConanFile from conan.tools.files import copy class MyPkg(ConanFile): name = "hello0" version = "1.2.1" exports_sources = "*" def package(self): copy(self, "*.cpp", self.source_folder, self.package_folder) copy(self, "*.h", self.source_folder, self.package_folder) """ class TestUpload: @pytest.mark.artifactory_ready def test_upload_dirty(self): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": GenConanfile("hello", "0.1")}) client.run("create .") pkg_folder = client.created_layout().package() set_dirty(pkg_folder) client.run("upload * -r=default -c", assert_error=True) assert "ERROR: Package hello/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709 i" \ "s corrupted, aborting upload." in client.out assert "Remove it with 'conan remove hello/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ in client.out # Test that removeing the binary allows moving forward client.run("remove hello/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709 -c") client.run("upload * -r=default --confirm") @pytest.mark.artifactory_ready def test_upload_force(self): client = TestClient(default_server_user=True, light=True) conanfile_ = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class MyPkg(ConanFile): name = "hello" version = "0.1" def package(self): copy(self, "myfile.sh", src=self.source_folder, dst=self.package_folder) """) client.save({"conanfile.py": conanfile_, "myfile.sh": "foo"}) client.run("export-pkg .") client.run("upload * --confirm -r default") assert "Uploading package 'hello" in client.out client.run("upload * --confirm -r default") assert "Uploading package" not in client.out if platform.system() == "Linux": package_file_path = os.path.join(client.current_folder, "myfile.sh") os.system('chmod +x "{}"'.format(package_file_path)) assert os.stat(package_file_path).st_mode & stat.S_IXUSR client.run("export-pkg .") client.run("upload * --confirm -r default") # Doesn't change revision, doesn't reupload assert "conan_package.tgz" not in client.out assert "skipping upload" in client.out assert "Compressing package..." not in client.out # with --force it really re-uploads it client.run("upload * --confirm --force -r default") assert "Uploading recipe 'hello" in client.out assert "Uploading package 'hello" in client.out if platform.system() == "Linux": client.run("remove '*' -c") client.run("install --requires=hello/0.1 --deployer=full_deploy") package_file_path = os.path.join(client.current_folder, "full_deploy", "host", "hello", "0.1", "myfile.sh") # Owner with execute permissions assert os.stat(package_file_path).st_mode & stat.S_IXUSR @pytest.mark.artifactory_ready def test_pattern_upload(self): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": conanfile}) client.run("create . --user=user --channel=testing") client.run("upload hello0/*@user/testing --confirm -r default") assert "Uploading recipe 'hello0/1.2.1@" in client.out assert "Uploading package 'hello0/1.2.1@" in client.out def test_pattern_upload_no_recipes(self): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": conanfile}) client.run("upload bogus/*@dummy/testing --confirm -r default", assert_error=True) assert "No recipes found matching pattern 'bogus/*@dummy/testing'" in client.out def test_broken_sources_tgz(self): # https://github.com/conan-io/conan/issues/2854 client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": conanfile, "source.h": "my source"}) client.run("create . --user=user --channel=testing") layout = client.exported_layout() def gzopen_patched(name, mode="r", fileobj=None, **kwargs): # noqa raise ConanException("Error gzopen %s" % name) with patch('conan.internal.api.uploader.gzopen_without_timestamps', new=gzopen_patched): client.run("upload * --confirm -r default --only-recipe", assert_error=True) assert "Error gzopen conan_sources.tgz" in client.out export_download_folder = layout.download_export() tgz = os.path.join(export_download_folder, "conan_sources.tgz") assert os.path.exists(tgz) assert is_dirty(tgz) client.run("upload * --confirm -r default --only-recipe") assert "Removing conan_sources.tgz, marked as dirty" in client.out assert os.path.exists(tgz) assert not is_dirty(tgz) def test_broken_package_tgz(self): # https://github.com/conan-io/conan/issues/2854 client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": conanfile, "source.h": "my source"}) client.run("create . --user=user --channel=testing") pref = client.created_layout().reference def gzopen_patched(name, fileobj, compresslevel=None): # noqa if name == "conan_package.tgz": raise ConanException("Error gzopen %s" % name) return gzopen_without_timestamps(name, fileobj) with patch('conan.internal.api.uploader.gzopen_without_timestamps', new=gzopen_patched): client.run("upload * --confirm -r default", assert_error=True) assert "Error gzopen conan_package.tgz" in client.out download_folder = client.get_latest_pkg_layout(pref).download_package() tgz = os.path.join(download_folder, "conan_package.tgz") assert os.path.exists(tgz) assert is_dirty(tgz) client.run("upload * --confirm -r default") assert "WARN: Removing conan_package.tgz, marked as dirty" in client.out assert os.path.exists(tgz) assert not is_dirty(tgz) def test_corrupt_upload(self): c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": conanfile, "include/hello.h": ""}) c.run("create . --user=frodo --channel=stable") package_folder = c.created_layout().package() save(os.path.join(package_folder, "added.txt"), "") os.remove(os.path.join(package_folder, "include/hello.h")) c.run("upload hello0/1.2.1@frodo/stable --check -r default", assert_error=True) assert ("hello0/1.2.1@frodo/stable#3afd661184b94bdac7fb2057e7bd9baa" ":da39a3ee5e6b4b0d3255bfef95601890afd80709" "#e70e86439dec07a0d5d3414648b0b16c: ERROR") in c.out assert "include/hello.h (manifest: d41d8cd98f00b204e9800998ecf8427e, file: None)" in c.out assert "added.txt (manifest: None, file: d41d8cd98f00b204e9800998ecf8427e)" in c.out assert "ERROR: There are corrupted artifacts, check the error logs" in c.out @pytest.mark.artifactory_ready def test_upload_modified_recipe(self): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": conanfile, "hello.cpp": "int i=0"}) client.run("export . --user=frodo --channel=stable") rrev = client.exported_recipe_revision() client.run("upload hello0/1.2.1@frodo/stable -r default") assert "Uploading recipe 'hello0/1.2.1@frodo/stable#" in client.out client2 = TestClient(servers=client.servers, inputs=["admin", "password"]) client2.save({"conanfile.py": conanfile + "\r\n#end", "hello.cpp": "int i=1"}) client2.run("export . --user=frodo --channel=stable") layout = client2.exported_layout() manifest, _ = layout.recipe_manifests() manifest.time += 10 manifest.save(layout.export()) client2.run("upload hello0/1.2.1@frodo/stable -r default") assert "Uploading recipe 'hello0/1.2.1@frodo/stable#" in client2.out # first client tries to upload again # The client tries to upload exactly the same revision already uploaded, so no changes client.run("upload hello0/1.2.1@frodo/stable -r default") assert f"'hello0/1.2.1@frodo/stable#{rrev}' already in server, skipping upload" in client.out @pytest.mark.artifactory_ready def test_upload_unmodified_recipe(self): client = TestClient(default_server_user=True, light=True) files = {"conanfile.py": GenConanfile("hello0", "1.2.1")} client.save(files) client.run("export . --user=frodo --channel=stable") rrev = client.exported_recipe_revision() client.run("upload hello0/1.2.1@frodo/stable -r default") assert "Uploading recipe 'hello0/1.2.1@frodo/stable#" in client.out client2 = TestClient(servers=client.servers, inputs=["admin", "password"]) client2.save(files) client2.run("export . --user=frodo --channel=stable") layout = client2.exported_layout() manifest, _ = layout.recipe_manifests() manifest.time += 10 manifest.save(layout.export()) client2.run("upload hello0/1.2.1@frodo/stable -r default") assert (f"Recipe 'hello0/1.2.1@frodo/stable#761f54e34d59deb172d6078add7050a7' already " f"in server, skipping upload") in client2.out # first client tries to upload again client.run("upload hello0/1.2.1@frodo/stable -r default") assert (f"Recipe 'hello0/1.2.1@frodo/stable#{rrev}' " f"already in server, skipping upload") in client.out @pytest.mark.artifactory_ready def test_upload_unmodified_package(self): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": conanfile, "hello.cpp": ""}) client.run("create . --user=frodo --channel=stable") prev1 = client.created_layout().reference client.run("upload hello0/1.2.1@frodo/stable -r default") client2 = TestClient(servers=client.servers, inputs=["admin", "password"]) client2.save({"conanfile.py": conanfile, "hello.cpp": ""}) client2.run("create . --user=frodo --channel=stable") prev2 = client2.created_layout().reference client2.run("upload hello0/1.2.1@frodo/stable -r default") assert f"'{repr(prev2.ref)}' already in server, skipping upload" in client2.out assert "Uploaded conan recipe 'hello0/1.2.1@frodo/stable' to 'default'" not in client2.out assert f"'{prev2.repr_notime()}' already in server, skipping upload" in client2.out # first client tries to upload again client.run("upload hello0/1.2.1@frodo/stable -r default") assert f"'{repr(prev1.ref)}' already in server, skipping upload" in client.out assert "Uploaded conan recipe 'hello0/1.2.1@frodo/stable' to 'default'" not in client.out assert f"'{prev1.repr_notime()}' already in server, skipping upload" in client2.out def test_upload_no_overwrite_all(self): conanfile_new = GenConanfile("hello", "1.0").\ with_import("from conan.tools.files import copy").\ with_exports_sources(["*"]).\ with_package('copy(self, "*", self.source_folder, self.package_folder)') client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": conanfile_new, "hello.h": ""}) client.run("create . --user=frodo --channel=stable") prev1 = client.created_layout().reference # First time upload client.run("upload hello/1.0@frodo/stable -r default") assert "Forbidden overwrite" not in client.out assert "Uploading recipe 'hello/1.0@frodo/stable" in client.out # CASE: Upload again client.run("upload hello/1.0@frodo/stable -r default") assert f"'{repr(prev1.ref)}' already in server, skipping upload" in client.out assert f"'{prev1.repr_notime()}' already in server, skipping upload" in client.out def test_skip_upload(self): """ Check that the option --skip does not upload anything """ client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": GenConanfile("hello0", "1.2.1").with_exports("*"), "file.txt": ""}) client.run("create .") client.run("upload * --dry-run -r default -c") assert "Compressing" in client.out client.run("search * -r default") # after dry run nothing should be on the server assert "hello" not in client.out # now upload, the stuff should NOT be recompressed client.run("upload * -c -r default") # check if compressed files are re-used assert "Compressing" not in client.out # now it should be on the server client.run("search * -r default") assert "hello0/1.2.1" in client.out def test_upload_without_sources(self): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") client.run("upload * --confirm -r default") client2 = TestClient(servers=client.servers, inputs=["admin", "password", "lasote", "mypass"]) client2.run("install --requires=pkg/0.1@user/testing") client2.run("remote remove default") server2 = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"lasote": "mypass"}) client2.servers = {"server2": server2} client2.update_servers() client2.run("upload * --confirm -r=server2") assert "Uploading recipe 'pkg" in client.out assert "Uploading package 'pkg" in client.out def test_upload_login_prompt_disabled_no_user(self): """ Without user info, uploads should fail when login prompt has been disabled. """ files = {"conanfile.py": GenConanfile("hello0", "1.2.1")} client = TestClient(default_server_user=True, light=True) client.save(files) conan_conf = "core:non_interactive=True" client.save_home({"global.conf": conan_conf}) client.run("create . --user=user --channel=testing") client.run("remote logout '*'") client.run("upload hello0/1.2.1@user/testing -r default", assert_error=True) assert "Conan interactive mode disabled" in client.out assert "-> conanmanifest.txt" not in client.out assert "-> conanfile.py" not in client.out assert "-> conan_export.tgz" not in client.out def test_upload_login_prompt_disabled_user_not_authenticated(self): # When a user is not authenticated, uploads should fail when login prompt has been disabled. files = {"conanfile.py": GenConanfile("hello0", "1.2.1")} client = TestClient(default_server_user=True, light=True) client.save(files) conan_conf = "core:non_interactive=True" client.save_home({"global.conf": conan_conf}) client.run("create . --user=user --channel=testing") client.run("remote logout '*'") client.run("remote set-user default lasote") client.run("upload hello0/1.2.1@user/testing -r default", assert_error=True) assert "Conan interactive mode disabled" in client.out assert "-> conanmanifest.txt" not in client.out assert "-> conanfile.py" not in client.out assert "-> conan_export.tgz" not in client.out assert "Please enter a password for" not in client.out def test_upload_login_prompt_disabled_user_authenticated(self): # When user is authenticated, uploads should work even when login prompt has been disabled. client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": GenConanfile("hello0", "1.2.1")}) conan_conf = "core:non_interactive=True" client.save_home({"global.conf": conan_conf}) client.run("create . --user=user --channel=testing") client.run("remote logout '*'") client.run("remote login default admin -p password") client.run("upload hello0/1.2.1@user/testing -r default") assert "Uploading recipe 'hello0/1.2.1@" in client.out assert "Uploading package 'hello0/1.2.1@" in client.out def test_upload_key_error(self): files = {"conanfile.py": GenConanfile("hello0", "1.2.1")} server1 = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"lasote": "mypass"}) server2 = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"lasote": "mypass"}) servers = OrderedDict() servers["server1"] = server1 servers["server2"] = server2 client = TestClient(servers=servers) client.save(files) client.run("create . --user=user --channel=testing") client.run("remote login server1 lasote -p mypass") client.run("remote login server2 lasote -p mypass") client.run("upload hello0/1.2.1@user/testing -r server1") client.run("remove * --confirm") client.run("install --requires=hello0/1.2.1@user/testing -r server1") client.run("remote remove server1") client.run("upload hello0/1.2.1@user/testing -r server2") assert "ERROR: 'server1'" not in client.out def test_upload_without_user_channel(self): server = TestServer(users={"user": "password"}, write_permissions=[("*/*@*/*", "*")]) servers = {"default": server} client = TestClient(servers=servers, inputs=["user", "password"]) client.save({"conanfile.py": GenConanfile()}) client.run('create . --name=lib --version=1.0') assert "lib/1.0: Package '{}' created".format(NO_SETTINGS_PACKAGE_ID) in client.out client.run('upload lib/1.0 -c -r default') assert "Uploading recipe 'lib/1.0" in client.out # Verify that in the remote it is stored as "_" pref = PkgReference.loads("lib/1.0@#0:{}#0".format(NO_SETTINGS_PACKAGE_ID)) path = server.server_store.export(pref.ref) assert "/lib/1.0/_/_/0/export" in path.replace("\\", "/") path = server.server_store.package(pref) assert "/lib/1.0/_/_/0/package" in path.replace("\\", "/") # Should be possible with explicit package client.run(f'upload lib/1.0#*:{NO_SETTINGS_PACKAGE_ID} -c -r default --force') assert "Uploading artifacts" in client.out def test_upload_without_cleaned_user(self): """ When a user is not authenticated, uploads failed first time https://github.com/conan-io/conan/issues/5878 """ class EmptyCapabilitiesResponse(object): def __init__(self): self.ok = False self.headers = {"X-Conan-Server-Capabilities": "", "Content-Type": "application/json"} self.status_code = 401 self.content = b'' class ServerCapabilitiesRequester(TestRequester): def __init__(self, *args, **kwargs): self._first_ping = True super(ServerCapabilitiesRequester, self).__init__(*args, **kwargs) def get(self, url, **kwargs): app, url = self._prepare_call(url, kwargs) assert app assert ("/v1/" in url and url.endswith("ping")) or "/v2" in url if url.endswith("ping") and self._first_ping: self._first_ping = False return EmptyCapabilitiesResponse() else: response = app.get(url, **kwargs) return TestingResponse(response) server = TestServer(users={"user": "password"}, write_permissions=[("*/*@*/*", "*")]) servers = {"default": server} client = TestClient(requester_class=ServerCapabilitiesRequester, servers=servers, inputs=["user", "password"]) files = {"conanfile.py": GenConanfile("hello0", "1.2.1")} client.save(files) client.run("create . --user=user --channel=testing") client.run("remote logout '*'") client.run("upload hello0/1.2.1@user/testing -r default") assert "Uploading recipe 'hello0/1.2.1@user/testing" in client.out def test_server_returns_200_ok(self): # https://github.com/conan-io/conan/issues/16104 # If server returns 200 ok, without headers, it raises an error class MyHttpRequester(TestRequester): def get(self, _, **kwargs): resp = Response() resp.status_code = 200 return resp client = TestClient(requester_class=MyHttpRequester, servers={"default": TestServer()}) client.save({"conanfile.py": GenConanfile("hello0", "1.2.1")}) client.run("create . ") client.run("upload * -c -r default", assert_error=True) assert "doesn't seem like a valid Conan remote" in client.out def test_upload_only_without_user_channel(): """ check that we can upload only the packages without user and channel https://github.com/conan-io/conan/issues/10579 """ c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("lib", "1.0")}) c.run('create .') c.run("create . --user=user --channel=channel") c.run("list *") assert "lib/1.0@user/channel" in c.out c.run('upload */*@ -c -r=default') assert "Uploading recipe 'lib/1.0" in c.out # FAILS! assert "lib/1.0@user/channel" not in c.out c.run("search * -r=default") assert "lib/1.0" in c.out assert "lib/1.0@user/channel" not in c.out c.run('upload */*@user/channel -c -r=default') assert "Uploading recipe 'lib/1.0@user/channel" in c.out c.run("search * -r=default") assert "lib/1.0@user/channel" in c.out assert "lib/1.0" in c.out def test_upload_with_python_requires(): # https://github.com/conan-io/conan/issues/14503 c = TestClient(default_server_user=True, light=True) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "dep/conanfile.py": GenConanfile("dep", "0.1").with_python_requires("tool/[>=0.1]")}) c.run("create tool") c.run("create dep") c.run("upload tool* -c -r=default") c.run("remove tool* -c") c.run("upload dep* -c -r=default") # This used to fail, but adding the enabled remotes to python_requires resolution, it works assert "tool/0.1: Downloaded recipe" in c.out def test_upload_list_only_recipe(): c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("liba", "0.1")}) c.run("create .") c.run("install --requires=liba/0.1 --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --format=json", redirect_stdout="installed.json") c.run("upload --list=installed.json --only-recipe -r=default -c") assert "conan_package.tgz" not in c.out @pytest.mark.parametrize("dry_run", [True, False]) def test_upload_json_output(dry_run): c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("liba", "0.1").with_settings("os") .with_shared_option(False)}) c.run("create . -s os=Linux") dry_run_arg = "--dry-run" if dry_run else "" c.run(f"upload * -r=default {dry_run_arg} -c --format=json") list_pkgs = json.loads(c.stdout) revs = list_pkgs["default"]["liba/0.1"]["revisions"]["a565bd5defd3a99e157698fcc6e23b25"] pkg = revs["packages"]["9e0f8140f0fe6b967392f8d5da9881e232e05ff8"] prev = pkg["revisions"]["f50f552c6e04b1f241e5f7864bc3957f"] assert pkg["info"] == {"settings": {"os": "Linux"}, "options": {"shared": "False"}} base_url = "v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25" assert revs["upload-urls"] == { "conanfile.py": { "url": f"{c.servers['default']}/{base_url}/files/conanfile.py", "checksum": sha1sum(revs["files"]["conanfile.py"]) }, "conanmanifest.txt": { "url": f"{c.servers['default']}/{base_url}/files/conanmanifest.txt", "checksum": sha1sum(revs["files"]["conanmanifest.txt"]) } } pkg_url = "9e0f8140f0fe6b967392f8d5da9881e232e05ff8/revisions/f50f552c6e04b1f241e5f7864bc3957f" assert prev["upload-urls"] == { "conan_package.tgz": { "url": f"{c.servers['default']}/{base_url}/packages/{pkg_url}/files/conan_package.tgz", "checksum": sha1sum(prev["files"]["conan_package.tgz"]) }, "conaninfo.txt": { "url": f"{c.servers['default']}/{base_url}/packages/{pkg_url}/files/conaninfo.txt", "checksum": sha1sum(prev["files"]["conaninfo.txt"]) }, "conanmanifest.txt": { "url": f"{c.servers['default']}/{base_url}/packages/{pkg_url}/files/conanmanifest.txt", "checksum": sha1sum(prev["files"]["conanmanifest.txt"]) } } if dry_run: # check we don't have anything about the upload-urls in the text formatter c.run("upload * -r=default -c --dry-run") assert "upload-urls" not in c.out assert "url:" not in c.out assert "checksum:" not in c.out ================================================ FILE: test/integration/conan_api/__init__.py ================================================ ================================================ FILE: test/integration/conan_api/exit_with_code_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_exit_with_code(): base = textwrap.dedent(""" import sys from conan import ConanFile class HelloConan(ConanFile): name = "hello0" version = "0.1" def build(self): sys.exit(34) """) client = TestClient(light=True) client.save({"conanfile.py": base}) client.run("install .") error_code = client.run("build .", assert_error=True) assert error_code == 34 assert "Exiting with code: 34" in client.out ================================================ FILE: test/integration/conan_api/list_test.py ================================================ from conan.api.conan_api import ConanAPI from conan.test.utils.env import environment_update from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_get_recipe_revisions(): """ Test the "api.list.recipe_revisions" """ client = TestClient(default_server_user=True) for rev in range(1, 4): client.save({"conanfile.py": GenConanfile("foo", "1.0").with_build_msg(f"{rev}")}) client.run("create .") client.run("upload * -r=default -c") api = ConanAPI(client.cache_folder) # Check the revisions locally ref = RecipeReference.loads("foo/1.0") sot = api.list.recipe_revisions(ref) sot = [r.repr_notime() for r in sot] assert ["foo/1.0#6707ddcdb444fd46f92d449d11700c5a", "foo/1.0#913d984a1b9b8a2821d8c4d4e9cf8d57", "foo/1.0#b87cdb893042ec4b371bc6aa82a0108f"] == sot def test_get_package_revisions(): """ Test the "api.list.package_revisions" """ client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile("foo", "1.0").with_package_file("f.txt", env_var="MYVAR")}) for rev in range(3): with environment_update({"MYVAR": f"{rev}"}): client.run("create . ") client.run("upload * -r=default -c") api = ConanAPI(client.cache_folder) # Check the revisions locally pref = PkgReference.loads("foo/1.0#77ead28a5fd4216349b5b2181f4d32d4:" "da39a3ee5e6b4b0d3255bfef95601890afd80709") sot = api.list.package_revisions(pref) sot = [r.repr_notime() for r in sot] assert ['foo/1.0#77ead28a5fd4216349b5b2181f4d32d4:' 'da39a3ee5e6b4b0d3255bfef95601890afd80709#8b34fcf0543672de78ce1fe4f7fb3daa', 'foo/1.0#77ead28a5fd4216349b5b2181f4d32d4:' 'da39a3ee5e6b4b0d3255bfef95601890afd80709#5bb077c148d587da50ce9c3212370b5d', 'foo/1.0#77ead28a5fd4216349b5b2181f4d32d4:' 'da39a3ee5e6b4b0d3255bfef95601890afd80709#370f42f40d3f353a83a0f529ba2be1ce'] == sot def test_search_recipes_no_user_channel_only(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name foo --version 1.0 --user user --channel channel") client.run("create . --name foo --version 1.0") client.run("list foo/1.0@") assert "foo/1.0@user/channel" not in client.out assert "foo/1.0" in client.out ================================================ FILE: test/integration/conan_api/test_cli.py ================================================ import os import pytest from conan.api.conan_api import ConanAPI from conan.cli.cli import Cli, main from conan.test.utils.env import environment_update from conan.test.utils.mocks import RedirectedTestOutput from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import redirect_output def test_cli(): """ make sure the CLi can be reused https://github.com/conan-io/conan/issues/14044 """ folder = temp_folder() api = ConanAPI(cache_folder=folder) cli = Cli(api) cli2 = Cli(api) stdout = RedirectedTestOutput() stderr = RedirectedTestOutput() with redirect_output(stderr, stdout): cli.run(["list", "*"]) cli.run(["list", "*"]) cli2.run(["list", "*"]) cli.run(["list", "*"]) stdout = RedirectedTestOutput() stderr = RedirectedTestOutput() with redirect_output(stderr, stdout): cli.run() # Running without args shows help, but doesn't error assert "Consumer commands" in stdout.getvalue() def test_basic_api(): api = ConanAPI(cache_folder=temp_folder()) result = api.remotes.list() assert result[0].name == "conancenter" def test_api_command(): # The ``CommandAPI`` requires a bit more of setup api = ConanAPI(cache_folder=temp_folder()) cli = Cli(api) cli.add_commands() result = api.command.run(["remote", "list"]) assert result[0].name == "conancenter" def test_main(): cache_folder = os.path.join(temp_folder(), "custom") with environment_update({"CONAN_HOME": cache_folder}): with pytest.raises(SystemExit) as e: main(["list", "*"]) assert e.type == SystemExit assert e.value.code == 0 # success ================================================ FILE: test/integration/conan_api/test_local_api.py ================================================ import os from conan.api.conan_api import ConanAPI from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.api.model import RecipeReference from conan.internal.util.files import save def test_local_api(): # https://github.com/conan-io/conan/issues/17484 current_folder = temp_folder() cache_folder = temp_folder() save(os.path.join(current_folder, "conanfile.py"), str(GenConanfile("foo", "1.0"))) api = ConanAPI(cache_folder) assert api.local.editable_packages.edited_refs == {} api.local.editable_add(".", cwd=current_folder) assert list(api.local.editable_packages.edited_refs) == [RecipeReference.loads("foo/1.0")] ================================================ FILE: test/integration/conan_api/test_profile_api.py ================================================ import pytest from conan.api.conan_api import ConanAPI from conan.errors import ConanException def test_profile_api(): # It must be an absolute path with pytest.raises(ConanException) as e: ConanAPI(cache_folder="test") assert "cache_folder has to be an absolute path" in str(e.value) ================================================ FILE: test/integration/conan_v2/__init__.py ================================================ ================================================ FILE: test/integration/conan_v2/test_legacy_cpp_info.py ================================================ import textwrap from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import load, save def test_legacy_names_filenames(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" def package_info(self): self.cpp_info.components["comp"].names["cmake_find_package"] = "hello" self.cpp_info.components["comp"].names["cmake_find_package_multi"] = "hello" self.cpp_info.components["comp"].build_modules["cmake_find_package"] = ["nice_rel_path"] self.cpp_info.components["comp"].build_modules["cmake_find_package"].append("some_file_name") self.cpp_info.components["comp"].build_modules["cmake_find_package_multi"] = ["nice_rel_path"] self.cpp_info.names["cmake_find_package"] = "absl" self.cpp_info.names["cmake_find_package_multi"] = "absl" self.cpp_info.filenames["cmake_find_package"] = "tensorflowlite" self.cpp_info.filenames["cmake_find_package_multi"] = "tensorflowlite" self.cpp_info.build_modules["cmake_find_package"] = ["nice_rel_path"] self.cpp_info.build_modules["cmake_find_package_multi"] = ["nice_rel_path"] self.env_info.whatever = "whatever-env_info" self.env_info.PATH.append("/path/to/folder") self.user_info.whatever = "whatever-user_info" """) c.save({"conanfile.py": conanfile}) c.run("create .") for name in ["cpp_info.names", "cpp_info.filenames", "env_info", "user_info", "cpp_info.build_modules"]: assert f"WARN: deprecated: '{name}' used in: pkg/1.0" in c.out c.save_home({"global.conf": 'core:skip_warnings=["deprecated"]'}) c.run("create .") for name in ["cpp_info.names", "cpp_info.filenames", "env_info", "user_info", "cpp_info.build_modules"]: assert f"'{name}' used in: pkg/1.0" not in c.out class TestLegacy1XRecipes: def test_legacy_imports(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" """) c.save({"pkg/conanfile.py": conanfile, "app/conanfile.py": GenConanfile("app", "1.0").with_requires("pkg/1.0")}) # With EDITABLE, we can emulate errors without exporting c.run("export pkg") layout = c.get_latest_ref_layout(RecipeReference.loads("pkg/1.0")) conanfile = layout.conanfile() content = load(conanfile) content = content.replace("from conan", "from conans") save(conanfile, content) c.run("install app", assert_error=True) assert "Recipe 'pkg/1.0' seems broken." in c.out assert "It is possible that this recipe is not Conan 2.0 ready" in c.out ================================================ FILE: test/integration/conanfile/__init__.py ================================================ ================================================ FILE: test/integration/conanfile/conan_data_test.py ================================================ import json import os import shutil import sys import textwrap import pytest import yaml from conan.api.model import RecipeReference from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import tgz_with_contents from conan.test.utils.tools import TestClient, GenConanfile from conan.internal.util.files import md5sum, sha1sum, sha256sum, load class TestConanData: def test_conan_exports_kept(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): exports = "myfile.txt" """) conandata = textwrap.dedent(""" foo: bar: "as" """) client.save({"conanfile.py": conanfile, "myfile.txt": "bar", "conandata.yml": conandata}) ref = RecipeReference.loads("lib/0.1") client.run(f"export . --name={ref.name} --version={ref.version}") export_folder = client.get_latest_ref_layout(ref).export() exported_data = os.path.join(export_folder, "conandata.yml") data = yaml.safe_load(load(exported_data)) assert data == {"foo": {"bar": "as"}} assert os.path.exists(os.path.join(export_folder, "myfile.txt")) def test_conan_data_everywhere(self): client = TestClient(light=True) conanfile = """from conan import ConanFile class Lib(ConanFile): def _assert_data(self): assert(self.conan_data["sources"]["all"]["url"] == "the url") assert(self.conan_data["sources"]["all"]["other"] == "field") self.output.info("My URL: {}".format(self.conan_data["sources"]["all"]["url"])) def configure(self): self._assert_data() def config_options(self): self._assert_data() def source(self): self._assert_data() def build(self): self._assert_data() def package(self): self._assert_data() def package_info(self): self._assert_data() """ client.save({"conanfile.py": conanfile, "conandata.yml": """ sources: all: url: "the url" other: "field" """}) ref = RecipeReference.loads("lib/0.1") client.run(f"create . --name={ref.name} --version={ref.version}") assert "File 'conandata.yml' found. Exporting it..." in client.out assert "My URL:" in client.out export_folder = client.get_latest_ref_layout(ref).export() assert os.path.exists(os.path.join(export_folder, "conandata.yml")) # Transitive loaded? client.save({"conanfile.txt": "[requires]\n{}".format(ref)}, clean_first=True) client.run("install . ") assert "My URL:" in client.out client.run("install . --build='*'") assert "My URL:" in client.out def test_conan_data_as_source_newtools(self): client = TestClient(light=True) file_server = TestFileServer() client.servers["file_server"] = file_server tgz_path = tgz_with_contents({"foo.txt": "foo"}) if sys.version_info.major == 3 and sys.version_info.minor >= 9: # Python 3.9 changed the tar algorithm. Conan tgz will have different checksums # https://github.com/conan-io/conan/issues/8020 md5_value = "f1d0dee6f0bf5b7747c013dd26183cdb" sha1_value = "d45ca9ad171ca9baa93f4da99904036aa71b0ddb" sha256_value = "b6880ef494974b8413a107429bde8d6b81a85c45a600040f5334a1d300c203b5" else: md5_value = "babc50837f9aaf46e134455966230e3e" sha1_value = "1e5b8ff7ae58b40d698fe3d4da6ad2a47ec6f4f3" sha256_value = "3ff04581cb0e2f9e976a9baad036f4ca9d884907c3d9382bb42a8616d3c20e42" assert md5_value == md5sum(tgz_path) assert sha1_value == sha1sum(tgz_path) assert sha256_value == sha256sum(tgz_path) shutil.copy2(tgz_path, file_server.store) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import get class Lib(ConanFile): def source(self): data = self.conan_data["sources"]["all"] get(self, **data) self.output.info("OK!") """) conandata = textwrap.dedent(""" sources: all: url: "{}/myfile.tar.gz" md5: "{}" sha1: "{}" sha256: "{}" """) client.save({"conanfile.py": conanfile, "conandata.yml": conandata.format(file_server.fake_url, md5_value, sha1_value, sha256_value)}) client.run(f"create . --name=pkg --version=0.1") assert "OK!" in client.out ref_layout = client.exported_layout() source_folder = ref_layout.source() downloaded_file = os.path.join(source_folder, "foo.txt") assert "foo" == load(downloaded_file) def test_invalid_yml(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile(), "conandata.yml": ">>>> ::"}) ref = RecipeReference.loads("lib/0.1") client.run(f"create . --name={ref.name} --version={ref.version}", assert_error=True) assert "ERROR: Error loading conanfile at" in client.out assert ": Invalid yml format at conandata.yml: while scanning a block scalar" in client.out def test_conan_data_development_flow(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def layout(self): self.folders.build = "tmp/build" def _assert_data(self): assert(self.conan_data["sources"]["all"]["url"] == "this url") assert(self.conan_data["sources"]["all"]["other"] == "field") self.output.info("My URL: {}".format(self.conan_data["sources"]["all"]["url"])) def source(self): self._assert_data() def build(self): self._assert_data() def package(self): self._assert_data() """) conandata = textwrap.dedent(""" sources: all: url: "this url" other: "field" """) client.save({"conanfile.py": conanfile, "conandata.yml": conandata}) client.run("source .") assert "My URL: this url" in client.out client.run("build . -of=tmp/build") assert "My URL: this url" in client.out client.run("export-pkg . --name=name --version=version") assert "My URL: this url" in client.out def test_conan_data_serialize(self): c = TestClient(light=True) conandata = textwrap.dedent(""" src: url: "this url" """) c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "conandata.yml": conandata}) c.run("graph info . --format=json") graph = json.loads(c.stdout) assert graph["graph"]["nodes"]["0"]["conandata"]["src"] == {"url": "this url"} c.run("inspect . --format=json") result = json.loads(c.stdout) assert result["conandata"]["src"] == {"url": "this url"} class TestConanDataUpdate: """ testing the update_conandata() method """ def test_conandata_update(self): """ test the update_conandata() helper """ c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import update_conandata class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): update_conandata(self, {"sources": {"0.1": {"commit": 123, "type": "git"}, "0.2": {"url": "new"} } }) def source(self): data = self.conan_data["sources"] self.output.info("0.1-commit: {}!!".format(data["0.1"]["commit"])) self.output.info("0.1-type: {}!!".format(data["0.1"]["type"])) self.output.info("0.1-url: {}!!".format(data["0.1"]["url"])) self.output.info("0.2-url: {}!!".format(data["0.2"]["url"])) """) conandata = textwrap.dedent("""\ sources: "0.1": url: myurl commit: 234 """) c.save({"conanfile.py": conanfile, "conandata.yml": conandata}) c.run("create .") assert "pkg/0.1: 0.1-commit: 123!!" in c.out assert "pkg/0.1: 0.1-type: git!!" in c.out assert "pkg/0.1: 0.1-url: myurl!!" in c.out assert "pkg/0.1: 0.2-url: new!!" in c.out def test_conandata_update_error(self): """ test the update_conandata() helper fails if used outside export() """ c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import update_conandata class Pkg(ConanFile): name = "pkg" version = "0.1" def source(self): update_conandata(self, {}) """) c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) assert "The 'update_conandata()' can only be used in the 'export()' method" in c.out def test_conandata_create_if_not_exist(self): """ test the update_conandata() creates the file if it doesn't exist """ c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import update_conandata class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): update_conandata(self, {"data": "value"}) """) c.save({"conanfile.py": conanfile}) c.run("export .") # It doesn't fail assert "pkg/0.1: Calling export()" in c.out def test_conandata_trim(): """ test the explict trim_conandata() helper """ c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import trim_conandata class Pkg(ConanFile): name = "pkg" def export(self): trim_conandata(self) """) conandata_yml = textwrap.dedent("""\ sources: "1.0": url: "url1" sha256: "sha1" patches: "1.0": - patch_file: "patches/some_patch" base_path: "source_subfolder" something: else """) c.save({"conanfile.py": conanfile, "conandata.yml": conandata_yml}) c.run("export . --version=1.0") layout = c.exported_layout() data1 = load(os.path.join(layout.export(), "conandata.yml")) assert "pkg/1.0: Exported: pkg/1.0#70612e15e4fc9af1123fe11731ac214f" in c.out conandata_yml2 = textwrap.dedent("""\ sources: "1.0": url: "url1" sha256: "sha1" "1.1": url: "url2" sha256: "sha2" patches: "1.1": - patch_file: "patches/some_patch2" base_path: "source_subfolder" "1.0": - patch_file: "patches/some_patch" base_path: "source_subfolder" something: else """) c.save({"conandata.yml": conandata_yml2}) c.run("export . --version=1.0") layout = c.exported_layout() data2 = load(os.path.join(layout.export(), "conandata.yml")) assert "1.1" not in data2 assert data1 == data2 assert "pkg/1.0: Exported: pkg/1.0#70612e15e4fc9af1123fe11731ac214f" in c.out # If I now try to create version 1.2 which has no patches, and then change a patch # its revision should not change either conandata_yml3 = textwrap.dedent("""\ sources: "1.0": url: "url1" sha256: "sha1" "1.1": url: "url2" sha256: "sha2" "1.3": url: "url3" sha256: "sha3" patches: "1.1": - patch_file: "patches/some_patch2" base_path: "source_subfolder" "1.0": - patch_file: "patches/some_patch" base_path: "source_subfolder" something: else""") c.save({"conandata.yml": conandata_yml3}) c.run("export . --version=1.3") initial_v13_rev = c.exported_recipe_revision() conandata_yml4 = textwrap.dedent("""\ sources: "1.0": url: "url1" sha256: "sha1" "1.1": url: "url2" sha256: "sha2" "1.3": url: "url3" sha256: "sha3" patches: "1.1": - patch_file: "patches/some_patch2-v2" base_path: "source_subfolder" "1.0": - patch_file: "patches/some_patch" base_path: "source_subfolder" something: else""") c.save({"conandata.yml": conandata_yml4}) c.run("export . --version=1.3") second_v13_rev = c.exported_recipe_revision() assert initial_v13_rev == second_v13_rev def test_trim_conandata_as_hook(): c = TestClient(light=True) c.save_home({"extensions/hooks/hook_trim.py": textwrap.dedent(""" from conan.tools.files import trim_conandata def post_export(conanfile): trim_conandata(conanfile) """)}) conandata_yml = textwrap.dedent("""\ sources: "1.0": url: "url1" sha256: "sha1" patches: "1.0": - patch_file: "patches/some_patch" base_path: "source_subfolder" something: else """) c.save({"conanfile.py": GenConanfile("pkg"), "conandata.yml": conandata_yml}) c.run("export . --version=1.0") layout = c.exported_layout() data1 = load(os.path.join(layout.export(), "conandata.yml")) assert "pkg/1.0: Exported: pkg/1.0#03af39add1c7c9d68dcdb10b6968a14d" in c.out conandata_yml2 = textwrap.dedent("""\ sources: "1.0": url: "url1" sha256: "sha1" "1.1": url: "url2" sha256: "sha2" patches: "1.1": - patch_file: "patches/some_patch2" base_path: "source_subfolder" "1.0": - patch_file: "patches/some_patch" base_path: "source_subfolder" something: else """) c.save({"conandata.yml": conandata_yml2}) c.run("export . --version=1.0") layout = c.exported_layout() data2 = load(os.path.join(layout.export(), "conandata.yml")) assert "1.1" not in data2 assert data1 == data2 assert "pkg/1.0: Exported: pkg/1.0#03af39add1c7c9d68dcdb10b6968a14d" in c.out @pytest.mark.parametrize("raise_if_missing", [True, False]) def test_trim_conandata_as_hook_without_conandata(raise_if_missing): c = TestClient(light=True) c.save_home({"extensions/hooks/hook_trim.py": textwrap.dedent(f""" from conan.tools.files import trim_conandata def post_export(conanfile): trim_conandata(conanfile, raise_if_missing={raise_if_missing}) """)}) c.save({"conanfile.py": GenConanfile("pkg")}) if raise_if_missing: c.run("export . --version=1.0", assert_error=True) else: c.run("export . --version=1.0") assert c.exported_recipe_revision() == "a9ec2e5fbb166568d4670a9cd1ef4b26" def test_trim_conandata_anchors(): """Anchors load correctly, because trim_conandata loads the yaml instead of replacing in place""" tc = TestClient(light=True) tc.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.files import trim_conandata class Pkg(ConanFile): name = "pkg" def export(self): trim_conandata(self) def generate(self): self.output.info("x: {}".format(self.conan_data["mapping"][self.version]["x"])) """), "conandata.yml": textwrap.dedent(""" mapping: "1.0": &anchor "x": "foo" "2.0": *anchor """)}) tc.run("create . --version=2.0") assert "x: foo" in tc.out pkg_layout = tc.exported_layout() conandata = tc.load(pkg_layout.conandata()) assert conandata == textwrap.dedent("""\ mapping: '2.0': x: foo """) ================================================ FILE: test/integration/conanfile/conanfile_errors_test.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestConanfileErrors: def test_copy_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" exports = "*" def package(self): self.copy2("*.h", dst="include", src=["include","platform"]) ''') files = {"conanfile.py": conanfile, "test.txt": "Hello world"} client.save(files) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello/0.1@lasote/stable --build='*'", assert_error=True) assert "hello/0.1@lasote/stable: Error in package() method, line 9" in client.out assert 'self.copy2("*.h", dst="include", src=["include","platform"]' in client.out assert "'HelloConan' object has no attribute 'copy2'" in client.out def test_copy_error2(self): client = TestClient(light=True) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" exports = "*" def package(self): self.copy("*.h", dst="include", src=["include","platform"]) ''') files = {"conanfile.py": conanfile, "test.txt": "Hello world"} client.save(files) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello/0.1@lasote/stable --build='*'", assert_error=True) assert "hello/0.1@lasote/stable: Error in package() method, line 9" in client.out assert 'self.copy("*.h", dst="include", src=["include","platform"]' in client.out # It results that the error is different in different Python2/3 and OSs # assert "'list' object has no attribute 'replace'" in client.out def test_package_info_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" exports = "*" def package_info(self): self.copy2() ''') files = {"conanfile.py": conanfile, "test.txt": "Hello world"} client.save(files) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello/0.1@lasote/stable --build='*'", assert_error=True) assert "hello/0.1@lasote/stable: Error in package_info() method, line 9" in client.out assert 'self.copy2()' in client.out assert "'HelloConan' object has no attribute 'copy2'" in client.out def test_config_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" exports = "*" def configure(self): self.copy2() ''') files = {"conanfile.py": conanfile, "test.txt": "Hello world"} client.save(files) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello/0.1@lasote/stable --build='*'", assert_error=True) assert "ERROR: hello/0.1@lasote/stable: Error in configure() method, line 9" in client.out assert "self.copy2()" in client.out assert "AttributeError: 'HelloConan' object has no attribute 'copy2'""" in client.out def test_source_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" exports = "*" def source(self): self.copy2() ''') files = {"conanfile.py": conanfile, "test.txt": "Hello world"} client.save(files) client.run("export . --user=lasote --channel=stable") client.run("install --requires=hello/0.1@lasote/stable --build='*'", assert_error=True) assert "hello/0.1@lasote/stable: Error in source() method, line 9" in client.out assert 'self.copy2()' in client.out assert "'HelloConan' object has no attribute 'copy2'" in client.out def test_duplicate_requires(self): client = TestClient(light=True) conanfile = textwrap.dedent(''' [requires] foo/0.1@user/testing foo/0.2@user/testing ''') files = {"conanfile.txt": conanfile} client.save(files) client.run("install . --build='*'", assert_error=True) assert "ERROR: Duplicated requirement" in client.out def test_duplicate_requires_py(self): client = TestClient(light=True) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" requires = "foo/0.1@user/testing", "foo/0.2@user/testing" ''') files = {"conanfile.py": conanfile} client.save(files) client.run("export .", assert_error=True) assert "Duplicated requirement" in client.out class TestWrongMethods: # https://github.com/conan-io/conan/issues/12961 @pytest.mark.parametrize("requires", ["requires", "tool_requires", "test_requires", "build_requires"]) def test_wrong_method_requires(self, requires): """ this is expected to be a relatively frequent user error, and the trace was very ugly and debugging complicated """ c = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): def {requires}(self): pass """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) expected = "requirements" if requires == "requires" else "build_requirements" assert f" Wrong '{requires}' definition, did you mean '{expected}()'?" in c.out def test_notduplicate_requires_py(): client = TestClient(light=True) conanfile = textwrap.dedent(''' from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" requires = "foo/0.1@user/testing" build_requires = "foo/0.2@user/testing" ''') files = {"conanfile.py": conanfile} client.save(files) client.run("export .") assert "hello/0.1: Exported" in client.out def test_requirements_change_options(): c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" def requirements(self): self.options["mydep"].myoption = 3 """) c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) assert "ERROR: hello/0.1: Dependencies options were defined incorrectly." in c.out @pytest.mark.parametrize("property_name", ["libdir", "bindir", "includedir"]) @pytest.mark.parametrize("property_content", [[], ["mydir1", "mydir2"]]) def test_shorthand_bad_interface(property_name, property_content): c = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" def package_info(self): self.cpp_info.{property_name}s = {property_content} self.output.info(self.cpp_info.{property_name}) """) c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) if property_content: assert (f"The {property_name} property is undefined because " f"{property_name}s has more than one element.") in c.out else: assert (f"The {property_name} property is undefined because " f"{property_name}s is empty.") in c.out def test_consumer_unexpected(): tc = TestClient(light=True) cmake_conanfile = textwrap.dedent(""" from conan import ConanFile class CMake(ConanFile): name = "cmake" version = "1.0" package_type = "application" def package_info(self): self.output.info("cmake/1.0 -> " + str(self._conan_is_consumer)) """) tc.save({ "cmake/conanfile.py": cmake_conanfile, "dep1/conanfile.py": GenConanfile("dep1", "1.0"), "conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep1/1.0"), "profile": "include(default)\n[tool_requires]\n*: cmake/1.0\n"}) tc.run("export dep1") tc.run("create cmake") tc.run("create . -b=missing -pr=profile") assert "cmake/1.0 -> True" not in tc.out @pytest.mark.parametrize("languages", [ "['C', 'CXX']", "['CXX']", "'CXX'", ]) def test_language_unexpected(languages): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile().with_class_attribute(f"languages = {languages}")}) tc.run("inspect .", assert_error=True) assert "Only 'C' and 'C++' languages are allowed in 'languages' attribute" in tc.out def test_empty_languages(): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile().with_class_attribute("languages = []")}) tc.run("inspect .") assert "Only 'C' and 'C++' languages are allowed in 'languages' attribute" not in tc.out ================================================ FILE: test/integration/conanfile/conanfile_helpers_test.py ================================================ import pytest from conan.test.utils.tools import TestClient class TestConanfileHelpers: @pytest.mark.parametrize("scope_imports", [True, False]) def test_helpers_same_name(self, scope_imports): helpers = ''' def build_helper(output): output.info("Building %d!") ''' other_helper = ''' def source_helper(output): output.info("Source %d!") ''' if scope_imports: file_content = '''from conan import ConanFile from myhelper import build_helper from myhelpers.other import source_helper class ConanFileToolsTest(ConanFile): exports = "*" def source(self): source_helper(self.output) def build(self): build_helper(self.output) ''' else: file_content = '''from conan import ConanFile import myhelper from myhelpers import other class ConanFileToolsTest(ConanFile): exports = "*" def source(self): other.source_helper(self.output) def build(self): myhelper.build_helper(self.output) ''' def files(number): return {"myhelper.py": helpers % number, "myhelpers/__init__.py": "", "myhelpers/other.py": other_helper % number, "conanfile.py": file_content} client = TestClient() client.save(files(1)) client.run("export . --name=test --version=1.9 --user=user --channel=testing") client.save(files(2), clean_first=True) client.run("export . --name=test2 --version=2.3 --user=user --channel=testing") files = {"conanfile.txt": """[requires] test/1.9@user/testing\n test2/2.3@user/testing"""} client.save(files, clean_first=True) client.run("install . --build='*'") assert "test/1.9@user/testing: Building 1!" in client.out assert "test/1.9@user/testing: Source 1!" in client.out assert "test2/2.3@user/testing: Building 2!" in client.out assert "test2/2.3@user/testing: Source 2!" in client.out ================================================ FILE: test/integration/conanfile/files/.gitattributes ================================================ *.txt -text -diff ================================================ FILE: test/integration/conanfile/files/conanfile_utf8.txt ================================================ [requires] [generators] CMakeToolchain ================================================ FILE: test/integration/conanfile/files/conanfile_utf8_with_bom.txt ================================================ [requires] [generators] CMakeToolchain ================================================ FILE: test/integration/conanfile/folders_access_test.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient conanfile_parent = """ from conan import ConanFile class parentLib(ConanFile): name = "parent" version = "1.0" def package_info(self): self.cpp_info.cxxflags.append("-myflag") self.buildenv_info.define("MyEnvVar", "MyEnvVarValue") """ conanfile = """ import os from conan import ConanFile from pathlib import Path class AConan(ConanFile): name = "lib" version = "1.0" # To save the folders and check later if the folder is the same copy_build_folder = None copy_source_folder = None copy_package_folder = None counter_package_calls = 0 no_copy_source = %(no_copy_source)s requires = "parent/1.0@conan/stable" running_local_command = %(local_command)s def source(self): assert(self.source_folder == os.getcwd()) assert(isinstance(self.source_path, Path)) assert(str(self.source_path) == self.source_folder) # Prevented to use them, it's dangerous, because the source is run only for the first # config, so only the first build_folder/package_folder would be modified assert(self.build_folder is None) assert(self.package_folder is None) assert(self.source_folder is not None) self.copy_source_folder = self.source_folder def build(self): assert(self.build_folder == os.getcwd()) assert(isinstance(self.build_path, Path)) assert(str(self.build_path) == self.build_folder) if self.no_copy_source: assert(self.copy_source_folder == self.source_folder) # Only in install else: assert(self.source_folder == self.build_folder) assert(self.package_folder is not None) assert(isinstance(self.package_path, Path)) assert(str(self.package_path) == self.package_folder) self.copy_build_folder = self.build_folder def package(self): assert(self.build_folder == os.getcwd()) assert(isinstance(self.build_path, Path)) assert(str(self.build_path) == self.build_folder) if self.no_copy_source: assert(self.copy_source_folder == self.source_folder) # Only in install else: assert(self.source_folder == self.build_folder) self.copy_package_folder = self.package_folder def package_info(self): assert(self.package_folder == os.getcwd()) assert(isinstance(self.package_path, Path)) assert(str(self.package_path) == self.package_folder) """ class TestFoldersAccess: """"Tests the presence of self.source_folder, self.build_folder, self.package_folder in the conanfile methods. Also the availability of the self.deps_cpp_info, self.deps_user_info and self.deps_env_info.""" @pytest.fixture(autouse=True) def setup(self): self.client = TestClient(light=True) self.client.save({"conanfile.py": conanfile_parent}) self.client.run("export . --user=conan --channel=stable") def test_source_local_command(self): c1 = conanfile % {"no_copy_source": False, "local_command": True} self.client.save({"conanfile.py": c1}, clean_first=True) self.client.run("source .") c1 = conanfile % {"no_copy_source": True, "local_command": True} self.client.save({"conanfile.py": c1}, clean_first=True) self.client.run("source .") def test_deploy(self): c1 = conanfile % {"no_copy_source": False, "local_command": False} self.client.save({"conanfile.py": c1}, clean_first=True) self.client.run("create . --user=user --channel=testing --build missing") self.client.run("install --requires=lib/1.0@user/testing") # Checks deploy def test_full_install(self): c1 = conanfile % {"no_copy_source": False, "local_command": False} self.client.save({"conanfile.py": c1}, clean_first=True) self.client.run("create . --user=conan --channel=stable --build='*'") c1 = conanfile % {"no_copy_source": True, "local_command": False} self.client.save({"conanfile.py": c1}, clean_first=True) self.client.run("create . --user=conan --channel=stable --build='*'") c1 = conanfile % {"no_copy_source": False, "local_command": False} self.client.save({"conanfile.py": c1}, clean_first=True) self.client.run("create . --user=conan --channel=stable --build='*'") class TestRecipeFolder: recipe_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load import os class Pkg(ConanFile): exports = "file.txt" def init(self): r = load(self, os.path.join(self.recipe_folder, "file.txt")) self.output.info("INIT: {}".format(r)) def set_name(self): r = load(self, os.path.join(self.recipe_folder, "file.txt")) self.output.info("SET_NAME: {}".format(r)) def configure(self): r = load(self, os.path.join(self.recipe_folder, "file.txt")) self.output.info("CONFIGURE: {}".format(r)) def requirements(self): r = load(self, os.path.join(self.recipe_folder, "file.txt")) self.output.info("REQUIREMENTS: {}".format(r)) def package(self): r = load(self, os.path.join(self.recipe_folder, "file.txt")) self.output.info("PACKAGE: {}".format(r)) def package_info(self): r = load(self, os.path.join(self.recipe_folder, "file.txt")) self.output.info("PACKAGE_INFO: {}".format(r)) """) def test_recipe_folder(self): client = TestClient(light=True) client.save({"conanfile.py": self.recipe_conanfile, "file.txt": "MYFILE!"}) client.run("export . --name=pkg --version=0.1 --user=user --channel=testing") assert "INIT: MYFILE!" in client.out assert "SET_NAME: MYFILE!" in client.out client.save({}, clean_first=True) client.run("install --requires=pkg/0.1@user/testing --build='*'") assert "pkg/0.1@user/testing: INIT: MYFILE!" in client.out assert "SET_NAME" not in client.out assert "pkg/0.1@user/testing: CONFIGURE: MYFILE!" in client.out assert "pkg/0.1@user/testing: REQUIREMENTS: MYFILE!" in client.out assert "pkg/0.1@user/testing: PACKAGE: MYFILE!" in client.out assert "pkg/0.1@user/testing: PACKAGE_INFO: MYFILE!" in client.out def test_local_flow(self): client = TestClient(light=True) client.save({"conanfile.py": self.recipe_conanfile, "file.txt": "MYFILE!"}) client.run("install .") assert "INIT: MYFILE!" in client.out assert "SET_NAME: MYFILE!" in client.out assert "conanfile.py: CONFIGURE: MYFILE!" in client.out assert "conanfile.py: REQUIREMENTS: MYFILE!" in client.out def test_editable(self): client = TestClient(light=True) client.save({"pkg/conanfile.py": self.recipe_conanfile, "pkg/file.txt": "MYFILE!", "consumer/conanfile.py": GenConanfile().with_require("pkg/0.1@user/stable")}) client.run("editable add pkg --name=pkg --version=0.1 --user=user --channel=stable") client.run("install consumer") client.assert_listed_require({"pkg/0.1@user/stable": "Editable"}) assert "pkg/0.1@user/stable: INIT: MYFILE!" in client.out assert "pkg/0.1@user/stable: CONFIGURE: MYFILE!" in client.out assert "pkg/0.1@user/stable: REQUIREMENTS: MYFILE!" in client.out assert "pkg/0.1@user/stable: PACKAGE_INFO: MYFILE!" in client.out ================================================ FILE: test/integration/conanfile/generators_list_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient class TestConanfileRepeatedGenerators: def test_conanfile_txt(self): conanfile = textwrap.dedent(""" [generators] CMakeDeps CMakeDeps """) t = TestClient() t.save({'conanfile.txt': conanfile}) t.run("install conanfile.txt") assert str(t.out).count("Generator 'CMakeDeps' calling 'generate()'") == 1 def test_conanfile_py(self): conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): settings = "build_type" generators = "CMakeDeps", "CMakeDeps" """) t = TestClient() t.save({'conanfile.py': conanfile}) t.run("install conanfile.py") assert str(t.out).count("Generator 'CMakeDeps' calling 'generate()'") == 1 def test_python_requires_inheritance(self): pyreq = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): pass class BaseConan(object): generators = "CMakeDeps", """) conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): settings = "build_type" python_requires = "base/1.0" python_requires_extend = "base.BaseConan" settings = "build_type" generators = "CMakeDeps", def init(self): base = self.python_requires["base"].module.BaseConan self.generators = base.generators + self.generators """) t = TestClient() t.save({'pyreq.py': pyreq, 'conanfile.py': conanfile}) t.run("export pyreq.py --name=base --version=1.0") t.run("install conanfile.py") assert str(t.out).count("Generator 'CMakeDeps' calling 'generate()'") == 1 def test_duplicated_generator_in_member_and_attribue(self): """ Ensure we raise an error when a generator is present both in the generators attribute and instanced in the generate() method by the user, which we didn't use to do before 2.0 """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Recipe(ConanFile): generators = "CMakeToolchain" def generate(self): tc = CMakeToolchain(self) tc.generate() """) t = TestClient() t.save({'conanfile.py': conanfile}) # This used to not throw any errors t.run("install .", assert_error=True) assert "was instantiated in the generate() method too" in t.out ================================================ FILE: test/integration/conanfile/init_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient class TestInit: def test_wrong_init(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def init(self): random_error """) client.save({"conanfile.py": conanfile}) client.run("export .", assert_error=True) assert "Error in init() method, line 5" in client.out assert "name 'random_error' is not defined" in client.out def test_init(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load import os import json class Lib(ConanFile): exports = "data.json" def init(self): data = load(self, os.path.join(self.recipe_folder, "data.json")) d = json.loads(data) self.license = d["license"] self.description = d["description"] def export(self): self.output.info("description: %s" % self.description) self.output.info("license: %s" % self.license) def build(self): self.output.info("description: %s" % self.description) self.output.info("license: %s" % self.license) """) data = '{"license": "MIT", "description": "MyDescription"}' client.save({"conanfile.py": conanfile, "data.json": data}) client.run("export . --name=pkg --version=version") assert "description: MyDescription" in client.out assert "license: MIT" in client.out client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "description: MyDescription" in client.out assert "license: MIT" in client.out def test_options_from_yml(self): client = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile class Lib(ConanFile): name = "pkg" version = "0.1" def init(self): self.options.update(self.conan_data["options"], self.conan_data["default_options"]) """) conandata = textwrap.dedent("""\ options: myopt1: [1, 2, 3] myopt2: [null, potato] myopt3: [null, ANY] default_options: myopt1: 2 """) client.save({"conanfile.py": conanfile, "conandata.yml": conandata}) client.run("create . ") client.run("create . -o &:myopt1=1 -o &:myopt2=potato -o &:myopt3=whatever") client.run("list *:*") assert "myopt1: 2" in client.out assert "myopt1: 1" in client.out assert "myopt2: potato" in client.out assert "myopt3: whatever" in client.out ================================================ FILE: test/integration/conanfile/invalid_configuration_test.py ================================================ import textwrap import pytest from conan.cli.exit_codes import ERROR_INVALID_CONFIGURATION from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestInvalidConfiguration: @pytest.fixture(autouse=True) def setup(self): self.client = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.errors import ConanInvalidConfiguration class MyPkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def configure(self): if self.settings.compiler.version == "190": raise ConanInvalidConfiguration("compiler.version=12 is invalid!!") """) self.client.save({"conanfile.py": conanfile}) settings = "-s os=Windows -s compiler=msvc -s compiler.version={ver} "\ "-s compiler.runtime=dynamic" self.settings_msvc15 = settings.format(ver="192") self.settings_msvc12 = settings.format(ver="190") def test_install_method(self): self.client.run("install . %s" % self.settings_msvc15) error = self.client.run("install . %s" % self.settings_msvc12, assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "Invalid configuration: compiler.version=12 is invalid!!" in self.client.out def test_info_method(self): self.client.run("graph info . %s" % self.settings_msvc15) error = self.client.run("graph info . %s" % self.settings_msvc12, assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "Invalid configuration: compiler.version=12 is invalid!!" in self.client.out def test_create_method(self): self.client.run("create . --name=name --version=ver %s" % self.settings_msvc15) error = self.client.run("create . --name=name --version=ver %s" % self.settings_msvc12, assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "name/ver: Invalid configuration: compiler.version=12 is invalid!!" in self.client.out def test_as_requirement(self): self.client.run("create . --name=name --version=ver %s" % self.settings_msvc15) self.client.save({"other/conanfile.py": GenConanfile().with_requirement("name/ver")}) self.client.run("create other/ --name=other --version=1.0 %s" % self.settings_msvc15) error = self.client.run("create other/ --name=other --version=1.0 %s" % self.settings_msvc12, assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "name/ver: Invalid configuration: compiler.version=12 is invalid!!" in self.client.out ================================================ FILE: test/integration/conanfile/load_requires_file_test.py ================================================ from conan.test.utils.tools import TestClient class TestLoadRequirementsTextFileTest: def test_load_reqs_from_text_file(self): client = TestClient() conanfile = """from conan import ConanFile def reqs(): try: content = open("reqs.txt", "r").read() lines = [line for line in content.splitlines() if line] return tuple(lines) except: return None class Test(ConanFile): exports = "reqs.txt" requires = reqs() """ client.save({"conanfile.py": conanfile}) client.run("create . --name=hello0 --version=0.1 --user=user --channel=channel") for i in (0, 1, 2): reqs = "hello%s/0.1@user/channel" % i client.save({"conanfile.py": conanfile, "reqs.txt": reqs}) client.run("create . --name=hello%s --version=0.1 --user=user --channel=channel" % (i + 1)) client.run("install --requires=hello3/0.1@user/channel") client.assert_listed_require({"hello0/0.1@user/channel": "Cache", "hello1/0.1@user/channel": "Cache", "hello2/0.1@user/channel": "Cache", "hello3/0.1@user/channel": "Cache"}) ================================================ FILE: test/integration/conanfile/no_copy_source_test.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient def test_no_copy_source(): conanfile = textwrap.dedent(''' from conan import ConanFile from conan.tools.files import copy, save, load import os class ConanFileToolsTest(ConanFile): name = "pkg" version = "0.1" exports_sources = "*" no_copy_source = True def source(self): save(self, "header.h", "artifact contents!") def build(self): self.output.info("Source files: %s" % load(self, os.path.join(self.source_folder, "file.h"))) save(self, "myartifact.lib", "artifact contents!") def package(self): copy(self, "*", self.source_folder, self.package_folder) copy(self, "*", self.build_folder, self.package_folder) ''') client = TestClient() client.save({"conanfile.py": conanfile, "file.h": "myfile.h contents"}) client.run("create .") assert "Source files: myfile.h contents" in client.out layout = client.created_layout() build_folder = layout.build() package_folder = layout.package() assert "file.h" not in os.listdir(build_folder) assert "header.h" not in os.listdir(build_folder) assert "file.h" in os.listdir(package_folder) assert "header.h" in os.listdir(package_folder) assert "myartifact.lib" in os.listdir(package_folder) ================================================ FILE: test/integration/conanfile/required_conan_version_test.py ================================================ import textwrap from unittest import mock from conan import __version__ from conan.test.utils.tools import TestClient def test_required_conan_version(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile required_conan_version = ">=100.0" class Lib(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=100.0)" in client.out client.run("source . ", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=100.0)" in client.out with mock.patch("conan.__version__", "101.0"): client.run("export . --name=pkg --version=1.0") with mock.patch("conan.__version__", "101.0-dev"): client.run("export . --name=pkg --version=1.0") client.run("install --requires=pkg/1.0@", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=100.0)" in client.out def test_required_conan_version_with_loading_issues(): # https://github.com/conan-io/conan/issues/11239 client = TestClient() conanfile = textwrap.dedent(""" from conan import missing_import required_conan_version = ">=100.0" class Lib(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=100.0)" in client.out # Assigning required_conan_version without spaces conanfile = textwrap.dedent(""" from conan import missing_import required_conan_version=">=100.0" class Lib(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=100.0)" in client.out # If the range is correct, everything works, of course conanfile = textwrap.dedent(""" from conan import ConanFile required_conan_version = ">1.0.0" class Lib(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0") assert "pkg/1.0: Exported" in client.out def test_comment_after_required_conan_version(): """ An error used to pop out if you tried to add a comment in the same line than required_conan_version, as it was trying to compare against >=10.0 # This should work instead of just >= 10.0 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from LIB_THAT_DOES_NOT_EXIST import MADE_UP_NAME required_conan_version = ">=10.0" # This should work class Lib(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=10.0)" in client.out def test_commented_out_required_conan_version(): """ Used to not be able to comment out required_conan_version if we had to fall back to regex check because of an error importing the recipe """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from LIB_THAT_DOES_NOT_EXIST import MADE_UP_NAME required_conan_version = ">=1.0" # required_conan_version = ">=100.0" class Lib(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=10.0", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=1.0)" not in client.out client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from LIB_THAT_DOES_NOT_EXIST import MADE_UP_NAME # required_conan_version = ">=10.0" class Lib(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>=10.0)" not in client.out def test_required_conan_version_invalid_syntax(): """ required_conan_version used to warn of mismatching versions if spaces were present, but now we have a nicer error""" # https://github.com/conan-io/conan/issues/12692 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile required_conan_version = ">= 1.0" class Lib(ConanFile): pass""") client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0", assert_error=True) assert f"Current Conan version ({__version__}) does not satisfy the defined one (>= 1.0)" not in client.out assert 'Error parsing version range ">="' in client.out ================================================ FILE: test/integration/conanfile/runner_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient class TestRunner: def test_ignore_error(self): conanfile = """from conan import ConanFile class Pkg(ConanFile): def source(self): ret = self.run("not_a_command", ignore_errors=True) self.output.info("RETCODE %s" % (ret!=0)) """ client = TestClient() client.save({"conanfile.py": conanfile}) client.run("source .") assert "RETCODE True" in client.out def test_runner_capture_output(self): conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def source(self): self.run("echo 'hello Conan!'") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("source .") assert "hello Conan!" in client.out def test_custom_stream_error(self): # https://github.com/conan-io/conan/issues/7888 conanfile = textwrap.dedent(""" from io import StringIO from conan import ConanFile class Pkg(ConanFile): def source(self): my_buf = StringIO() self.run('echo Hello', stdout=my_buf) self.output.info("Buffer got msgs {}".format(my_buf.getvalue())) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("source .") assert 'conanfile.py: Buffer got msgs Hello' in client.out def test_custom_stream_stderr(self): conanfile = textwrap.dedent(""" from io import StringIO from conan import ConanFile class Pkg(ConanFile): def source(self): my_buf = StringIO() self.run('echo Hello 1>&2', stderr=my_buf) self.output.info("Buffer got stderr msgs {}".format(my_buf.getvalue())) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("source .") assert 'conanfile.py: Buffer got stderr msgs Hello' in client.out ================================================ FILE: test/integration/conanfile/same_userchannel_test.py ================================================ import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestUserChannelTestPackage: def test(self): # https://github.com/conan-io/conan/issues/2501 client = TestClient(light=True) conanfile = """from conan import ConanFile class SayConan(ConanFile): pass """ test = """from conan import ConanFile class SayConan(ConanFile): def requirements(self): self.output.info("USER: %s!!" % self.user) self.output.info("CHANNEL: %s!!" % self.channel) self.requires(self.tested_reference_str) def test(self): pass """ client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test}) client.run("create . --name=pkg --version=0.1 --user=conan --channel=testing") assert "pkg/0.1@conan/testing (test package): USER: conan!!" in client.out assert "pkg/0.1@conan/testing (test package): CHANNEL: testing!!" in client.out class TestSameUserChannel: @pytest.fixture(autouse=True) def setup(self): self.client = TestClient(light=True) conanfile = """ from conan import ConanFile class SayConan(ConanFile): name = "say" version = "0.1" build_policy = "missing" def build(self): self.output.info("Building %s/%s") """ for user, channel in ("lasote", "stable"), ("other", "testing"): self.client.save({"conanfile.py": conanfile % (user, channel)}) self.client.run(f"export . --user={user} --channel={channel}") self.conanfile = """ from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" build_policy = "missing" def requirements(self): user_channel = "{}/{}".format(self.user, self.channel) if self.user else "" self.requires("say/0.1@{}".format(user_channel)) def build(self): self.output.info("Building %s/%s" % (self.user, self.channel) ) """ self.test_conanfile = str(GenConanfile().with_test("pass")) self.client.save({"conanfile.py": self.conanfile, "test/conanfile.py": self.test_conanfile}) def test_create(self): self.client.run("create . --user=lasote --channel=stable") assert "say/0.1@lasote/stable: Building lasote/stable" in self.client.out assert "hello/0.1@lasote/stable: Building lasote/stable" in self.client.out assert "other/testing" not in self.client.out self.client.save({"conanfile.py": self.conanfile, "test/conanfile.py": self.test_conanfile.replace("lasote/stable", "other/testing")}) self.client.run("create . --user=other --channel=testing") assert "say/0.1@other/testing: Building other/testing" in self.client.out assert "hello/0.1@other/testing: Building other/testing" in self.client.out assert "lasote/stable" not in self.client.out def test_local_commands(self): self.client.run("install .", assert_error=True) assert "ERROR: Package 'say/0.1' not resolved: No remote defined" in self.client.out self.client.run("install . --user=lasote --channel=stable") assert "say/0.1@lasote/stable: Building lasote/stable" in self.client.out assert "other/testing" not in self.client.out self.client.run("install . --user=other --channel=testing") assert "say/0.1@other/testing: Building other/testing" in self.client.out assert "lasote/stable" not in self.client.out # Now use the default_ methods to declare user and channel self.client = TestClient(light=True) conanfile = """ from conan import ConanFile class SayConan(ConanFile): name = "say" version = "0.1" build_policy = "missing" user = "userfoo" def build(self): self.output.info("Building %s/%s" % (self.user, self.channel) ) @property def channel(self): return "channelbar" """ self.client.save({"conanfile.py": conanfile}) self.client.run("install .") self.client.run("build .") assert "Building userfoo/channelbar" in self.client.out class TestBuildRequireUserChannel: def test(self): # https://github.com/conan-io/conan/issues/2254 client = TestClient(light=True) conanfile = """ from conan import ConanFile class SayConan(ConanFile): def build_requirements(self): self.output.info("MYUSER: %s" % self.user) self.output.info("MYCHANNEL: %s" % self.channel) """ client.save({"conanfile.py": conanfile}) client.run("install . --user=myuser --channel=mychannel") assert "MYUSER: myuser" in client.out assert "MYCHANNEL: mychannel" in client.out ================================================ FILE: test/integration/conanfile/set_name_version_test.py ================================================ import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID from conan.internal.util.files import mkdir class TestSetVersionName: @pytest.mark.parametrize("user_channel", ["", "@user/channel"]) def test_set_version_name(self, user_channel): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def set_name(self): self.name = "pkg" def set_version(self): self.version = "2.1" """) client.save({"conanfile.py": conanfile}) user_channel_arg = "--user=user --channel=channel" if user_channel else "" client.run("export . %s" % user_channel_arg) assert "pkg/2.1%s: Exported" % user_channel in client.out # installing it doesn't break client.run("install --requires=pkg/2.1%s --build=missing" % (user_channel or "@")) client.assert_listed_require({f"pkg/2.1{user_channel}": "Cache"}) client.assert_listed_binary({f"pkg/2.1{user_channel}": (NO_SETTINGS_PACKAGE_ID, "Build")}) client.run("install --requires=pkg/2.1%s --build=missing" % (user_channel or "@")) client.assert_listed_require({f"pkg/2.1{user_channel}": "Cache"}) client.assert_listed_binary({f"pkg/2.1{user_channel}": (NO_SETTINGS_PACKAGE_ID, "Cache")}) # Local flow should also work client.run("install .") assert "conanfile.py (pkg/2.1):" in client.out def test_set_version_name_file(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class Lib(ConanFile): def set_name(self): self.name = load(self, "name.txt") def set_version(self): self.version = load(self, "version.txt") """) client.save({"conanfile.py": conanfile, "name.txt": "pkg", "version.txt": "2.1"}) client.run("export . --user=user --channel=testing") assert "pkg/2.1@user/testing: Exported" in client.out client.run("install --requires=pkg/2.1@user/testing --build=missing") client.assert_listed_binary({f"pkg/2.1@user/testing": (NO_SETTINGS_PACKAGE_ID, "Build")}) client.run("install --requires=pkg/2.1@user/testing") client.assert_listed_binary({f"pkg/2.1@user/testing": (NO_SETTINGS_PACKAGE_ID, "Cache")}) # Local flow should also work client.run("install .") assert "conanfile.py (pkg/2.1):" in client.out def test_set_version_name_errors(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def set_name(self): self.name = "pkg" def set_version(self): self.version = "2.1" """) client.save({"conanfile.py": conanfile}) client.run("export . --name=other --version=1.1 --user=user --channel=testing") assert "pkg/2.1@user/testing" in client.out client.run("export . --version=1.1 --user=user --channel=testing") assert "pkg/2.1@user/testing" in client.out # These are checked but match and don't conflict client.run("export . --version=2.1 --user=user --channel=testing") client.run("export . --name=pkg --version=2.1 --user=user --channel=testing") # Local flow should also fail client.run("install . --name=other --version=1.2") assert "" in client.out def test_set_version_name_appending(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def set_name(self): self.name = self.name + ".extra" def set_version(self): self.version = self.version + ".extra" """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg --version=1.0") assert "pkg.extra/1.0.extra: Exported" in client.out def test_set_version_name_only_not_cli(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def set_name(self): self.name = self.name or "pkg" def set_version(self): self.version = self.version or "2.0" """) client.save({"conanfile.py": conanfile}) client.run("export . --name=other --version=1.1 --user=user --channel=testing") assert "other/1.1@user/testing: Exported" in client.out client.run("export . --version=1.1 --user=user --channel=testing") assert "pkg/1.1@user/testing: Exported" in client.out client.run("export . --user=user --channel=testing") assert "pkg/2.0@user/testing: Exported" in client.out # Local flow should also work client.run("install . --name=other --version=1.2") assert "conanfile.py (other/1.2)" in client.out client.run("install .") assert "conanfile.py (pkg/2.0)" in client.out def test_set_version_name_crash(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def set_name(self): self.name = error """) client.save({"conanfile.py": conanfile}) client.run("export .", assert_error=True) assert "ERROR: conanfile.py: Error in set_name() method, line 5" in client.out assert "name 'error' is not defined" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): def set_version(self): self.version = error """) client.save({"conanfile.py": conanfile}) client.run("export .", assert_error=True) assert "ERROR: conanfile.py: Error in set_version() method, line 5" in client.out assert "name 'error' is not defined" in client.out def test_set_version_cwd(self): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class Lib(ConanFile): name = "pkg" def set_version(self): self.version = load(self, "version.txt") """) client.save({"conanfile.py": conanfile}) mkdir(os.path.join(client.current_folder, "build")) with client.chdir("build"): client.save({"version.txt": "2.1"}, clean_first=True) client.run("export .. ") assert "pkg/2.1: Exported" in client.out def test_set_version_forbidden_chars(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "$$$")}) c.run("export .", assert_error=True) assert "ERROR: Invalid package version '$$$'" in c.out conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): name = "pkg" def set_version(self): self.version = "$$$" """) c.save({"conanfile.py": conanfile}) c.run("export .", assert_error=True) assert "ERROR: Invalid package version '$$$'" in c.out def test_set_version_carriage_return(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", r"1.0\n")}) c.run("export .", assert_error=True) assert "ERROR: Invalid package version '1.0" in c.out conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class Lib(ConanFile): name = "pkg" def set_version(self): self.version = load(self, "version.txt") """) c.save({"conanfile.py": conanfile, "version.txt": "1.0\n"}) c.run("export .", assert_error=True) assert "ERROR: Invalid package version '1.0" in c.out def test_set_version_channel(): """ Dynamically setting the user and channel is possible with the ``set_version()`` not recommended, but possible. """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class Lib(ConanFile): name = "pkg" version = "0.1" def set_version(self): self.user = "myuser" self.channel = "mychannel" """) c.save({"conanfile.py": conanfile}) c.run("export .") assert "pkg/0.1@myuser/mychannel: Exported" in c.out ================================================ FILE: test/integration/conanfile/test_attributes_scope.py ================================================ import textwrap from conan.test.utils.tools import TestClient class TestAttributesScope: def test_cppinfo_not_in_package_id(self): # self.cpp_info is not available in 'package_id' t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_id(self): self.cpp_info.libs = ["A"] """) t.save({'conanfile.py': conanfile}) t.run('create . --name=name --version=version', assert_error=True) assert "'self.cpp_info' access in 'package_id()' method is forbidden" in t.out def test_settings_not_in_package_id(self): # self.cpp_info is not available in 'package_id' t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_id(self): self.settings """) t.save({'conanfile.py': conanfile}) t.run('create . --name=name --version=version', assert_error=True) assert "'self.settings' access in 'package_id()' method is forbidden" in t.out def test_options_not_in_package_id(self): # self.cpp_info is not available in 'package_id' t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_id(self): self.options """) t.save({'conanfile.py': conanfile}) t.run('create . --name=name --version=version', assert_error=True) assert "'self.options' access in 'package_id()' method is forbidden" in t.out def test_info_not_in_package_info(self): t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.info """) t.save({'conanfile.py': conanfile}) t.run('create . --name=name --version=version', assert_error=True) assert "'self.info' access in 'package_info()' method is forbidden" in t.out def test_info_not_in_package(self): # self.info is not available in 'package' t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package(self): self.info.clear() """) t.save({'conanfile.py': conanfile}) t.run('create . --name=name --version=version -s os=Linux', assert_error=True) assert "'self.info' access in 'package()' method is forbidden" in t.out def test_no_settings(self): # self.setting is not available in 'source' t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): settings = "os", def source(self): self.settings.os """) t.save({'conanfile.py': conanfile}) t.run('create . --name=name --version=version -s os=Linux', assert_error=True) assert "'self.settings' access in 'source()' method is forbidden" in t.out def test_no_options(self): # self.setting is not available in 'source' t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): options = {'shared': [True, False]} def source(self): self.options.shared """) t.save({'conanfile.py': conanfile}) t.run('create . --name=name --version=version -o shared=False', assert_error=True) assert "'self.options' access in 'source()' method is forbidden" in t.out ================================================ FILE: test/integration/conanfile/test_conanfile_txt_encodings.py ================================================ import os import shutil import pytest from conan.test.utils.tools import TestClient class TestEncodings: @pytest.mark.parametrize("filename", ["conanfile_utf8.txt", "conanfile_utf8_with_bom.txt", "conanfile_utf16le_with_bom.txt", "conanfile_utf16be_with_bom.txt"]) def test_encoding(self, filename): c = TestClient() path = os.path.join(os.path.dirname(__file__), "files", filename) shutil.copy(path, os.path.join(c.current_folder, "conanfile.txt")) c.run("install .") assert "Installing packages" in c.out def test_error(self): c = TestClient() conanfile = b"\x81\x8D\x8F\x90\x9D" open(os.path.join(c.current_folder, "conanfile.txt"), "wb").write(conanfile) c.run("install .", assert_error=True) assert "ERROR: Cannot load conanfile.txt" in c.out assert "It is recommended to use utf-8 encoding" in c.out class TestProfileEncodings: def test_encoding(self): c = TestClient() c.save({"conanfile.txt": ""}) # BOM for utf-7 open(os.path.join(c.current_folder, "profile"), "wb").write(b'\x2b\x2f\x76\x38') c.run("install . -pr=profile") assert "Installing packages" in c.out def test_error(self): c = TestClient() c.save({"conanfile.txt": ""}) open(os.path.join(c.current_folder, "profile"), "wb").write(b"\x81\x8D\x8F\x90\x9D") c.run("install . -pr=profile", assert_error=True) assert "ERROR: Cannot load profile" in c.out assert "It is recommended to use utf-8 encoding" in c.out ================================================ FILE: test/integration/conanfile/test_conanfile_txt_test_requires.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_test_requires(): c = TestClient() c.save({"test/conanfile.py": GenConanfile("test", "0.1"), "consumer/conanfile.txt": "[test_requires]\ntest/0.1"}) c.run("create test") c.run("install consumer") c.assert_listed_require({"test/0.1": "Cache"}, test=True) c.assert_listed_binary({"test/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}, test=True) def test_test_requires_options(): c = TestClient() c.save({"test/conanfile.py": GenConanfile("test", "0.1").with_option("myoption", [1, 2, 3]), "consumer/conanfile.txt": "[test_requires]\ntest/0.1\n[options]\n*:myoption=2"}) c.run("create test -o myoption=2") c.assert_listed_binary({"test/0.1": ("a3cb1345b8297bfdffea4ef4bb1b2694c54d1d69", "Build")}) c.run("install consumer") c.assert_listed_require({"test/0.1": "Cache"}, test=True) c.assert_listed_binary({"test/0.1": ("a3cb1345b8297bfdffea4ef4bb1b2694c54d1d69", "Cache")}, test=True) ================================================ FILE: test/integration/conanfile/test_cpp_info_serialize.py ================================================ import json import textwrap from conan.test.utils.tools import TestClient def test_cpp_info_serialize_round_trip(): """ test that serialize and deserialize CppInfo works """ # TODO: Define standard name for file c = TestClient() conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools import CppInfo class Pkg(ConanFile): name = "pkg" version = "0.1" def package(self): cpp_info = CppInfo(self) cpp_info.includedirs = ["myinc"] cpp_info.libs = ["mylib", "myother"] cpp_info.libdirs = ["mylibs"] cpp_info.type = "static-library" cpp_info.set_property("myprop", "myvalue") cpp_info.components["comp"].libs = [] cpp_info.components["comp"].type = None p = os.path.join(self.package_folder, "cpp_info.json") cpp_info.save(p) def package_info(self): cpp_info = CppInfo(self).load("cpp_info.json") self.cpp_info = cpp_info """) c.save({"conanfile.py": conanfile}) c.run("create . --format=json") graph = json.loads(c.stdout) cpp_info = graph["graph"]["nodes"]["1"]["cpp_info"]["root"] assert cpp_info["includedirs"][0].endswith("myinc") assert cpp_info["libdirs"][0].endswith("mylibs") assert cpp_info["libs"] == ["mylib", "myother"] assert cpp_info["type"] == "static-library" assert cpp_info["properties"] == {"myprop": "myvalue"} comp = graph["graph"]["nodes"]["1"]["cpp_info"]["comp"] assert comp["type"] is None ================================================ FILE: test/integration/conanfile/test_deploy_method.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_deploy_method(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy, save class Pkg(ConanFile): name = "{name}" version = "0.1" {requires} def package(self): save(self, os.path.join(self.package_folder, f"my{name}file.txt"), "HELLO!!!!") def deploy(self): copy(self, "*", src=self.package_folder, dst=self.deploy_folder) """) c.save({"dep/conanfile.py": conanfile.format(name="dep", requires=""), "pkg/conanfile.py": conanfile.format(name="pkg", requires="requires='dep/0.1'")}) c.run("create dep") assert "Executing deploy()" not in c.out c.run("create pkg") assert "Executing deploy()" not in c.out # Doesn't install by default c.run("install --requires=pkg/0.1") assert "Executing deploy()" not in c.out # Doesn't install with other patterns c.run("install --requires=pkg/0.1 --deployer-package=other") assert "Executing deploy()" not in c.out # install can deploy all c.run("install --requires=pkg/0.1 --deployer-package=* --deployer-folder=mydeploy") assert "dep/0.1: Executing deploy()" in c.out assert "pkg/0.1: Executing deploy()" in c.out assert c.load("mydeploy/mydepfile.txt") == "HELLO!!!!" assert c.load("mydeploy/mypkgfile.txt") == "HELLO!!!!" # install can deploy only "pkg" c.run("install --requires=pkg/0.1 --deployer-package=pkg/* --deployer-folder=mydeploy") assert "dep/0.1: Executing deploy()" not in c.out assert "pkg/0.1: Executing deploy()" in c.out def test_deploy_local(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): name = "pkg" version = "0.1" def deploy(self): copy(self, "*", src=self.package_folder, dst=self.deploy_folder) """) c.save({"conanfile.py": conanfile}) c.run("install . --deployer-package=*", assert_error=True) assert "ERROR: conanfile.py (pkg/0.1): Error in deploy() method, line 8" in c.out assert "copy() received 'src=None' argument" in c.out # Without name/version same error conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def deploy(self): assert self.package_folder # will fail if None """) c.save({"conanfile.py": conanfile}) c.run("install . --deployer-package=*", assert_error=True) assert "ERROR: conanfile.py: Error in deploy() method, line 5" in c.out # I can exclude the current consumer, it won't fail c.run("install . --deployer-package=!& --deployer-package=*") assert "Install finished successfully" in c.out def test_deploy_method_tool_requires(): # Test to validate https://github.com/conan-io/docs/issues/3649 c = TestClient() tool = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): name = "tool" version = "0.1" type = "application" def package(self): with chdir(self, self.package_folder): echo = "@echo off\necho MYTOOL RUNNING!!" save(self, "mytool.bat", echo) save(self, "mytool.sh", echo) os.chmod("mytool.sh", 0o777) def package_info(self): self.buildenv_info.prepend_path("PATH", self.package_folder) """) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.env import VirtualBuildEnv class Pkg(ConanFile): name = "pkg" version = "0.1" tool_requires = "tool/0.1" settings = "os" def deploy(self): self.output.info(f"DEPLOY {os.getcwd()}") venv = VirtualBuildEnv(self) with venv.vars().apply(): ext = "bat" if self.settings.os == "Windows" else "sh" self.run(f"mytool.{ext}") """) c.save({"tool/conanfile.py": tool, "pkg/conanfile.py": conanfile}) c.run("create tool") c.run("create pkg") # install can deploy all c.run("install --requires=pkg/0.1 -c:b tools.graph:skip_binaries=False --deployer-package=*") assert "MYTOOL RUNNING!!" in c.out ================================================ FILE: test/integration/conanfile/test_deprecated.py ================================================ import textwrap from conan.test.utils.tools import TestClient, GenConanfile class TestDeprecated: def test_no_deprecated(self): t = TestClient() t.save({'taskflow.py': GenConanfile("cpp-taskflow", "1.0").with_deprecated("''")}) t.run("create taskflow.py") assert "Deprecated" not in t.out def test_deprecated_simple(self): t = TestClient() t.save({'taskflow.py': GenConanfile("cpp-taskflow", "1.0").with_deprecated("True")}) t.run("create taskflow.py") assert "Deprecated\n cpp-taskflow/1.0" in t.out assert "WARN: risk: There are deprecated packages in the graph" in t.out t.run("create taskflow.py --user=conan --channel=stable") assert "Deprecated\n cpp-taskflow/1.0" in t.out def test_deprecated_with(self): t = TestClient() t.save({'taskflow.py': GenConanfile("cpp-taskflow", "1.0").with_deprecated('"taskflow"')}) t.run("create taskflow.py") assert "Deprecated\n cpp-taskflow/1.0: taskflow" in t.out t.run("create taskflow.py --user=conan --channel=stable") assert "Deprecated\n cpp-taskflow/1.0@conan/stable: taskflow" in t.out def test_deprecated_custom_text(self): tc = TestClient() tc.save({"old/conanfile.py": GenConanfile("maths", 1.0).with_deprecated('"This is not secure, use maths/[>=2.0]"'), "new/conanfile.py": GenConanfile("maths", 2.0)}) tc.run("create new/conanfile.py") tc.run("create old/conanfile.py") assert "maths/1.0: This is not secure, use maths/[>=2.0]" in tc.out tc.run("install --requires=maths/1.0") assert "maths/1.0: This is not secure, use maths/[>=2.0]" in tc.out def test_deprecated_configure(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Version class Pkg(ConanFile): name = "pkg" def configure(self): if Version(self.version) < "1.0": self.deprecated = True """) tc.save({"conanfile.py": conanfile}) tc.run("graph info . --version=0.0") assert "deprecated: True" in tc.out tc.run("graph info . --version=2.0") assert "deprecated: None" in tc.out ================================================ FILE: test/integration/conanfile/test_exception_printing.py ================================================ import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient conanfile = """ from conan import ConanFile class ExceptionsTest(ConanFile): name = "exceptions" version = "0.1" def {method}(self): {method_contents} def _aux_method(self): raise Exception('Oh! an error!') """ @pytest.mark.parametrize("direct", [True, False]) @pytest.mark.parametrize("method", ["source", "build", "package", "package_info", "configure", "build_id", "package_id", "requirements", "config_options", "layout", "generate", "export", "export_sources", "build_requirements", "init"]) def test_all_methods(direct, method): client = TestClient() if direct: throw = "raise Exception('Oh! an error!')" else: throw = "self._aux_method()" client.save({"conanfile.py": conanfile.format(method=method, method_contents=throw)}) client.run("create . ", assert_error=True) assert "Error in %s() method, line 9" % method in client.out assert "Oh! an error!" in client.out if not direct: assert "while calling '_aux_method', line 12" in client.out def test_complete_traceback_debug(): """ in debug level (-vv), the trace is shown (this is for recipe methods exceptions) """ client = TestClient() throw = "self._aux_method()" client.save({"conanfile.py": conanfile.format(method="source", method_contents=throw)}) client.run("create . -vv", assert_error=True) assert "Exception: Oh! an error!" in client.out assert "ERROR: Traceback (most recent call last):" in client.out def test_complete_traceback_trace(): """ in debug level (-vvv), the full trace is shown for ConanExceptions """ client = TestClient() client.run("install --requires=pkg/1.0 -vvv", assert_error=True) assert "Traceback (most recent call last)" in client.out def test_notracebacks_cmakedeps(): """ CMakeDeps prints traceback """ c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_generator("CMakeDeps")}) c.run("install .", assert_error=True) assert "Traceback" not in c.out assert "ERROR: Error in generator 'CMakeDeps'" in c.out ================================================ FILE: test/integration/conanfile/test_finalize_method.py ================================================ import json import os from pathlib import Path import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import load, save conanfile_dep = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, copy class TestConan(ConanFile): name = "dep" version = "1.0" def package(self): save(self, os.path.join(self.package_folder, "file.txt"), "Hello World!") save(self, os.path.join(self.package_folder, "file2.txt"), "Hello World 2!") def finalize(self): self.output.info(f"Running finalize method in {self.package_folder}") copy(self, "file.txt", src=self.immutable_package_folder, dst=self.package_folder) save(self, os.path.join(self.package_folder, "finalized.txt"), "finalized file") def package_info(self): self.output.info(f"Running package_info method in {self.package_folder}") """) class TestBasicLocalFlows: @pytest.fixture def client(self): tc = TestClient(light=True) tc.save({"dep/conanfile.py": conanfile_dep}) tc.run("export dep") return tc def test_basic_finalize_method(self, client): client.run("create dep") layout = client.created_layout() assert layout.package().endswith("p") assert f"Package folder {layout.package()}" in client.out assert f"Running finalize method in {layout.finalize()}" in client.out assert f"Running package_info method in {layout.finalize()}" in client.out client.run("install --requires=dep/1.0") assert f"Running package_info method in {layout.finalize()}" in client.out # Only issue is that the PackageLayout has no idea about the redirected package folder # So we have to know to check for it in tests, but oh well assert "finalized.txt" in os.listdir(layout.finalize()) assert "finalized.txt" not in os.listdir(layout.package()) def test_dependency_finalize_method(self, client): client.save({"app/conanfile.py": textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): name = "app" version = "1.0" requires = "dep/1.0" def generate(self): self.output.info("Running generate method") dep_pkg_folder = self.dependencies["dep"].package_folder self.output.info(f"Dep package folder: {dep_pkg_folder}") """)}) client.run("create dep") dep_layout = client.created_layout() client.run("create app") assert f"Dep package folder: {dep_layout.package()}" not in client.out assert f"Dep package folder: {dep_layout.finalize()}" in client.out def test_no_non_info_access(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("dep", "1.0") .with_finalize("self.output.info('settings.os: ' + self.settings.os)")}) client.run("create .", assert_error=True) assert "'self.settings' access in 'finalize()' method is forbidden" in client.out def test_finalize_moves_from_package(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("dep", "1.0") .with_import("from conan.tools.files import save, rename", "import os") .with_option("move", [True, False]) .with_package('save(self, os.path.join(self.package_folder, "file.txt"), "Hello World!")', "save(self, os.path.join(self.package_folder, 'file2.txt'), 'Hello World 2!')") # This is NOT allowed, moving from package to finalize is forbidden, only as test to ensure consistency .with_finalize("rename(self, os.path.join(self.immutable_package_folder, 'file.txt'), os.path.join(self.package_folder, 'file.txt')) if self.info.options.move else None")}) client.run("create . -o=dep/*:move=True") dep_moved_layout = client.created_layout() assert "file.txt" in os.listdir(dep_moved_layout.finalize()) assert "file.txt" not in os.listdir(dep_moved_layout.package()) client.run("create . -o=dep/*:move=False") dep_kept_layout = client.created_layout() assert "file.txt" not in os.listdir(dep_kept_layout.finalize()) assert "file.txt" in os.listdir(dep_kept_layout.package()) # Now we can check that the package_id is the same for both assert dep_moved_layout.reference.package_id != dep_kept_layout.reference.package_id # This now breaks if we try to cache check-integrity the moved package client.run(f"cache check-integrity {dep_moved_layout.reference}", assert_error=True) assert "There are corrupted artifacts" in client.out client.run(f"cache check-integrity {dep_kept_layout.reference}") assert "There are corrupted artifacts" not in client.out def test_cache_path_command(self, client): client.run("create dep") dep_layout = client.created_layout() pref = client.created_package_reference("dep/1.0") client.run(f"cache path {pref}") assert dep_layout.package() not in client.out assert dep_layout.finalize() in client.out def test_remove_deletes_correct_folders(self, client): client.run("create dep") dep_layout = client.created_layout() client.run("remove * -c") assert not os.path.exists(dep_layout.package()) assert not os.path.exists(dep_layout.finalize()) def test_save_restore_cache(self, client): # Not created in the cache, just exported, nothing breaks because there is not even a package there client.run("cache save *:*") # Check pkglist.json has not been created inside conan cache folder assert not any(Path(client.cache_folder).rglob("pgklist.json")) client.run("remove * -c") client.run("cache restore conan_cache_save.tgz") # Check the extracted pkglist does not persist in the cache after restore assert not any(Path(client.cache_folder).rglob("pgklist.json")) # Now create the package and then save/restore client.run("create dep") dep_layout = client.created_layout() client.run("cache save *:* -f=json", redirect_stdout="saved.json") saved = json.loads(client.load("saved.json")) pref = dep_layout.reference saved_pkg_folder = saved["Local Cache"]["dep/1.0"]["revisions"][pref.ref.revision]["packages"][pref.package_id]["revisions"][pref.revision]["package_folder"] assert saved_pkg_folder in dep_layout.package().replace("\\", "/") client.run("remove * -c") assert not os.path.exists(dep_layout.package()) assert not os.path.exists(dep_layout.finalize()) client.run("cache restore conan_cache_save.tgz") client.run(f"cache path {dep_layout.reference}") package_folder = client.out.strip() # The finalize() folder does not exist as restoring is not considered usage, so it never runs # so this is just the immutable package_folder assert "finalized.txt" not in os.listdir(package_folder) # But as soon as you call conan finalize, finalize() is called and so it's used, # so package_folder will be the finalize folder client.run("install --requires=dep/1.0") client.run(f"cache path {dep_layout.reference}") package_folder = client.out.strip() assert "finalized.txt" in os.listdir(package_folder) def test_graph_info_output(self, client): client.run("create dep") dep_layout = client.created_layout() client.run("install --requires=dep/1.0 -f=json", redirect_stdout="finalize.json") finalize_output = json.loads(client.load("finalize.json")) assert finalize_output["graph"]["nodes"]["1"]["package_folder"] == dep_layout.finalize() assert finalize_output["graph"]["nodes"]["1"]["immutable_package_folder"] == dep_layout.package() def test_create_pkglist_output(self, client): client.run("create dep -f=json", redirect_stdout="created.json") created_pkgid = client.created_package_id("dep/1.0") client.run("list --graph=created.json --graph-binaries=build") assert created_pkgid in client.out def test_vendorized_basic(self, client): client.run("create dep") client.save({"vendor/conanfile.py": GenConanfile("vendor", "1.0") .with_import("from conan.tools.files import copy") .with_class_attribute("vendor=True") .with_requires("dep/1.0") .with_package("copy(self, 'file.txt', src=self.dependencies['dep'].package_folder, dst=self.package_folder)", "copy(self, 'finalized.txt', src=self.dependencies['dep'].package_folder, dst=self.package_folder)", "copy(self, 'file2.txt', src=self.dependencies['dep'].immutable_package_folder, dst=self.package_folder)")}) client.run("create vendor") vendor_layout = client.created_layout() assert "file.txt" in os.listdir(vendor_layout.package()) assert "finalized.txt" in os.listdir(vendor_layout.package()) assert "file2.txt" in os.listdir(vendor_layout.package()) def test_check_integrity(self, client): client.run("create dep") dep_layout = client.created_layout() client.run(f"cache check-integrity {dep_layout.reference}") assert "There are corrupted artifacts" not in client.out # Even if we re-change the finalize folder contents, it should still be fine save(os.path.join(dep_layout.finalize(), "finalized.txt"), "Modified!") client.run(f"cache check-integrity {dep_layout.reference}") assert "There are corrupted artifacts" not in client.out # But as soon as we change the package, it should still fail like a normal package would save(os.path.join(dep_layout.package(), "file.txt"), "Modified!") client.run(f"cache check-integrity {dep_layout.reference}", assert_error=True) assert "There are corrupted artifacts" in client.out @pytest.mark.parametrize("with_finalize_method", [True, False]) def test_access_immutable_from_consumer(self, client, with_finalize_method): if not with_finalize_method: client.save({"dep/conanfile.py": GenConanfile("dep", "1.0")}) client.save({"app/conanfile.py": GenConanfile("app", "1.0") .with_requires("dep/1.0") .with_package("dep = self.dependencies['dep/1.0']", "self.output.info(f'Immutable package: {dep.immutable_package_folder}')", # TODO: Think about if we want this interface # "self.output.info(f'finalize: {dep.finalize_folder}')", "self.output.info(f'Package: {dep.package_folder}')")}) client.run("create dep") dep_layout = client.created_layout() client.run("create app") assert f"app/1.0: Immutable package: {dep_layout.package()}" in client.out # assert f"app/1.0: finalize: {dep_layout.finalize()}" in client.out if with_finalize_method: assert f"app/1.0: Package: {dep_layout.finalize()}" in client.out else: assert f"app/1.0: Package: {dep_layout.package()}" in client.out def test_cache_modification_of_custom_conf_based_on_settings(self): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("dep", "1.0") .with_import("from conan.tools.files import save", "import os") .with_option("myoption", [True, False]) .with_option("otheroption", [True, False]) .with_default_option("myoption", False) .with_default_option("otheroption", False) .with_setting("os") .with_package_id("del self.info.options.myoption") .with_finalize("save(self, os.path.join(self.package_folder, 'file.txt'), 'Hello World!')", "save(self, os.path.join(self.package_folder, 'os.conf'), str(self.info.settings.os))", "save(self, os.path.join(self.package_folder, 'option.conf'), str(self.info.options.get_safe('myoption')))", "save(self, os.path.join(self.package_folder, 'otheroption.conf'), str(self.info.options.otheroption))")}) tc.run("create . -s=os=Linux -o=&:myoption=True -o=&:otheroption=True") layout = tc.created_layout() assert "file.txt" in os.listdir(layout.finalize()) assert tc.load(os.path.join(layout.finalize(), "os.conf")) == "Linux" # This is problematic, it means that the mapping for finalize() and package_id would not be 1:1 and could be outdated assert tc.load(os.path.join(layout.finalize(), "option.conf")) == "None" assert tc.load(os.path.join(layout.finalize(), "otheroption.conf")) == "True" class TestToolRequiresFlows: def test_tool_requires(self): tc = TestClient(light=True) tc.save({"dep/conanfile.py": textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, copy class TestConan(ConanFile): name = "dep" version = "1.0" package_type = "application" def package(self): save(self, os.path.join(self.package_folder, "bin", "executable.txt"), "Base") def finalize(self): self.output.info(f"Running finalize method in {self.package_folder}") copy(self, "*", src=self.immutable_package_folder, dst=self.package_folder) save(self, os.path.join(self.package_folder, "bin", "finalized.txt"), "finalized file") def package_info(self): self.output.info(f"Running package_info method in {self.package_folder}") self.cpp_info.bindirs = ["bin"] """), "app/conanfile.py": textwrap.dedent(""" from conan import ConanFile import os class TestConan(ConanFile): name = "app" version = "1.0" def build_requirements(self): self.tool_requires("dep/1.0") def build(self): self.output.info("Running build method") bindir = self.dependencies.build['dep'].cpp_info.bindir self.output.info(f"Dep bindir: {bindir}") self.output.info(f"Is finalized? {os.path.exists(os.path.join(bindir, 'finalized.txt'))}") """)}) tc.run("create dep --build-require") dep_layout = tc.created_layout() tc.run("create app") # This fails. cpp_info is using the original package folder to construct the final path assert f"Dep bindir: {dep_layout.finalize()}" in tc.out assert "app/1.0: Is finalized? True" in tc.out def test_test_package_uses_created_tool_which_modifies_pkgfolder(self): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("app", "1.0") .with_import("from conan.tools.files import save") .with_package_type("application") .with_package("save(self, 'file.txt', 'Hello World!')") .with_package_info({"bindirs": ["bin"]}) .with_finalize("save(self, 'finalized.txt', 'finalized file')"), "test_package/conanfile.py": GenConanfile() .with_import("from conan.tools.files import save", "import os") .with_test_reference_as_build_require() .with_test("bindir = self.dependencies.build[self.tested_reference_str].cpp_info.bindir", "self.output.info(f'Bindir: {bindir}')", "save(self, os.path.join(bindir, '__pycache__.pyc'), 'Test file')")}) tc.run("create . --build-require") app_layout = tc.created_layout() assert f"Bindir: {os.path.join(app_layout.finalize(), 'bin')}" in tc.out tc.run(f"cache check-integrity {app_layout.reference}") assert "There are corrupted artifacts" not in tc.out class TestRemoteFlows: @pytest.fixture def client(self): tc = TestClient(light=True, default_server_user=True) tc.save({"dep/conanfile.py": conanfile_dep}) tc.run("export dep") return tc def test_remote_upload_finalize_method(self, client): client.run("create dep") created_pref = client.created_package_reference("dep/1.0") client.run("upload * -r=default -c") # Only the package folder is uploaded, not the finalize folder uploaded_pref_path = client.servers["default"].test_server.server_store.package(created_pref) manifest_contents = load(os.path.join(uploaded_pref_path, "conanmanifest.txt")) assert "file.txt" in manifest_contents assert "finalized.txt" not in manifest_contents client.run("remove * -c") client.run(f"download {created_pref} -r=default") downloaded_pref_layout = client.get_latest_pkg_layout(created_pref) assert "file.txt" in os.listdir(downloaded_pref_layout.package()) # Download is not an "usage" of the package, so no finalize() is yet executed assert "finalized.txt" not in os.listdir(downloaded_pref_layout.package()) assert not os.path.exists(os.path.join(downloaded_pref_layout.finalize())) client.run(f"cache path {created_pref}") package_folder = client.out.strip() assert package_folder == downloaded_pref_layout.package() assert package_folder.endswith("p") # Now this finalize will run the finalize() method client.run("install --requires=dep/1.0") assert f"Running finalize method in {downloaded_pref_layout.finalize()}" in client.out client.run("remove * -c") client.run("install --requires=dep/1.0 -r=default") assert "dep/1.0: Calling finalize()" assert f"Running finalize method in {downloaded_pref_layout.finalize()}" in client.out def test_upload_verify_integrity(self, client): client.run("create dep") dep_layout = client.created_layout() client.run("upload * -r=default -c --check") assert f"dep/1.0#{dep_layout.reference.ref.revision}:{dep_layout.reference.package_id}" \ f"#{dep_layout.reference.revision}: Integrity check: ok" in client.out assert "There are corrupted artifacts" not in client.out ================================================ FILE: test/integration/conanfile/test_print_in_conanfile.py ================================================ import json import textwrap from conan.test.utils.tools import TestClient def test_print_in_conanfile(): """ Tests that prints in conanfiles will not ruin json stdout outputs """ c = TestClient(light=True) other = textwrap.dedent(""" def myprint(text): print(text) """) conanfile = textwrap.dedent(""" import other from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.1" def generate(self): print("Hello world!!") other.myprint("Bye world!!") """) c.save({"other.py": other, "conanfile.py": conanfile}) c.run("install . --format=json") assert "Hello world!!" in c.stderr assert "Bye world!!" in c.stderr info = json.loads(c.stdout) # the json is correctly loaded assert "graph" in info ================================================ FILE: test/integration/conanfile/test_version_str.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_conan_version_str(): """ conanfile.version should always be a string. If comparison neeeded use Version(self.version) or self.ref.version """ # https://github.com/conan-io/conan/issues/10372 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Version class Lib(ConanFile): name = "pkg" version = "1.0" def _assert_data(self): assert isinstance(self.version, str) assert not isinstance(self.version, Version) def configure(self): self._assert_data() def source(self): self._assert_data() """) client.save({"conanfile.py": conanfile}) client.run("create .") ================================================ FILE: test/integration/configuration/__init__.py ================================================ ================================================ FILE: test/integration/configuration/client_certs_test.py ================================================ import os import textwrap from requests import Response from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, TestRequester from conan.internal.util.files import save conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): settings = "os", "compiler" def source(self): download(self, "http://foo.bar/file", "filename.txt") """) class MyHttpRequester(TestRequester): def get(self, _, **kwargs): resp = Response() # resp._content = b'{"results": []}' resp.status_code = 200 resp._content = b'' # This will be captured in the TestClient.out too print("KWARGS auth: {}".format(kwargs["auth"])) print("KWARGS verify: {}".format(kwargs["verify"])) print("KWARGS cert: {}".format(kwargs["cert"])) return resp class TestClientCerts: def test_pic_custom_path_client_certs(self): folder = temp_folder() mycert_path = os.path.join(folder, "mycert.crt").replace("\\", '/') mykey_path = os.path.join(folder, "mycert.key").replace("\\", '/') save(mycert_path, "Fake Cert") save(mykey_path, "Fake Key") client = TestClient(requester_class=MyHttpRequester) conan_conf = textwrap.dedent(""" core.net.http:client_cert= ("{}", "{}") """.format(mycert_path, mykey_path)) client.save_home({"global.conf": conan_conf}) client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") assert "KWARGS cert: ('{}', '{}')".format(mycert_path, mykey_path) in client.out ================================================ FILE: test/integration/configuration/conf/__init__.py ================================================ ================================================ FILE: test/integration/configuration/conf/test_auth_source_plugin.py ================================================ import json import os import textwrap from unittest import mock from unittest.mock import MagicMock import pytest from conan import __version__ from conan.internal.model.conf import ConfDefinition from conan.internal.rest.conan_requester import ConanRequester from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save class TestAuthSourcePlugin: @pytest.fixture def setup_test_client(self): client = TestClient(default_server_user=True, light=True) file_server = TestFileServer() client.servers["file_server"] = file_server client.save_home({"global.conf": "tools.files.download:retry=0"}) save(os.path.join(file_server.store, "myfile.txt"), "Bye, world!") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download class Pkg2(ConanFile): name = "pkg" version = "1.0" def source(self): download(self, "{file_server.fake_url}/basic-auth/myfile.txt", "myfile.txt") """) client.save({"conanfile.py": conanfile}) return client, file_server.fake_url def test_error_source_plugin(self, setup_test_client): """ Test when the plugin fails, we want a clear message and a helpful trace """ c, url = setup_test_client auth_plugin = textwrap.dedent("""\ def auth_source_plugin(url): raise Exception("Test Error") """) c.save_home({"extensions/plugins/auth_source.py": auth_plugin}) c.run("source conanfile.py", assert_error=True) assert "Test Error" in c.out @pytest.mark.parametrize("password", ["password", "bad-password"]) def test_auth_source_plugin_direct_credentials(self, password, setup_test_client): """ Test when the plugin give a correct and wrong password, we want a message about the success or fail in login """ should_fail = password == "bad-password" c, url = setup_test_client auth_plugin = textwrap.dedent(f"""\ def auth_source_plugin(url): return {json.dumps({'user': 'user', 'password': password})} """) c.save_home({"extensions/plugins/auth_source.py": auth_plugin}) c.run("source conanfile.py", assert_error=should_fail) if should_fail: assert "AuthenticationException" in c.out else: assert os.path.exists(os.path.join(c.current_folder, "myfile.txt")) def test_auth_source_plugin_fallback(self, setup_test_client): """ Test when the plugin do not give any user or password, we want the code to continue with the rest of the input methods """ c, url = setup_test_client auth_plugin = textwrap.dedent("""\ def auth_source_plugin(url): return None """) c.save_home({"extensions/plugins/auth_source.py": auth_plugin}) source_credentials = json.dumps({"credentials": [{"url": url, "token": "password"}]}) c.save_home({"source_credentials.json": source_credentials}) c.run("source conanfile.py") # As the auth plugin is not returning any password the code is falling back to the rest of # the input methods in this case provided by source_credentials.json. assert os.path.exists(os.path.join(c.current_folder, "myfile.txt")) def test_source_auth_plugin_headers(self): auth_plugin = textwrap.dedent(f"""\ def auth_source_plugin(url): return {json.dumps({'headers': {"myheader": "myvalue"}})} """) cache_folder = temp_folder() save(os.path.join(cache_folder, "extensions/plugins/auth_source.py"), auth_plugin) mock_http_requester = MagicMock() with mock.patch("conan.internal.rest.conan_requester.requests", mock_http_requester): requester = ConanRequester(ConfDefinition(), cache_folder) requester.get(url="aaa", source_credentials=True) headers = requester._http_requester.get.call_args[1]["headers"] assert headers["myheader"] == "myvalue" assert f"Conan/{__version__}" in headers["User-Agent"] def test_source_credentials_headers(self): cache_folder = temp_folder() source_credentials = json.dumps({"credentials": [{"url": "aaa", "token": "mytok", "headers": {"myheader2": "myvalue2"}}]}) save(os.path.join(cache_folder, "source_credentials.json"), source_credentials) mock_http_requester = MagicMock() with mock.patch("conan.internal.rest.conan_requester.requests", mock_http_requester): requester = ConanRequester(ConfDefinition(), cache_folder) requester.get(url="aaa", source_credentials=True) headers = requester._http_requester.get.call_args[1]["headers"] assert headers["myheader2"] == "myvalue2" assert f"Conan/{__version__}" in headers["User-Agent"] ================================================ FILE: test/integration/configuration/conf/test_conf.py ================================================ import os import platform import textwrap import pytest from unittest.mock import patch from conan import conan_version from conan.internal.api.detect import detect_api from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.internal.util.files import save from conan.test.utils.tools import TestClient @pytest.fixture def client(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def generate(self): for k, v in self.conf.items(): self.output.info("{}${}".format(k, v)) """) client.save({"conanfile.py": conanfile}) return client def test_basic_composition(client): profile1 = textwrap.dedent("""\ [conf] tools.build:verbosity=quiet tools.microsoft.msbuild:vs_version=Slow tools.cmake.cmaketoolchain:generator=Extra """) profile2 = textwrap.dedent("""\ [conf] tools.build:verbosity=quiet tools.microsoft.msbuild:max_cpu_count=High tools.meson.mesontoolchain:backend=Super """) client.save({"profile1": profile1, "profile2": profile2}) client.run("install . -pr=profile1") assert "tools.build:verbosity$quiet" in client.out assert "tools.microsoft.msbuild:vs_version$Slow" in client.out assert "tools.cmake.cmaketoolchain:generator$Extra" in client.out client.run("install . -pr=profile1 -pr=profile2") assert "tools.build:verbosity$quiet" in client.out assert "tools.microsoft.msbuild:vs_version$Slow" in client.out assert "tools.microsoft.msbuild:max_cpu_count$High" in client.out assert "tools.cmake.cmaketoolchain:generator$Extra" in client.out assert "tools.meson.mesontoolchain:backend$Super" in client.out client.run("install . -pr=profile2 -pr=profile1") assert "tools.build:verbosity$quiet" in client.out assert "tools.microsoft.msbuild:vs_version$Slow" in client.out assert "tools.microsoft.msbuild:max_cpu_count$High" in client.out assert "tools.cmake.cmaketoolchain:generator$Extra" in client.out assert "tools.meson.mesontoolchain:backend$Super" in client.out def test_basic_inclusion(client): profile1 = textwrap.dedent("""\ [conf] tools.build:verbosity=quiet tools.microsoft.msbuild:vs_version=Slow tools.cmake.cmaketoolchain:generator=Extra """) profile2 = textwrap.dedent("""\ include(profile1) [conf] tools.build:verbosity=quiet tools.microsoft.msbuild:max_cpu_count=High tools.meson.mesontoolchain:backend=Super """) client.save({"profile1": profile1, "profile2": profile2}) client.run("install . -pr=profile2") assert "tools.build:verbosity$quiet" in client.out assert "tools.microsoft.msbuild:vs_version$Slow" in client.out assert "tools.microsoft.msbuild:max_cpu_count$High" in client.out assert "tools.cmake.cmaketoolchain:generator$Extra" in client.out assert "tools.meson.mesontoolchain:backend$Super" in client.out def test_composition_conan_conf(client): conf = textwrap.dedent("""\ tools.build:verbosity=quiet tools.microsoft.msbuild:vs_version=Slow tools.cmake.cmaketoolchain:generator=Extra """) client.save_home({"global.conf": conf}) profile = textwrap.dedent("""\ [conf] tools.build:verbosity=quiet tools.microsoft.msbuild:max_cpu_count=High tools.meson.mesontoolchain:backend=Super """) client.save({"profile": profile}) client.run("install . -pr=profile") assert "tools.build:verbosity$quiet" in client.out assert "tools.microsoft.msbuild:vs_version$Slow" in client.out assert "tools.microsoft.msbuild:max_cpu_count$High" in client.out assert "tools.cmake.cmaketoolchain:generator$Extra" in client.out assert "tools.meson.mesontoolchain:backend$Super" in client.out def test_core_global_conf_not_in_profile(client): client.save_home({"global.conf": "core.upload:retry=1"}) profile = textwrap.dedent("""\ [conf] tools.build:verbosity=quiet """) client.save({"profile": profile}) client.run("install . -pr=profile") assert "tools.build:verbosity$quiet" in client.out # The core conf is not shown assert "core.upload:retry=1" not in client.out # also with -cc command line argument client.run("install . -pr=profile -cc core.upload:retry=1") assert "tools.build:verbosity$quiet" in client.out # The core conf is not shown assert "core.upload:retry=1" not in client.out def test_new_config_file(client): conf = textwrap.dedent("""\ tools.build:verbosity=quiet user.mycompany.myhelper:myconfig=myvalue *:tools.cmake.cmaketoolchain:generator=X """) client.save_home({"global.conf": conf}) client.run("install .") assert "tools.build:verbosity$quiet" in client.out assert "user.mycompany.myhelper:myconfig$myvalue" in client.out assert "tools.cmake.cmaketoolchain:generator$X" in client.out assert "read_only" not in client.out conf = textwrap.dedent("""\ tools.build:verbosity=notice user.mycompany.myhelper:myconfig=myvalue *:tools.cmake.cmaketoolchain:generator=X cache:read_only=True """) client.save_home({"global.conf": conf}) client.run("install .", assert_error=True) assert "[conf] 'cache:read_only' does not exist in configuration list" in client.out @patch("conan.__version__", "1.26.0") def test_new_config_file_required_version(): client = TestClient() conf = textwrap.dedent("""\ core:required_conan_version=>=2.0 """) client.save_home({"global.conf": conf}) client.run("install .", assert_error=True) assert ("Current Conan version (1.26.0) does not satisfy the defined one (>=2.0)" in client.out) def test_composition_conan_conf_overwritten_by_cli_arg(client): conf = textwrap.dedent("""\ tools.build:verbosity=quiet tools.microsoft.msbuild:max_cpu_count=Slow """) client.save_home({"global.conf": conf}) profile = textwrap.dedent("""\ [conf] tools.build:verbosity=quiet tools.microsoft.msbuild:vs_version=High """) client.save({"profile": profile}) client.run("install . -pr=profile -c tools.build:verbosity=verbose " "-c tools.meson.mesontoolchain:backend=Super") assert "tools.build:verbosity$verbose" in client.out assert "tools.microsoft.msbuild:max_cpu_count$Slow" in client.out assert "tools.microsoft.msbuild:vs_version$High" in client.out assert "tools.meson.mesontoolchain:backend$Super" in client.out def test_composition_conan_conf_different_data_types_by_cli_arg(client): """ Testing if you want to introduce a list/dict via cli >> conan install . -c "tools.build.flags:ccflags+=['-Werror']" >> conan install . -c "tools.microsoft.msbuildtoolchain:compile_options={'ExceptionHandling': 'Async'}" """ conf = textwrap.dedent("""\ tools.build:cflags=["-Wall"] """) client.save_home({"global.conf": conf}) client.run('install . -c "tools.build:cflags+=[\'-Werror\']" ' '-c "tools.microsoft.msbuildtoolchain:compile_options={\'ExceptionHandling\': \'Async\'}"') assert "tools.build:cflags$['-Wall', '-Werror']" in client.out assert "tools.microsoft.msbuildtoolchain:compile_options${'ExceptionHandling': 'Async'}" in client.out def test_jinja_global_conf(client): client.save_home({"global.conf": "user.mycompany:parallel = {{os.cpu_count()/2}}\n" "user.mycompany:other = {{platform.system()}}\n" "user.mycompany:dist = {{distro.id() if distro else '42'}}\n" "user.conan:version = {{conan_version}}-{{conan_version>0.1}}"}) client.run("install .") assert "user.mycompany:parallel={}".format(os.cpu_count()/2) in client.out assert "user.mycompany:other={}".format(platform.system()) in client.out assert f"user.conan:version={conan_version}-True" in client.out if platform.system() == "Linux": import distro assert "user.mycompany:dist={}".format(distro.id()) in client.out else: assert "user.mycompany:dist=42" in client.out def test_jinja_global_conf_include(client): global_conf = textwrap.dedent("""\ {% include "user_global.conf" %} {% import "user_global.conf" as vars %} user.mycompany:dist = {{vars.myvar*2}} """) user_global_conf = textwrap.dedent("""\ {% set myvar = 42 %} user.mycompany:parallel = {{myvar}} """) client.save_home({"global.conf": global_conf}) save(os.path.join(client.cache_folder, "user_global.conf"), user_global_conf) client.run("install .") assert "user.mycompany:parallel=42" in client.out assert "user.mycompany:dist=84" in client.out def test_jinja_global_conf_paths(): c = TestClient() global_conf = 'user.mycompany:myfile = {{os.path.join(conan_home_folder, "myfile")}}' c.save_home({"global.conf": global_conf}) c.run("config show *") cache_folder = c.cache_folder.replace("\\", "/") assert f"user.mycompany:myfile: {os.path.join(cache_folder, 'myfile')}" in c.out def test_profile_detect_os_arch(): """ testing OS & ARCH just to test that detect_api is injected """ c = TestClient() global_conf = textwrap.dedent(""" user.myteam:myconf1={{detect_api.detect_os()}} user.myteam:myconf2={{detect_api.detect_arch()}} """) c.save_home({"global.conf": global_conf}) c.run("config show *") _os = detect_api.detect_os() _arch = detect_api.detect_arch() assert f"user.myteam:myconf1: {_os}" in c.out assert f"user.myteam:myconf2: {_arch}" in c.out def test_empty_conf_valid(): tc = TestClient() profile = textwrap.dedent(r""" [conf] user:unset= """) conanfile = textwrap.dedent(r""" from conan import ConanFile class BasicConanfile(ConanFile): name = "pkg" version = "1.0" def generate(self): self.output.warning(f'My unset conf variable is: "{self.conf.get("user:unset")}"') self.output.warning(f'My unset conf is {"NOT" if self.conf.get("user:unset") == None else ""} set') """) tc.save({"conanfile.py": conanfile, "profile": profile}) tc.run("create .") assert 'pkg/1.0: WARN: My unset conf is NOT set' in tc.out tc.run("create . -pr=profile") assert 'pkg/1.0: WARN: My unset conf variable is: ""' in tc.out assert 'pkg/1.0: WARN: My unset conf is set' in tc.out tc.run("create . -c user:unset=") assert 'pkg/1.0: WARN: My unset conf variable is: ""' in tc.out assert 'pkg/1.0: WARN: My unset conf is set' in tc.out tc.run('create . -c user:unset=""') assert 'pkg/1.0: WARN: My unset conf variable is: ""' in tc.out assert 'pkg/1.0: WARN: My unset conf is set' in tc.out # And ensure this actually works for the normal case, just in case tc.run("create . -c user:unset=Hello") assert 'pkg/1.0: WARN: My unset conf variable is: "Hello"' in tc.out assert 'pkg/1.0: WARN: My unset conf is set' in tc.out def test_nonexisting_conf(): c = TestClient() c.save({"conanfile.txt": ""}) c.run("install . -c tools.unknown:conf=value", assert_error=True) assert "ERROR: [conf] 'tools.unknown:conf' does not exist in configuration" in c.out c.run("install . -c user.some:var=value") # This doesn't fail c.run("install . -c user.some.var=value", assert_error=True) assert "ERROR: User conf 'user.some.var' invalid format, not 'user.org.group:conf'" in c.out c.run("install . -c tool.build:verbosity=v", assert_error=True) assert "ERROR: [conf] 'tool.build:verbosity' does not exist in configuration" in c.out def test_nonexisting_conf_global_conf(): c = TestClient() c.save_home({"global.conf": "tools.unknown:conf=value"}) c.save({"conanfile.txt": ""}) c.run("install . ", assert_error=True) assert "ERROR: [conf] 'tools.unknown:conf' does not exist in configuration" in c.out def test_global_conf_auto_created(): c = TestClient() c.run("config list") # all commands will trigger global_conf = c.load_home("global.conf") assert "# core:non_interactive = True" in global_conf def test_command_line_core_conf(): c = TestClient() c.run("config show * -cc core:default_profile=potato") assert "core:default_profile: potato" in c.out c.run("config show * -cc core:default_profile=potato -cc core:default_build_profile=orange") assert "core:default_profile: potato" in c.out assert "core:default_build_profile: orange" in c.out c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") tfolder = temp_folder() c.run(f'list * -cc core.cache:storage_path="{tfolder}"') assert "WARN: There are no matching recipe references" in c.out c.run(f'list *') assert "WARN: There are no matching recipe references" not in c.out assert "pkg/0.1" in c.out c.run("list * -cc user.xxx:yyy=zzz", assert_error=True) assert "ERROR: Only core. values are allowed in --core-conf. Got user.xxx:yyy=zzz" in c.out def test_build_test_consumer_only(): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def generate(self): skip = self.conf.get("tools.build:skip_test", check_type=bool) self.output.info(f'SKIP-TEST: {skip}') """) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" requires = "dep/0.1" def generate(self): self.output.info(f'SKIP-TEST: {self.conf.get("tools.build:skip_test")}') """) c.save_home({"global.conf": "tools.build:skip_test=True\n&:tools.build:skip_test=False"}) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg, "pkg/test_package/conanfile.py": GenConanfile().with_test("pass")}) c.run("create dep") assert "dep/0.1: SKIP-TEST: False" in c.out c.run('create pkg --build=* -tf=""') assert "dep/0.1: SKIP-TEST: True" in c.out assert "pkg/0.1: SKIP-TEST: False" in c.out c.run('create pkg --build=*') assert "dep/0.1: SKIP-TEST: True" in c.out assert "pkg/0.1: SKIP-TEST: False" in c.out c.run('install pkg --build=*') assert "dep/0.1: SKIP-TEST: True" in c.out assert "conanfile.py (pkg/0.1): SKIP-TEST: False" in c.out def test_conf_should_be_immutable(): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def generate(self): self.conf.append("user.myteam:myconf", "value1") self.output.info(f'user.myteam:myconf: {self.conf.get("user.myteam:myconf")}') """) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" requires = "dep/0.1" def generate(self): self.output.info(f'user.myteam:myconf: {self.conf.get("user.myteam:myconf")}') """) c.save_home({"global.conf": 'user.myteam:myconf=["root_value"]'}) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg}) c.run("create dep") assert "dep/0.1: user.myteam:myconf: ['root_value', 'value1']" in c.out c.run('create pkg --build=*') assert "dep/0.1: user.myteam:myconf: ['root_value', 'value1']" in c.out # The pkg/0.1 output should be non-modified assert "pkg/0.1: user.myteam:myconf: ['root_value']" in c.out def test_especial_strings_fail(): # https://github.com/conan-io/conan/issues/15777 c = TestClient() global_conf = textwrap.dedent(""" user.mycompany:myfile = re user.mycompany:myother = fnmatch user.mycompany:myfunct = re.search user.mycompany:mydict = {1: 're', 2: 'fnmatch'} """) c.save_home({"global.conf": global_conf}) c.run("config show *") assert "user.mycompany:myfile: re" in c.out assert "user.mycompany:myother: fnmatch" in c.out assert "user.mycompany:myfunct: re.search" in c.out assert "user.mycompany:mydict: {1: 're', 2: 'fnmatch'}" in c.out def test_conf_test_package(): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def generate(self): self.output.info(f'user.myteam:myconf: {self.conf.get("user.myteam:myconf")}') """) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def generate(self): self.output.info(f'user.myteam:myconf: {self.conf.get("user.myteam:myconf")}') def test(self): pass """) c.save({"conanfile.py": dep, "test_package/conanfile.py": pkg}) c.run("create . -c &:user.myteam:myconf=myvalue") assert "dep/0.1: user.myteam:myconf: myvalue" in c.out assert "dep/0.1 (test package): user.myteam:myconf: myvalue" in c.out ================================================ FILE: test/integration/configuration/conf/test_conf_copy.py ================================================ from conan.internal.model.conf import Conf def test_copy_conaninfo_conf(): conf = Conf() conf.define("core:non_interactive", True) conf.define("tools.cmake.cmaketoolchain:generator", True) conf.define("tools.deployer:symlinks", True) conf.define("user.myconf:cmake-test", True) pattern = [".*"] conf.define("tools.info.package_id:confs", pattern) result = conf.copy_conaninfo_conf().dumps() assert "tools.info.package_id:confs=%s" % pattern in result assert "core:non_interactive" in result assert "tools.cmake.cmaketoolchain:generator=True" in result assert "tools.deployer:symlinks" in result assert "user.myconf:cmake-test" in result pattern = [r"tools\..*"] conf.define("tools.info.package_id:confs", pattern) result = conf.copy_conaninfo_conf().dumps() assert "tools.info.package_id:confs=%s" % pattern in result assert "core:non_interactive" not in result assert "tools.cmake.cmaketoolchain:generator=True" in result assert "tools.deployer:symlinks" in result assert "user.myconf:cmake-test" not in result pattern = [".*cmake"] conf.define("tools.info.package_id:confs", pattern) result = conf.copy_conaninfo_conf().dumps() assert "tools.info.package_id:confs=%s" % pattern not in result assert "core:non_interactive" not in result assert "tools.cmake.cmaketoolchain:generator=True" in result assert "tools.deployer:symlinks" not in result assert "user.myconf:cmake-test" in result pattern = ["(tools.deploy|core)"] conf.define("tools.info.package_id:confs", pattern) result = conf.copy_conaninfo_conf().dumps() assert "tools.info.package_id:confs=%s" % pattern not in result assert "core:non_interactive" in result assert "tools.cmake.cmaketoolchain:generator=True" not in result assert "tools.deployer:symlinks" in result assert "user.myconf:cmake-test" not in result ================================================ FILE: test/integration/configuration/conf/test_conf_from_br.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_basic(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.conf_info.define_path("tools.android:ndk_path", "MY-NDK!!!") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=android_ndk --version=1.0") consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" build_requires = "android_ndk/1.0" def generate(self): self.output.info("NDK: %s" % self.conf.get("tools.android:ndk_path")) """) # CMakeToolchain needs compiler definition linux_profile = textwrap.dedent(""" [settings] os = Linux arch = x86_64 compiler = gcc compiler.version = 4.9 compiler.libcxx = libstdc++ """) android_profile = textwrap.dedent(""" include(linux) [conf] tools.android:ndk_path=MY-SYSTEM-NDK!!! """) client.save({"conanfile.py": consumer, "linux": linux_profile, "android": android_profile}, clean_first=True) client.run("install . -pr=linux") assert "conanfile.py: NDK: MY-NDK!!!" in client.out client.run("install . -pr:b=default -pr:h=android") assert "conanfile.py: NDK: MY-SYSTEM-NDK!!!" in client.out def test_basic_append(): c = TestClient() tool = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.conf_info.append("user.myorg:myconf", "myorgconf!!!") """) consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "tool/1.0" def generate(self): self.output.info("user.myorg:myconf: %s" % self.conf.get("user.myorg:myconf")) """) c.save({"tool/conanfile.py": tool, "consumer/conanfile.py": consumer}) c.run("create tool --name=tool --version=1.0") c.run("install consumer") assert "conanfile.py: user.myorg:myconf: ['myorgconf!!!']" in c.out c.run("install consumer -c user.myorg:myconf=value", assert_error=True) assert "ERROR: It's not possible to compose str values and list ones" in c.out c.run("install consumer -c user.myorg:myconf=+value") assert "conanfile.py: user.myorg:myconf: ['value', 'myorgconf!!!']" in c.out class TestImportant: @staticmethod def client(action, value): value = '' if not value else f', {value}' conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.conf_info.{action}("tools.android:ndk_path!"{value}) """) app = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "android_ndk/1.0" def generate(self): self.output.info("NDK: %s" % self.conf.get("tools.android:ndk_path")) """) c = TestClient(light=True) c.save({"conanfile.py": conanfile}) c.run("create . --name=android_ndk --version=1.0") c.save({"conanfile.py": app}, clean_first=True) return c @pytest.mark.parametrize("action", ["define", "define_path"]) def test_important_define(self, action): c = self.client(action, '"MY-NDK!!"') c.run("install") assert "conanfile.py: NDK: MY-NDK!!" in c.out c.run("install -c tools.android:ndk_path=OTHERNDK!!") assert "conanfile.py: NDK: MY-NDK!!" in c.out c.run("install -c tools.android:ndk_path+=OTHERNDK!!", assert_error=True) assert "ERROR: It's not possible to compose list values and str ones." in c.out c.run("install -c tools.android:ndk_path!=OTHERNDK!!") assert "conanfile.py: NDK: OTHERNDK!!" in c.out @pytest.mark.parametrize("action", ["define", "define_path"]) def test_important_define_list(self, action): c = self.client(action, '["MY-NDK!!"]') c.run("install") assert "conanfile.py: NDK: ['MY-NDK!!']" in c.out c.run("install -c tools.android:ndk_path=OTHERNDK!!", assert_error=True) assert "ERROR: It's not possible to compose str values and list ones." in c.out c.run("install -c tools.android:ndk_path+=OTHER! -c tools.android:ndk_path=+OTHER2!") assert "conanfile.py: NDK: ['MY-NDK!!']" in c.out c.run("install -c tools.android:ndk_path!=OTHERNDK!!", assert_error=True) assert "ERROR: It's not possible to compose str values and list ones." in c.out c.run("install -c tools.android:ndk_path!+=OTHER! -c tools.android:ndk_path!=+OTHER2!") assert "conanfile.py: NDK: ['OTHER2!', 'MY-NDK!!', 'OTHER!']" in c.out @pytest.mark.parametrize("action", ["append", "append_path"]) def test_important_append(self, action): c = self.client(action, '"MY-NDK!!"') c.run("install") assert "conanfile.py: NDK: ['MY-NDK!!']" in c.out c.run("install -c tools.android:ndk_path=OTHER!", assert_error=True) assert "ERROR: It's not possible to compose str values and list ones." in c.out c.run("install -c tools.android:ndk_path+=OTHER! -c tools.android:ndk_path=+OTHER2!") assert "conanfile.py: NDK: ['OTHER2!', 'OTHER!', 'MY-NDK!!']" in c.out c.run("install -c tools.android:ndk_path!+=OTHER! -c tools.android:ndk_path!=+OTHER2!") assert "conanfile.py: NDK: ['OTHER2!', 'MY-NDK!!', 'OTHER!']" in c.out @pytest.mark.parametrize("action", ["prepend", "prepend_path"]) def test_important_prepend(self, action): c = self.client(action, '"MY-NDK!!"') c.run("install") assert "conanfile.py: NDK: ['MY-NDK!!']" in c.out c.run("install -c tools.android:ndk_path=OTHER!", assert_error=True) assert "ERROR: It's not possible to compose str values and list ones." in c.out c.run("install -c tools.android:ndk_path+=OTHER! -c tools.android:ndk_path=+OTHER2!") assert "conanfile.py: NDK: ['MY-NDK!!', 'OTHER2!', 'OTHER!']" in c.out c.run("install -c tools.android:ndk_path!+=OTHER! -c tools.android:ndk_path!=+OTHER2!") assert "conanfile.py: NDK: ['OTHER2!', 'MY-NDK!!', 'OTHER!']" in c.out def test_important_unset(self): c = self.client("unset", "") c.run("install") assert "conanfile.py: NDK: None" in c.out c.run("install -c tools.android:ndk_path=OTHER!") assert "conanfile.py: NDK: None" in c.out c.run("install -c tools.android:ndk_path+=OTHER! -c tools.android:ndk_path=+OTHER2!") assert "conanfile.py: NDK: None" in c.out c.run("install -c tools.android:ndk_path!=OTHER!") assert "conanfile.py: NDK: OTHER!" in c.out c.run("install -c tools.android:ndk_path!+=OTHER! -c tools.android:ndk_path!=+OTHER2!") assert "conanfile.py: NDK: ['OTHER2!', 'OTHER!']" in c.out @pytest.mark.parametrize("action", ["update", "update_path"]) def test_important_update(self, action): c = self.client(action, {"MYVALUE": "MY-NDK!!"}) c.run("install") assert "conanfile.py: NDK: {'MYVALUE': 'MY-NDK!!'}" in c.out c.run("install -c tools.android:ndk_path=OTHER!", assert_error=True) assert "ERROR: It's not possible to compose str values and dict ones." in c.out c.run("install -c tools.android:ndk_path=\"{'MYVALUE': 'OTHERVALUE!!', 'V2': 'O2!!'}\"") assert "conanfile.py: NDK: {'MYVALUE': 'MY-NDK!!'}" in c.out c.run("install -c tools.android:ndk_path*=\"{'MYVALUE': 'OTHERVALUE!!', 'V2': 'O2!!'}\"") assert "conanfile.py: NDK: {'MYVALUE': 'MY-NDK!!', 'V2': 'O2!!'}" in c.out c.run("install -c tools.android:ndk_path!=OTHER!", assert_error=True) assert "ERROR: It's not possible to compose str values and dict ones." in c.out c.run("install -c tools.android:ndk_path!=\"{'MYVALUE': 'OTHERVALUE!!', 'V2': 'O2!!'}\"") assert "conanfile.py: NDK: {'MYVALUE': 'OTHERVALUE!!', 'V2': 'O2!!'}" in c.out c.run("install -c tools.android:ndk_path!*=\"{'MYVALUE': 'OTHERVALUE!!', 'V2': 'O2!!'}\"") assert "conanfile.py: NDK: {'MYVALUE': 'OTHERVALUE!!', 'V2': 'O2!!'}" in c.out def test_basic_conf_through_cli(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.output.info("NDK build: %s" % self.conf.get("tools.android:ndk_path")) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=android_ndk --version=1.0") consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" build_requires = "android_ndk/1.0" def generate(self): self.output.info("NDK host: %s" % self.conf.get("tools.android:ndk_path")) """) # CMakeToolchain needs compiler definition linux_profile = textwrap.dedent(""" [settings] os = Linux arch = x86_64 compiler = gcc compiler.version = 4.9 compiler.libcxx = libstdc++ """) android_profile = textwrap.dedent(""" include(linux) [conf] tools.android:ndk_path=MY-SYSTEM-NDK!!! """) client.save({"conanfile.py": consumer, "linux": linux_profile, "android": android_profile}, clean_first=True) client.run('install . -c:b=tools.android:ndk_path="MY-NDK!!!" ' '-c:h=tools.android:ndk_path="MY-SYSTEM-NDK!!!" -pr:b=default -pr:h=android') assert "android_ndk/1.0: NDK build: MY-NDK!!!" in client.out assert "conanfile.py: NDK host: MY-SYSTEM-NDK!!!" in client.out def test_declared_generators_get_conf(): # https://github.com/conan-io/conan/issues/9571 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.conf_info.append("tools.cmake.cmaketoolchain:user_toolchain", "mytoolchain.cmake") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=mytool --version=1.0") consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" build_requires = "mytool/1.0" """) client.save({"conanfile.py": consumer}, clean_first=True) client.run("install . -pr:b=default") toolchain = client.load("conan_toolchain.cmake") assert 'include("mytoolchain.cmake")' in toolchain consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" build_requires = "mytool/1.0" def generate(self): CMakeToolchain(self).generate() """) client.save({"conanfile.py": consumer}, clean_first=True) client.run("install . -pr:b=default") toolchain = client.load("conan_toolchain.cmake") assert 'include("mytoolchain.cmake")' in toolchain def test_propagate_conf_info(): """ test we can use the conf_info to propagate information from the dependencies to the consumers. The propagation is explicit. TO DISCUSS: Should conf be aggregated always from all requires? TODO: Backport to Conan 1.X so UserInfo is not longer necessary in 1.X """ # https://github.com/conan-io/conan/issues/9571 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.conf_info.define("user:myinfo1", "val1") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=dep1 --version=1.0") client.run("create . --name=dep2 --version=1.0") consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep1/1.0", "dep2/1.0" def generate(self): c1 = self.dependencies["dep1"].conf_info.get("user:myinfo1") c2 = self.dependencies["dep2"].conf_info.get("user:myinfo1") self.output.info("CONF1: {}".format(c1)) self.output.info("CONF2: {}".format(c2)) """) client.save({"conanfile.py": consumer}, clean_first=True) client.run("install . ") assert "conanfile.py: CONF1: val1" in client.out assert "conanfile.py: CONF2: val1" in client.out def test_conf_transitive_tool(): """ # https://github.com/conan-io/conan/issues/14421 app --(tool_requires)--> tool/0.1 -> lib/0.1 -(tool_require)-> libbuilder/0.1 -> zlib/0.1 profile-host = Debug profile-build = Release """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): version = "0.1" settings = "build_type" {} def package_info(self): self.output.info(f"host: {{self.settings.build_type}}") self.output.info(f"build: {{self.settings_build.build_type}}") if self.settings_target is not None: self.output.info(f"target: {{self.settings_target.build_type}}") """) client.save({"zlib/conanfile.py": conanfile.format(""), "libbuilder/conanfile.py": conanfile.format("requires='zlib/0.1'"), "lib/conanfile.py": conanfile.format("tool_requires='libbuilder/0.1'"), "tool/conanfile.py": conanfile.format("requires='lib/0.1'"), "app/conanfile.py": conanfile.format("tool_requires='tool/0.1'")}) client.run("export zlib --name=zlib") client.run("export libbuilder --name=libbuilder") client.run("export lib --name=lib") client.run("export tool --name=tool") client.run("create app --name=app -s:b build_type=Release -s:h build_type=Debug --build=missing") for lib in "zlib", "libbuilder": assert f"{lib}/0.1: host: Release" in client.out assert f"{lib}/0.1: build: Release" in client.out assert f"{lib}/0.1: target: Release" in client.out # used to create lib/0.1 that is Release! for lib in "lib", "tool": assert f"{lib}/0.1: host: Release" in client.out assert f"{lib}/0.1: build: Release" in client.out assert f"{lib}/0.1: target: Debug" in client.out # used to create app/0.1 that is Debug! assert "app/0.1: host: Debug" in client.out assert "app/0.1: build: Release" in client.out def test_conf_both_build_and_host(): """ # https://github.com/conan-io/conan/issues/14421 app --(requires)-----> protobuf(lib-host-release) -(tool_require)-> tool/0.1 \\- (tool_require)-> protobuf(protoc-build-release) -(tool_require) -> tool/0.1 profile-host = Debug profile-build = Release """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "tool" version = "0.1" def package_info(self): myvalue = str(self.settings_target.build_type) self.conf_info.define("user.team:myconf", myvalue) """) client.save({"conanfile.py": conanfile}) client.run("create . --build-require") myprotobuf = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "myprotobuf" version = "0.1" settings = "build_type" tool_requires = "tool/0.1" def generate(self): self.output.info(f"MYCONF {self.context}: {self.conf.get('user.team:myconf')}") """) client.save({"myprotobuf/conanfile.py": myprotobuf, "app/conanfile.py": GenConanfile().with_requires("myprotobuf/0.1") .with_tool_requires("myprotobuf/0.1")}, clean_first=True) client.run('export myprotobuf') client.run("install app --build=missing -s:h build_type=Debug -s:b build_type=Release") assert "myprotobuf/0.1: MYCONF build: Release" in client.out assert "myprotobuf/0.1: MYCONF host: Debug" in client.out @pytest.mark.parametrize("conf", ['define("user.myorg:myconf", self.options.myopt)', 'define("user.myorg:myconf", self.settings.os)', 'append("user.myorg:myconf", self.settings.os)', 'prepend("user.myorg:myconf", self.settings.os)']) def test_error_bad_types(conf): c = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): name = "tool" version = "0.1" settings = "os" options = {{"myopt": [True, False]}} default_options = {{"myopt": True}} def package_info(self): self.conf_info.{conf} """) c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) assert "ERROR: tool/0.1: Error in package_info() method, line 10" assert f'self.conf_info.{conf}' in c.out assert "Invalid 'conf' type, please use Python types (int, str, ...)" in c.out @pytest.mark.parametrize("conf", ["tools.myorg.myconf", "user.myorg.myconf"]) def test_error_missing_colon_define(conf): c = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): name = "tool" version = "0.1" def package_info(self): self.conf_info.define("{conf}", 42) """) c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) assert "ERROR: tool/0.1: Error in package_info() method:" in c.out if conf == "tools.myorg.myconf": assert "[conf] 'tools.myorg.myconf' does not exist in configuration" in c.out else: assert f"User conf '{conf}' invalid format, not 'user.org.group:conf'" in c.out def test_error_missing_colon_consume(): c = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): def generate(self): self.conf.get("user.myorg.myconf") """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "User conf 'user.myorg.myconf' invalid format, not 'user.org.group:conf'" in c.out ================================================ FILE: test/integration/configuration/conf/test_conf_package_id.py ================================================ import json import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.fixture def client(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_id(self): self.info.conf.define("user.myconf:myitem", self.conf.get("user.myconf:myitem")) """) client.save({"conanfile.py": conanfile}) return client def test_package_id(client): profile1 = textwrap.dedent("""\ [conf] user.myconf:myitem=1""") profile2 = textwrap.dedent("""\ [conf] user.myconf:myitem=2""") client.save({"profile1": profile1, "profile2": profile2}) client.run("create . --name=pkg --version=0.1 -pr=profile1") client.assert_listed_binary({"pkg/0.1": ("7501c16e6c5c93e534dd760829859340e2dc73bc", "Build")}) client.run("create . --name=pkg --version=0.1 -pr=profile2") client.assert_listed_binary({"pkg/0.1": ("4eb2bd276f75d2df8fa0f4b58bd86014b7f51693", "Build")}) def test_json_output(client): client.run("create . --name=pkg --version=0.1 -c user.myconf:myitem=1 --format=json") graph = json.loads(client.stdout) assert graph["graph"]["nodes"]["1"]["info"]["conf"] == {'user.myconf:myitem': 1} ================================================ FILE: test/integration/configuration/conf/test_conf_profile.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture def client(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def run(self, cmd, env=None, **kwargs): # INTERCEPTOR of running self.output.info("RECIPE-RUN: {}".format(cmd)) def build(self): cmake = CMake(self) cmake.build() """) client.save({"conanfile.py": conanfile}) return client def test_cmake_no_config(client): profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release """) client.save({"myprofile": profile}) client.run("create . --name=pkg --version=0.1 -pr=myprofile") assert "-verbosity" not in client.out def test_cmake_config(client): profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release [conf] tools.build:verbosity=quiet """) client.save({"myprofile": profile}) client.run("create . --name=pkg --version=0.1 -pr=myprofile") assert "-verbosity:Quiet" in client.out client.run("create . --name=pkg --version=0.1 -pr=myprofile -c=tools.env.virtualenv:powershell=powershell.exe") assert "/verbosity:Quiet" in client.out def test_cmake_config_error(client): profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release [conf] tools.build:verbosity=non-existing """) client.save({"myprofile": profile}) client.run("create . --name=pkg --version=0.1 -pr=myprofile", assert_error=True) assert "Unknown value 'non-existing' for 'tools.build:verbosity'" in client.out def test_cmake_config_package(client): profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release [conf] dep*:tools.build:verbosity=quiet """) client.save({"myprofile": profile}) client.run("create . --name=pkg --version=0.1 -pr=myprofile") assert "-verbosity" not in client.out client.run("create . --name=dep --version=0.1 -pr=myprofile") assert "-verbosity:Quiet" in client.out def test_cmake_config_package_not_scoped(client): profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release [conf] tools.build:verbosity=quiet """) client.save({"myprofile": profile}) client.run("create . --name=pkg --version=0.1 -pr=myprofile") assert "-verbosity:Quiet" in client.out client.run("create . --name=dep --version=0.1 -pr=myprofile") assert "-verbosity:Quiet" in client.out def test_config_profile_forbidden(client): profile = textwrap.dedent("""\ [conf] cache:verbosity=Minimal """) client.save({"myprofile": profile}) client.run("install . --name=pkg --version=0.1 -pr=myprofile", assert_error=True) assert ("ERROR: Error reading 'myprofile' profile: [conf] " "'cache:verbosity' not allowed in profiles" in client.out) def test_msbuild_config(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuild class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def build(self): ms = MSBuild(self) self.output.info(ms.command("Project.sln")) """) client.save({"conanfile.py": conanfile}) profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release [conf] tools.build:verbosity=quiet """) client.save({"myprofile": profile}) client.run("create . --name=pkg --version=0.1 -pr=myprofile") assert "-verbosity:Quiet" in client.out def test_msbuild_compile_options(): # This works in all platforms because MSBuildToolchain works even in Linux, it will # just skip generating the conanvcvars.bat client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "MSBuildToolchain" """) client.save({"conanfile.py": conanfile}) profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release [conf] tools.microsoft.msbuildtoolchain:compile_options={"ExceptionHandling": "Async"} """) client.save({"myprofile": profile}) client.run("install . -pr=myprofile") msbuild_tool = client.load("conantoolchain_release_x64.props") assert "Async" in msbuild_tool def test_conf_package_patterns(): client = TestClient() conanfile = GenConanfile() generate = """ def generate(self): value = self.conf.get("user.build:myconfig") self.output.warning("{} Config:{}".format(self.ref.name, value)) """ client.save({"dep/conanfile.py": str(conanfile) + generate, "pkg/conanfile.py": str(conanfile.with_requirement("dep/0.1", visible=False)) + generate, "consumer/conanfile.py": str(conanfile.with_requires("pkg/0.1") .with_settings("os", "build_type")) + generate}) client.run("export dep --name=dep --version=0.1") client.run("export pkg --name=pkg --version=0.1") # This pattern applies to no package profile = """ [settings] os=Windows [conf] invented/*:user.build:myconfig=Foo """ client.save({"profile": profile}) client.run("install consumer --build=* --profile profile") assert "WARN: dep Config:None" in client.out assert "WARN: pkg Config:None" in client.out assert "WARN: None Config:None" in client.out # This patterns applies to dep profile = """ [settings] os=Windows [conf] dep/*:user.build:myconfig=Foo """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep Config:Foo" in client.out assert "WARN: pkg Config:None" in client.out assert "WARN: None Config:None" in client.out profile = """ [settings] os=Windows [conf] dep/0.1:user.build:myconfig=Foo """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep Config:Foo" in client.out assert "WARN: pkg Config:None" in client.out assert "WARN: None Config:None" in client.out # The global pattern applies to all profile = """ [settings] os=Windows [conf] dep/*:user.build:myconfig=Foo pkg/*:user.build:myconfig=Foo2 user.build:myconfig=Var """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep Config:Var" in client.out assert "WARN: pkg Config:Var" in client.out assert "WARN: None Config:Var" in client.out # "&" pattern for the consumer profile = """ [settings] os=Windows [conf] dep/*:user.build:myconfig=Foo pkg/*:user.build:myconfig=Foo2 &:user.build:myconfig=Var """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep Config:Foo" in client.out assert "WARN: pkg Config:Foo2" in client.out assert "WARN: None Config:Var" in client.out def test_config_package_append(client): profile1 = textwrap.dedent("""\ [conf] user.myteam:myconf=["a", "b", "c"] """) profile2 = textwrap.dedent("""\ include(profile1) [conf] mypkg*:user.myteam:myconf+=["d"] mydep*:user.myteam:myconf=+["e"] """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def generate(self): self.output.info(f"MYCONF: {self.conf.get('user.myteam:myconf')}") def build(self): self.output.info(f"MYCONFBUILD: {self.conf.get('user.myteam:myconf')}") """) client.save({"profile1": profile1, "profile2": profile2, "conanfile.py": conanfile}) client.run("install . --name=mypkg --version=0.1 -pr=profile2") assert "conanfile.py (mypkg/0.1): MYCONF: ['a', 'b', 'c', 'd']" in client.out client.run("install . --name=mydep --version=0.1 -pr=profile2") assert "conanfile.py (mydep/0.1): MYCONF: ['e', 'a', 'b', 'c']" in client.out client.run("create . --name=mypkg --version=0.1 -pr=profile2") assert "mypkg/0.1: MYCONFBUILD: ['a', 'b', 'c', 'd']" in client.out client.run("create . --name=mydep --version=0.1 -pr=profile2") assert "mydep/0.1: MYCONFBUILD: ['e', 'a', 'b', 'c']" in client.out def test_conf_patterns_user_channel(): # https://github.com/conan-io/conan/issues/14139 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def configure(self): self.output.info(f"CONF: {self.conf.get('user.myteam:myconf')}") self.output.info(f"CONF2: {self.conf.get('user.myteam:myconf2')}") """) profile = textwrap.dedent("""\ [conf] user.myteam:myconf=myvalue1 user.myteam:myconf2=other1 *@user/channel:user.myteam:myconf=myvalue2 *@*/*:user.myteam:myconf2=other2 """) client.save({"dep/conanfile.py": conanfile, "app/conanfile.py": GenConanfile().with_requires("dep1/0.1", "dep2/0.1@user/channel"), "profile": profile}) client.run("create dep --name=dep1 --version=0.1") client.run("create dep --name=dep2 --version=0.1 --user=user --channel=channel") client.run("install app -pr=profile") assert "dep1/0.1: CONF: myvalue1" in client.out assert "dep2/0.1@user/channel: CONF: myvalue2" in client.out assert "dep1/0.1: CONF2: other1" in client.out assert "dep2/0.1@user/channel: CONF2: other2" in client.out ================================================ FILE: test/integration/configuration/custom_setting_test_package_test.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save class TestConditionalReqsTest: def test_conditional_requirements(self): conanfile = GenConanfile("hello", "0.1").with_settings("os", "build_type", "product") test_conanfile = ''' from conan import ConanFile class TestConanLib(ConanFile): settings = "os", "build_type", "product" def requirements(self): self.output.info("TestSettings: %s, %s, %s" % (self.settings.os, self.settings.build_type, self.settings.product)) self.requires(self.tested_reference_str) def test(self): pass ''' client = TestClient() save(client.paths.settings_path_user, "product: [onion, potato]") client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) client.run("create . -s os=Windows -s product=onion -s build_type=Release") assert "hello/0.1 (test package): TestSettings: Windows, Release, onion" in client.out ================================================ FILE: test/integration/configuration/default_profile_test.py ================================================ import os import textwrap from conan.internal.paths import CONANFILE from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient from conan.test.utils.env import environment_update from conan.internal.util.files import save class TestDefaultProfile: def test_conanfile_txt_incomplete_profile(self): conanfile = GenConanfile() client = TestClient() client.save({CONANFILE: conanfile}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=stable") assert "pkg/0.1@lasote/stable: Package '%s' created" % NO_SETTINGS_PACKAGE_ID in client.out client.save({"conanfile.txt": "[requires]\npkg/0.1@lasote/stable"}, clean_first=True) client.run('install .') assert "pkg/0.1@lasote/stable: Already installed!" in client.out def test_change_default_profile(self): br = ''' import os from conan import ConanFile from conan.tools.env import VirtualBuildEnv class MyConanfile(ConanFile): name = "mylib" version = "0.1" def build(self): build_env = VirtualBuildEnv(self).vars() with build_env.apply(): assert(os.environ.get("Value1") == "A") ''' tmp = temp_folder() default_profile_path = os.path.join(tmp, "myprofile") save(default_profile_path, "[buildenv]\nValue1=A") client = TestClient() client.save_home({"global.conf": "core:default_profile={}".format(default_profile_path)}) client.save({CONANFILE: br}) client.run("export . --user=lasote --channel=stable") client.run('install --requires=mylib/0.1@lasote/stable --build="*"') # Now use a name, in the default profile folder os.unlink(default_profile_path) save(os.path.join(client.paths.profiles_path, "other"), "[buildenv]\nValue1=A") client.save_home({"global.conf": "core:default_profile=other"}) client.save({CONANFILE: br}) client.run("export . --user=lasote --channel=stable") client.run('install --requires=mylib/0.1@lasote/stable --build="*"') def test_profile_applied_ok(self): br = textwrap.dedent(''' from conan import ConanFile class BuildRequireConanfile(ConanFile): name = "br" version = "1.0" def package_info(self): self.buildenv_info.define("MyVAR", "from_build_require") ''') cf = textwrap.dedent(''' import os, platform from conan import ConanFile class MyConanfile(ConanFile): requires = "br/1.0" def build(self): if platform.system() == "Windows": self.run("set MyVAR") else: self.run("printenv MyVAR") ''') client = TestClient() client.save({"br/conanfile.py": br, "consumer/conanfile.py": cf, "profile": "[buildenv]\nMyVAR=23"}) client.run("create br") client.run("build consumer") assert "from_build_require" in client.out client.run("build consumer -pr=profile") assert "23" in client.out assert "from_build_require" not in client.out def test_env_default_profile(self): conanfile = ''' import os from conan import ConanFile from conan.tools.env import VirtualBuildEnv class MyConanfile(ConanFile): def build(self): build_env = VirtualBuildEnv(self).vars() with build_env.apply(): self.output.info(">>> env_variable={}".format(os.environ.get('env_variable'))) ''' client = TestClient() client.save({CONANFILE: conanfile}) # Test with the 'default' profile env_variable = "env_variable=profile_default" client.save_home({"profiles/default": "[settings]\nos=Windows\n[buildenv]\n" + env_variable}) client.run("create . --name=name --version=version --user=user --channel=channel") assert ">>> " + env_variable in client.out # Test with a profile set using and environment variable tmp = temp_folder() env_variable = "env_variable=profile_environment" default_profile_path = os.path.join(tmp, 'env_profile') save(default_profile_path, "[settings]\nos=Windows\n[buildenv]\n" + env_variable) with environment_update({'CONAN_DEFAULT_PROFILE': default_profile_path}): client.run("create . --name=name --version=version --user=user --channel=channel") assert ">>> " + env_variable in client.out # Use relative path defined in environment variable env_variable = "env_variable=relative_profile" rel_path = os.path.join('..', 'env_rel_profile') assert not os.path.isabs(rel_path) default_profile_path = os.path.join(client.paths.profiles_path, rel_path) save(default_profile_path, "[settings]\nos=Windows\n[buildenv]\n" + env_variable) with environment_update({'CONAN_DEFAULT_PROFILE': rel_path}): client.run("create . --name=name --version=version --user=user --channel=channel") assert ">>> " + env_variable in client.out # Use non existing path profile_path = os.path.join(tmp, "this", "is", "a", "path") assert os.path.isabs(profile_path) with environment_update({'CONAN_DEFAULT_PROFILE': profile_path}): client.run("create . --name=name --version=version --user=user --channel=channel", assert_error=True) assert "You need to create a default profile" in client.out def test_default_profile(self): c = TestClient() # Test with a profile set using and environment variable tmp = temp_folder() profile = os.path.join(tmp, 'myprofile') save(profile, "[settings]\nos=FreeBSD\n") with environment_update({'CONAN_DEFAULT_BUILD_PROFILE': profile}): c.run("profile show") assert "os=FreeBSD" in c.out # build profiles def test_conf_default_two_profiles(): client = TestClient() save(os.path.join(client.paths.profiles_path, "mydefault"), "[settings]\nos=FreeBSD") save(os.path.join(client.paths.profiles_path, "mydefault_build"), "[settings]\nos=Android") global_conf = textwrap.dedent(""" core:default_profile=mydefault core:default_build_profile=mydefault_build """) client.save_home({"global.conf": global_conf}) client.save({"conanfile.txt": ""}) client.run("install .") assert "Profile host:" in client.out assert "os=FreeBSD" in client.out assert "Profile build:" in client.out assert "os=Android" in client.out ================================================ FILE: test/integration/configuration/invalid_settings_test.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient class TestSettingsLoad: def test_invalid_settings(self): client = TestClient() client.save({os.path.join(client.cache_folder, 'settings.yml'): "your buggy file", "conanfile.txt": ""}) client.run("install .", assert_error=True) assert "ERROR: Invalid settings.yml format: 'settings' is not a dictionary" in client.out client.save({os.path.join(client.cache_folder, 'settings.yml'): "os:\n Windows: [1, ]", "conanfile.txt": ""}) client.run("install .", assert_error=True) assert "ERROR: Invalid settings.yml format: 'settings.os=Windows' is not a dictionary" \ in client.out def test_invalid_yaml(self): client = TestClient() client.save({os.path.join(client.cache_folder, 'settings.yml'): textwrap.dedent(""" Almost: - a - valid yaml """), "conanfile.txt": ""}) client.run("install .", assert_error=True) assert "ERROR: Invalid settings.yml format: while parsing a block mapping" in client.out ================================================ FILE: test/integration/configuration/profile_test.py ================================================ import json import os import platform import textwrap from collections import OrderedDict from textwrap import dedent import pytest from conan.internal.paths import CONANFILE from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.profiles import create_profile as _create_profile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import load, save conanfile_scope_env = """ from conan import ConanFile class AConan(ConanFile): name = "hello0" version = "0.1" settings = "os", "compiler", "arch" def build(self): # Print environment vars if self.settings.os == "Windows": self.run("SET") else: self.run("env") """ def create_profile(folder, name, settings=None, package_settings=None, options=None): _create_profile(folder, name, settings, package_settings, options) content = load(os.path.join(folder, name)) content = "include(default)\n \n" + content save(os.path.join(folder, name), content) class TestProfile: @pytest.fixture(autouse=True) def setup(self): self.client = TestClient() def test_profile_relative_cwd(self): self.client.save({"conanfile.txt": "", "sub/sub/profile": ""}) self.client.current_folder = os.path.join(self.client.current_folder, "sub") self.client.run("install .. -pr=sub/profile2", assert_error=True) assert "ERROR: Profile not found: sub/profile2" in self.client.out self.client.run("install .. -pr=sub/profile") assert "Installing packages" in self.client.out def test_bad_syntax(self): self.client.save({CONANFILE: conanfile_scope_env}) self.client.run("export . --user=lasote --channel=stable") profile = ''' [settings ''' clang_profile_path = os.path.join(self.client.paths.profiles_path, "clang") save(clang_profile_path, profile) self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang", assert_error=True) assert "Error reading 'clang' profile" in self.client.out assert "Bad syntax" in self.client.out profile = ''' [settings] [invented] ''' save(clang_profile_path, profile) self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang", assert_error=True) assert "Unrecognized field 'invented'" in self.client.out assert "Error reading 'clang' profile" in self.client.out profile = ''' [settings] as ''' save(clang_profile_path, profile) self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang", assert_error=True) assert "Error reading 'clang' profile: Invalid setting line 'as'" in self.client.out profile = ''' [settings] os = a value ''' save(clang_profile_path, profile) self.client.run("install --requires=hello0/0.1@lasote/stable --build missing -pr clang", assert_error=True) # stripped "a value" assert "'a value' is not a valid 'settings.os'" in self.client.out @pytest.mark.parametrize("path", ["", "./local_profiles/", None]) def test_install_with_missing_profile(self, path): if path is None: # Not good practice to introduce temp_folder() in the expand because it randomize # the test names causing issues to split them in N processes path = temp_folder() + "/" self.client.save({CONANFILE: conanfile_scope_env}) self.client.run('install . -pr "%sscopes_env"' % path, assert_error=True) assert "ERROR: Profile not found:" in self.client.out assert "scopes_env" in self.client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Windows profiles") def test_install_profile_settings(self): # Create a profile and use it profile_settings = OrderedDict([("compiler", "msvc"), ("compiler.version", "191"), ("compiler.runtime", "dynamic"), ("arch", "x86")]) create_profile(self.client.paths.profiles_path, "vs_12_86", settings=profile_settings, package_settings={}) self.client.save({"conanfile.py": conanfile_scope_env}) self.client.run("export . --user=lasote --channel=stable") self.client.run("install . --build missing -pr vs_12_86") info = self.client.out for setting, value in profile_settings.items(): assert "%s=%s" % (setting, value) in info # Try to override some settings in install command self.client.run("install . --build missing -pr vs_12_86 -s compiler.version=191") info = self.client.out for setting, value in profile_settings.items(): if setting != "compiler.version": assert "%s=%s" % (setting, value) in info else: assert "compiler.version=191" in info # Use package settings in profile tmp_settings = OrderedDict() tmp_settings["compiler"] = "gcc" tmp_settings["compiler.libcxx"] = "libstdc++11" tmp_settings["compiler.version"] = "4.8" package_settings = {"hello0/*": tmp_settings} create_profile(self.client.paths.profiles_path, "vs_12_86_hello0_gcc", settings=profile_settings, package_settings=package_settings) # Try to override some settings in install command self.client.run("install . --build missing -pr vs_12_86_hello0_gcc -s compiler.version=191") info = self.client.out assert "compiler=gcc" in info assert "compiler.libcxx=libstdc++11" in info # If other package is specified compiler is not modified package_settings = {"NoExistsRecipe": tmp_settings} create_profile(self.client.paths.profiles_path, "vs_12_86_hello0_gcc", settings=profile_settings, package_settings=package_settings) # Mix command line package settings with profile package_settings = {"hello0/*": tmp_settings} create_profile(self.client.paths.profiles_path, "vs_12_86_hello0_gcc", settings=profile_settings, package_settings=package_settings) # Try to override some settings in install command self.client.run("install . --build missing -pr vs_12_86_hello0_gcc" " -s compiler.version=191 -s hello0/*:compiler.libcxx=libstdc++") info = self.client.out assert "compiler=gcc" in info assert "compiler.libcxx=libstdc++11" not in info assert "compiler.libcxx=libstdc++" in info def test_install_profile_package_settings(self): conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = 'hello0' version = '0.1' settings = "os", "compiler", "arch", "build_type" def configure(self): self.output.info(self.settings.compiler) self.output.info(self.settings.compiler.version) """) self.client.save({"conanfile.py": conanfile}) # Create a profile and use it profile_settings = OrderedDict([("os", "Windows"), ("compiler", "msvc"), ("compiler.version", "191"), ("compiler.runtime", "dynamic"), ("arch", "x86")]) # Use package settings in profile tmp_settings = OrderedDict() tmp_settings["compiler"] = "gcc" tmp_settings["compiler.libcxx"] = "libstdc++11" tmp_settings["compiler.version"] = "4.8" package_settings = {"*@lasote/*": tmp_settings} _create_profile(self.client.paths.profiles_path, "myprofile", settings=profile_settings, package_settings=package_settings) # Try to override some settings in install command self.client.run("install . --user=lasote --channel=testing -pr myprofile") info = self.client.out assert "(hello0/0.1@lasote/testing): gcc" in info assert "(hello0/0.1@lasote/testing): 4.8" in info package_settings = {"*@other/*": tmp_settings} _create_profile(self.client.paths.profiles_path, "myprofile", settings=profile_settings, package_settings=package_settings) # Try to override some settings in install command self.client.run("install . --user=lasote --channel=testing -pr myprofile") info = self.client.out assert "(hello0/0.1@lasote/testing): msvc" in info assert "(hello0/0.1@lasote/testing): 191" in info assert "(hello0/0.1@lasote/testing): gcc" not in info assert "(hello0/0.1@lasote/testing): 4.8" not in info def test_package_settings_no_user_channel(self): conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def build(self): self.output.info("SETTINGS! os={}!!".format(self.settings.os)) """) profile = textwrap.dedent(""" [settings] os=Windows # THIS FAILED BEFORE WITH NO MATCH mypkg/0.1:os=Linux mypkg/0.1@user/channel:os=FreeBSD """) client = TestClient() client.save({"conanfile.py": conanfile, "profile": profile}) client.run("create . --name=mypkg --version=0.1 --user=user --channel=channel -pr=profile") assert "mypkg/0.1@user/channel: SETTINGS! os=FreeBSD!!" in client.out client.run("create . --name=mypkg --version=0.1 -pr=profile") assert "mypkg/0.1: SETTINGS! os=Linux!!" in client.out def test_install_profile_options(self): create_profile(self.client.paths.profiles_path, "vs_12_86", options={"hello0*:language": 1, "hello0*:static": False}) self.client.save({"conanfile.py": GenConanfile("hello0", "1").with_option("language", [1, 2]) .with_option("static", [True, False])}) self.client.run("install . --build missing -pr vs_12_86") info = self.client.out assert "language=1" in info assert "static=False" in info def test_scopes_env(self): # Create a profile and use it c = TestClient() c.save({"profile": "include(default)\n[buildenv]\nCXX=/path/tomy/g++", "conanfile.py": conanfile_scope_env}) c.run("build . -pr=profile") assert "CXX=/path/tomy/g++" in c.out # The env variable shouldn't persist after install command assert os.environ.get("CC", None) != "/path/tomy/gcc" assert os.environ.get("CXX", None) != "/path/tomy/g++" def test_info_with_profiles(self): self.client.run("remove '*' -c") # Create a simple recipe to require winreq_conanfile = ''' from conan import ConanFile class winrequireDefaultNameConan(ConanFile): name = "winrequire" version = "0.1" settings = "os", "compiler", "arch", "build_type" ''' files = {"conanfile.py": winreq_conanfile} self.client.save(files) self.client.run("export . --user=lasote --channel=stable") # Now require the first recipe depending on OS=windows conanfile = '''from conan import ConanFile import os class DefaultNameConan(ConanFile): name = "hello" version = "0.1" settings = "os", "compiler", "arch", "build_type" def requirements(self): if self.settings.os == "Windows": self.requires("winrequire/0.1@lasote/stable") ''' files = {"conanfile.py": conanfile} self.client.save(files) self.client.run("export . --user=lasote --channel=stable") # Create a profile that doesn't activate the require create_profile(self.client.paths.profiles_path, "scopes_env", settings={"os": "Linux"}) # Install with the previous profile self.client.run("graph info --requires=hello/0.1@lasote/stable --profile scopes_env") assert '''Requires: winrequire/0.1@lasote/stable''' not in self.client.out # Create a profile that activate the require create_profile(self.client.paths.profiles_path, "scopes_env", settings={"os": "Windows"}) # Install with the previous profile self.client.run("graph info --requires=hello/0.1@lasote/stable --profile scopes_env") assert ' winrequire/0.1@lasote/stable' in self.client.out class TestProfileAggregation: profile1 = dedent(""" [settings] os=Windows arch=x86_64 """) profile2 = dedent(""" [settings] arch=x86 build_type=Debug compiler=msvc compiler.version=191 compiler.runtime=dynamic """) conanfile = dedent(""" from conan import ConanFile import os class DefaultNameConan(ConanFile): settings = "os", "compiler", "arch", "build_type" def build(self): self.output.warning("ENV1:%s" % os.getenv("ENV1")) self.output.warning("ENV2:%s" % os.getenv("ENV2")) self.output.warning("ENV3:%s" % os.getenv("ENV3")) """) consumer = dedent(""" from conan import ConanFile import os class DefaultNameConan(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "lib/1.0@user/channel" """) @pytest.fixture(autouse=True) def setup(self): self.client = TestClient() self.client.save({CONANFILE: self.conanfile, "profile1": self.profile1, "profile2": self.profile2}) self._pkg_lib_10_id = "9b7f1e80c96289e8d9a3e7ded02830525090d5d4" def test_info(self): # The latest declared profile has priority self.client.run("create . --name=lib --version=1.0 --user=user --channel=channel --profile profile1 -pr profile2") self.client.save({CONANFILE: self.consumer}) self.client.run("graph info . --profile profile1 --profile profile2") assert self._pkg_lib_10_id in self.client.out def test_export_pkg(self): self.client.run("export-pkg . --name=lib --version=1.0 --user=user --channel=channel -pr profile1 -pr profile2") # ID for the expected settings applied: x86, Visual Studio 15,... assert self._pkg_lib_10_id in self.client.out def test_profile_crazy_inheritance(self): profile1 = dedent(""" [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic """) profile2 = dedent(""" include(profile1) [settings] os=Linux """) self.client.save({"profile1": profile1, "profile2": profile2}) self.client.run("create . --name=lib --version=1.0 --profile profile2 -pr profile1") assert dedent("""\ [settings] arch=x86_64 compiler=msvc compiler.runtime=dynamic compiler.runtime_type=Release compiler.version=191 os=Windows""") in self.client.out def test_profile_from_cache_path(): """ When passing relative folder/profile as profile file, it MUST be used conan install . -pr=profiles/default /tmp/profiles/default MUST be consumed as target profile https://github.com/conan-io/conan/pull/8685 """ client = TestClient() path = os.path.join(client.paths.profiles_path, "android", "profile1") save(path, "[settings]\nos=Android") client.save({"conanfile.txt": ""}) client.run("install . -pr=android/profile1") assert "os=Android" in client.out def test_profile_from_relative_pardir(): """ When passing relative ../path as profile file, it MUST be used conan install . -pr=../profiles/default /tmp/profiles/default MUST be consumed as target profile """ client = TestClient() client.save({"profiles/default": "[settings]\nos=AIX", "current/conanfile.txt": ""}) with client.chdir("current"): client.run("install . -pr=../profiles/default") assert "os=AIX" in client.out def test_profile_from_relative_dotdir(): """ When passing relative ./path as profile file, it MUST be used conan install . -pr=./profiles/default /tmp/profiles/default MUST be consumed as target profile """ client = TestClient() client.save({os.path.join("profiles", "default"): "[settings]\nos=AIX", os.path.join("current", "conanfile.txt"): ""}) client.run("install ./current -pr=./profiles/default") assert "os=AIX" in client.out def test_profile_from_temp_absolute_path(): """ When passing absolute path as profile file, it MUST be used conan install . -pr=/tmp/profiles/default /tmp/profiles/default MUST be consumed as target profile """ client = TestClient() client.save({"profiles/default": "[settings]\nos=AIX", "current/conanfile.txt": ""}) profile_path = os.path.join(client.current_folder, "profiles", "default") recipe_path = os.path.join(client.current_folder, "current", "conanfile.txt") client.run('install "{}" -pr="{}"'.format(recipe_path, profile_path)) assert "os=AIX" in client.out def test_consumer_specific_settings(): client = TestClient() dep = str(GenConanfile().with_settings("build_type").with_option("shared", [True, False]) .with_default_option("shared", False)) configure = """ def configure(self): self.output.warning("I'm {} and my build type is {}".format(self.name, self.settings.build_type)) self.output.warning("I'm {} and my shared is {}".format(self.name, self.options.shared)) """ dep += configure client.save({"conanfile.py": dep}) client.run("create . --name=dep --version=1.0") client.run("create . --name=dep --version=1.0 -s build_type=Debug -o dep*:shared=True") consumer = str(GenConanfile().with_settings("build_type").with_requires("dep/1.0") .with_option("shared", [True, False]).with_default_option("shared", False)) consumer += configure client.save({"conanfile.py": consumer}) # Regular install with release client.run("install . -s build_type=Release") assert "I'm dep and my build type is Release" in client.out assert "I'm None and my build type is Release" in client.out assert "I'm dep and my shared is False" in client.out assert "I'm None and my shared is False" in client.out # Now the dependency by name client.run("install . -s dep/*:build_type=Debug -o dep/*:shared=True") assert "I'm dep and my build type is Debug" in client.out assert "I'm None and my build type is Release" in client.out assert "I'm dep and my shared is True" in client.out assert "I'm None and my shared is False" in client.out # Now the consumer using & client.run("install . -s &:build_type=Debug -o shared=True") assert "I'm dep and my build type is Release" in client.out assert "I'm None and my build type is Debug" in client.out assert "I'm dep and my shared is False" in client.out assert "I'm None and my shared is True" in client.out # Now use a conanfile.txt client.save({"conanfile.txt": textwrap.dedent(""" [requires] dep/1.0 """)}, clean_first=True) # Regular install with release client.run("install . -s build_type=Release") assert "I'm dep and my build type is Release" in client.out # Now the dependency by name client.run("install . -s dep*:build_type=Debug -o dep*:shared=True") assert "I'm dep and my build type is Debug" in client.out assert "I'm dep and my shared is True" in client.out # Test that the generators take the setting if platform.system() != "Windows": # Toolchain in windows is multiconfig # Now the consumer using & client.run("install . -s &:build_type=Debug -g CMakeToolchain") assert "I'm dep and my build type is Release" in client.out # Verify the cmake toolchain takes Debug assert "I'm dep and my shared is False" in client.out presets = json.loads(client.load("CMakePresets.json")) assert presets["configurePresets"][0]["cacheVariables"]['CMAKE_BUILD_TYPE'] == "Debug" def test_create_and_priority_of_consumer_specific_setting(): client = TestClient() conanfile = str(GenConanfile().with_settings("build_type").with_name("foo").with_version("1.0")) configure = """ def configure(self): self.output.warning("I'm {} and my build type is {}".format(self.name, self.settings.build_type)) """ conanfile += configure client.save({"conanfile.py": conanfile}) client.run("create . -s foo*:build_type=Debug") assert "I'm foo and my build type is Debug" in client.out client.run("create . -s foo*:build_type=Debug -s &:build_type=Release") assert "I'm foo and my build type is Release" in client.out # The order DOES matter client.run("create . -s &:build_type=Release -s foo*:build_type=Debug ") assert "I'm foo and my build type is Debug" in client.out # With test_package also works test = str(GenConanfile().with_test("pass").with_setting("build_type")) test += configure client.save({"test_package/conanfile.py": test}) client.run("create . -s &:build_type=Debug -s build_type=Release") assert "I'm foo and my build type is Debug" in client.out # the test package recipe has debug too assert "I'm None and my build type is Debug" in client.out def test_package_consumer_is_only_the_tested_one(): """ the &:xxx pattern applies only to the package being tested, not other requires """ c = TestClient(light=True) test = textwrap.dedent(""" from conan import ConanFile class Tool(ConanFile): def requirements(self): self.requires(self.tested_reference_str) self.requires("dep/1.0") def test(self): pass """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_option("myoption", [1, 2]), "pkg/test_package/conanfile.py": test}) c.run("create dep") c.run("create pkg -o &:myoption=1") # This would crash if myoption is applied to dep assert 'pkg/0.1 (test package): Running test()' in c.out def test_consumer_specific_settings_from_profile(): client = TestClient() conanfile = str(GenConanfile().with_settings("build_type").with_name("hello")) configure = """ def configure(self): self.output.warning("I'm {} and my build type is {}".format(self.name, self.settings.build_type)) """ conanfile += configure profile = textwrap.dedent(""" include(default) [settings] &:build_type=Debug """) client.save({"conanfile.py": conanfile, "my_profile.txt": profile}) client.run("install . --profile my_profile.txt") assert "I'm hello and my build type is Debug" in client.out def test_consumer_invalid_profile_multiple_groups(): """ Issue related: https://github.com/conan-io/conan/issues/16448 """ client = TestClient() conanfile = GenConanfile(name="hello", version="1.0").with_settings("os", "arch", "build_type", "compiler") prof1 = textwrap.dedent("""\ [settings] arch=x86_64 os=Linux build_type=Release compiler=clang compiler.libcxx=libstdc++11 compiler.version=18 compiler.cppstd=20 [conf] tools.build:compiler_executables={'c': '/usr/bin/clang-18', 'cpp': '/usr/bin/clang++-18'} [settings] # Empty and duplicated section [options] package/*:option=Whatever [conf] # Another one """) client.save({ "conanfile.py": conanfile, "myprofs/myprofile": prof1 }) client.run("build . --profile:host myprofs/myprofile -g CMakeToolchain", assert_error=True) assert ("ERROR: Error reading 'myprofs/myprofile' profile: ConfigParser: " "Duplicated section: [settings]") in client.out def test_compose_numeric_values_scoped_pkg(): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("hello", "0.1") .with_package("self.output.info('user.var.value: ' + str(self.conf.get('user.var:value')))"), "profile": """[conf]\nuser.var:value=8.1\nhello/*:user.var:value=10\n"""}) tc.run("create . -pr=profile") assert "user.var.value: 10" in tc.out def test_nullify_settings(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "build_type" def configure(self): if not self.settings.build_type: self.output.info("BUILD TYPE NOT DEFINED!!!") else: self.output.info(f"I am a {self.settings.build_type} pkg!!!") """) c.save({"conanfile.py": conanfile, "profile": "[settings]\nbuild_type=Release\npkg1/*:build_type=~"}) c.run("create . --name=pkg1 --version=0.1") c.run("create . --name=pkg2 --version=0.1") c.run("create . --name=pkg3 --version=0.1") c.save({"conanfile.py": GenConanfile().with_requires("pkg1/0.1", "pkg2/0.1", "pkg3/0.1")}) c.run("graph info . -s build_type=Release -s pkg1/*:build_type=~") assert "pkg1/0.1: BUILD TYPE NOT DEFINED!!!" in c.out assert "pkg2/0.1: I am a Release pkg!!!" in c.out assert "pkg3/0.1: I am a Release pkg!!!" in c.out c.run("graph info . -pr=profile") assert "pkg1/0.1: BUILD TYPE NOT DEFINED!!!" in c.out assert "pkg2/0.1: I am a Release pkg!!!" in c.out assert "pkg3/0.1: I am a Release pkg!!!" in c.out ================================================ FILE: test/integration/configuration/proxies_conf_test.py ================================================ import os import textwrap from requests import Response from conan.test.utils.tools import TestClient, TestRequester from conan.test.utils.env import environment_update class TestProxiesConfTest: def test_requester_with_host_specific_proxies(self): class MyHttpRequester(TestRequester): def get(self, _, **kwargs): resp = Response() # resp._content = b'{"results": []}' resp.status_code = 200 resp._content = b'' print(kwargs["proxies"]) return resp client = TestClient(requester_class=MyHttpRequester) client.save_home({"global.conf": 'core.net.http:proxies = {"myproxykey": "myvalue"}'}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): settings = "os", "compiler" def source(self): download(self, "MyUrl", "filename.txt") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") assert "{'myproxykey': 'myvalue'}" in client.out def test_new_proxy_exclude(self): class MyHttpRequester(TestRequester): def get(self, _, **kwargs): resp = Response() # resp._content = b'{"results": []}' resp.status_code = 200 resp._content = b'' print("is excluded!" if "proxies" not in kwargs else "is not excluded!") return resp client = TestClient(requester_class=MyHttpRequester) client.save_home({"global.conf": 'core.net.http:no_proxy_match = ["MyExcludedUrl*", "*otherexcluded_one*"]\n' 'core.net.http:proxies = {"http": "value"}'}) for url in ("**otherexcluded_one***", "MyUrl", "MyExcludedUrl***", "**MyExcludedUrl***"): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): settings = "os", "compiler" def source(self): download(self, "{}", "filename.txt") """).format(url) client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") if url in ("MyUrl", "**MyExcludedUrl***"): assert "is not excluded!" in client.out else: assert "is excluded!" in client.out def test_environ_kept(self): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): settings = "os", "compiler" def source(self): download(self, "http://foo.bar/file", "filename.txt") """) class MyHttpRequester(TestRequester): def get(self, _, **kwargs): resp = Response() # resp._content = b'{"results": []}' resp.status_code = 200 resp._content = b'' assert "HTTP_PROXY" in os.environ print("My requester!") return resp client = TestClient(requester_class=MyHttpRequester) client.save({"conanfile.py": conanfile}) with environment_update({"HTTP_PROXY": "my_system_proxy"}): client.run("create . --name=foo --version=1.0") assert "My requester!" in client.out def test_environ_removed(self): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import download class Pkg(ConanFile): settings = "os", "compiler" def source(self): download(self, "http://MyExcludedUrl/file", "filename.txt") """) class MyHttpRequester(TestRequester): def get(self, _, **kwargs): resp = Response() # resp._content = b'{"results": []}' resp.status_code = 200 resp._content = b'' assert "HTTP_PROXY" not in os.environ assert "http_proxy" not in os.environ print("My requester!") return resp client = TestClient(requester_class=MyHttpRequester) client.save_home({"global.conf": 'core.net.http:clean_system_proxy = True'}) with environment_update({"http_proxy": "my_system_proxy"}): client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") assert "My requester!" in client.out ================================================ FILE: test/integration/configuration/requester_test.py ================================================ import os from unittest import mock from unittest.mock import Mock, MagicMock from conan import __version__ from conan.internal.rest.conan_requester import ConanRequester from conan.internal.model.conf import ConfDefinition from conan.test.utils.tools import temp_folder from conan.internal.util.files import save class MockRequesterGet(Mock): verify = None def get(self, _, **kwargs): self.verify = kwargs.get('verify', None) class TestConanRequesterCacertPath: def test_default_no_verify(self): mocked_requester = MockRequesterGet() with mock.patch("conan.internal.rest.conan_requester.requests", mocked_requester): requester = ConanRequester(ConfDefinition()) requester.get(url="aaa", verify=False) assert requester._http_requester.verify is False def test_default_verify(self): mocked_requester = MockRequesterGet() with mock.patch("conan.internal.rest.conan_requester.requests", mocked_requester): requester = ConanRequester(ConfDefinition()) requester.get(url="aaa", verify=True) assert requester._http_requester.verify is True def test_cache_config(self): file_path = os.path.join(temp_folder(), "whatever_cacert") save(file_path, "") config = ConfDefinition() config.update("core.net.http:cacert_path", file_path) mocked_requester = MockRequesterGet() with mock.patch("conan.internal.rest.conan_requester.requests", mocked_requester): requester = ConanRequester(config) requester.get(url="bbbb", verify=True) assert requester._http_requester.verify == file_path class TestConanRequesterHeaders: def test_user_agent(self): mock_http_requester = MagicMock() with mock.patch("conan.internal.rest.conan_requester.requests", mock_http_requester): requester = ConanRequester(ConfDefinition()) requester.get(url="aaa") headers = requester._http_requester.get.call_args[1]["headers"] assert "Conan/%s" % __version__ in headers["User-Agent"] requester.get(url="aaa", headers={"User-Agent": "MyUserAgent"}) headers = requester._http_requester.get.call_args[1]["headers"] assert "MyUserAgent" == headers["User-Agent"] ================================================ FILE: test/integration/configuration/required_version_test.py ================================================ import os from unittest import mock from conan import __version__ from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save class TestRequiredVersion: @mock.patch("conan.__version__", "1.26.0") def test_wrong_version(self): required_version = "1.23.0" client = TestClient(light=True) client.save_home({"global.conf": f"core:required_conan_version={required_version}"}) client.run("help", assert_error=True) assert ("Current Conan version (1.26.0) does not satisfy the defined " f"one ({required_version})") in client.out @mock.patch("conan.__version__", "1.22.0") def test_exact_version(self): required_version = "1.22.0" client = TestClient(light=True) client.save_home({"global.conf": f"core:required_conan_version={required_version}"}) client.run("--help") @mock.patch("conan.__version__", "2.1.0") def test_lesser_version(self): required_version = "<3.0" client = TestClient(light=True) client.save_home({"global.conf": f"core:required_conan_version={required_version}"}) client.run("--help") @mock.patch("conan.__version__", "2.20.0") def test_lesser_version_error_parse(self): required_version = "2.20" client = TestClient(light=True) client.save_home({"global.conf": f"core:required_conan_version={required_version}"}) client.run("--help") @mock.patch("conan.__version__", "1.0.0") def test_greater_version(self): required_version = ">0.1.0" client = TestClient(light=True) client.save_home({"global.conf": f"core:required_conan_version={required_version}"}) client.run("--help") @mock.patch("conan.__version__", "1.0.0") def test_greater_version_cc_override(self): client = TestClient(light=True) client.save_home({"global.conf": f"core:required_conan_version=>0.1.0"}) client.run("config home -cc core:required_conan_version=>2.0", assert_error=True) assert f"Current Conan version (1.0.0) does not satisfy the defined one (>2.0)" in client.out @mock.patch("conan.__version__", "1.0.0") def test_version_check_before_hooks(self): client = TestClient(light=True) client.save_home({"global.conf": f"core:required_conan_version=>2.0", # The hook shouldn't matter, version check happens earlier "extensions/hooks/hook_trim.py": "some error here"}) client.run("config home", assert_error=True) assert f"Current Conan version (1.0.0) does not satisfy the defined one (>2.0)" in client.out def test_bad_format(self): required_version = "1.0.0.0-foobar" cache_folder = temp_folder() save(os.path.join(cache_folder, "global.conf"), f"core:required_conan_version={required_version}") c = TestClient(cache_folder, light=True) c.run("version", assert_error=True) assert (f"Current Conan version ({__version__}) does not satisfy " f"the defined one ({required_version})") in c.out ================================================ FILE: test/integration/configuration/test_auth_remote_plugin.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient class TestAuthRemotePlugin: def test_error_auth_remote_plugin(self): """ Test when the plugin fails, we want a clear message and a helpful trace """ c = TestClient(default_server_user=True) auth_plugin = textwrap.dedent("""\ def auth_remote_plugin(remote, user=None): raise Exception("Test Error") """) c.save_home({"extensions/plugins/auth_remote.py": auth_plugin}) c.run("remote logout default") c.run("remote login default", assert_error=True) assert "Error while processing 'auth_remote.py' plugin" in c.out assert "ERROR: Error while processing 'auth_remote.py' plugin, line " in c.out @pytest.mark.parametrize("password", ["password", "bad-password"]) def test_auth_remote_plugin_direct_credentials(self, password): """ Test when the plugin give a correct and wrong password, we want a message about the success or fail in login """ should_fail = password == "bad-password" c = TestClient(default_server_user=True) auth_plugin = textwrap.dedent(f"""\ def auth_remote_plugin(remote, user=None): return "admin", "{password}" """) c.save_home({"extensions/plugins/auth_remote.py": auth_plugin}) c.run("remote logout default") c.run("remote login default", assert_error=should_fail) if should_fail: assert "ERROR: Wrong user or password. [Remote: default]" in c.out else: assert ("Changed user of remote 'default' from 'None' (anonymous) to " "'admin' (authenticated)") in c.out def test_auth_remote_plugin_fallback(self): """ Test when the plugin do not give any user or password, we want the code to continue with the rest of the input methods """ c = TestClient(default_server_user=True) auth_plugin = textwrap.dedent("""\ def auth_remote_plugin(remote, user=None): return None, None """) c.save_home({"extensions/plugins/auth_remote.py": auth_plugin}) c.run("remote logout default") c.run("remote login default") # As the auth plugin is not returning any password the code is falling back to the rest of # the input methods in this case the stdin provided by TestClient. assert ("Changed user of remote 'default' from 'None' (anonymous) to " "'admin' (authenticated)") in c.out ================================================ FILE: test/integration/configuration/test_custom_symlinked_home.py ================================================ import os import platform import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID @pytest.mark.skipif(platform.system() == "Windows", reason="Uses symlinks") def test_custom_symlinked_home(): base_cache = temp_folder() real_cache = os.path.join(base_cache, "real_cache") os.makedirs(real_cache) symlink_cache = os.path.join(base_cache, "symlink_cache") os.symlink(real_cache, symlink_cache) c = TestClient(cache_folder=symlink_cache) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") assert "symlink_cache" in c.out assert "real_cache" not in c.out c.run("cache path pkg/0.1") assert "symlink_cache" in c.out assert "real_cache" not in c.out c.run(f"cache path pkg/0.1:{NO_SETTINGS_PACKAGE_ID}") assert "symlink_cache" in c.out assert "real_cache" not in c.out ================================================ FILE: test/integration/configuration/test_profile_jinja.py ================================================ import json import platform import textwrap import os from conan import conan_version from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update def test_profile_template(): client = TestClient() tpl = textwrap.dedent("""\ # This is just to check that subprocess is injected in the render and can be used {% set check_output = subprocess.check_output %} [settings] os = {{ {"Darwin": "Macos"}.get(platform.system(), platform.system()) }} build_type = {{ os.getenv("MY_BUILD_TYPE") }} """) client.save({"conanfile.py": GenConanfile(), "profile1": tpl}) with environment_update({"MY_BUILD_TYPE": "Debug"}): client.run("install . -pr=profile1") current_os = {"Darwin": "Macos"}.get(platform.system(), platform.system()) assert "os={}".format(current_os) assert "build_type=Debug" def test_profile_template_variables(): client = TestClient() tpl = textwrap.dedent(""" {% set a = "FreeBSD" %} [settings] os = {{ a }} """) client.save({"conanfile.py": GenConanfile(), "profile1": tpl}) client.run("install . -pr=profile1") assert "os=FreeBSD" in client.out def test_profile_template_import(): client = TestClient() tpl1 = textwrap.dedent(""" {% import "profile_vars" as vars %} [settings] os = {{ vars.a }} """) tpl2 = textwrap.dedent(""" {% set a = "FreeBSD" %} """) client.save({"conanfile.py": GenConanfile(), "profile1": tpl1, "profile_vars": tpl2}) client.run("install . -pr=profile1") assert "os=FreeBSD" in client.out def test_profile_template_import_sibling(): # https://github.com/conan-io/conan/issues/17431 client = TestClient() tpl1 = textwrap.dedent(r""" {% import "sub2/profile_vars" as vars %} [settings] os = {{ vars.a }} """) tpl2 = textwrap.dedent(""" {% set a = "FreeBSD" %} """) client.save_home({"profiles/sub1/profile1": tpl1, "profiles/sub2/profile_vars": tpl2}) client.save({"conanfile.py": GenConanfile()}) client.run("install . -pr=sub1/profile1") assert "os=FreeBSD" in client.out def test_profile_template_include(): client = TestClient() tpl1 = textwrap.dedent(""" {% include "profile_vars" %} """) tpl2 = textwrap.dedent(""" {% set a = "FreeBSD" %} [settings] os = {{ a }} """) client.save({"conanfile.py": GenConanfile(), "profile1": tpl1, "profile_vars": tpl2}) client.run("install . -pr=profile1") assert "os=FreeBSD" in client.out def test_profile_template_include_sibling(): # https://github.com/conan-io/conan/issues/17431 client = TestClient() tpl1 = textwrap.dedent(r""" {% include "sub2/profile_vars" %} """) tpl2 = textwrap.dedent(""" {% set a = "FreeBSD" %} [settings] os = {{ a }} """) client.save_home({"profiles/sub1/profile1": tpl1, "profiles/sub2/profile_vars": tpl2}) client.save({"conanfile.py": GenConanfile()}) client.run("install . -pr=sub1/profile1") assert "os=FreeBSD" in client.out def test_profile_template_include_from_cache(): # https://github.com/conan-io/conan/issues/17431 client = TestClient() tpl1 = textwrap.dedent(r""" {% include "sub2/profile_vars" %} """) tpl2 = textwrap.dedent(""" {% set a = "FreeBSD" %} [settings] os = {{ a }} """) client.save_home({"profiles/sub2/profile_vars": tpl2}) client.save({"conanfile.py": GenConanfile(), "sub1/profile1": tpl1}) client.run("install . -pr=sub1/profile1") assert "os=FreeBSD" in client.out def test_profile_template_profile_dir(): client = TestClient() tpl1 = textwrap.dedent(""" [conf] user.toolchain:mydir = {{ os.path.join(profile_dir, "toolchain.cmake") }} """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class Pkg(ConanFile): def generate(self): content = load(self, self.conf.get("user.toolchain:mydir")) self.output.info("CONTENT: {}".format(content)) """) client.save({"conanfile.py": conanfile, "anysubfolder/profile1": tpl1, "anysubfolder/toolchain.cmake": "MyToolchainCMake!!!"}) client.run("install . -pr=anysubfolder/profile1") assert "conanfile.py: CONTENT: MyToolchainCMake!!!" in client.out def test_profile_conf_backslash(): # https://github.com/conan-io/conan/issues/15726 c = TestClient() profile = textwrap.dedent(r""" [conf] user.team:myconf = "hello\test" """) c.save({"profile": profile}) c.run("profile show -pr=profile") assert r"hello\test" in c.out def test_profile_version(): client = TestClient() tpl1 = textwrap.dedent(""" [options] *:myoption={{conan_version}} *:myoption2={{conan_version<13 and conan_version>1.0}} """) client.save({"conanfile.py": GenConanfile(), "profile1.jinja": tpl1}) client.run("install . -pr=profile1.jinja") assert f"*:myoption={conan_version}" in client.out assert "*:myoption2=True" in client.out def test_profile_template_profile_name(): """ The property profile_name should be parsed as the profile file name when rendering profiles """ client = TestClient() tpl1 = textwrap.dedent(""" [conf] user.profile:name = {{ profile_name }} """) tpl2 = textwrap.dedent(""" include(default) [conf] user.profile:name = {{ profile_name }} """) default = textwrap.dedent(""" [settings] os=Windows [conf] user.profile:name = {{ profile_name }} """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def configure(self): self.output.info("PROFILE NAME: {}".format(self.conf.get("user.profile:name"))) """) client.save({"conanfile.py": conanfile, "profile_folder/foobar": tpl1, "another_folder/foo.profile": tpl1, "include_folder/include_default": tpl2, os.path.join(client.paths.profiles_path, "baz"): tpl1, os.path.join(client.paths.profiles_path, "default"): default}) # show only file name as profile name client.run("install . -pr=profile_folder/foobar") assert "conanfile.py: PROFILE NAME: foobar" in client.out # profile_name should include file extension client.run("install . -pr=another_folder/foo.profile") assert "conanfile.py: PROFILE NAME: foo.profile" in client.out # default profile should compute profile_name by default too client.run("install . -pr=default") assert "conanfile.py: PROFILE NAME: default" in client.out # profile names should show only their names client.run("install . -pr=baz") assert "conanfile.py: PROFILE NAME: baz" in client.out # included profiles should respect the inherited profile name client.run("install . -pr=include_folder/include_default") assert "conanfile.py: PROFILE NAME: include_default" in client.out def test_profile_root_name(): c = TestClient() root = textwrap.dedent(""" [conf] user.profile:name = {{ profile_name }} user.profile:root_name = {{ root_profile_name }} """) tpl = textwrap.dedent(""" include(myprofile) """) c.save({"myprofile": root, "other": tpl}) c.run("profile show -pr=other") assert "user.profile:name=myprofile" in c.out assert "user.profile:root_name=other" in c.out class TestProfileDetectAPI: def test_profile_detect_os_arch(self): """ testing OS & ARCH just to test the UX and interface """ client = TestClient() tpl1 = textwrap.dedent(""" [settings] os={{detect_api.detect_os()}} arch={{detect_api.detect_arch()}} """) client.save({"profile1": tpl1}) client.run("profile show -pr=profile1 --context=host") pr = client.get_default_host_profile() the_os = pr.settings['os'] arch = pr.settings['arch'] expected = textwrap.dedent(f"""\ [settings] arch={arch} os={the_os} """) assert expected in client.out def test_profile_detect_compiler_missing_error(self): client = TestClient(light=True) tpl1 = textwrap.dedent(""" {% set compiler, version, compiler_exe = detect_api.detect_clang_compiler(compiler_exe="not-existing-compiler") %} {% set version = detect_api.default_compiler_version(compiler, version) %} """) client.save({"profile1": tpl1}) client.run("profile show -pr=profile1", assert_error=True) assert "No version provided to 'detect_api.default_compiler_version()' for None compiler" in client.out def test_profile_jinja_error(): c = TestClient(light=True) profile = textwrap.dedent(""" {% set arr = [ {# comment that triggers the error #} 'smth' ] %}""") c.save({"profile1": profile, "profile2": "include(profile1)", "profile3": "include(profile2)", "conanfile.txt": ""}) c.run("profile show -pr=profile1", assert_error=True) assert "ERROR: Error while rendering the profile template file" in c.out c.run("profile show -pr=profile2", assert_error=True) assert "ERROR: Error reading 'profile2' profile: Error while " \ "rendering the profile template file " in c.out assert "unexpected char '#' at 21" in c.out c.run("profile show -pr=profile3", assert_error=True) assert "ERROR: Error reading 'profile3' profile: Error reading 'profile2' profile: Error while " \ "rendering the profile template file " in c.out assert "unexpected char '#' at 21" in c.out # Also install messages c.run("install . -pr=profile1", assert_error=True) assert "ERROR: Error while rendering the profile template file" in c.out assert "unexpected char '#' at 21" in c.out c.run("install . -pr=profile2", assert_error=True) assert "ERROR: Error reading 'profile2' profile: Error while " \ "rendering the profile template file " in c.out assert "unexpected char '#' at 21" in c.out c.run("install . -pr=profile3", assert_error=True) assert "ERROR: Error reading 'profile3' profile: Error reading 'profile2' profile: Error while " \ "rendering the profile template file " in c.out assert "unexpected char '#' at 21" in c.out def test_profile_macro_per_package(): client = TestClient() tpl1 = textwrap.dedent(""" {% macro set_windows_settings(lib) %} {{lib}}/*:os=Windows {{lib}}/*:arch=x86 {% endmacro %} {% macro set_windows_conf(lib) %} {{lib}}/*:user.conf:key = 2 {% endmacro %} [settings] os = Linux {{set_windows_settings("mypkg")}} [conf] user.conf:key = 1 {{set_windows_conf("mypkg")}} """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "mypkg" version = "0.1" settings = "os", "arch" def generate(self): value = self.conf.get("user.conf:key") self.output.info(f"os={self.settings.os}!!") self.output.info(f"arch={self.settings.arch}!!") self.output.info(f"user.conf:key={value}!!!!") """) client.save({"conanfile.py": conanfile, "profile1": tpl1}) client.run("install . -pr=profile1") assert "conanfile.py (mypkg/0.1): user.conf:key=2!!!!" in client.out assert "conanfile.py (mypkg/0.1): os=Windows!!" in client.out assert "conanfile.py (mypkg/0.1): arch=x86!!" in client.out def test_profile_jinja_context(): """ Test the ``context=build/host`` injection """ c = TestClient() tpl1 = textwrap.dedent(""" [conf] {% if context == "host" %} user.profile:common = myhost {% elif context == "build" %} user.profile:common = mybuild {% else %} user.profile:common= nocontext [settings] os = Linux {% endif %} """) tpl2 = textwrap.dedent(""" include(common) [conf] {% if context == "host" %} user.profile:base = myhost {% elif context == "build" %} user.profile:base = mybuild {% else %} user.profile:base = nocontext [settings] arch = x86 {% endif %} """) c.save({"common": tpl1, "base": tpl2}) c.run("profile show -pr:a=base --format=json") profiles = json.loads(c.stdout) assert profiles["host"]["conf"] == {"user.profile:common": "myhost", "user.profile:base": "myhost"} assert profiles["build"]["conf"] == {"user.profile:common": "mybuild", "user.profile:base": "mybuild"} # Now lets test when profile is neither build/host, like when used in ``conan list`` c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("os", "arch")}) c.run("create . -s os=Linux -s arch=x86") c.run("create . -s os=Windows -s arch=armv8") # This will pick the Linux+x86 package binary c.run("list *:* --filter-profile=base") assert "os: Windows" not in c.out assert "arch: armv8" not in c.out assert "os: Linux" in c.out assert "arch: x86" in c.out ================================================ FILE: test/integration/configuration/test_profile_plugin.py ================================================ import os import textwrap from conan.internal.cache.home_paths import HomePaths from conan.test.utils.tools import TestClient class TestErrorsProfilePlugin: """ when the plugin fails, we want a clear message and a helpful trace """ def test_error_profile_plugin(self): c = TestClient() profile_plugin = textwrap.dedent("""\ def profile_plugin(profile): settings = profile.kk """) c.save_home({"extensions/plugins/profile.py": profile_plugin}) c.run("install --requires=zlib/1.2.3", assert_error=True) assert "Error while processing 'profile.py' plugin, line 2" in c.out assert "settings = profile.kk" in c.out def test_remove_plugin_file(self): c = TestClient() c.run("version") # to trigger the creation os.remove(HomePaths(c.cache_folder).profile_plugin_path) c.run("profile show", assert_error=True) assert "ERROR: The 'profile.py' plugin file doesn't exist" in c.out def test_regresion_29(self): # https://github.com/conan-io/conan/issues/17247 c = TestClient() c.save({"conanfile.txt": ""}) c.run("install . -s compiler=clang -s compiler.version=19 -s compiler.cppstd=26") # doesn't fail anymore c.run("install . -s compiler=apple-clang -s compiler.version=16 -s compiler.cppstd=26") # doesn't fail anymore c.run("install . -s compiler=gcc -s compiler.version=14 -s compiler.cppstd=26") # doesn't fail anymore def test_android_ndk_version(): c = TestClient() c.run("profile show -s os=Android") assert "os.ndk_version" not in c.out c.run("profile show -s os=Android -s os.ndk_version=r26") assert "os.ndk_version=r26" in c.out c.run("profile show -s os=Android -s os.ndk_version=r26a") assert "os.ndk_version=r26a" in c.out ================================================ FILE: test/integration/configuration/test_profile_priority.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient from conan.internal.util.files import save def test_profile_local_folder_priority_cache(): """ includes or args without "./" will resolve to the cache first """ c = TestClient() c.save({"profiles/default": f"include(otherprofile)", "profiles/otherprofile": "[settings]\nos=AIX", "conanfile.txt": ""}) save(os.path.join(c.paths.profiles_path, "otherprofile"), "[settings]\nos=FreeBSD") # Must use local path, otherwise look for it in the cache c.run("install . -pr=./profiles/default") assert "os=FreeBSD" in c.out def test_profile_local_folder_priority_relative(): """ The local include(./profile) must have priority over a file with same name in cache """ c = TestClient() c.save({"profiles/default": f"include(./otherprofile)", "profiles/otherprofile": "[settings]\nos=AIX", "conanfile.txt": ""}) save(os.path.join(c.paths.profiles_path, "otherprofile"), "[settings]\nos=FreeBSD") # Must use local path, otherwise look for it in the cache c.run("install . -pr=./profiles/default") assert "os=AIX" in c.out def test_profile_cache_folder_priority(): """ The cache include(./profile) must have priority over a file with same name in local """ c = TestClient() c.save({"otherprofile": "[settings]\nos=FreeBSD", "conanfile.txt": ""}) save(os.path.join(c.paths.profiles_path, "default"), "include(./otherprofile)") save(os.path.join(c.paths.profiles_path, "otherprofile"), "[settings]\nos=AIX") c.run("install . -pr=default") assert "os=AIX" in c.out def test_profile_cli_priority(): c = TestClient() profile1 = textwrap.dedent("""\ [settings] os=AIX [conf] user.myconf:myvalue1=1 user.myconf:myvalue2=[2] user.myconf:myvalue3={"3": "4", "a": "b"} user.myconf:myvalue4={"1": "2"} user.myconf:myvalue5={"6": "7"} """) profile2 = textwrap.dedent("""\ [settings] os=FreeBSD [conf] user.myconf:myvalue1=2 user.myconf:myvalue2+=4 user.myconf:myvalue3*={"3": "5"} user.myconf:myvalue5={"6": "7"} """) c.save({"profile1": profile1, "profile2": profile2}) c.run("profile show -pr=./profile1 -pr=./profile2") assert "os=FreeBSD" in c.out assert "user.myconf:myvalue1=2" in c.out assert "user.myconf:myvalue2=[2, 4]" in c.out assert "user.myconf:myvalue3={'3': '5', 'a': 'b'}" in c.out assert "user.myconf:myvalue4={'1': '2'}" in c.out assert "user.myconf:myvalue5={'6': '7'}" in c.out def test_profiles_patterns_include(): # https://github.com/conan-io/conan/issues/16718 c = TestClient() msvc = textwrap.dedent(""" [settings] compiler=msvc compiler.cppstd=14 compiler.version=193 os=Windows test*/*:compiler.cppstd=14 """) clang = textwrap.dedent(""" include(./msvc) [settings] test*/*:compiler=clang test*/*:compiler.cppstd=17 test*/*:compiler.runtime_version=v144 test*/*:compiler.version=18 """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "test_pkg" version = "0.1" settings = "os", "compiler" def generate(self): self.output.info(f"MyCompiler={self.settings.compiler}!!!") self.output.info(f"MyCompilerVersion={self.settings.compiler.version}!!!") self.output.info(f"MyCompilerCpp={self.settings.compiler.cppstd}!!!") """) c.save({"conanfile.py": conanfile, "msvc": msvc, "clang": clang}) c.run("install . -pr=clang") assert "conanfile.py (test_pkg/0.1): MyCompiler=clang!!!" in c.out assert "conanfile.py (test_pkg/0.1): MyCompilerVersion=18!!!" in c.out assert "conanfile.py (test_pkg/0.1): MyCompilerCpp=17!!!" in c.out ================================================ FILE: test/integration/cps/__init__.py ================================================ ================================================ FILE: test/integration/cps/test_cps.py ================================================ import json import os import textwrap import pytest from conan.cps import CPS from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save_files def test_cps(): c = TestClient() c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type") .with_class_attribute("license='MIT'")}) c.run("create pkg") settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64" c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps") pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps")) assert pkg["name"] == "pkg" assert pkg["version"] == "0.1" assert pkg["license"] == "MIT" assert pkg["configurations"] == ["release"] assert pkg["default_components"] == ["pkg"] pkg_comp = pkg["components"]["pkg"] assert pkg_comp["type"] == "interface" mapping = json.loads(c.load("build/cps/cpsmap-msvc-191-x86_64-release.json")) for _, path_cps in mapping.items(): assert os.path.exists(path_cps) def test_cps_static_lib(): c = TestClient() c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_package_file("lib/pkg.a", "-") .with_settings("build_type") .with_package_info(cpp_info={"libs": ["pkg"]})}) c.run("create pkg") settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64" c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps") pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps")) assert pkg["name"] == "pkg" assert pkg["version"] == "0.1" assert pkg["configurations"] == ["release"] assert pkg["default_components"] == ["pkg"] pkg_comp = pkg["components"]["pkg"] assert pkg_comp["type"] == "archive" assert pkg_comp["location"] is not None def test_cps_header(): c = TestClient() c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_package_type("header-library")}) c.run("create pkg") settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64" c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps") pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps")) assert pkg["name"] == "pkg" assert pkg["version"] == "0.1" assert "configurations" not in "pkg" assert pkg["default_components"] == ["pkg"] pkg_comp = pkg["components"]["pkg"] assert pkg_comp["type"] == "interface" assert "location" not in pkg_comp def test_cps_in_pkg(): c = TestClient() cps = textwrap.dedent("""\ { "cps_version": "0.12.0", "name": "zlib", "version": "1.3.1", "configurations": ["release"], "default_components": ["zlib"], "components": { "zlib": { "type": "archive", "includes": ["@prefix@/include"], "location": "@prefix@/lib/zlib.a" } } } """) cps = "".join(cps.splitlines()) conanfile = textwrap.dedent(f""" import os from conan.tools.files import save from conan import ConanFile class Pkg(ConanFile): name = "zlib" version = "1.3.1" def package(self): cps = '{cps}' cps_path = os.path.join(self.package_folder, "zlib.cps") save(self, cps_path, cps) def package_info(self): from conan.cps import CPS self.cpp_info = CPS.load("zlib.cps").to_conan() """) c.save({"pkg/conanfile.py": conanfile}) c.run("create pkg") settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64" c.run(f"install --requires=zlib/1.3.1 {settings} -g CPSDeps") mapping = json.loads(c.load("build/cps/cpsmap-msvc-191-x86_64-release.json")) for _, path_cps in mapping.items(): assert os.path.exists(path_cps) assert not os.path.exists(os.path.join(c.current_folder, "zlib.cps")) assert not os.path.exists(os.path.join(c.current_folder, "build", "cps", "zlib.cps")) c.run(f"install --requires=zlib/1.3.1 {settings} -g CMakeDeps") cmake = c.load("zlib-release-x86_64-data.cmake") assert 'set(zlib_INCLUDE_DIRS_RELEASE "${zlib_PACKAGE_FOLDER_RELEASE}/include")' in cmake assert 'set(zlib_LIB_DIRS_RELEASE "${zlib_PACKAGE_FOLDER_RELEASE}/lib")' assert 'set(zlib_LIBS_RELEASE zlib)' in cmake def test_cps_shared_in_pkg(): c = TestClient() cps = textwrap.dedent("""\ { "components" : { "mypkg" : { "includes" : [ "@prefix@/include" ], "type" : "dylib" } }, "cps_path" : "@prefix@/cps", "cps_version" : "0.13.0", "name" : "mypkg" } """) cps_release = textwrap.dedent("""\ { "components" : { "mypkg" : { "link_location" : "@prefix@/lib/mypkg.lib", "location" : "@prefix@/bin/mypkg.dll" } }, "configuration" : "Release", "name" : "mypkg" } """) cps = "".join(cps.splitlines()) cps_release = "".join(cps_release.splitlines()) conanfile = textwrap.dedent(f""" import os, json from conan.tools.files import save from conan import ConanFile class Pkg(ConanFile): name = "mypkg" version = "1.0" def package(self): cps = '{cps}' cps_path = os.path.join(self.package_folder, "mypkg.cps") save(self, cps_path, cps) cps = '{cps_release}' cps_path = os.path.join(self.package_folder, "mypkg@release.cps") save(self, cps_path, cps) def package_info(self): from conan.cps import CPS self.cpp_info = CPS.load("mypkg.cps").to_conan() """) c.save({"pkg/conanfile.py": conanfile}) c.run("create pkg") settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64" c.run(f"install --requires=mypkg/1.0 {settings} -g CMakeDeps") cmake = c.load("mypkg-release-x86_64-data.cmake") assert 'set(mypkg_INCLUDE_DIRS_RELEASE "${mypkg_PACKAGE_FOLDER_RELEASE}/include")' in cmake assert 'set(mypkg_LIB_DIRS_RELEASE "${mypkg_PACKAGE_FOLDER_RELEASE}/lib")' assert 'set(mypkg_LIBS_RELEASE mypkg)' in cmake def test_cps_merge(): folder = temp_folder() cps_base = textwrap.dedent("""{ "components": { "mypkg":{ "includes": ["@prefix@/include"], "type": "archive", "definitions": { "*": {"MY_DEFINE": null }} } }, "cps_path": "@prefix@/cps", "cps_version": "0.13.0", "name": "mypkg" } """) cps_conf = textwrap.dedent("""{ "components" : { "mypkg" : { "link_languages" : [ "cpp" ], "location" : "@prefix@/lib/mypkg.lib" } }, "configuration" : "Release", "name" : "mypkg" } """) save_files(folder, {"mypkg.cps": cps_base, "mypkg@release.cps": cps_conf}) cps = CPS.load(os.path.join(folder, "mypkg.cps")) json_cps = cps.serialize() mypkg = json_cps["components"]["mypkg"] assert mypkg["includes"] == ["@prefix@/include"] assert mypkg["location"] == "@prefix@/lib/mypkg.lib" assert mypkg["type"] == "archive" assert mypkg["link_languages"] == ["cpp"] assert mypkg["definitions"] == {'*': {'MY_DEFINE': None}} def test_extended_cpp_info(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" languages = ["C++"] def package_info(self): self.cpp_info.libs = ["mylib"] self.cpp_info.location = "my_custom_location" self.cpp_info.type = "static-library" self.cpp_info.defines = ["MY_DEFINE", "MY_OTHER_DEFINE=1"] """) c.save({"conanfile.py": conanfile}) c.run("create .") settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64" c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps") pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps")) assert pkg["name"] == "pkg" assert pkg["version"] == "0.1" assert pkg["default_components"] == ["pkg"] pkg_comp = pkg["components"]["pkg"] assert pkg_comp["type"] == "archive" assert pkg_comp["location"] == "my_custom_location" assert pkg_comp["link_languages"] == ["cpp"] assert pkg_comp["definitions"] == {'cpp': {'MY_DEFINE': None, 'MY_OTHER_DEFINE': '1'}} @pytest.mark.parametrize("as_comp", [True, False]) def test_cps_component_single(as_comp): c = TestClient() cpp_info_comp = '.components["core"]' if as_comp else "" conanfile = textwrap.dedent(f"""\ from conan import ConanFile from conan.tools.files import save import os class mypkgRecipe(ConanFile): name = "mypkg" version = "0.1" requires = "dep/0.1" def package(self): save(self, os.path.join(self.package_folder, "lib", "libcore.a"), "") def package_info(self): self.cpp_info{cpp_info_comp}.libs = ["core"] self.cpp_info{cpp_info_comp}.requires = ["dep::comp1"] from conan.cps import CPS cps = CPS.from_conan(self) self.cpp_info = cps.to_conan() """) c.save({"conanfile.py": conanfile, "dep/conanfile.py": GenConanfile("dep", "0.1") .with_package_file("lib/comp1.a", "-") .with_package_file("lib/comp2.a", "-") .with_package_info(cpp_info={"components": {"comp1": {"libs": ["comp1"]}, "comp2": {"libs": ["comp2"]}}})}) c.run("create dep") c.run("create") c.run(f"install --requires=mypkg/0.1 -g CMakeConfigDeps") mypkg_targets = c.load("mypkg-Targets-release.cmake") if as_comp: assert "add_library(mypkg::core" in mypkg_targets assert "# Requirement mypkg::core -> dep::comp1" in mypkg_targets else: assert "add_library(mypkg::core" not in mypkg_targets assert "add_library(mypkg::mypkg" in mypkg_targets assert "# Requirement mypkg::mypkg -> dep::comp1" in mypkg_targets ================================================ FILE: test/integration/cross_building/__init__.py ================================================ ================================================ FILE: test/integration/cross_building/build_requires_from_profile_test.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient, GenConanfile class TestBuildRequiresFromProfile: profile_host = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=192 compiler.runtime=dynamic [tool_requires] br2/version """) profile_build = textwrap.dedent(""" [settings] os=Macos arch=x86_64 compiler=apple-clang compiler.version=11.0 compiler.libcxx=libc++ build_type=Release [tool_requires] br3/version """) library_conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): name = "library" version = "version" build_requires = "br1/version" """) def test_br_from_profile_host_and_profile_build(self): t = TestClient() t.save({'profile_host': self.profile_host, 'profile_build': self.profile_build, 'library.py': self.library_conanfile, 'br1.py': GenConanfile(), 'br2.py': GenConanfile(), 'br3.py': GenConanfile()}) t.run("export br1.py --name=br1 --version=version") t.run("export br2.py --name=br2 --version=version") t.run("export br3.py --name=br3 --version=version") t.run("create library.py --profile:host=profile_host --profile:build=profile_build " "--build='*'") class TestBuildRequiresContextHostFromProfile: toolchain = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): name = "mytoolchain" version = "1.0" settings = "os" def package_info(self): self.output.info("PackageInfo OS=%s" % self.settings.os) """) gtest = textwrap.dedent(""" from conan import ConanFile import os class Recipe(ConanFile): name = "gtest" version = "1.0" settings = "os" def build(self): self.output.info("Build OS=%s" % self.settings.os) def package_info(self): self.output.info("PackageInfo OS=%s" % self.settings.os) """) library_conanfile = textwrap.dedent(""" from conan import ConanFile import os class Recipe(ConanFile): name = "library" version = "version" settings = "os" def build_requirements(self): self.test_requires("gtest/1.0") def build(self): self.output.info("Build OS=%s" % self.settings.os) """) profile_host = textwrap.dedent(""" [settings] os = Linux [tool_requires] mytoolchain/1.0 """) profile_build = textwrap.dedent(""" [settings] os = Windows """) def test_br_from_profile_host_and_profile_build(self): t = TestClient() t.save({'profile_host': self.profile_host, 'profile_build': self.profile_build, 'library.py': self.library_conanfile, 'mytoolchain.py': self.toolchain, "gtest.py": self.gtest}) t.run("create mytoolchain.py -pr:h=profile_host -pr:b=profile_build --build-require") t.run("create gtest.py -pr=profile_host -pr:b=profile_build") assert "mytoolchain/1.0: PackageInfo OS=Windows" in t.out assert "gtest/1.0: PackageInfo OS=Linux" in t.out t.run("create gtest.py -pr=profile_host -pr:b=profile_build --build=*") assert "mytoolchain/1.0: PackageInfo OS=Windows" in t.out assert "gtest/1.0: Build OS=Linux" in t.out assert "gtest/1.0: PackageInfo OS=Linux" in t.out t.run("create library.py -pr:h=profile_host -pr:b=profile_build") assert "gtest/1.0: PackageInfo OS=Linux" in t.out assert "library/version: Build OS=Linux" in t.out assert "mytoolchain/1.0: PackageInfo OS=Windows" in t.out t.run("create library.py -pr:h=profile_host -pr:b=profile_build --build=*") assert "gtest/1.0: Build OS=Linux" in t.out assert "gtest/1.0: PackageInfo OS=Linux" in t.out assert "library/version: Build OS=Linux" in t.out assert "mytoolchain/1.0: PackageInfo OS=Windows" in t.out class TestBuildRequiresBothContexts: toolchain_creator = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): name = "creator" version = "1.0" settings = "os" def package_info(self): self.output.info("PackageInfo OS=%s" % self.settings.os) """) toolchain = textwrap.dedent(""" from conan import ConanFile import os class Recipe(ConanFile): name = "mytoolchain" version = "1.0" settings = "os" def build(self): self.output.info("Build OS=%s" % self.settings.os) def package_info(self): self.output.info("PackageInfo OS=%s" % self.settings.os) """) gtest = textwrap.dedent(""" from conan import ConanFile import os class Recipe(ConanFile): name = "gtest" version = "1.0" settings = "os" def build(self): self.output.info("Build OS=%s" % self.settings.os) def package_info(self): self.output.info("PackageInfo OS=%s" % self.settings.os) """) library_conanfile = textwrap.dedent(""" from conan import ConanFile import os class Recipe(ConanFile): name = "library" version = "version" settings = "os" def build_requirements(self): self.test_requires("gtest/1.0") def build(self): self.output.info("Build OS=%s" % self.settings.os) """) profile_host = textwrap.dedent(""" [settings] os = Linux [tool_requires] mytoolchain/1.0 """) profile_build = textwrap.dedent(""" [settings] os = Windows [tool_requires] mytoolchain*:creator/1.0 """) def test_build_requires_both_contexts(self): t = TestClient() t.save({'profile_host': self.profile_host, 'profile_build': self.profile_build, 'library.py': self.library_conanfile, 'creator.py': self.toolchain_creator, 'mytoolchain.py': self.toolchain, "gtest.py": self.gtest}) t.run("create creator.py -pr=profile_build") t.run("create mytoolchain.py -pr:h=profile_host -pr:b=profile_build --build-require") assert "creator/1.0: PackageInfo OS=Windows" in t.out assert "mytoolchain/1.0: Build OS=Windows" in t.out # new way, the toolchain can now run in Windows, but gtest in Linux t.run("create gtest.py --profile=profile_host --profile:build=profile_build") assert "creator/1.0: PackageInfo" not in t.out # Creator is skipped now, not needed assert "gtest/1.0: PackageInfo OS=Linux" in t.out t.run("create gtest.py --profile=profile_host --profile:build=profile_build --build=*") assert "creator/1.0: PackageInfo OS=Windows" in t.out assert "gtest/1.0: PackageInfo OS=Linux" in t.out # Declaring the build_requires in the recipe works, it is just the profile that is # not transitive toolchain = textwrap.dedent(""" from conan import ConanFile import os class Recipe(ConanFile): name = "mytoolchain" version = "1.0" settings = "os" build_requires = "creator/1.0" def build(self): self.output.info("Build OS=%s" % self.settings.os) def package_info(self): self.output.info("PackageInfo OS=%s" % self.settings.os) """) t.save({'mytoolchain.py': toolchain}) t.run("create mytoolchain.py --profile:host=profile_build -pr:b=profile_build") assert "creator/1.0: PackageInfo OS=Windows" in t.out assert "mytoolchain/1.0: Build OS=Windows" in t.out t.run("create gtest.py --profile=profile_host --profile:build=profile_build --build=*") assert "creator/1.0: PackageInfo OS=Windows" in t.out assert "mytoolchain/1.0: Build OS=Windows" in t.out assert "gtest/1.0: Build OS=Linux" in t.out t.run("create library.py -pr:h=profile_host --profile:build=profile_build --build=*") assert "creator/1.0: PackageInfo OS=Windows" in t.out assert "mytoolchain/1.0: Build OS=Windows" in t.out assert "gtest/1.0: Build OS=Linux" in t.out assert "gtest/1.0: PackageInfo OS=Linux" in t.out assert "library/version: Build OS=Linux" in t.out def test_consumer_get_profile_tool_requires(): # [tool_requires] defined in a profile will result in applying them in the # consumer, both if using a conanfile.txt of using a --requires t = TestClient(light=True) tool = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "tool" version = "1.0" def package_info(self): self.buildenv_info.define("MYVAR", "MYVALUE") """) t.save({'profile': "[tool_requires]\ntool/1.0", 'dep.py': GenConanfile("dep", "1.0"), 'tool.py': tool, 'conanfile.txt': ""}) t.run("create tool.py") t.run("create dep.py") t.run("install . --profile ./profile") env = t.load("conanbuildenv.sh") assert 'export MYVAR="MYVALUE"' in env os.remove(os.path.join(t.current_folder, "conanbuildenv.sh")) # also when using --requires t.run("install --requires=dep/1.0 --profile ./profile") env = t.load("conanbuildenv.sh") assert 'export MYVAR="MYVALUE"' in env ================================================ FILE: test/integration/cross_building/test_cross_build_options.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_cross_build_options(): # https://github.com/conan-io/conan/issues/8443 c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" options = {"fPIC": [True, False]} default_options = {"fPIC": True} settings = "os" def config_options(self): if self.settings.os == "Windows": del self.options.fPIC """) consumer = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): requires = "dep/0.1" tool_requires = "dep/0.1" """) c.save({"dep/conanfile.py": dep, "consumer/conanfile.py": consumer}) c.run("create dep -s os=Android -s os.api_level=22") c.run("create dep -s os=Windows") c.run("install consumer -s:b os=Windows -s:h os=Android -s:h os.api_level=22") # The above command used to crash, because options not there, so it works now without problems assert "Finalizing install" in c.out c.assert_listed_binary({"dep/0.1": ("4333c2a38a8baf674c4e77fe475c146a4685f77a", "Cache")}) c.assert_listed_binary({"dep/0.1": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Cache")}, build=True) ================================================ FILE: test/integration/cross_building/test_package_test.py ================================================ import textwrap from jinja2 import Template from conan.test.utils.tools import TestClient from conan.internal.util.files import save class TestTestPackage: conanfile_tpl = Template(textwrap.dedent(""" from conan import ConanFile import os from conan.tools.env import VirtualBuildEnv class Recipe(ConanFile): settings = "os" {{ build_requires|default("") }} {% if test %} def requirements(self): self.requires(self.tested_reference_str) {% endif %} {% raw %} def build(self): self.output.info(">> settings.os: {}".format(self.settings.os)) self.output.info(">> settings_build.os: {}".format(self.settings_build.os)) build_env = VirtualBuildEnv(self).vars() with build_env.apply(): self.output.info(">> tools.get_env('INFO'): {}".format(os.getenv("INFO"))) def package_info(self): self.buildenv_info.define("INFO", "{}-{}".format(self.name, self.settings.os)) def test(self): pass {% endraw %} """)) conanfile_br = conanfile_tpl.render() conanfile = conanfile_tpl.render(build_requires='build_requires = "br1/version"') conanfile_test = conanfile_tpl.render(build_requires='build_requires = "br2/version"', test=True) settings_yml = textwrap.dedent(""" os: Host: Build: """) def test_command(self): t = TestClient() save(t.paths.settings_path, self.settings_yml) t.save({'br.py': self.conanfile_br, 'conanfile.py': self.conanfile, 'test_package/conanfile.py': self.conanfile_test, 'profile_host': '[settings]\nos=Host', 'profile_build': '[settings]\nos=Build', }) t.run("export br.py --name=br1 --version=version") # It is necessary to build first the test_package build_requires br2 t.run("create br.py --name=br2 --version=version -tf=\"\" --build-require " "--profile:host=profile_host --profile:build=profile_build") t.run("create conanfile.py --name=name --version=version --build=missing" " --profile:host=profile_host --profile:build=profile_build") # Build requires are built in the 'build' context: assert "br1/version: >> settings.os: Build" in t.out assert "br1/version: >> settings_build.os: Build" in t.out # Package 'name' is built for the 'host' context (br1 as build_requirement) assert "name/version: >> settings.os: Host" in t.out assert "name/version: >> settings_build.os: Build" in t.out # Test_package is executed with the same profiles as the package itself assert "name/version (test package): >> settings.os: Host" in t.out assert "name/version (test package): >> settings_build.os: Build" in t.out t.run("test test_package/conanfile.py name/version@ " "--profile:host=profile_host --profile:build=profile_build") assert "name/version (test package): >> settings.os: Host" in t.out assert "name/version (test package): >> settings_build.os: Build" in t.out assert "name/version (test package): >> tools.get_env('INFO'): br2-Build" in t.out ================================================ FILE: test/integration/editable/__init__.py ================================================ ================================================ FILE: test/integration/editable/editable_add_test.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestEditablePackageTest: def test_install_ok(self): ref = "--name=lib --version=version --user=user --channel=name" t = TestClient() t.save({'conanfile.py': GenConanfile()}) t.run('editable add {}'.format(ref)) assert "Reference 'lib/version@user/name' in editable mode" in t.out def test_editable_list_search(self): ref = "--name=lib --version=version --user=user --channel=name" t = TestClient() t.save({'conanfile.py': GenConanfile()}) t.run('editable add . {}'.format(ref)) t.run("editable list") assert "lib/version@user/name" in t.out assert " Path:" in t.out def test_missing_subarguments(self): t = TestClient() t.run("editable", assert_error=True) assert "ERROR: Exiting with code: 2" in t.out def test_conanfile_name(self): t = TestClient() t.save({'othername.py': GenConanfile("lib", "version")}) t.run('editable add ./othername.py --user=user --channel=name') assert "Reference 'lib/version@user/name' in editable mode" in t.out t.run('install --requires=lib/version@user/name') t.assert_listed_require({"lib/version@user/name": "Editable"}) def test_pyrequires_remote(self): t = TestClient(default_server_user=True) t.save({"conanfile.py": GenConanfile("pyreq", "1.0")}) t.run("create .") t.run("upload pyreq/1.0 -c -r=default") t.run("remove pyreq/1.0 -c") t.save({"conanfile.py": GenConanfile("pkg", "1.0").with_python_requires("pyreq/1.0")}) t.run("editable add . -nr", assert_error=True) assert "Cannot resolve python_requires 'pyreq/1.0': No remote defined" in t.out t.run("editable add .") assert "Reference 'pkg/1.0' in editable mode" in t.out def test_editable_no_name_version_test_package(): tc = TestClient() tc.save({"conanfile.py": GenConanfile(), "test_package/conanfile.py": GenConanfile("test_package") .with_test("self.output.info('Testing the package')")}) tc.run("editable add . --name=foo", assert_error=True) assert "ERROR: Editable package recipe should declare its name and version" in tc.out tc.run("editable add . --version=1.0", assert_error=True) assert "ERROR: Editable package recipe should declare its name and version" in tc.out tc.run("editable add .", assert_error=True) assert "ERROR: Editable package recipe should declare its name and version" in tc.out ================================================ FILE: test/integration/editable/editable_remove_test.py ================================================ import os import shutil import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestRemoveEditablePackageTest: @pytest.fixture() def client(self): t = TestClient() t.save({'conanfile.py': GenConanfile()}) t.run('editable add . --name=lib --version=version --user=user --channel=name') t.run("editable list") assert "lib" in t.out return t def test_unlink(self, client): client.run('editable remove -r=lib/version@user/name') assert "Removed editable 'lib/version@user/name':" in client.out client.run("editable list") assert "lib" not in client.out def test_unlink_pattern(self, client): client.run('editable remove -r=*') assert "Removed editable 'lib/version@user/name':" in client.out client.run("editable list") assert "lib" not in client.out def test_remove_path(self, client): client.run("editable remove") assert "Removed editable 'lib/version@user/name':" in client.out client.run("editable list") assert "lib" not in client.out def test_unlink_not_linked(self, client): client.run('editable remove -r=otherlib/version@user/name') assert "WARN: No editables were removed" in client.out client.run("editable list") assert "lib" in client.out def test_removed_folder(self,): # https://github.com/conan-io/conan/issues/15038 c = TestClient() c.save({'pkg/conanfile.py': GenConanfile()}) c.run('editable add pkg --name=lib --version=version') shutil.rmtree(os.path.join(c.current_folder, "pkg")) # https://github.com/conan-io/conan/issues/16164 # Making it possible, repeated issue c.run("editable remove pkg") assert "Removed editable 'lib/version'" in c.out c.run("editable list") assert "lib" not in c.out ================================================ FILE: test/integration/editable/forbidden_commands_test.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestOtherCommands: def test_commands_not_blocked(self): """ there is no reason to really block commands and operations over editable packages except for others doing an install that depends on the editable """ t = TestClient(default_server_user=True) t.save({'conanfile.py': GenConanfile("lib", "0.1"), "test_package/conanfile.py": GenConanfile().with_test("pass")}) t.run('editable add .') # Nothing in the cache t.run("list *") assert "There are no matching recipe references" in t.out t.run('list lib/0.1:*') assert "ERROR: Recipe 'lib/0.1' not found" in t.out t.run('export . ') assert "lib/0.1: Exported" in t.out t.run("list *") assert "lib/0.1" in t.out t.run('list lib/0.1:*') assert "PID:" not in t.out # One binary is listed t.run('export-pkg .') assert "lib/0.1: Exporting package" in t.out t.run('list lib/0.1:*') assert "lib/0.1" in t.out # One binary is listed t.run('upload lib/0.1 -r default') assert "Uploading recipe 'lib/0.1" in t.out t.run("remove * -c") # Nothing in the cache t.run("list *") assert "There are no matching recipe references" in t.out t.run('list lib/0.1:*') assert "ERROR: Recipe 'lib/0.1' not found" in t.out def test_create_editable(self): """ test that an editable can be built with conan create """ t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "lib" version = "0.1" def build(self): self.output.info("MYBUILDFOLDER: {}".format(self.build_folder)) """) t.save({'conanfile.py': conanfile, "test_package/conanfile.py": GenConanfile().with_test("pass"), "consumer/conanfile.txt": "[requires]\nlib/0.1"}) t.run('editable add .') t.run("list *") assert "There are no matching" in t.out t.run("create .") package_id = "da39a3ee5e6b4b0d3255bfef95601890afd80709" t.assert_listed_require({"lib/0.1": "Editable"}) t.assert_listed_binary({"lib/0.1": (package_id, "EditableBuild")}) assert f"lib/0.1: MYBUILDFOLDER: {t.current_folder}" in t.out t.run("list *") assert "lib/0.1" in t.out # Because the create actually exports, TODO: avoid exporting? t.run("install consumer --build=*") t.assert_listed_require({"lib/0.1": "Editable"}) t.assert_listed_binary({"lib/0.1": (package_id, "EditableBuild")}) assert f"lib/0.1: MYBUILDFOLDER: {t.current_folder}" in t.out t.run("install consumer --build=editable") t.assert_listed_require({"lib/0.1": "Editable"}) t.assert_listed_binary({"lib/0.1": (package_id, "EditableBuild")}) assert f"lib/0.1: MYBUILDFOLDER: {t.current_folder}" in t.out ================================================ FILE: test/integration/editable/test_editable_envvars.py ================================================ import os import re import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_editable_envvars(): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" def layout(self): self.folders.source = "mysource" self.folders.build = "mybuild" self.layouts.source.runenv_info.append_path("MYRUNPATH", "mylocalsrc") self.layouts.build.buildenv_info.define_path("MYBUILDPATH", "mylocalbuild") self.layouts.package.buildenv_info.define_path("MYBUILDPATH", "mypkgbuild") self.layouts.package.runenv_info.append_path("MYRUNTPATH", "mypkgsrc") def package_info(self): self.buildenv_info.define("OTHERVAR", "randomvalue") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile().with_settings("os").with_requires("dep/1.0")}) c.run("editable add dep --name=dep --version=1.0") c.run("install pkg -s os=Linux -s:b os=Linux") build_path = os.path.join(c.current_folder, "dep", "mybuild", "mylocalbuild") buildenv = c.load("pkg/conanbuildenv.sh") assert f'export MYBUILDPATH="{build_path}"' in buildenv runenv = c.load("pkg/conanrunenv.sh") # The package_info() buildenv is aggregated, no prob # But don't use same vars assert 'export OTHERVAR="randomvalue"' in buildenv run_path = os.path.join(c.current_folder, "dep", "mysource", "mylocalsrc") assert f'export MYRUNPATH="${{MYRUNPATH:-}}${{MYRUNPATH:+:}}{run_path}"' in runenv c.run("editable remove dep") c.run("create dep") c.run("install pkg -s os=Linux -s:b os=Linux") buildenv = c.load("pkg/conanbuildenv.sh") assert 'mypkgbuild' in buildenv assert "mylocalbuild" not in buildenv assert 'export OTHERVAR="randomvalue"' in buildenv runenv = c.load("pkg/conanrunenv.sh") assert 'mypkgsrc' in runenv assert "mylocalsrc" not in runenv def test_editable_envvars_package_info(): """ if the ``layout()`` defines cpp.build.runenv_info/conf_info and the ``package_info()`` also defines them, the ``package_info()`` values have precedence. This is not symmetrical with ``cpp_info.includedirs/xxxdirs``. The recommended solution is to use ``self.layouts.package.runenvinfo/conf_info`` instead. """ c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" def layout(self): self.layouts.build.runenv_info.define("SOME_PATH", "mypath_layout") self.cpp.build.includedirs = ["mylayoutinclude"] self.layouts.build.conf_info.define("user:myconf", "mylayoutconf") def package_info(self): print("Running package_info!!") self.runenv_info.define("SOME_PATH", "mypath_pkginfo") self.cpp_info.includedirs = ["mypkginfoinclude"] self.conf_info.define("user:myconf", "mypkginfoconf") """) pkg = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "pkg" version = "1.0" settings = "os", "build_type" requires = "dep/1.0" def generate(self): myconf = self.dependencies["dep"].conf_info.get("user:myconf") self.output.info(f"DEP CONFINFO {myconf}") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg}) c.run("editable add dep ") c.run("install pkg -s os=Linux -s:b os=Linux -g CMakeDeps") assert "conanfile.py (pkg/1.0): DEP CONFINFO mylayoutconf" in c.out cmake = c.load("pkg/dep-release-data.cmake") assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/mylayoutinclude")' in cmake runenv = c.load("pkg/conanrunenv-release.sh") assert f'export SOME_PATH="mypath_layout"' in runenv def test_editable_envvars_package(): """ This test shows that ``self.layouts.package.runenvinfo/conf_info`` works, ignored while in editable, but used when regular package """ c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" def layout(self): self.layouts.build.runenv_info.define("SOME_VALUE", "mypath_layout") self.cpp.build.includedirs = ["mylayoutinclude"] self.layouts.build.conf_info.define("user:myconf", "mylayoutconf") self.layouts.package.runenv_info.define("SOME_VALUE", "mypath_pkginfo") self.cpp.package.includedirs = ["mypkginfoinclude"] self.layouts.package.conf_info.define("user:myconf", "mypkginfoconf") """) pkg = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "pkg" version = "1.0" settings = "os", "build_type" requires = "dep/1.0" def generate(self): myconf = self.dependencies["dep"].conf_info.get("user:myconf") self.output.info(f"DEP CONFINFO {myconf}") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg}) c.run("editable add dep ") c.run("install pkg -s os=Linux -s:b os=Linux -g CMakeDeps") assert "conanfile.py (pkg/1.0): DEP CONFINFO mylayoutconf" in c.out cmake = c.load("pkg/dep-release-data.cmake") assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/mylayoutinclude")' in cmake runenv = c.load("pkg/conanrunenv-release.sh") assert f'export SOME_VALUE="mypath_layout"' in runenv c.run("editable remove dep") c.run("create dep") c.run("install pkg -s os=Linux -s:b os=Linux -g CMakeDeps") assert "conanfile.py (pkg/1.0): DEP CONFINFO mypkginfoconf" in c.out cmake = c.load("pkg/dep-release-data.cmake") assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/mypkginfoinclude")' in cmake runenv = c.load("pkg/conanrunenv-release.sh") assert f'export SOME_VALUE="mypath_pkginfo"' in runenv def test_editable_conf(): c = TestClient() # TODO: Define if we want conf.xxxx_path(), instead of (..., path=True) methods dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): def layout(self): self.folders.source = "mysource" self.folders.build = "mybuild" self.layouts.source.conf_info.append_path("user:myconf", "mylocalsrc") self.layouts.build.conf_info.append_path("user:myconf", "mylocalbuild") self.layouts.build.conf_info.update_path("user:mydictconf", {"a": "mypatha", "b": "mypathb"}) self.layouts.build.conf_info.define_path("user:mydictconf2", {"c": "mypathc"}) """) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/1.0" def generate(self): conf = self.dependencies["dep"].conf_info.get("user:myconf") self.output.info(f"CONF: {conf}") dictconf = self.dependencies["dep"].conf_info.get("user:mydictconf", check_type=dict) self.output.info(f"CONFDICT: {dictconf}") dictconf2 = self.dependencies["dep"].conf_info.get("user:mydictconf2", check_type=dict) self.output.info(f"CONFDICT: {dictconf2}") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg}) c.run("editable add dep --name=dep --version=1.0") c.run("install pkg -s os=Linux -s:b os=Linux") out = str(c.out).replace("\\\\", "\\") conf_source = os.path.join(c.current_folder, "dep", "mybuild", "mylocalbuild") conf_build = os.path.join(c.current_folder, "dep", "mysource", "mylocalsrc") confdict1 = os.path.join(c.current_folder, "dep", "mybuild", "mypatha") confdict2 = os.path.join(c.current_folder, "dep", "mybuild", "mypathc") assert conf_source in out assert conf_build in out assert confdict1 in out assert confdict2 in out def test_editable_conf_tool_require_builtin(): c = TestClient() dep = textwrap.dedent(""" import os from conan import ConanFile class Dep(ConanFile): name = "androidndk" version = "1.0" def layout(self): self.layouts.build.conf_info.define_path("tools.android:ndk_path", "mybuild") self.layouts.package.conf_info.define_path("tools.android:ndk_path", "mypkg") """) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "androidndk/1.0" def generate(self): ndk = self.conf.get("tools.android:ndk_path") self.output.info(f"NDK: {ndk}!!!") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg}) c.run("editable add dep") c.run("install pkg -s os=Linux -s:b os=Linux") ndk_path = os.path.join(c.current_folder, "dep", "mybuild") assert f"conanfile.py: NDK: {ndk_path}!!!" in c.out c.run("editable remove dep") c.run("create dep") c.run("install pkg -s os=Linux -s:b os=Linux") assert re.search("conanfile.py: NDK: .*mypkg!!!", str(c.out)) ================================================ FILE: test/integration/editable/test_editable_import.py ================================================ import os import shutil import textwrap from conan.test.utils.tools import TestClient class TestEditableImport: def test_copy_from_dep_in_generate(self): """ as we are removing the imports explicit functionality, test if the editable can still work for the "imports" case. It seem possible if: - "dep" package, both in cache and editable mode, defines correctly its layout - Tricky: "dep" package musth use self.source_folder+res instead of layout to package() - IMPORTANT: Consumers should use dep.cpp_info.resdirs[0], not dep.package_folder """ t = TestClient() dep = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): exports_sources = "*" def layout(self): self.folders.source = "src" self.cpp.source.resdirs = ["res"] def package(self): resdir = os.path.join(self.source_folder, "res") copy(self, "*", resdir, os.path.join(self.package_folder, "data")) def package_info(self): self.cpp_info.resdirs = ["data"] """) consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): requires = "dep/0.1" def generate(self): dep = self.dependencies["dep"] resdir = dep.cpp_info.resdirs[0] copy(self, "*", resdir, os.path.join(self.build_folder, "imports")) """) t.save({'dep/conanfile.py': dep, 'dep/src/res/myfile.txt': "mydata", "consumer/conanfile.py": consumer}) t.run("create dep --name=dep --version=0.1") t.run("install consumer") assert t.load("consumer/imports/myfile.txt") == "mydata" t.run("remove * -c") t.run('editable add dep --name=dep --version=0.1') shutil.rmtree(os.path.join(t.current_folder, "consumer", "imports")) t.run("install consumer") assert t.load("consumer/imports/myfile.txt") == "mydata" ================================================ FILE: test/integration/editable/test_editable_layout.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_editable_folders_root(): """ Editables with self.folders.root = ".." should work too """ c = TestClient() pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.root = ".." self.folders.build = "mybuild" self.cpp.build.libdirs = ["mylibdir"] def generate(self): self.output.info(f"PKG source_folder {self.source_folder}") def package_info(self): self.output.info(f"PKG package_folder {self.package_folder}") """) app = textwrap.dedent(""" from conan import ConanFile class App(ConanFile): requires = "pkg/0.1" def generate(self): pkg_libdir = self.dependencies["pkg"].cpp_info.libdir self.output.info(f"PKG LIBDIR {pkg_libdir}") """) c.save({"libs/pkg/conanfile.py": pkg, "conanfile.py": app}) c.run("editable add libs/pkg") c.run("install . --build=editable") libdir = os.path.join(c.current_folder, "libs", "mybuild", "mylibdir") assert f"conanfile.py: PKG LIBDIR {libdir}" in c.out assert f"pkg/0.1: PKG package_folder {c.current_folder}" in c.out assert f"pkg/0.1: PKG source_folder {c.current_folder}" in c.out def test_editable_folders_sibiling_root(): """ Editables with self.folders.root = ".." should work too for sibling folders """ c = TestClient() pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def layout(self): self.folders.root = ".." self.folders.build = "mybuild" self.cpp.build.libdirs = ["mylibdir"] def generate(self): self.output.info(f"PKG source_folder {self.source_folder}") def package_info(self): self.output.info(f"PKG package_folder {self.package_folder}") """) app = textwrap.dedent(""" from conan import ConanFile class App(ConanFile): requires = "pkg/0.1" def generate(self): pkg_libdir = self.dependencies["pkg"].cpp_info.libdir self.output.info(f"PKG LIBDIR {pkg_libdir}") """) c.save({"pkg/conanfile.py": pkg, "app/conanfile.py": app}) c.run("editable add pkg") c.run("install app --build=editable") libdir = os.path.join(c.current_folder, "mybuild", "mylibdir") assert f"conanfile.py: PKG LIBDIR {libdir}" in c.out assert f"pkg/0.1: PKG source_folder {c.current_folder}" in c.out assert f"pkg/0.1: PKG package_folder {c.current_folder}" in c.out def test_editable_matching_folder_names(): # https://github.com/conan-io/conan/issues/14091 client = TestClient() client.save({"hello-say/conanfile.py": GenConanfile("say", "0.1"), "hello/conanfile.py": GenConanfile("hello", "0.1").with_settings("build_type") .with_require("say/0.1")}) client.run("editable add hello-say") client.run("build hello-say") client.run("install hello -g CMakeDeps -s:b os=Linux") data = client.load("hello/say-release-data.cmake") hello_say_folder = os.path.join(client.current_folder, "hello-say").replace("\\", "/") assert f'set(say_PACKAGE_FOLDER_RELEASE "{hello_say_folder}")' in data env = client.load("hello/conanbuildenv-release.sh") assert "$script_folder/deactivate_conanbuildenv-release.sh" in env.replace("\\", "/") def test_install_editable_build_folder_vars(): """ Make sure that the consumer generates the editable files following build_folder_vars """ c = TestClient() profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 build_type=Release [conf] tools.cmake.cmake_layout:build_folder_vars=['settings.os', 'settings.arch'] """) lib = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Lib(ConanFile): name = "lib" version = "0.1" settings = "os", "arch", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) """) app = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class App(ConanFile): settings = "os", "arch", "build_type" requires = "lib/0.1" generators = "CMakeToolchain" def layout(self): cmake_layout(self) """) c.save({"lib/conanfile.py": lib, "app/conanfile.py": app, "profile": profile}) c.run("editable add lib") c.run("build lib -pr=profile") c.run("install app -pr=profile --build=editable") path = os.path.join(c.current_folder, "lib", "build", "windows-x86_64", "Release", "generators") assert f"lib/0.1: Writing generators to {path}" in c.out ================================================ FILE: test/integration/editable/test_editable_ranges.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_editable_ranges(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep"), "dep2/conanfile.py": GenConanfile("dep", "0.2"), "other/conanfile.py": GenConanfile("other", "23.0"), # shouldn't be resolved! "app/conanfile.py": GenConanfile("app", "0.1").with_requires("dep/[>=0.1]")}) c.run("editable add other") # This shouldn't be used c.run("editable add dep --version=0.1") # this should be used c.run("editable add dep --version=2.0 --user=myuser --channel=mychannel") # This shouldn't be used c.run("install app") c.assert_listed_require({"dep/0.1": "Editable"}) assert "dep/[>=0.1]: dep/0.1" in c.out # new version, uses new one c.run("editable add dep2") c.run("install app") c.assert_listed_require({"dep/0.2": "Editable"}) assert "dep/[>=0.1]: dep/0.2" in c.out assert "dep/0.1" not in c.out # If a newer one is in cache, it resolves to cache c.run("create dep --version=0.3") c.run("install app") c.assert_listed_require({"dep/0.3": "Cache"}) assert "dep/[>=0.1]: dep/0.3" in c.out assert "dep/0.1" not in c.out ================================================ FILE: test/integration/editable/test_editables_layout.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_cpp_info_editable(): client = TestClient() conan_hello = str(GenConanfile()) conan_hello += """ def layout(self): self.folders.source = "my_sources" self.folders.build = "my_build" self.cpp.build.includedirs = ["my_include"] self.cpp.build.libdirs = ["my_libdir"] self.cpp.build.libs = ["hello"] self.cpp.build.objects = ["myobjs/myobject.o"] self.cpp.build.frameworkdirs = [] # Empty list is also explicit priority declaration self.cpp.source.cxxflags = ["my_cxx_flag"] self.cpp.source.includedirs = ["my_include_source"] self.cpp.source.builddirs = ["my_builddir_source"] self.cpp.source.set_property("cmake_build_modules", ["mypath/mybuildmodule"]) self.cpp.package.libs = ["lib_when_package"] def package_info(self): # when editable: This one will be discarded because declared in build self.cpp_info.includedirs = ["package_include"] # when editable: This one will be discarded because declared in build self.cpp_info.libs.append("lib_when_package2") self.cpp_info.objects = ["myobject.o"] self.cpp_info.set_property("cmake_build_modules", ["mymodules/mybuildmodule"]) # when editable: This one will be discarded because declared in source self.cpp_info.cxxflags.append("my_cxx_flag2") # when editable: This one will be discarded because declared in source self.cpp_info.frameworkdirs.append("package_frameworks_path") # when editable: This one WONT be discarded as has not been declared in the editables layout self.cpp_info.cflags.append("my_c_flag") """ client.save({"conanfile.py": conan_hello}) client.run("create . --name=hello --version=1.0") package_folder = client.created_layout().package().replace("\\", "/") + "/" conan_consumer = textwrap.dedent(""" import os from conan import ConanFile, tools class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "hello/1.0" def build(self): info = self.dependencies["hello"].cpp_info self.output.warning("**includedirs:{}**".format(info.includedirs)) self.output.warning("**libdirs:{}**".format(info.libdirs)) self.output.warning("**builddirs:{}**".format(info.builddirs)) self.output.warning("**frameworkdirs:{}**".format(info.frameworkdirs)) self.output.warning("**libs:{}**".format(info.libs)) self.output.warning("**objects:{}**".format(info.objects)) self.output.warning("**build_modules:{}**".format(info.get_property("cmake_build_modules"))) self.output.warning("**cxxflags:{}**".format(info.cxxflags)) self.output.warning("**cflags:{}**".format(info.cflags)) """) # When hello is not editable client2 = TestClient(client.cache_folder) client2.save({"conanfile.py": conan_consumer}) client2.run("create . --name=lib --version=1.0") out = str(client2.out).replace(r"\\", "/").replace(package_folder, "") assert "**includedirs:['package_include']**" in out assert "**libdirs:['lib']**" in out assert "**builddirs:[]**" in out assert "**frameworkdirs:['package_frameworks_path']**" in out assert "**libs:['lib_when_package', 'lib_when_package2']**" in out assert "**objects:['myobject.o']**" in out assert "**build_modules:['mymodules/mybuildmodule']**" in out assert "**cxxflags:['my_cxx_flag2']**" in out assert "**cflags:['my_c_flag']**" in out # When hello is editable client.save({"conanfile.py": conan_hello}) client.run("editable add . --name=hello --version=1.0") # Create the consumer again, now it will use the hello editable client2.run("create . --name=lib --version=1.0") base_folder = client.current_folder.replace("\\", "/") + "/" out = str(client2.out).replace(r"\\", "/").replace(base_folder, "") assert "**includedirs:['my_sources/my_include_source', 'my_build/my_include']**" in out assert "**libdirs:['my_build/my_libdir']**" in out assert "**builddirs:['my_sources/my_builddir_source']**" in out assert "**libs:['hello']**" in out assert "**objects:['my_build/myobjs/myobject.o']**" in out assert "**build_modules:['my_sources/mypath/mybuildmodule']**" in out assert "**cxxflags:['my_cxx_flag']**" in out assert "**cflags:['my_c_flag']**" in out assert "**frameworkdirs:[]**" in out def test_cpp_info_components_editable(): client = TestClient() conan_hello = str(GenConanfile()) conan_hello += """ def layout(self): self.folders.source = "my_sources" self.folders.build = "my_build" self.cpp.build.components["foo"].includedirs = ["my_include_foo"] self.cpp.build.components["foo"].libdirs = ["my_libdir_foo"] self.cpp.build.components["foo"].libs = ["hello_foo"] self.cpp.build.components["foo"].objects = ["myobjs/myobject.o"] self.cpp.build.components["var"].includedirs = ["my_include_var"] self.cpp.build.components["var"].libdirs = ["my_libdir_var"] self.cpp.build.components["var"].libs = ["hello_var"] self.cpp.source.components["foo"].cxxflags = ["my_cxx_flag_foo"] self.cpp.source.components["foo"].includedirs = ["my_include_source_foo"] self.cpp.source.components["foo"].builddirs = ["my_builddir_source_foo"] self.cpp.source.components["foo"].set_property("cmake_build_modules", ["mypath/mybuildmodule"]) self.cpp.source.components["var"].cxxflags = ["my_cxx_flag_var"] self.cpp.source.components["var"].includedirs = ["my_include_source_var"] self.cpp.source.components["var"].builddirs = ["my_builddir_source_var"] self.cpp.package.components["foo"].libs = ["lib_when_package_foo"] self.cpp.package.components["var"].libs = ["lib_when_package_var"] def package_info(self): # when editable: This one will be discarded because declared in build self.cpp_info.components["foo"].includedirs = ["package_include_foo"] # when editable: This one will be discarded because declared in build self.cpp_info.components["foo"].libs.append("lib_when_package2_foo") self.cpp_info.components["foo"].objects = ["myobject.o"] self.cpp_info.components["foo"].set_property("cmake_build_modules", ["mymodules/mybuildmodule"]) # when editable: This one will be discarded because declared in source self.cpp_info.components["foo"].cxxflags.append("my_cxx_flag2_foo") # when editable: This one WONT be discarded as has not been declared in the editables layout self.cpp_info.components["foo"].cflags.append("my_c_flag_foo") # when editable: This one will be discarded because declared in build self.cpp_info.components["var"].includedirs = ["package_include_var"] # when editable: This one will be discarded because declared in build self.cpp_info.components["var"].libs.append("lib_when_package2_var") # when editable: This one will be discarded because declared in source self.cpp_info.components["var"].cxxflags.append("my_cxx_flag2_var") # when editable: This one WONT be discarded as has not been declared in the editables layout self.cpp_info.components["var"].cflags.append("my_c_flag_var") """ client.save({"conanfile.py": conan_hello}) client.run("create . --name=hello --version=1.0") package_folder = client.created_layout().package().replace("\\", "/") + "/" conan_consumer = textwrap.dedent(""" import os from conan import ConanFile, tools class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "hello/1.0" def build(self): info = self.dependencies["hello"].cpp_info self.output.warning("**FOO includedirs:{}**".format(info.components["foo"].includedirs)) self.output.warning("**FOO libdirs:{}**".format(info.components["foo"].libdirs)) self.output.warning("**FOO builddirs:{}**".format(info.components["foo"].builddirs)) self.output.warning("**FOO libs:{}**".format(info.components["foo"].libs)) self.output.warning("**FOO cxxflags:{}**".format(info.components["foo"].cxxflags)) self.output.warning("**FOO cflags:{}**".format(info.components["foo"].cflags)) self.output.warning("**FOO objects:{}**".format(info.components["foo"].objects)) self.output.warning("**FOO build_modules:{}**".format( info.components["foo"].get_property("cmake_build_modules"))) self.output.warning("**VAR includedirs:{}**".format(info.components["var"].includedirs)) self.output.warning("**VAR libdirs:{}**".format(info.components["var"].libdirs)) self.output.warning("**VAR builddirs:{}**".format(info.components["var"].builddirs)) self.output.warning("**VAR libs:{}**".format(info.components["var"].libs)) self.output.warning("**VAR cxxflags:{}**".format(info.components["var"].cxxflags)) self.output.warning("**VAR cflags:{}**".format(info.components["var"].cflags)) """) # When hello is not editable client2 = TestClient(client.cache_folder) client2.save({"conanfile.py": conan_consumer}) client2.run("create . --name=lib --version=1.0") out = str(client2.out).replace(r"\\", "/").replace(package_folder, "") assert "**FOO includedirs:['package_include_foo']**" in out assert "**FOO libdirs:['lib']**" in out # The components does have default dirs assert "**FOO builddirs:[]**" in out # The components don't have default dirs for builddirs assert "**FOO libs:['lib_when_package_foo', 'lib_when_package2_foo']**" in out assert "**FOO objects:['myobject.o']**" in out assert "**FOO build_modules:['mymodules/mybuildmodule']**" in out assert "**FOO cxxflags:['my_cxx_flag2_foo']**" in out assert "**FOO cflags:['my_c_flag_foo']**" in out assert "**VAR includedirs:['package_include_var']**" in out assert "**VAR libdirs:['lib']**" in out # The components does have default dirs assert "**VAR builddirs:[]**" in out # The components don't have default dirs assert "**VAR libs:['lib_when_package_var', 'lib_when_package2_var']**" in out assert "**VAR cxxflags:['my_cxx_flag2_var']**" in out assert "**VAR cflags:['my_c_flag_var']**" in out # When hello is editable client.save({"conanfile.py": conan_hello}) client.run("editable add . --name=hello --version=1.0") # Create the consumer again, now it will use the hello editable client2.run("create . --name=lib --version=1.0") base_folder = client.current_folder.replace("\\", "/") + "/" out = str(client2.out).replace(r"\\", "/").replace(base_folder, "") assert "**FOO includedirs:['my_sources/my_include_source_foo', " \ "'my_build/my_include_foo']**" in out assert "**FOO libdirs:['my_build/my_libdir_foo']**" in out assert "**FOO builddirs:['my_sources/my_builddir_source_foo']**" in out assert "**FOO libs:['hello_foo']**" in out assert "**FOO objects:['my_build/myobjs/myobject.o']**" in out assert "**FOO build_modules:['my_sources/mypath/mybuildmodule']**" in out assert "**FOO cxxflags:['my_cxx_flag_foo']**" in out assert "**FOO cflags:['my_c_flag_foo']**" in out assert "**VAR includedirs:['my_sources/my_include_source_var', " \ "'my_build/my_include_var']**" in out assert "**VAR libdirs:['my_build/my_libdir_var']**" in out assert "**VAR builddirs:['my_sources/my_builddir_source_var']**" in out assert "**VAR libs:['hello_var']**" in out assert "**VAR cxxflags:['my_cxx_flag_var']**" in out assert "**VAR cflags:['my_c_flag_var']**" in out def test_editable_package_folder(): """ This test checks the behavior that self.package_folder is NOT defined (i.e = None) for editable packages, so it cannot be used in ``package_info()`` method """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler", "arch", "build_type" def package_info(self): self.output.info("PKG FOLDER={}!!!".format(self.package_folder)) def layout(self): cmake_layout(self) """) c.save({"conanfile.py": conanfile}) c.run("create .") c.run("editable add .") c.run("install --requires=pkg/0.1@") assert "pkg/0.1: PKG FOLDER=None!!!" def test_editable_components_absolute_paths(): """ this was failing in https://github.com/conan-io/conan/issues/14777 because components aggregation had a bug """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "alpha" version = "1.0" def layout(self): self.cpp.source.components["headers"].includedirs = ["include222"] self.cpp.build.components["alpha1"].libdirs = ["alpha1",] self.cpp.build.components["alpha2"].libdirs = ["alpha2"] self.cpp.build.components["alpha_exe"].bindirs = ["alpha_exe"] """) test = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps" def requirements(self): self.requires(self.tested_reference_str) def test(self): pass """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test}) c.run("editable add .") c.run("create .") # This used to crash due to "include" is not absolute path, now it works assert "Testing the package" in c.out ================================================ FILE: test/integration/editable/transitive_editable_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient, GenConanfile def test_transitive_editables_half_diamond(): # https://github.com/conan-io/conan/issues/4445 client = TestClient(light=True) client.save({"libc/conanfile.py": GenConanfile("libc", "0.1"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_require("libc/0.1"), "liba/conanfile.py": GenConanfile("liba", "0.1").with_requires("libb/0.1", "libc/0.1")}) client.run("editable add libc") client.run("create libb") client.run("install liba") assert "libc/0.1 - Editable" in client.out with client.chdir("liba/build"): client.run("install ..") assert "libc/0.1 - Editable" in client.out def test_transitive_editable_test_requires(): """ This test was crashing because editable packages was "SKIP"ing some dependencies as "test_requires", but editables need the dependencies to generate() and to build() https://github.com/conan-io/conan/issues/13543 """ c = TestClient() pkga = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps, cmake_layout class Pkg(ConanFile): name = "pkga" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" def build_requirements(self): self.test_requires("gtest/1.0") def layout(self): cmake_layout(self) def generate(self): cd = CMakeDeps(self) cd.generate() """) pkgb = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): name = "pkgb" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires("pkga/1.0") def layout(self): cmake_layout(self) """) c.save({"gtest/conanfile.py": GenConanfile("gtest", "1.0"), "pkga/conanfile.py": pkga, "pkgb/conanfile.py": pkgb}) c.run("create gtest") c.run("build pkga") c.run("editable add pkga") # This used to crash, due to paths in test_requires not being processed (package_info() not # being called c.run("build pkgb") def test_transitive_editables_python_requires_version_range(): # https://github.com/conan-io/conan/issues/14411 client = TestClient(default_server_user=True, light=True) client.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("dep/[*]")}) client.run("editable add pkg", assert_error=True) assert "Version range '*' from requirement 'dep/[*]' required by 'python_requires' " \ "could not be resolved" in client.out client.run("export dep") client.run("upload * -r=default -c") client.run("remove * -c") client.run("editable add pkg") # now it works, able to return from server if necessary assert "dep/0.1: Downloaded recipe revision" in client.out def test_transitive_editables_build(): # https://github.com/conan-io/conan/issues/6064 c = TestClient() libb = textwrap.dedent("""\ from conan import ConanFile class LibB(ConanFile): name = "libb" version = "0.1" build_policy = "missing" settings = "os", "compiler", "arch" def build_requirements(self): self.build_requires("liba/[>=0.0]") def requirements(self): self.requires("liba/[>=0.0]") """) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1"), "libb/conanfile.py": libb, "app/conanfile.txt": "[requires]\nlibb/0.1"}) c.run("editable add liba") c.run("editable add libb") c.run("install app --build=*") # It doesn't crash # Try also with 2 profiles c.run("install app -s:b os=Windows --build=*") # it doesn't crash def test_transitive_editable_cascade_build(): """ https://github.com/conan-io/conan/issues/15292 """ c = TestClient(light=True) pkga = GenConanfile("pkga", "1.0").with_package_type("static-library") pkgb = GenConanfile("pkgb", "1.0").with_requires("pkga/1.0").with_package_type("static-library") pkgc = GenConanfile("pkgc", "1.0").with_requires("pkgb/1.0").with_package_type("static-library") app = GenConanfile("app", "1.0").with_requires("pkgc/1.0").with_package_type("application") c.save({"pkga/conanfile.py": pkga, "pkgb/conanfile.py": pkgb, "pkgc/conanfile.py": pkgc, "app/conanfile.py": app}) c.run("create pkga") c.run("create pkgb") pkgb_id = c.created_package_id("pkgb/1.0") c.run("create pkgc") pkgc_id = c.created_package_id("pkgc/1.0") c.run("install app") # now we want to investigate pkga c.run("editable add pkga") pkga = GenConanfile("pkga", "1.0").with_package_type("static-library")\ .with_class_attribute("some=42") c.save({"pkga/conanfile.py": pkga}) c.run("install app") # The consumers didn't need a new binary, even if I modified pkg c.assert_listed_binary({"pkgb/1.0": (pkgb_id, "Cache"), "pkgc/1.0": (pkgc_id, "Cache")}) c.run("install app --build=editable") c.assert_listed_binary({"pkgb/1.0": (pkgb_id, "Cache"), "pkgc/1.0": (pkgc_id, "Cache")}) # But I can command perfectly what to do and when c.run("install app --build=editable --build=cascade") c.assert_listed_binary({"pkgb/1.0": (pkgb_id, "Build"), "pkgc/1.0": (pkgc_id, "Build")}) # Changes in pkga are good, we export it c.run("export pkga") # Now we remove the editable c.run("editable remove pkga") c.run("install app", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkga/1.0'" in c.out def test_transitive_editable_cascade_package_id(): """ https://github.com/conan-io/conan/issues/15292 """ c = TestClient(light=True) pkga = GenConanfile("pkga", "1.0").with_package_type("static-library") pkgb = GenConanfile("pkgb", "1.0").with_requires("pkga/1.0").with_package_type("static-library") pkgc = GenConanfile("pkgc", "1.0").with_requires("pkgb/1.0").with_package_type("shared-library") app = GenConanfile("app", "1.0").with_requires("pkgc/1.0").with_package_type("application") c.save({"pkga/conanfile.py": pkga, "pkgb/conanfile.py": pkgb, "pkgc/conanfile.py": pkgc, "app/conanfile.py": app}) c.run("create pkga") c.run("create pkgb") pkgb_id = c.created_package_id("pkgb/1.0") c.run("create pkgc") c.run("install app") # now we want to investigate pkga, we put it in editable mode c.run("editable add pkga") pkga = GenConanfile("pkga", "1.0").with_package_type("static-library")\ .with_class_attribute("some=42") c.save({"pkga/conanfile.py": pkga}) c.run("install app", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkgc/1.0'" in c.out c.run("install app --build=missing") pkgc_id = c.created_package_id("pkgc/1.0") # The consumers didn't need a new binary, even if I modified pkg c.assert_listed_binary({"pkgb/1.0": (pkgb_id, "Cache"), "pkgc/1.0": (pkgc_id, "Build")}) # Changes in pkga are good, we export it c.run("export pkga") # Now we remove the editable c.run("editable remove pkga") c.run("install app", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkgc/1.0'" in c.out c.run("install app --build=missing") pkgc_id = c.created_package_id("pkgc/1.0") # The consumers didn't need a new binary, even if I modified pkg c.assert_listed_binary({"pkgb/1.0": (pkgb_id, "Cache"), "pkgc/1.0": (pkgc_id, "Build")}) def test_warning_from_cache(): c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga", "1.0"), "pkgb/conanfile.py": GenConanfile("pkgb", "1.0").with_requires("pkga/1.0"), "pkgc/conanfile.py": GenConanfile("pkgc", "1.0").with_requires("pkgb/1.0")}) c.run("editable add pkga") c.run("create pkgb") assert "pkgb/1.0: WARN: risk: Package is being built in the cache using editable" in c.out c.run("create pkgc") assert "pkgc/1.0: WARN: risk: Package is being built in the cache using editable" in c.out c.run("create pkgc --build=*") assert "pkgb/1.0: WARN: risk: Package is being built in the cache using editable" in c.out assert "pkgc/1.0: WARN: risk: Package is being built in the cache using editable" in c.out ================================================ FILE: test/integration/environment/__init__.py ================================================ ================================================ FILE: test/integration/environment/test_buildenv_profile.py ================================================ import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture def client(): conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def generate(self): for var in (1, 2): v = self.buildenv.vars(self).get("MyVar{}".format(var)) self.output.info("MyVar{}={}!!".format(var, v)) """) profile1 = textwrap.dedent(""" [buildenv] MyVar1=MyValue1_1 MyVar2=MyValue2_1 """) client = TestClient() client.save({"conanfile.py": conanfile, "profile1": profile1}) return client def test_buildenv_profile_cli(client): profile2 = textwrap.dedent(""" [buildenv] MyVar1=MyValue1_2 MyVar2+=MyValue2_2 """) client.save({"profile2": profile2}) client.run("install . -pr=profile1 -pr=profile2") assert "conanfile.py: MyVar1=MyValue1_2!!" in client.out assert "conanfile.py: MyVar2=MyValue2_1 MyValue2_2" in client.out def test_buildenv_profile_include(client): profile2 = textwrap.dedent(""" include(profile1) [buildenv] MyVar1=MyValue1_2 MyVar2+=MyValue2_2 """) client.save({"profile2": profile2}) client.run("install . -pr=profile2") assert "conanfile.py: MyVar1=MyValue1_2!!" in client.out assert "conanfile.py: MyVar2=MyValue2_1 MyValue2_2" in client.out def test_buildenv_package_patterns(): client = TestClient() conanfile = GenConanfile() generate = """ def generate(self): value = self.buildenv.vars(self).get("my_env_var") or "None" self.output.warning("{} ENV:{}".format(self.ref.name, value)) """ client.save({"dep/conanfile.py": str(conanfile) + generate, "pkg/conanfile.py": str(conanfile.with_requirement("dep/0.1", visible=False)) + generate, "consumer/conanfile.py": str(conanfile.with_requires("pkg/0.1") .with_settings("os", "build_type", "arch")) + generate}) client.run("export dep --name=dep --version=0.1") client.run("export pkg --name=pkg --version=0.1") # This pattern applies to no package profile = """ include(default) [buildenv] invented/*:my_env_var=Foo """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep ENV:None" in client.out assert "WARN: pkg ENV:None" in client.out assert "WARN: None ENV:None" in client.out # This patterns applies to dep profile = """ include(default) [buildenv] dep/*:my_env_var=Foo """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep ENV:Foo" in client.out assert "WARN: pkg ENV:None" in client.out assert "WARN: None ENV:None" in client.out profile = """ include(default) [buildenv] dep/0.1:my_env_var=Foo """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep ENV:Foo" in client.out assert "WARN: pkg ENV:None" in client.out assert "WARN: None ENV:None" in client.out # The global pattern applies to all profile = """ include(default) [buildenv] dep/*:my_env_var=Foo pkg/*:my_env_var=Foo my_env_var=Var """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep ENV:Var" in client.out assert "WARN: pkg ENV:Var" in client.out assert "WARN: None ENV:Var" in client.out # "&" pattern for the consumer profile = """ include(default) [buildenv] dep/*:my_env_var=Foo pkg/*:my_env_var=Foo2 &:my_env_var=Var """ client.save({"profile": profile}) client.run("install consumer --build='*' --profile profile") assert "WARN: dep ENV:Foo" in client.out assert "WARN: pkg ENV:Foo2" in client.out assert "WARN: None ENV:Var" in client.out def test_buildenv_error_unset(): # https://github.com/conan-io/conan/issues/19285#issuecomment-3569891282 c = TestClient() profile = textwrap.dedent(""" [buildenv] CLASSPATH=! OTHERPATH= """) c.save({"conanfile.txt": "", "profile": profile}) c.run("install . -pr=profile -s:a os=Linux") env = c.load("conanbuildenv.sh") assert "unset CLASSPATH" in env assert 'export OTHERPATH=""' in env def test_buildenv_priority_copy(): # https://github.com/conan-io/conan/issues/19570 c = TestClient() profile = textwrap.dedent(""" [buildenv] alib/*:CUSTOM_PATH=+(path)/only_alib CUSTOM_PATH=+(path)/common """) lib = textwrap.dedent(""" from conan import ConanFile class AlibConan(ConanFile): version = "1.0" def build(self): v = self.buildenv.vars(self).get("CUSTOM_PATH") self.output.info(f"[{self.name}] CUSTOM_PATH={v}!!!") """) conanfile_txt = textwrap.dedent(""" [requires] alib/1.0 blib/1.0 """) c.save({"lib/conanfile.py": lib, "conanfile.txt": conanfile_txt, "profile": profile}) c.run("export lib --name=alib") c.run("export lib --name=blib") c.run("install . -pr=profile -s os=Windows --build=missing") assert f"alib/1.0: [alib] CUSTOM_PATH=/common{os.pathsep}/only_alib!!!" in c.out assert "blib/1.0: [blib] CUSTOM_PATH=/common!!!" in c.out ================================================ FILE: test/integration/environment/test_env.py ================================================ import os import platform import subprocess import textwrap import pytest from conan.test.utils.env import environment_update from conan.test.utils.mocks import ConanFileMock from conan.tools.env.environment import environment_wrap_command from conan.test.utils.tools import TestClient, GenConanfile @pytest.fixture() def client(): openssl = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): settings = "os" package_type = "shared-library" def package(self): with chdir(self, self.package_folder): echo = "@echo off\necho MYOPENSSL={}!!".format(self.settings.os) save(self, "bin/myopenssl.bat", echo) save(self, "bin/myopenssl.sh", echo) os.chmod("bin/myopenssl.sh", 0o777) """) cmake = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): settings = "os" requires = "openssl/1.0" def package(self): with chdir(self, self.package_folder): echo = "@echo off\necho MYCMAKE={}!!".format(self.settings.os) save(self, "mycmake.bat", echo + "\ncall myopenssl.bat") save(self, "mycmake.sh", echo + "\n myopenssl.sh") os.chmod("mycmake.sh", 0o777) def package_info(self): # Custom buildenv not defined by cpp_info self.buildenv_info.prepend_path("PATH", self.package_folder) self.buildenv_info.define("MYCMAKEVAR", "MYCMAKEVALUE!!") """) gtest = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): settings = "os" def package(self): with chdir(self, self.package_folder): prefix = "@echo off\n" if self.settings.os == "Windows" else "" echo = "{}echo MYGTEST={}!!".format(prefix, self.settings.os) save(self, "bin/mygtest.bat", echo) save(self, "bin/mygtest.sh", echo) os.chmod("bin/mygtest.sh", 0o777) def package_info(self): self.runenv_info.define("MYGTESTVAR", "MyGTestValue{}".format(self.settings.os)) """) client = TestClient() client.save({"cmake/conanfile.py": cmake, "gtest/conanfile.py": gtest, "openssl/conanfile.py": openssl}) client.run("export openssl --name=openssl --version=1.0") client.run("export cmake --name=mycmake --version=1.0") client.run("export gtest --name=mygtest --version=1.0") myrunner_bat = "@echo off\necho MYGTESTVAR=%MYGTESTVAR%!!\n" myrunner_sh = "echo MYGTESTVAR=$MYGTESTVAR!!\n" client.save({"myrunner.bat": myrunner_bat, "myrunner.sh": myrunner_sh}, clean_first=True) os.chmod(os.path.join(client.current_folder, "myrunner.sh"), 0o777) return client @pytest.mark.parametrize("gtest_run_true", [True, False]) def test_complete(client, gtest_run_true): """By default, a test require has the run=False trait, so the PATH to the bat cannot be accessed""" conanfile = textwrap.dedent(""" import os from conan import ConanFile class Pkg(ConanFile): requires = "openssl/1.0" tool_requires = "mycmake/1.0" def build_requirements(self): {} def build(self): self.run("mycmake.bat", env="conanbuildenv") assert os.path.exists(os.path.join(self.generators_folder, "conanrunenv.sh")) """) if gtest_run_true: test_require = 'self.test_requires("mygtest/1.0", run=True)' else: test_require = 'self.test_requires("mygtest/1.0")' conanfile = conanfile.format(test_require) client.save({"conanfile.py": conanfile}) client.run("install . -s:b os=Windows -s:h os=Linux --build=missing") # Run the BUILD environment if platform.system() == "Windows": cmd = environment_wrap_command(ConanFileMock(), "conanbuildenv", client.current_folder, "mycmake.bat") client.run_command(cmd) assert "MYCMAKE=Windows!!" in client.out assert "MYOPENSSL=Windows!!" in client.out # Run the RUN environment if platform.system() != "Windows": cmd = environment_wrap_command(ConanFileMock(), "conanrunenv", client.current_folder, "mygtest.sh && .{}myrunner.sh".format(os.sep)) client.run_command(cmd, assert_error=not gtest_run_true) if gtest_run_true: assert "MYGTEST=Linux!!" in client.out assert "MYGTESTVAR=MyGTestValueLinux!!" in client.out if platform.system() == "Windows": client.run("build . -s:h os=Linux") assert "MYCMAKE=Windows!!" in client.out assert "MYOPENSSL=Windows!!" in client.out def test_profile_included_multiple(): client = TestClient() conanfile = textwrap.dedent("""\ import os, platform from conan import ConanFile class Pkg(ConanFile): def generate(self): buildenv = self.buildenv.vars(self) self.output.info("MYVAR1: {}!!!".format(buildenv.get("MYVAR1"))) self.output.info("MYVAR2: {}!!!".format(buildenv.get("MYVAR2"))) self.output.info("MYVAR3: {}!!!".format(buildenv.get("MYVAR3"))) """) myprofile = textwrap.dedent(""" [buildenv] MYVAR1=MyVal1 MYVAR3+=MyVal3 """) other_profile = textwrap.dedent(""" [buildenv] MYVAR1=MyValOther1 MYVAR2=MyValOther2 MYVAR3=MyValOther3 """) client.save({"conanfile.py": conanfile, "myprofile": myprofile, "myprofile_include": "include(other_profile)\n" + myprofile, "other_profile": other_profile}) # The reference profile has priority client.run("install . -pr=myprofile_include") assert "MYVAR1: MyVal1!!!" in client.out assert "MYVAR2: MyValOther2!!!" in client.out assert "MYVAR3: MyValOther3 MyVal3!!!" in client.out # Equivalent to include is to put it first, then the last has priority client.run("install . -pr=other_profile -pr=myprofile") assert "MYVAR1: MyVal1!!!" in client.out assert "MYVAR2: MyValOther2!!!" in client.out assert "MYVAR3: MyValOther3 MyVal3!!!" in client.out def test_profile_buildenv(): client = TestClient() conanfile = textwrap.dedent("""\ import os, platform from conan import ConanFile class Pkg(ConanFile): def generate(self): self.buildenv.vars(self).save_script("pkgenv") if platform.system() != "Windows": os.chmod("pkgenv.sh", 0o777) """) # Some scripts in a random system folders, path adding to the profile [env] compiler_bat = "@echo off\necho MYCOMPILER!!\necho MYPATH=%PATH%" compiler_sh = "echo MYCOMPILER!!\necho MYPATH=$PATH" compiler2_bat = "@echo off\necho MYCOMPILER2!!\necho MYPATH2=%PATH%" compiler2_sh = "echo MYCOMPILER2!!\necho MYPATH2=$PATH" myprofile = textwrap.dedent(""" [buildenv] PATH+=(path){} mypkg*:PATH=! mypkg*:PATH+=(path){} """.format(os.path.join(client.current_folder, "compiler"), os.path.join(client.current_folder, "compiler2"))) client.save({"conanfile.py": conanfile, "myprofile": myprofile, "compiler/mycompiler.bat": compiler_bat, "compiler/mycompiler.sh": compiler_sh, "compiler2/mycompiler.bat": compiler2_bat, "compiler2/mycompiler.sh": compiler2_sh}) os.chmod(os.path.join(client.current_folder, "compiler", "mycompiler.sh"), 0o777) os.chmod(os.path.join(client.current_folder, "compiler2", "mycompiler.sh"), 0o777) client.run("install . -pr=myprofile") # Run the BUILD environment ext = "bat" if platform.system() == "Windows" else "sh" # TODO: Decide on logic .bat vs .sh cmd = environment_wrap_command(ConanFileMock(), "conanbuildenv", client.current_folder, "mycompiler.{}".format(ext)) client.run_command(cmd) assert "MYCOMPILER!!" in client.out assert "MYPATH=" in client.out # Now with pkg-specific env-var client.run("install . --name=mypkg --version=1.0 -pr=myprofile") client.run_command(cmd) assert "MYCOMPILER2!!" in client.out assert "MYPATH2=" in client.out def test_transitive_order(): # conanfile.py -(br)-> cmake -> openssl (unknown=static) # \ \-(br)-> gcc # \--------(br)-> gcc # \---------------------> openssl gcc = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.runenv_info.append("MYVAR", "MyGCCValue") """) openssl = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): settings = "os" tool_requires = "gcc/1.0" package_type = "shared-library" def package_info(self): self.runenv_info.append("MYVAR", "MyOpenSSL{}Value".format(self.settings.os)) """) cmake = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): requires = "openssl/1.0" tool_requires = "gcc/1.0" def package_info(self): self.runenv_info.append("MYVAR", "MyCMakeRunValue") self.buildenv_info.append("MYVAR", "MyCMakeBuildValue") """) client = TestClient() client.save({"gcc/conanfile.py": gcc, "cmake/conanfile.py": cmake, "openssl/conanfile.py": openssl}) client.run("export gcc --name=gcc --version=1.0") client.run("export openssl --name=openssl --version=1.0") client.run("export cmake --name=cmake --version=1.0") consumer = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import VirtualBuildEnv, VirtualRunEnv class Pkg(ConanFile): requires = "openssl/1.0" tool_requires = "cmake/1.0", "gcc/1.0" def generate(self): buildenv = VirtualBuildEnv(self).vars() self.output.info("BUILDENV: {}!!!".format(buildenv.get("MYVAR"))) runenv = VirtualRunEnv(self).vars() self.output.info("RUNENV: {}!!!".format(runenv.get("MYVAR"))) """) client.save({"conanfile.py": consumer}, clean_first=True) client.run("install . -s:b os=Windows -s:h os=Linux --build='*'") assert "BUILDENV: MyOpenSSLWindowsValue MyGCCValue "\ "MyCMakeRunValue MyCMakeBuildValue!!!" in client.out assert "RUNENV: MyOpenSSLLinuxValue!!!" in client.out # Even if the generator is duplicated in command line (it used to fail due to bugs) client.run("install . -s:b os=Windows -s:h os=Linux --build='*' -g VirtualRunEnv -g VirtualBuildEnv") assert "BUILDENV: MyOpenSSLWindowsValue MyGCCValue "\ "MyCMakeRunValue MyCMakeBuildValue!!!" in client.out assert "RUNENV: MyOpenSSLLinuxValue!!!" in client.out def test_buildenv_from_requires(): openssl = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def package_info(self): self.buildenv_info.append("OpenSSL_ROOT", "MyOpenSSL{}Value".format(self.settings.os)) """) poco = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): requires = "openssl/1.0" settings = "os" def package_info(self): self.buildenv_info.append("Poco_ROOT", "MyPoco{}Value".format(self.settings.os)) """) client = TestClient() client.save({"poco/conanfile.py": poco, "openssl/conanfile.py": openssl}) client.run("export openssl --name=openssl --version=1.0") client.run("export poco --name=poco --version=1.0") consumer = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import VirtualBuildEnv class Pkg(ConanFile): requires = "poco/1.0" def generate(self): buildenv = VirtualBuildEnv(self).vars() self.output.info("BUILDENV POCO: {}!!!".format(buildenv.get("Poco_ROOT"))) self.output.info("BUILDENV OpenSSL: {}!!!".format(buildenv.get("OpenSSL_ROOT"))) """) client.save({"conanfile.py": consumer}, clean_first=True) client.run("install . -s:b os=Windows -s:h os=Linux --build='*' -g VirtualBuildEnv") assert "BUILDENV POCO: MyPocoLinuxValue!!!" in client.out assert "BUILDENV OpenSSL: MyOpenSSLLinuxValue!!!" in client.out def test_diamond_repeated(): pkga = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.runenv_info.define("MYVAR1", "PkgAValue1") self.runenv_info.append("MYVAR2", "PkgAValue2") self.runenv_info.prepend("MYVAR3", "PkgAValue3") self.runenv_info.prepend("MYVAR4", "PkgAValue4") """) pkgb = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): requires = "pkga/1.0" def package_info(self): self.runenv_info.append("MYVAR1", "PkgBValue1") self.runenv_info.append("MYVAR2", "PkgBValue2") self.runenv_info.prepend("MYVAR3", "PkgBValue3") self.runenv_info.prepend("MYVAR4", "PkgBValue4") """) pkgc = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): requires = "pkga/1.0" def package_info(self): self.runenv_info.append("MYVAR1", "PkgCValue1") self.runenv_info.append("MYVAR2", "PkgCValue2") self.runenv_info.prepend("MYVAR3", "PkgCValue3") self.runenv_info.prepend("MYVAR4", "PkgCValue4") """) pkgd = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): requires = "pkgb/1.0", "pkgc/1.0" def package_info(self): self.runenv_info.append("MYVAR1", "PkgDValue1") self.runenv_info.append("MYVAR2", "PkgDValue2") self.runenv_info.prepend("MYVAR3", "PkgDValue3") self.runenv_info.define("MYVAR4", "PkgDValue4") """) pkge = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import VirtualRunEnv class Pkg(ConanFile): requires = "pkgd/1.0" def generate(self): env = VirtualRunEnv(self) runenv = env.vars(scope="run") self.output.info("MYVAR1: {}!!!".format(runenv.get("MYVAR1"))) self.output.info("MYVAR2: {}!!!".format(runenv.get("MYVAR2"))) self.output.info("MYVAR3: {}!!!".format(runenv.get("MYVAR3"))) self.output.info("MYVAR4: {}!!!".format(runenv.get("MYVAR4"))) env.generate() """) client = TestClient() client.save({"pkga/conanfile.py": pkga, "pkgb/conanfile.py": pkgb, "pkgc/conanfile.py": pkgc, "pkgd/conanfile.py": pkgd, "pkge/conanfile.py": pkge}) client.run("export pkga --name=pkga --version=1.0") client.run("export pkgb --name=pkgb --version=1.0") client.run("export pkgc --name=pkgc --version=1.0") client.run("export pkgd --name=pkgd --version=1.0") client.run("install pkge --build=missing") # PkgB has higher priority (included first) so it is appended last and prepended first (wrtC) assert "MYVAR1: PkgAValue1 PkgCValue1 PkgBValue1 PkgDValue1!!!" in client.out assert "MYVAR2: PkgAValue2 PkgCValue2 PkgBValue2 PkgDValue2!!!" in client.out assert "MYVAR3: PkgDValue3 PkgBValue3 PkgCValue3 PkgAValue3!!!" in client.out assert "MYVAR4: PkgDValue4!!!" in client.out # No settings always sh conanrun = client.load("pkge/conanrunenv.sh") assert "PATH" not in conanrun assert 'export MYVAR1="PkgAValue1 PkgCValue1 PkgBValue1 PkgDValue1"' in conanrun assert ('export MYVAR2="${MYVAR2:-}${MYVAR2:+ }PkgAValue2 PkgCValue2 ' 'PkgBValue2 PkgDValue2"') in conanrun assert ('export MYVAR3="PkgDValue3 PkgBValue3 PkgCValue3 ' 'PkgAValue3${MYVAR3:+ $MYVAR3}"') in conanrun assert 'export MYVAR4="PkgDValue4"' in conanrun @pytest.mark.parametrize("require_run", [True, False]) def test_environment_scripts_generated_envvars(require_run): """If the regular require doesn't declare the 'run' trait, the conanrunenv won't be there, unless for example, the require_pkg has the pkg_type to Application""" consumer_pkg = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import VirtualBuildEnv, VirtualRunEnv class Pkg(ConanFile): settings = "os" requires = "require_pkg/1.0" tool_requires = "build_require_pkg/1.0" generators = "VirtualRunEnv", "VirtualBuildEnv" """) client = TestClient() conanfile_br = (GenConanfile().with_package_file("bin/myapp", "myexe") .with_package_file("lib/mylib", "mylibcontent") .with_settings("os")) conanfile_require = (GenConanfile().with_package_file("bin/myapp", "myexe") .with_package_file("lib/mylib", "mylibcontent") .with_settings("os")) if require_run: conanfile_require.with_package_type("application") client.save({"build_require_pkg/conanfile.py": conanfile_br, "require_pkg/conanfile.py": conanfile_require, "consumer_pkg/conanfile.py": consumer_pkg}) client.run("export build_require_pkg --name=build_require_pkg --version=1.0") client.run("export require_pkg --name=require_pkg --version=1.0") client.run("install consumer_pkg --build='*'") if platform.system() == "Windows": conanbuildenv = client.load("consumer_pkg/conanbuildenv.bat") if require_run: conanrunenv = client.load("consumer_pkg/conanrunenv.bat") assert "LD_LIBRARY_PATH" not in conanbuildenv assert "LD_LIBRARY_PATH" not in conanrunenv else: assert not os.path.exists("consumer_pkg/conanrunenv.bat") else: if require_run: conanbuildenv = client.load("consumer_pkg/conanbuildenv.sh") conanrunenv = client.load("consumer_pkg/conanrunenv.sh") assert "LD_LIBRARY_PATH" in conanbuildenv assert "LD_LIBRARY_PATH" in conanrunenv else: assert not os.path.exists("consumer_pkg/conanrunenv.sh") if require_run: # Build context LINUX - Host context LINUX client.run("install consumer_pkg -s:b os=Linux -s:h os=Linux --build='*'") conanbuildenv = client.load("consumer_pkg/conanbuildenv.sh") conanrunenv = client.load("consumer_pkg/conanrunenv.sh") assert "LD_LIBRARY_PATH" in conanbuildenv assert "LD_LIBRARY_PATH" in conanrunenv # Build context WINDOWS - Host context WINDOWS client.run("install consumer_pkg -s:b os=Windows -s:h os=Windows --build='*'") conanbuildenv = client.load("consumer_pkg/conanbuildenv.bat") conanrunenv = client.load("consumer_pkg/conanrunenv.bat") assert "LD_LIBRARY_PATH" not in conanbuildenv assert "LD_LIBRARY_PATH" not in conanrunenv # Build context LINUX - Host context WINDOWS client.run("install consumer_pkg -s:b os=Linux -s:h os=Windows --build='*'") conanbuildenv = client.load("consumer_pkg/conanbuildenv.sh") conanrunenv = client.load("consumer_pkg/conanrunenv.bat") assert "LD_LIBRARY_PATH" in conanbuildenv assert "LD_LIBRARY_PATH" not in conanrunenv # Build context WINDOWS - Host context LINUX client.run("install consumer_pkg -s:b os=Windows -s:h os=Linux --build='*'") conanbuildenv = client.load("consumer_pkg/conanbuildenv.bat") conanrunenv = client.load("consumer_pkg/conanrunenv.sh") assert "LD_LIBRARY_PATH" not in conanbuildenv assert "LD_LIBRARY_PATH" in conanrunenv @pytest.mark.parametrize("deactivation_mode", ["function", None]) def test_multiple_deactivate(deactivation_mode): conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import Environment class Pkg(ConanFile): def generate(self): e1 = Environment() e1.define("VAR1", "Value1") e1.vars(self).save_script("mybuild1") e2 = Environment() e2.define("VAR2", "Value2") e2.vars(self).save_script("mybuild2") """) display_bat = textwrap.dedent("""\ @echo off echo VAR1=%VAR1%!! echo VAR2=%VAR2%!! """) display_sh = textwrap.dedent("""\ echo VAR1=$VAR1!! echo VAR2=$VAR2!! """) client = TestClient() client.save({"conanfile.py": conanfile, "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) client.run(f"install . {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": deactivate_bat = "deactivate_conanbuild" if deactivation_mode else "deactivate_conanbuild.bat" cmd = f"conanbuild.bat && display.bat && {deactivate_bat} && display.bat" else: deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() assert "VAR1=Value1!!" in out assert "VAR2=Value2!!" in out assert 3 == str(out).count("Restoring environment") assert "VAR1=!!" in out assert "VAR2=!!" in out @pytest.mark.parametrize("deactivation_mode", ["function", None]) def test_multiple_deactivate_order(deactivation_mode): """ https://github.com/conan-io/conan/issues/13693 """ conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import Environment class Pkg(ConanFile): def generate(self): e1 = Environment() e1.define("MYVAR", "Value1") e1.vars(self).save_script("mybuild1") e2 = Environment() e2.define("MYVAR", "Value2") e2.vars(self).save_script("mybuild2") """) display_bat = textwrap.dedent("""\ @echo off echo MYVAR=%MYVAR%!! """) display_sh = textwrap.dedent("""\ echo MYVAR=$MYVAR!! """) client = TestClient() client.save({"conanfile.py": conanfile, "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) client.run(f"install . {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() assert "MYVAR=Value2!!" in out assert 3 == str(out).count("Restoring environment") assert "MYVAR=!!" in out @pytest.mark.skipif(platform.system() == "Windows", reason="Shell script test") @pytest.mark.parametrize("deactivation_mode", ["function", None]) def test_deactivate_missing_vars_stay_missing(deactivation_mode): """ Tests that these two cases preserve variable status 1. export FOO= ./conanrunenv.sh that changes FOO to something with a value ./deactivate_conanrunenv.sh FOO is still empty 2. BAR is not defined ./conanrunenv.sh that changes BAR to something with a value ./deactivate_conanrunenv.sh BAR is still not defined 3. BAZ is defined to some value ./conanrunenv.sh that unsets BAZ ./deactivate_conanrunenv.sh BAZ is still defined to some value 4. FOOBAR is empty ./conanrunenv.sh that unsets FOOBAR ./deactivate_conanrunenv.sh FOOBAR is still empty """ conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import Environment class Pkg(ConanFile): def generate(self): e1 = Environment() e1.define("FOO", "Value1") e1.define("BAR", "Value2") e1.unset("BAZ") e1.unset("FOOBAR") e1.vars(self).save_script("mybuild1") """) display_sh = textwrap.dedent("""\ echo FOO=$FOO!! if [ -n "${BAR+x}" ]; then echo "BAR EXISTS!!"; fi; echo BAZ=$BAZ!! echo FOOBAR=$FOOBAR!! """) client = TestClient(light=True) client.save({"conanfile.py": conanfile, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) client.run(f"install . {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = (f'export FOO=&& export BAZ=Value3 && export FOOBAR=' f'&& . ./conanbuild.sh && {deactivate_cmd} && ./display.sh') out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() assert "FOO=!!" in out assert "BAR EXISTS!!" not in out assert "BAZ=Value3!!" in out assert "FOOBAR=!!" in out @pytest.mark.skipif(platform.system() != "Windows", reason="Path problem in Windows only") @pytest.mark.parametrize("num_deps", [3, ]) def test_massive_paths(num_deps): """ This test proves that having too many dependencies that will result in a very long PATH env-var in the consumer by one VirtualXXXEnv environment, will overflow. https://github.com/conan-io/conan/issues/9565 This seems an unsolvable limitation, the only alternatives are: - shorten the paths in general (shorter cache paths) - add exclusively the paths of needed things (better visibility) Seems that Conan 2.0 will improve over these things, allowing larger dependencies graphs without failing. Besides that, it might use the deployers to workaround shared-libs running scenarios. The test is parameterized for being fast an passing, but if we add a num_deps >= 80 approx, it will start to enter the failing scenarios. Not adding the >=80 scenario, because that tests takes 1 minute by itself, not worth the value. """ client = TestClient(path_with_spaces=False) compiler_bat = "@echo off\necho MYTOOL {}!!\n" conanfile = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): exports_sources = "*" package_type = "application" def package(self): copy(self, "*", self.build_folder, os.path.join(self.package_folder, "bin")) """) for i in range(num_deps): client.save({"conanfile.py": conanfile, "mycompiler{}.bat".format(i): compiler_bat.format(i)}) client.run("create . --name=pkg{} --version=0.1".format(i)) conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): settings = "os" requires = {} generators = "VirtualRunEnv" """) requires = ", ".join('"pkg{}/0.1"'.format(i) for i in range(num_deps)) conanfile = conanfile.format(requires) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -c tools.env.virtualenv:powershell=True") assert os.path.isfile(os.path.join(client.current_folder, "conanrunenv.ps1")) assert not os.path.isfile(os.path.join(client.current_folder, "conanrunenv.bat")) for i in range(num_deps): cmd = environment_wrap_command(ConanFileMock(), "conanrunenv", client.current_folder, "mycompiler{}.bat".format(i)) # if num_deps > 50: # to be safe if we change the "num_deps" number # client.run_command(cmd, assert_error=True) # assert "is not recognized as an internal" in client.out client.run_command(cmd) assert "MYTOOL {}!!".format(i) in client.out # Test .bats now client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install .") assert not os.path.isfile(os.path.join(client.current_folder, "conanrunenv.ps1")) assert os.path.isfile(os.path.join(client.current_folder, "conanrunenv.bat")) for i in range(num_deps): cmd = environment_wrap_command(ConanFileMock(), "conanrunenv", client.current_folder, "mycompiler{}.bat".format(i)) # if num_deps > 50: # to be safe if we change the "num_deps" number # client.run_command(cmd, assert_error=True) # # This also fails, but without an error message (in my terminal, it kills the terminal!) # else: client.run_command(cmd) assert "MYTOOL {}!!".format(i) in client.out @pytest.mark.parametrize("deactivation_mode", ["function", None]) def test_profile_build_env_spaces(deactivation_mode): display_bat = textwrap.dedent("""\ @echo off echo VAR1=%VAR1%!! """) display_sh = textwrap.dedent("""\ echo VAR1=$VAR1!! """) client = TestClient() client.save({"conanfile.txt": "", "profile": "[buildenv]\nVAR1 = VALUE1", "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) client.run(f"install . -g VirtualBuildEnv -pr=profile {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() assert "VAR1= VALUE1!!" in out assert "Restoring environment" in out assert "VAR1=!!" in out def test_deactivate_location(): conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import Environment class Pkg(ConanFile): def package_info(self): self.buildenv_info.define("FOO", "BAR") """) client = TestClient() client.save({"pkg.py": conanfile}) client.run("create pkg.py --name pkg --version 1.0") client.run("install --requires pkg/1.0@ -g VirtualBuildEnv -of=myfolder -s build_type=Release -s arch=x86_64") source_cmd, script_ext = ("myfolder\\", ".bat") if platform.system() == "Windows" else (". ./myfolder/", ".sh") cmd = "{}conanbuild{}".format(source_cmd, script_ext) subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() assert not os.path.exists(os.path.join(client.current_folder, "deactivate_conanbuildenv-release-x86_64{}".format(script_ext))) assert os.path.exists(os.path.join(client.current_folder, "myfolder", "deactivate_conanbuildenv-release-x86_64{}".format(script_ext))) @pytest.mark.skipif(platform.system() == "Windows", reason="Requires sh") def test_skip_virtualbuildenv_run(): # Build require conanfile = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.buildenv_info.define("FOO", "BAR") """) client = TestClient() client.save({"pkg.py": conanfile}) client.run("create pkg.py --name pkg --version 1.0") # consumer conanfile = textwrap.dedent(r""" import os from conan import ConanFile class Consumer(ConanFile): tool_requires = "pkg/1.0" exports_sources = "my_script.sh" # This can be removed at Conan 2 generators = "VirtualBuildEnv" def build(self): path = os.path.join(self.source_folder, "my_script.sh") os.chmod(path, 0o777) self.run("'{}'".format(path)) """) my_script = 'echo FOO is $FOO' client.save({"conanfile.py": conanfile, "my_script.sh": my_script}) client.run("create . --name consumer --version 1.0") assert "FOO is BAR" in client.out # If we pass env=None no "conanbuild" is applied # self.run("'{}'".format(path), env=None) conanfile = conanfile.replace(".format(path))", ".format(path), env=None)") client.save({"conanfile.py": conanfile}) client.run("create . --name consumer --version 1.0") assert "FOO is BAR" not in client.out def test_files_always_created(): """ test that even if there are no env-variables, the generators always create files, they will be mostly empty, but exist """ c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "consumer/conanfile.txt": "[requires]\ndep/0.1"}) c.run("create dep") c.run("install consumer -g VirtualBuildEnv -g VirtualRunEnv -of=.") ext = "bat" if platform.system() == "Windows" else "sh" arch = c.get_default_host_profile().settings['arch'] assert os.path.isfile(os.path.join(c.current_folder, f"conanbuild.{ext}")) assert os.path.isfile(os.path.join(c.current_folder, f"conanrun.{ext}")) assert os.path.isfile(os.path.join(c.current_folder, f"conanbuildenv-release-{arch}.{ext}")) assert os.path.isfile(os.path.join(c.current_folder, f"conanbuildenv-release-{arch}.{ext}")) def test_error_with_dots_virtualenv(): # https://github.com/conan-io/conan/issues/12163 tool = textwrap.dedent(r""" from conan import ConanFile class ToolConan(ConanFile): name = "tool" version = "0.1" settings = "arch", "os" def package_info(self): self.buildenv_info.define("DUMMY", "123456") """) test_package = textwrap.dedent(r""" from conan import ConanFile import os class ToolTestConan(ConanFile): name = "app" version = "0.1" settings = "os", "compiler", "build_type", "arch" generators = "VirtualBuildEnv" def build_requirements(self): self.tool_requires("tool/0.1") def build(self): self.run("echo DUMMY=$DUMMY") self.run("set") """) client = TestClient() client.save({"dep/conanfile.py": tool, "consumer/conanfile.py": test_package}) client.run("create dep -s:b arch=armv8.3") client.run("create consumer -s arch=armv8.3") assert "DUMMY=123456" in client.out def test_runenv_info_propagated(): """ A runenv_info in a recipe, which is required by an executable that is used in another place as a tool_require (also its own test_package), should be propagated test_package --(tool_requires)->tools-(requires)->lib(defines runenv_info) https://github.com/conan-io/conan/issues/12939 """ c = TestClient() # NOTE: The lib contains ``package_type = "shared-library"`` to force propagation of runenv_info lib = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): name = "lib" version = "0.1" settings = "build_type" package_type = "shared-library" def package_info(self): self.runenv_info.define("MYLIBVAR", f"MYLIBVALUE:{self.settings.build_type}") """) tool_test_package = textwrap.dedent(""" import platform from conan import ConanFile class TestTool(ConanFile): settings = "build_type" generators = "VirtualBuildEnv" def build_requirements(self): self.tool_requires(self.tested_reference_str) def build(self): self.output.info(f"Building TEST_PACKAGE IN {self.settings.build_type}!!") if platform.system()!= "Windows": self.run("echo MYLIBVAR=$MYLIBVAR") else: self.run("set MYLIBVAR") def test(self): pass """) c.save({"lib/conanfile.py": lib, "tool/conanfile.py": GenConanfile("tool", "0.1").with_requires("lib/0.1"), "tool/test_package/conanfile.py": tool_test_package}) c.run("create lib -s build_type=Release") c.run("create tool --build-require -s:b build_type=Release -s:h build_type=Debug") assert "tool/0.1 (test package): Building TEST_PACKAGE IN Debug!!" in c.out assert "MYLIBVAR=MYLIBVALUE:Release" in c.out def test_deactivate_relocatable_substitute(): c = TestClient() # this cannot be tested in CI, because permissions over root folder # c.current_folder = "/build" c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("install . -s os=Linux -s:b os=Linux") conanbuild = c.load("conanbuildenv.sh") result = os.path.join("$script_folder", "deactivate_conanbuildenv.sh") assert f'"{result}"' in conanbuild class TestDotEnv: def test_dotenv(self): c = TestClient() other_path = os.path.join(c.current_folder, "my", "rel", "path") myprofile = textwrap.dedent(f""" [buildenv] MYVAR1=MyVal1 MYVAR3+=MyVal3 MYPATH+=(path)/some/path/here MYOTHER_PATH=+(path){other_path} [runenv] MYRUNVAR=SomeVal1 [conf] tools.env:dotenv=True """) c.save({"conanfile.txt": "", "myprofile": myprofile}) c.run("install . -pr=myprofile -s build_type=Release") dotenv = c.load("conanbuildenv-Release.env") expected = os.path.join(c.current_folder, "my", "rel", "path") assert f'MYOTHER_PATH="{expected}"' in dotenv assert f'MYPATH="/some/path/here"' in dotenv assert 'MYVAR3="MyVal3"' in dotenv assert 'MYVAR1="MyVal1"' in dotenv dotenv = c.load("conanrunenv-Release.env") assert f'MYRUNVAR="SomeVal1"' in dotenv def test_generate_only_dotenv(self): # If for some reason a recipe only wants the dot env files, doable c = TestClient() myprofile = textwrap.dedent(f""" [buildenv] MYVAR1=MyVal1 [runenv] MYRUNVAR=SomeVal1 """) conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.env import VirtualBuildEnv, VirtualRunEnv class Lib(ConanFile): def generate(self): VirtualBuildEnv(self).vars().save_dotenv("mybuild.env") VirtualRunEnv(self) """) c.save({"conanfile.py": conanfile, "myprofile": myprofile}) c.run("install . -pr=myprofile") assert set(os.listdir(c.current_folder)) == {'conanfile.py', 'mybuild.env', 'myprofile'} assert 'MYVAR1="MyVal1"' in c.load("mybuild.env") @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires shell") @pytest.mark.parametrize("path", [False, True]) def test_hardened_sh(path): c = TestClient() echo_sh = textwrap.dedent(""" set -eu . ./conanbuild.sh echo "MYENVVAR1=$MYENVVAR1!!" echo "MYENVVAR2=$MYENVVAR2!!" echo "MYENVVAR3=$MYENVVAR3!!" """) path = "(path)" if path else "" myprofile = textwrap.dedent(f""" [buildenv] MYENVVAR1=+{path}value1 MYENVVAR2+={path}value2 MYENVVAR3=+{path}value3 MYENVVAR3+={path}value4 """) c.save({"conanfile.txt": "", "myprofile": myprofile, "myecho.sh": echo_sh}) os.chmod(os.path.join(c.current_folder, "myecho.sh"), 0o777) c.run("install . -pr=myprofile") c.run_command("./myecho.sh") sep = ":" if path else " " assert "MYENVVAR1=value1!!" in c.out assert "MYENVVAR2=value2!!" in c.out assert f"MYENVVAR3=value3{sep}value4!!" in c.out with environment_update({"MYENVVAR1": "init1", "MYENVVAR2": "init2", "MYENVVAR3": "init3"}): c.run_command("./myecho.sh") assert f"MYENVVAR1=value1{sep}init1!!" in c.out assert f"MYENVVAR2=init2{sep}value2!!" in c.out assert f"MYENVVAR3=value3{sep}init3{sep}value4!!" in c.out ================================================ FILE: test/integration/environment/test_runenv_profile.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.fixture def client(): conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): generators = "VirtualRunEnv" def generate(self): for var in (1, 2): v = self.runenv.vars(self).get("MyVar{}".format(var)) self.output.info("MyVar{}={}!!".format(var, v)) """) profile1 = textwrap.dedent(""" [runenv] MyVar1=MyValue1_1 MyVar2=MyValue2_1 """) client = TestClient() client.save({"conanfile.py": conanfile, "profile1": profile1}) return client def test_buildenv_profile_cli(client): client.run("install . -pr=profile1") assert "conanfile.py: MyVar1=MyValue1_1!!" in client.out assert "conanfile.py: MyVar2=MyValue2_1!!" in client.out env = client.load("conanrunenv.sh") assert "MyValue1_1" in env assert "MyValue2_1" in env ================================================ FILE: test/integration/extensions/__init__.py ================================================ ================================================ FILE: test/integration/extensions/hooks/__init__.py ================================================ ================================================ FILE: test/integration/extensions/hooks/hook_test.py ================================================ import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save complete_hook = """ import os def pre_export(conanfile): conanfile.output.info("Hello") # TODO: To have the export_folder here needs a bit more deep refactoring assert conanfile.export_folder is None assert conanfile.recipe_folder, "recipe_folder not defined" def post_export(conanfile): conanfile.output.info("Hello") assert conanfile.export_folder, "export_folder not defined" assert conanfile.export_sources_folder, "export_sources_folder not defined" assert conanfile.recipe_folder, "recipe_folder not defined" def pre_validate(conanfile): conanfile.output.info("Hello") def post_validate(conanfile): conanfile.output.info("Hello") def pre_source(conanfile): conanfile.output.info("Hello") assert conanfile.source_folder, "source_folder not defined" def post_source(conanfile): conanfile.output.info("Hello") assert conanfile.source_folder, "source_folder not defined" def pre_generate(conanfile): conanfile.output.info("Hello") assert conanfile.generators_folder, "generators_folder not defined" def post_generate(conanfile): conanfile.output.info("Hello") assert conanfile.generators_folder, "generators_folder not defined" def pre_build(conanfile): conanfile.output.info("Hello") assert conanfile.source_folder, "source_folder not defined" assert conanfile.build_folder, "build_folder not defined" def post_build(conanfile): conanfile.output.info("Hello") assert conanfile.source_folder, "source_folder not defined" assert conanfile.build_folder, "build_folder not defined" def pre_package(conanfile): conanfile.output.info("Hello") assert conanfile.source_folder, "source_folder not defined" assert conanfile.build_folder, "build_folder not defined" assert conanfile.package_folder, "package_folder not defined" def post_package(conanfile): conanfile.output.info("Hello") assert conanfile.source_folder, "source_folder not defined" assert conanfile.build_folder, "build_folder not defined" assert conanfile.package_folder, "package_folder not defined" def pre_package_info(conanfile): conanfile.output.info("Hello") def post_package_info(conanfile): conanfile.output.info("Hello") def post_package_id(conanfile): conanfile.output.info("Hello") """ class TestHooks: def test_complete_hook(self): c = TestClient() hook_path = os.path.join(c.paths.hooks_path, "complete_hook", "hook_complete.py") save(hook_path, complete_hook) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("source .") hook_msg = "[HOOK - complete_hook/hook_complete.py]" assert f"conanfile.py (pkg/0.1): {hook_msg} pre_source(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_source(): Hello" in c.out c.run("install .") assert f"conanfile.py (pkg/0.1): {hook_msg} pre_validate(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_validate(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} pre_generate(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_generate(): Hello" in c.out c.run("build .") assert f"conanfile.py (pkg/0.1): {hook_msg} pre_validate(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_validate(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} pre_build(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_build(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} pre_generate(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_generate(): Hello" in c.out c.run("export . ") assert f"pkg/0.1: {hook_msg} pre_export(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_export(): Hello" in c.out c.run("export-pkg . ") assert f"pkg/0.1: {hook_msg} pre_export(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_export(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} pre_package(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_package(): Hello" in c.out assert f"conanfile.py (pkg/0.1): {hook_msg} post_package_id(): Hello" in c.out c.run("create . ") assert f"pkg/0.1: {hook_msg} pre_validate(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_validate(): Hello" in c.out assert f"pkg/0.1: {hook_msg} pre_export(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_export(): Hello" in c.out assert f"pkg/0.1: {hook_msg} pre_source(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_source(): Hello" in c.out assert f"pkg/0.1: {hook_msg} pre_build(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_build(): Hello" in c.out assert f"pkg/0.1: {hook_msg} pre_package(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_package(): Hello" in c.out assert f"pkg/0.1: {hook_msg} pre_package_info(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_package_info(): Hello" in c.out assert f"pkg/0.1: {hook_msg} post_package_id(): Hello" in c.out def test_import_hook(self): """ Test that a hook can import another random python file """ custom_module = textwrap.dedent(""" def my_printer(output): output.info("my_printer(): CUSTOM MODULE") """) my_hook = textwrap.dedent(""" from custom_module.custom import my_printer def pre_export(conanfile): my_printer(conanfile.output) """) c = TestClient() hook_path = os.path.join(c.paths.hooks_path, "my_hook", "hook_my_hook.py") init_path = os.path.join(c.paths.hooks_path, "my_hook", "custom_module", "__init__.py") custom_path = os.path.join(c.paths.hooks_path, "my_hook", "custom_module", "custom.py") c.save({init_path: "", custom_path: custom_module, hook_path: my_hook, "conanfile.py": GenConanfile("pkg", "1.0")}) c.run("export . ") assert "[HOOK - my_hook/hook_my_hook.py] pre_export(): my_printer(): CUSTOM MODULE" \ in c.out def test_hook_raising(self): """ Test output when a hook raises """ c = TestClient() my_hook = textwrap.dedent(""" def pre_export(conanfile): raise Exception("Boom") """) hook_path = os.path.join(c.paths.hooks_path, "my_hook", "hook_my_hook.py") c.save({hook_path: my_hook, "conanfile.py": GenConanfile("pkg", "1.0")}) c.run("export . ", assert_error=True) assert "ERROR: [HOOK - my_hook/hook_my_hook.py] pre_export(): Boom" in c.out def test_post_build_fail(self): """ Test the post_build_fail hook """ c = TestClient() my_hook = textwrap.dedent(""" def post_build_fail(conanfile): conanfile.output.info("Hello") """) hook_path = os.path.join(c.paths.hooks_path, "my_hook", "hook_my_hook.py") save(hook_path, my_hook) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def build(self): raise Exception("Boom!") """) c.save({"conanfile.py": conanfile}) c.run("build . ", assert_error=True) assert "conanfile.py: [HOOK - my_hook/hook_my_hook.py] post_build_fail(): Hello" in c.out assert "ERROR: conanfile.py: Error in build() method, line 5" in c.out def test_validate_hook(self): """ The validate hooks are executed only if the method is declared in the recipe. """ c = TestClient() hook_path = os.path.join(c.paths.hooks_path, "testing", "hook_complete.py") save(hook_path, complete_hook) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def validate(self): pass """) c.save({"conanfile.py": conanfile}) c.run("create . --name=foo --version=0.1.0") assert f"foo/0.1.0: [HOOK - testing/hook_complete.py] pre_validate(): Hello" in c.out assert f"foo/0.1.0: [HOOK - testing/hook_complete.py] post_validate(): Hello" in c.out @pytest.mark.parametrize("hook_name", ["pre_validate", "post_validate"]) def test_validate_invalid_configuration(hook_name): """ When raising ConanInvalidConfiguration in the pre_validate and post_validate hooks, it should be forwarded and preserve the same exception type. """ hook = textwrap.dedent(f""" from conan.errors import ConanInvalidConfiguration def {hook_name}(conanfile): raise ConanInvalidConfiguration("Invalid configuration in {hook_name} hook") """) conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.errors import ConanException class Pkg(ConanFile): def validate(self): if "{hook_name}" == "pre_validate": raise ConanException("Should not reach this point") """) client = TestClient() hook_path = os.path.join(client.paths.hooks_path, "custom_hooks", "hook_pre_validate.py") client.save({"conanfile.py": conanfile, hook_path: hook}) client.run("build . ", assert_error=True) assert f"ERROR: conanfile.py: Invalid ID: Invalid: Invalid configuration in {hook_name} hook" in client.out assert "Should not reach this point" not in client.out ================================================ FILE: test/integration/extensions/hooks/test_post_export.py ================================================ import os import textwrap from conan.internal.model.manifest import FileTreeManifest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save def test_called_before_digest(): """ Test that 'post_export' hook is called before computing the digest of the exported folders """ t = TestClient() complete_hook = textwrap.dedent("""\ import os from conan.tools.files import save def post_export(conanfile): save(conanfile, os.path.join(conanfile.export_folder, "myfile.txt"), "content!!") """) hook_path = os.path.join(t.paths.hooks_path, "complete_hook", "hook_complete.py") save(hook_path, complete_hook) t.save({'conanfile.py': GenConanfile("pkg", "0.1")}) t.run("export .") ref_layout = t.exported_layout() manifest = FileTreeManifest.load(ref_layout.export()) assert "myfile.txt" in manifest.file_sums ================================================ FILE: test/integration/extensions/hooks/test_post_package.py ================================================ import os import textwrap from conan.internal.model.manifest import FileTreeManifest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save def test_post_package(): """ Test that 'post_package' hook is called before computing the manifest """ t = TestClient() complete_hook = textwrap.dedent("""\ import os from conan.tools.files import save def post_package(conanfile): save(conanfile, os.path.join(conanfile.package_folder, "myfile.txt"), "content!!") """) hook_path = os.path.join(t.paths.hooks_path, "complete_hook", "hook_complete.py") save(hook_path, complete_hook) t.save({'conanfile.py': GenConanfile("pkg", "0.1")}) t.run("create .") pref_layout = t.created_layout() manifest = FileTreeManifest.load(pref_layout.package()) assert "myfile.txt" in manifest.file_sums t.run("remove * -c") t.run("export-pkg .") pref_layout = t.created_layout() manifest = FileTreeManifest.load(pref_layout.package()) assert "myfile.txt" in manifest.file_sums ================================================ FILE: test/integration/extensions/test_cppstd_compat.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_compatible_cppstd(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler" def package_info(self): self.output.info("PackageInfo!: Cppstd version: %s!" % self.settings.compiler.cppstd) """) profile = textwrap.dedent(""" [settings] os = Linux compiler=gcc compiler.version=12 compiler.libcxx=libstdc++ """) c.save({"conanfile.py": conanfile, "myprofile": profile}) # Create package with cppstd 17 c.run("create . -pr=myprofile -s compiler.cppstd=17") package_id = "95dcfeb51c04968b4ee960ee393cf2c1ebcf7782" assert f"pkg/0.1: Package '{package_id}' created" in c.out # package can be used with a profile gcc cppstd20 falling back to 17 c.save({"conanfile.py": GenConanfile().with_require("pkg/0.1")}) c.run("install . -pr=myprofile -s compiler.cppstd=20") assert f"Using compatible package '{package_id}'" assert "pkg/0.1: PackageInfo!: Cppstd version: 17!" in c.out c.assert_listed_binary({"pkg/0.1": (f"{package_id}", "Cache")}) assert "pkg/0.1: Already installed!" in c.out def test_compatible_cppstd_missing_compiler(): c = TestClient() settings_user = textwrap.dedent(""" compiler: mycompiler: version: ["12"] libcxx: ["libstdc++"] cppstd: [15, 17, 20] """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler" def package_info(self): self.output.info("PackageInfo!: Cppstd version: %s!" % self.settings.compiler.cppstd) """) profile = textwrap.dedent(""" [settings] os = Linux compiler=mycompiler compiler.version=12 compiler.libcxx=libstdc++ """) c.save({"conanfile.py": conanfile, "myprofile": profile}) c.save_home({"settings_user.yml": settings_user}) # Create package with cppstd 17 c.run("create . -pr=myprofile -s compiler.cppstd=17") package_id = "51a90090adb1cbd330a64b4c3b3b32af809af4f9" assert f"pkg/0.1: Package '{package_id}' created" in c.out # package can't be used with cppstd 20 and fallback to 17, because mycompiler is not known # to the default cppstd_compat function. Ensure that it does not break and warns the user c.save({"conanfile.py": GenConanfile().with_require("pkg/0.1")}) c.run("install . -pr=myprofile -s compiler.cppstd=20", assert_error=True) assert "Missing binary: pkg/0.1:b4b07859713551e8aac612f8080888c58b4711ae" in c.out assert 'pkg/0.1: WARN: No cppstd compatibility defined for compiler "mycompiler"' in c.out ================================================ FILE: test/integration/extensions/test_plugin_cmd_wrapper.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save def test_plugin_cmd_wrapper(): c = TestClient() plugins = os.path.join(c.cache_folder, "extensions", "plugins") wrapper = textwrap.dedent(""" def cmd_wrapper(cmd, **kwargs): return 'echo "{}"'.format(cmd) """) save(os.path.join(plugins, "cmd_wrapper.py"), wrapper) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def generate(self): self.run("Hello world") self.run("Other stuff") """) c.save({"conanfile.py": conanfile}) c.run("install .") assert 'Hello world' in c.out assert 'Other stuff' in c.out def test_plugin_cmd_wrapper_conanfile(): """ we can get the name of the caller conanfile too """ c = TestClient() plugins = os.path.join(c.cache_folder, "extensions", "plugins") wrapper = textwrap.dedent(""" def cmd_wrapper(cmd, conanfile, **kwargs): return 'echo "{}!:{}!"'.format(conanfile.ref, cmd) """) save(os.path.join(plugins, "cmd_wrapper.py"), wrapper) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def generate(self): self.run("Hello world") """) c.save({"conanfile.py": conanfile}) c.run("install . --name=pkg --version=0.1") assert 'pkg/0.1!:Hello world!' in c.out def test_plugin_profile_error_vs(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "1.0")}) c.run("create . -s compiler=msvc -s compiler.version=15 -s compiler.cppstd=14", assert_error=True) assert "The provided compiler.cppstd=14 is not supported by msvc 15. Supported values are: []" in c.out c.run("create . -s compiler=msvc -s compiler.version=170 -s compiler.cppstd=14", assert_error=True) assert "The provided compiler.cppstd=14 is not supported by msvc 170. Supported values are: []" in c.out c.run("create . -s compiler=msvc -s compiler.version=190 -s compiler.cppstd=14") assert "Installing packages" in c.out def test_plugin_profile_error_vscstd(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_class_attribute("languages = 'C'")}) c.run("create . -s compiler=msvc -s compiler.version=190 -s compiler.cstd=23", assert_error=True) assert "The provided compiler.cstd=23 is not supported by msvc 190. Supported values are: []" in c.out c.run("create . -s compiler=msvc -s compiler.version=193 -s compiler.cstd=23", assert_error=True) assert "The provided compiler.cstd=23 is not supported by msvc 193. Supported values are: ['11', '17']" in c.out c.run("create . -s compiler=msvc -s compiler.version=193 -s compiler.cstd=17") assert "Installing packages" in c.out ================================================ FILE: test/integration/extensions/test_profile_plugin.py ================================================ import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("compiler, version, cppstd, correct", [ ("gcc", "4.1", "11", False), ("gcc", "4.4", "11", True), ("gcc", "4.4", "14", False), ("gcc", "4.4", "17", False), ("gcc", "4.4", "20", False), ("gcc", "4.8", "11", True), ("gcc", "4.8", "14", True), ("gcc", "4.8", "17", False), ("gcc", "4.8", "20", False), ("gcc", "5", "17", True), ("gcc", "5", "20", False), ("gcc", "8", "17", True), ("gcc", "8", "20", True), ("clang", "3.3", "11", True), ("clang", "3.3", "14", False), ("clang", "3.4", "14", True), ("clang", "3.4", "17", False), ("clang", "3.5", "17", True), ("clang", "3.5", "20", False), ("clang", "5.0", "20", False), ("clang", "6.0", "20", True), ("apple-clang", "5.0", "98", True), ("apple-clang", "5.0", "11", True), ("apple-clang", "5.1", "14", True), ("apple-clang", "5.1", "17", False), ("apple-clang", "6.1", "17", True), ("apple-clang", "6.1", "20", False), ("apple-clang", "10.0", "20", True), ("msvc", "170", "14", False), ("msvc", "190", "14", True), ("msvc", "190", "20", False), ("msvc", "191", "14", True), ("msvc", "191", "17", True), ("msvc", "191", "20", False), ("msvc", "193", "20", True) ]) def test_invalid_cppstd(compiler, version, cppstd, correct): c = TestClient() c.save({"conanfile.py": GenConanfile()}) c.run("install . -s compiler={} -s compiler.version={} " "-s compiler.cppstd={}".format(compiler, version, cppstd), assert_error=not correct) if not correct: assert "ConanException: The provided compiler.cppstd" in c.out ================================================ FILE: test/integration/extensions/test_profile_plugin_runtime.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Only windows") class TestConanSettingsPreprocessor: def test_runtime_auto(self): c = TestClient() # Ensure that compiler.runtime is now in the default Win MSVC profile default_profile = c.load_home("profiles/default") assert "compiler.runtime" in default_profile conanfile = textwrap.dedent("""\ from conan import ConanFile class HelloConan(ConanFile): name = "hello0" version = "0.1" settings = "os", "compiler", "build_type" def configure(self): self.output.info(f"Runtime_type: {self.settings.compiler.runtime_type}") """) c.save({"conanfile.py": conanfile}) c.run("create .") assert "Runtime_type: Release" in c.out c.run("create . -s build_type=Debug") assert "Runtime_type: Debug" in c.out def test_runtime_not_present_ok(self): c = TestClient() c.save({"conanfile.txt": ""}) c.run("install .") default_settings = c.load_home("settings.yml") default_settings = default_settings.replace("runtime:", "# runtime:") default_settings = default_settings.replace("runtime_type:", "# runtime_type:") profile = "[settings]\nos=Windows\ncompiler=msvc\ncompiler.version=191" c.save_home({"settings.yml": default_settings, "profiles/default": profile}) # Ensure the runtime setting is not there anymore c.run('install . -s compiler.runtime="dynamic"', assert_error=True) assert "'settings.compiler.runtime' doesn't exist for 'msvc'" in c.out # Now install, the preprocessor shouldn't fail nor do anything c.run("install .") assert "Installing packages" in c.out ================================================ FILE: test/integration/generators/__init__.py ================================================ ================================================ FILE: test/integration/generators/generators_test.py ================================================ from conan.test.utils.tools import TestClient class TestGenerators: def test_error(self): client = TestClient() client.save({"conanfile.txt": "[generators]\nunknown"}) client.run("install . --build=*", assert_error=True) assert "ERROR: Invalid generator 'unknown'. Available types:" in client.out ================================================ FILE: test/integration/generators/order_libs_test.py ================================================ import platform from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_library_order(): # # project -> sdl2_ttf -> freetype ------------> bzip2 # \ \----->libpng -> zlib # \------> sdl2----------------->/ # Order: sdl2_ttf freetype sdl2 libpng zlib bzip c = TestClient() def _export(libname, refs=None): conanfile = GenConanfile(libname, "0.1").with_package_info(cpp_info={"libs": [libname]}) for r in refs or []: conanfile.with_requires(f"{r}/0.1") c.save({"conanfile.py": conanfile}, clean_first=True) c.run("export .") _export("zlib") _export("bzip2") _export("sdl2", ["zlib"]) _export("libpng", ["zlib"]) _export("freetype", ["bzip2", "libpng"]) _export("sdl2_ttf", ["freetype", "sdl2"]) _export("app", ["sdl2_ttf"]) c.run("install . --build missing -g AutotoolsDeps") deps = "conanautotoolsdeps.bat" if platform.system() == "Windows" else "conanautotoolsdeps.sh" autotoolsdeps = c.load(deps) assert '-lsdl2_ttf -lfreetype -lsdl2 -llibpng -lzlib -lbzip2' in autotoolsdeps # TODO: Add a test that manages too system libs ================================================ FILE: test/integration/generators/test_custom_global_generators.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save def test_custom_global_generator(): c = TestClient() generator = textwrap.dedent(""" class MyCustomGenerator: def __init__(self, conanfile): self._conanfile = conanfile def generate(self): pkg = self._conanfile.dependencies["pkg"].ref self._conanfile.output.info(f"DEP: {pkg}!!") """) save(os.path.join(c.paths.custom_generators_path, "mygen.py"), generator) conanfile = textwrap.dedent(""" [requires] pkg/0.1 [generators] MyCustomGenerator """) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"), "conanfile.txt": conanfile}) c.run("create pkg") c.run("install .") assert "conanfile.txt: Generator 'MyCustomGenerator' calling 'generate()'" in c.out assert "conanfile.txt: DEP: pkg/0.1!!" in c.out # By CLI also works conanfile = textwrap.dedent(""" [requires] pkg/0.1 """) c.save({"conanfile.txt": conanfile}) c.run("install . -g MyCustomGenerator") assert "conanfile.txt: Generator 'MyCustomGenerator' calling 'generate()'" in c.out assert "conanfile.txt: DEP: pkg/0.1!!" in c.out # In conanfile.py also works conanfile = textwrap.dedent(""" from conan import ConanFile class MyPkg(ConanFile): requires = "pkg/0.1" generators = "MyCustomGenerator" """) c.save({"conanfile.py": conanfile}, clean_first=True) c.run("install . ") assert "conanfile.py: Generator 'MyCustomGenerator' calling 'generate()'" in c.out assert "conanfile.py: DEP: pkg/0.1!!" in c.out def test_custom_global_generator_imports(): """ our custom generator can use python imports """ c = TestClient() generator = textwrap.dedent(""" from _myfunc import mygenerate class MyCustomGenerator: def __init__(self, conanfile): self._conanfile = conanfile def generate(self): mygenerate(self._conanfile) """) myaux = textwrap.dedent(""" def mygenerate(conanfile): conanfile.output.info("MYGENERATE WORKS!!") """) save(os.path.join(c.paths.custom_generators_path, "mygen.py"), generator) save(os.path.join(c.paths.custom_generators_path, "_myfunc.py"), myaux) c.save({"conanfile.txt": ""}) c.run("install . -g MyCustomGenerator") assert "conanfile.txt: Generator 'MyCustomGenerator' calling 'generate()'" in c.out assert "conanfile.txt: MYGENERATE WORKS!!" in c.out def test_custom_global_generator_multiple(): """ multiple custom generators with different names load correctly """ c = TestClient() for num in range(3): generator = textwrap.dedent(f""" class MyGenerator{num}: def __init__(self, conanfile): self._conanfile = conanfile def generate(self): self._conanfile.output.info(f"MyGenerator{num}!!") """) save(os.path.join(c.paths.custom_generators_path, f"mygen{num}.py"), generator) conanfile = textwrap.dedent(""" [requires] pkg/0.1 [generators] MyGenerator0 MyGenerator1 MyGenerator2 """) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"), "conanfile.txt": conanfile}) c.run("create pkg") c.run("install .") assert "conanfile.txt: Generator 'MyGenerator0' calling 'generate()'" in c.out assert "conanfile.txt: Generator 'MyGenerator1' calling 'generate()'" in c.out assert "conanfile.txt: Generator 'MyGenerator2' calling 'generate()'" in c.out assert "conanfile.txt: MyGenerator0!!" in c.out assert "conanfile.txt: MyGenerator1!!" in c.out assert "conanfile.txt: MyGenerator2!!" in c.out ================================================ FILE: test/integration/generators/test_generators_from_br.py ================================================ import json import textwrap from conan.test.utils.tools import GenConanfile, TestClient def test_inject_generators_conf(): tc = TestClient(light=True) tc.save({"tool/conanfile.py": textwrap.dedent(""" from conan import ConanFile class MyGenerator: def __init__(self, conanfile): self.conanfile = conanfile def generate(self): self.conanfile.output.info("MyGenerator generated") class ToolConan(ConanFile): name = "tool" version = "0.1" def package_info(self): self.generator_info = ["CMakeToolchain", MyGenerator] """)}) tc.save({"consumer/conanfile.py": GenConanfile("consumer", "0.1") .with_tool_requires("tool/0.1") .with_class_attribute("generators = 'VirtualBuildEnv', 'VirtualRunEnv'")}) tc.run("create tool") tc.run("create consumer") assert "WARN: experimental: Tool-require tool/0.1 adding generators: " \ "['CMakeToolchain', 'MyGenerator']" in tc.out assert "Generator 'CMakeToolchain' calling 'generate()'" in tc.out assert "Generator 'MyGenerator' calling 'generate()'" in tc.out assert "Generator 'VirtualBuildEnv' calling 'generate()'" in tc.out assert "Generator 'VirtualRunEnv' calling 'generate()'" in tc.out assert "CMakeToolchain generated: conan_toolchain.cmake" in tc.out assert "MyGenerator generated" in tc.out def test_inject_generators_error(): c = TestClient(light=True) c.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class ToolConan(ConanFile): name = "tool" version = "0.1" def package_info(self): self.generator_info = "CMakeToolchain" """)}) c.run("create .") c.run("install --tool-requires=tool/0.1", assert_error=True) assert "ERROR: tool/0.1 'generator_info' must be a list" in c.out def test_inject_vars(): tc = TestClient(light=True) tc.save({ "tool/envars_generator1.sh": textwrap.dedent(""" export VAR1="value1" export PATH="$PATH:/additional/path" """), "tool/envars_generator2.sh": textwrap.dedent(""" export VAR2="value2" """), "tool/conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.env import create_env_script, register_env_script import os class GeneratorSet: def __init__(self, conanfile): self.conanfile = conanfile def generate(self): content = 'call envars_generator.sh' name = f"conantest.sh" create_env_script(self.conanfile, content, name) register_env_script(self.conanfile, "tool/envars_generator2.sh") class ToolConan(ConanFile): name = "tool" version = "0.1" def package_info(self): self.generator_info = [GeneratorSet] """)}) tc.run("create tool") tc.run(f"install --tool-requires=tool/0.1") content = tc.load("conanbuild.sh") assert "conantest.sh" in content assert "envars_generator2.sh" in content def test_json_serialization_error(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyGenerator: def __init__(self, conanfile): self._conanfile = conanfile def generate(self): pass class MyToolReq(ConanFile): name = "mygenerator-tool" version = "1.0" def package_info(self): self.generator_info = [MyGenerator] """) c.save({"tool/conanfile.py": conanfile}) c.run("create tool") c.run("install --tool-requires=mygenerator-tool/1.0 --format=json") graph = json.loads(c.stdout) assert graph["graph"]["nodes"]["0"]["generators"] == ["MyGenerator"] ================================================ FILE: test/integration/graph/__init__.py ================================================ ================================================ FILE: test/integration/graph/conflict_diamond_test.py ================================================ import json import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestConflictDiamondTest: def test_version_diamond_conflict(self): """ test that we obtain a version conflict with a diamond, and that we can fix it by defining an override in the "game" consumer game -> engine/1.0 -> math/1.0 |---> ai/1.0 -----> math/1.0.1 (conflict) """ c = TestClient(light=True) c.save({"math/conanfile.py": GenConanfile("math"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("math/1.0"), "ai/conanfile.py": GenConanfile("ai", "1.0").with_requires("math/1.0.1"), "game/conanfile.py": GenConanfile("game", "1.0").with_requires("engine/1.0", "ai/1.0"), }) c.run("create math --version=1.0") c.run("create math --version=1.0.1") c.run("create math --version=1.0.2") c.run("create engine") c.run("create ai") c.run("install game", assert_error=True) assert "Version conflict: Conflict between math/1.0.1 and math/1.0 in the graph" in c.out # This shouldnt error, so we are able to diagnose our dependency graph # The UX still need to be improved, but this is start c.run("graph info game --filter=requires", assert_error=True) assert "math/1.0" in c.out def _game_conanfile(version, reverse=False): if reverse: """ game ---(override)--_> math/newversion |---> engine/1.0 -> math/1.0 |---> ai/1.0 -----> math/1.0.1 (conflict solved by override) """ return GenConanfile("game", "1.0")\ .with_requirement(f"math/{version}", override=True)\ .with_requirement("engine/1.0")\ .with_requirement("ai/1.0") else: """ game --> engine/1.0 -> math/1.0 |---> ai/1.0 -----> math/1.0.1 (conflict solved by override) |---(override)--_> math/newversion """ return GenConanfile("game", "1.0").with_requirement("engine/1.0") \ .with_requirement("ai/1.0") \ .with_requirement(f"math/{version}", override=True) for v in ("1.0", "1.0.1", "1.0.2"): c.save({"game/conanfile.py": _game_conanfile(v)}) c.run("install game") c.assert_overrides({"math/1.0": [f"math/{v}"], "math/1.0.1": [f"math/{v}"]}) c.assert_listed_require({f"math/{v}": "Cache"}) # Check that order of requirements doesn't affect for v in ("1.0", "1.0.1", "1.0.2"): c.save({"game/conanfile.py": _game_conanfile(v, reverse=True)}) c.run("install game") c.assert_overrides({"math/1.0": [f"math/{v}"], "math/1.0.1": [f"math/{v}"]}) c.assert_listed_require({f"math/{v}": "Cache"}) c.run("install --requires=engine/1.0 --requires=ai/1.0", assert_error=True) assert "Conflict between math/1.0.1 and math/1.0 in the graph" in c.out assert "Conflict originates from ai/1.0" in c.out @pytest.mark.parametrize("version_range", [True, False]) def test_conflict_user(version_range): # https://github.com/conan-io/conan/issues/17875 v = "[^1.0]" if version_range else "1.0" c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_requires(f"dep/{v}@user1"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires(f"pkg/{v}@user1", f"dep/{v}@user2")}) c.run("create dep --user=user1") c.run("create dep --user=user2") c.run("create pkg --user=user1") c.run("install app", assert_error=True) assert f"Version conflict: Conflict between dep/{v}@user1 and dep/{v}@user2" in c.out def test_conflict_user_order(): # https://github.com/conan-io/conan/issues/17875 c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0@user1"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("pkg/1.0@user1", "dep/[>=1.0]@user2")}) c.run("create dep --user=user1") c.run("create dep --user=user2") c.run("create pkg --user=user1") c.run("install app", assert_error=True) assert "ERROR: Version conflict: Conflict between dep/1.0@user1 and dep/[>=1.0]@user2" in c.out @pytest.mark.parametrize("test", [True, False]) @pytest.mark.parametrize("order", [True, False]) class TestErrorVisibleFalse: def test_subgraph_conflict(self, order, test): # cli--> pkg1/1.0 -(visible=False) --------------> pkg3/1.0 (conflict) # \----> pkg2/1.0 --------------------> pkg3/1.1 (conflict) # This conflict is good, the default dependencies are incompatible in definition tc = TestClient(light=True) pkg1 = GenConanfile("pkg1", "1.0") if order: pkg1.with_requirement("pkg3/1.0", visible=False, test=test).with_requirement("pkg2/1.0") else: pkg1.with_requirement("pkg2/1.0").with_requirement("pkg3/1.0", visible=False, test=test) tc.save({"pkg3/conanfile.py": GenConanfile("pkg3"), "pkg2/conanfile.py": GenConanfile("pkg2", "1.0").with_requirement("pkg3/1.1"), "pkg1/conanfile.py": pkg1}) tc.run("export pkg3 --version=1.0") tc.run("export pkg3 --version=1.1") tc.run("export pkg2") # Creating this pkg1 does generate a conflict tc.run("export pkg1") tc.run("graph info --requires=pkg1/1.0", assert_error=True) assert "ERROR: Version conflict: Conflict between pkg3/1.1 and pkg3/1.0" in tc.out def test_subgraph_conflict_second_level(self, order, test): # cli--> pkg1/1.0 -(visible=False) ---> gtest/1.0 -> zlib/1.0 # \----> boost/1.0 ---------------------> zlib/1.1 (conflict) # This conflict is good, the default dependencies are incompatible in definition tc = TestClient(light=True) pkg = GenConanfile("pkg1", "1.0") if order: pkg.with_requirement("gtest/1.0", visible=False, test=test).with_requirement("boost/1.0") else: pkg.with_requirement("boost/1.0").with_requirement("gtest/1.0", visible=False, test=test) tc.save({"zlib/conanfile.py": GenConanfile("zlib"), "boost/conanfile.py": GenConanfile("boost", "1.0").with_requirement("zlib/1.1"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requirement("zlib/1.0"), "pkg1/conanfile.py": pkg}) tc.run("export zlib --version=1.0") tc.run("export zlib --version=1.1") tc.run("export boost") tc.run("export gtest") # Creating this pkg1 does generate a conflict tc.run("export pkg1") tc.run("graph info --requires=pkg1/1.0", assert_error=True) if order: assert "ERROR: Version conflict: Conflict between zlib/1.1 and zlib/1.0" in tc.out assert "Conflict originates from boost/1.0" in tc.out else: assert "ERROR: Version conflict: Conflict between zlib/1.0 and zlib/1.1" in tc.out assert "Conflict originates from gtest/1.0" in tc.out def test_subgraph_no_conflict(self, order, test): # cli--> pkg1/1.0 -(visible=False) --------------> pkg3/1.0 (no conflict) # \----> pkg2/1.0 --------------------> pkg3/1.0 (no conflict) # This doesn't conflict, but package topology is affected, converging to a direct dependency # of a visible one # cli--> pkg1/1.0 -(visible=True) --------------> pkg3/1.0 (no conflict) # \----> pkg2/1.0 ----------------------/ tc = TestClient(light=True) pkg1 = GenConanfile("pkg1", "1.0") if order: pkg1.with_requirement("pkg3/1.0", visible=False, test=test).with_requirement("pkg2/1.0") else: pkg1.with_requirement("pkg2/1.0").with_requirement("pkg3/1.0", visible=False, test=test) tc.save({"pkg3/conanfile.py": GenConanfile("pkg3"), "pkg2/conanfile.py": GenConanfile("pkg2", "1.0").with_requirement("pkg3/1.0"), "pkg1/conanfile.py": pkg1}) tc.run("export pkg3 --version=1.0") tc.run("export pkg2") tc.run("export pkg1") tc.run("graph info --requires=pkg1/1.0 --format=json") if not test: assert "WARN: risk: Packages required both with visible=True and visible=False" in tc.out assert "pkg3/1.0: Required by pkg1/1.0" in tc.out else: assert "WARN: risk" not in tc.out graph = json.loads(tc.stdout) assert len(graph["graph"]["nodes"]) == 4 # Including the CLI 0-3 pkg1 = graph["graph"]["nodes"]["1"] deps = pkg1["dependencies"] assert len(deps) == 2 assert "pkg1/1.0" in pkg1["ref"] if order: dep_pkg2 = deps["3"] dep_pkg3 = deps["2"] else: dep_pkg2 = deps["2"] dep_pkg3 = deps["3"] assert dep_pkg2["ref"] == "pkg2/1.0" assert dep_pkg2["visible"] is True assert dep_pkg3["ref"] == "pkg3/1.0" assert dep_pkg2["visible"] is True def test_subgraph_no_conflict_second_level(self, order, test): # cli--> pkg1/1.0 -(visible=False) ---> gtest/1.0 -> zlib/1.0 # \----> boost/1.0 ---------------------> zlib/1.0 (noconflict) tc = TestClient(light=True) pkg = GenConanfile("pkg1", "1.0") if order: pkg.with_requirement("gtest/1.0", visible=False, test=test).with_requirement("boost/1.0") else: pkg.with_requirement("boost/1.0").with_requirement("gtest/1.0", visible=False, test=test) tc.save({"zlib/conanfile.py": GenConanfile("zlib"), "boost/conanfile.py": GenConanfile("boost", "1.0").with_requirement("zlib/1.0"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requirement("zlib/1.0"), "pkg1/conanfile.py": pkg}) tc.run("export zlib --version=1.0") tc.run("export boost") tc.run("export gtest") # Creating this pkg1 does generate a conflict tc.run("export pkg1") tc.run("graph info --requires=pkg1/1.0 --format=json") assert "WARN: risk: Packages required both with visible=True and visible=False" not in tc.out graph = json.loads(tc.stdout) assert len(graph["graph"]["nodes"]) == 5 # Including the CLI 0-4 pkg1 = graph["graph"]["nodes"]["1"] deps = pkg1["dependencies"] assert len(deps) == 3 zlib = deps["3"] assert "zlib/1.0" in zlib["ref"] assert zlib["visible"] is True def test_transitive_conflict(self, order, test): # cli --------------------------------------------> pkg3/1.1 # \--> pkg1/1.0 -(visible=False) -> pkg3/1.0 (conflict) # \----> pkg2/1.0 --------------------> pkg3/1.1 (no conflict) tc = TestClient(light=True) pkg1 = GenConanfile("pkg1", "1.0") if order: pkg1.with_requirement("pkg3/1.0", visible=False, test=test).with_requirement("pkg2/1.0") else: pkg1.with_requirement("pkg2/1.0").with_requirement("pkg3/1.0", visible=False, test=test) tc.save({"pkg3/conanfile.py": GenConanfile("pkg3"), "pkg2/conanfile.py": GenConanfile("pkg2", "1.0").with_requirement("pkg3/1.1"), "pkg1/conanfile.py": pkg1}) tc.run("export pkg3 --version=1.0") tc.run("export pkg3 --version=1.1") tc.run("export pkg2") tc.run("export pkg1") tc.run("graph info --requires=pkg3/1.1 --requires=pkg1/1.0 --format=html", assert_error=True, redirect_stdout="graph.html") # Check that the graph.html is generated assert os.path.exists(os.path.join(tc.current_folder, "graph.html")) if order: assert "ERROR: Version conflict: Conflict between pkg3/1.1 and pkg3/1.0" in tc.out else: assert "ERROR: Version conflict: Conflict between pkg3/1.0 and pkg3/1.1" in tc.out assert "Conflict originates from pkg1/1.0" in tc.out def test_transitive_conflict_second_level(self, order, test): # cli --------------------------------------------> zlib/1.1 # \--> pkg1/1.0 -(visible=False) -gtest---------> zlib/1.0 (conflict) # \----> boost/1.0 -------------------> zlib/1.1 (no conflict) tc = TestClient(light=True) pkg = GenConanfile("pkg1", "1.0") if order: pkg.with_requirement("gtest/1.0", visible=False, test=test).with_requirement("boost/1.0") else: pkg.with_requirement("boost/1.0").with_requirement("gtest/1.0", visible=False, test=test) tc.save({"zlib/conanfile.py": GenConanfile("zlib"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requirement("zlib/1.0"), "boost/conanfile.py": GenConanfile("boost", "1.0").with_requirement("zlib/1.1"), "pkg1/conanfile.py": pkg}) tc.run("export zlib --version=1.0") tc.run("export zlib --version=1.1") tc.run("export gtest") tc.run("export boost") tc.run("export pkg1") tc.run("graph info --requires=zlib/1.1 --requires=pkg1/1.0 --format=html", assert_error=True, redirect_stdout="graph.html") # Check that the graph.html is generated assert os.path.exists(os.path.join(tc.current_folder, "graph.html")) if order: assert "ERROR: Version conflict: Conflict between zlib/1.1 and zlib/1.0" in tc.out assert "Conflict originates from pkg1/1.0" in tc.out else: assert "ERROR: Version conflict: Conflict between zlib/1.0 and zlib/1.1" in tc.out assert "Conflict originates from gtest/1.0" in tc.out def test_transitive_version_range_no_conflict(self, order, test): # if in the case above, we use a version-range, we can avoid the conflict tc = TestClient(light=True) pkg1 = GenConanfile("pkg1", "1.0") if order: pkg1.with_requirement("pkg3/[*]", visible=False, test=test).with_requirement("pkg2/1.0") else: pkg1.with_requirement("pkg2/1.0").with_requirement("pkg3/[*]", visible=False, test=test) tc.save({"pkg3/conanfile.py": GenConanfile("pkg3"), "pkg2/conanfile.py": GenConanfile("pkg2", "1.0").with_requirement("pkg3/1.1"), "pkg1/conanfile.py": pkg1}) tc.run("export pkg3 --version=1.0") tc.run("export pkg3 --version=1.1") tc.run("export pkg2") # Creating this pkg1 does generate a conflict tc.run("export pkg1") tc.run("graph info --requires=pkg3/1.1 --requires=pkg1/1.0 --format=json") if not test: assert "WARN: risk: Packages required both with visible=True and visible=False" in tc.out assert "pkg3/1.1: Required by pkg1/1.0" in tc.out else: assert "WARN: risk" not in tc.out graph = json.loads(tc.stdout) assert len(graph["graph"]["nodes"]) == 4 # This was having an orphan node!!! pkg1 = graph["graph"]["nodes"]["2"] deps = pkg1["dependencies"] assert len(deps) == 2 assert "pkg1/1.0" in pkg1["ref"] dep_pkg2 = deps["3"] dep_pkg3 = deps["1"] assert dep_pkg2["ref"] == "pkg2/1.0" assert dep_pkg2["visible"] is True assert dep_pkg3["ref"] == "pkg3/1.1" assert dep_pkg2["visible"] is True def test_transitive_version_range_no_conflict_second_level(self, order, test): # cli --------------------------------------------> zlib/1.1 # \--> pkg1/1.0 -(visible=False) -gtest---------> zlib/[*] (no conflict, range) # \----> boost/1.0 -------------------> zlib/1.1 (no conflict) tc = TestClient(light=True) pkg = GenConanfile("pkg1", "1.0") if order: pkg.with_requirement("gtest/1.0", visible=False, test=test).with_requirement("boost/1.0") else: pkg.with_requirement("boost/1.0").with_requirement("gtest/1.0", visible=False, test=test) tc.save({"zlib/conanfile.py": GenConanfile("zlib"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requirement("zlib/[*]"), "boost/conanfile.py": GenConanfile("boost", "1.0").with_requirement("zlib/1.1"), "pkg1/conanfile.py": pkg}) tc.run("export zlib --version=1.0") tc.run("export zlib --version=1.1") tc.run("export gtest") tc.run("export boost") tc.run("export pkg1") tc.run("graph info --requires=zlib/1.1 --requires=pkg1/1.0 --build=missing") assert "pkg1/1.0: WARN: risk: This package has 2 different dependencies" not in tc.out # This doesn't conflict, but depending on the order, it is possible to have 2 differnet # dependency graphs. If gtest is evaluated first, there will be no diamond, and # gtest->zlib/1.1 will be private, and there will be another branch with another zlib/1.1 # node as regular requires. # In both cases, it seems that zlib is not Skipped when it is necessary to build gtest def test_transitive_orphans(self, order, test): tc = TestClient(light=True) pkg1 = GenConanfile("pkg1", "1.0") if order: pkg1.with_requirement("pkg3/[*]", visible=False, test=test).with_requirement("pkg2/1.0") else: pkg1.with_requirement("pkg2/1.0").with_requirement("pkg3/[*]", visible=False, test=test) tc.save({"pkg4/conanfile.py": GenConanfile("pkg4", "0.1"), "pkg3/conanfile.py": GenConanfile("pkg3").with_requires("pkg4/0.1"), "pkg2/conanfile.py": GenConanfile("pkg2", "1.0").with_requirement("pkg3/1.1"), "pkg1/conanfile.py": pkg1}) tc.run("export pkg4") tc.run("export pkg3 --version=1.0") tc.run("export pkg3 --version=1.1") tc.run("export pkg2") # Creating this pkg1 does generate a conflict tc.run("export pkg1") tc.run("graph info --requires=pkg3/1.1 --requires=pkg1/1.0 --format=json") if not test: assert "WARN: risk: Packages required both with visible=True and visible=False" in tc.out assert "pkg3/1.1: Required by pkg1/1.0" in tc.out else: assert "WARN: risk" not in tc.out graph = json.loads(tc.stdout) assert len(graph["graph"]["nodes"]) == 5 # This was having an orphan node!!! pkg1 = graph["graph"]["nodes"]["3"] assert "pkg1/1.0" in pkg1["ref"] deps = pkg1["dependencies"] assert len(deps) == 3 dep_pkg2 = deps["4"] dep_pkg3 = deps["1"] assert dep_pkg2["ref"] == "pkg2/1.0" assert dep_pkg2["visible"] is True assert dep_pkg3["ref"] == "pkg3/1.1" assert dep_pkg2["visible"] is True ================================================ FILE: test/integration/graph/core/__init__.py ================================================ ================================================ FILE: test/integration/graph/core/graph_manager_base.py ================================================ import os import textwrap import pytest import yaml from conan.api.conan_api import ConanAPI from conan.internal.cache.cache import PkgCache from conan.internal.cache.home_paths import HomePaths from conan.internal.default_settings import default_settings_yml from conan.internal.model.conf import ConfDefinition from conan.internal.model.manifest import FileTreeManifest from conan.internal.model.options import Options from conan.internal.model.profile import Profile from conan.api.model import RecipeReference from conan.internal.model.settings import Settings from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import GenConanfile from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import save class GraphManagerTest: @pytest.fixture(autouse=True) def setUp(self): cache_folder = temp_folder() cache = PkgCache(cache_folder, ConfDefinition()) home = HomePaths(cache_folder) save(os.path.join(home.profiles_path, "default"), "") save(home.settings_path, "os: [Windows, Linux]") self.cache = cache self.cache_folder = cache_folder def recipe_cache(self, reference, requires=None, option_shared=None): ref = RecipeReference.loads(reference) conanfile = GenConanfile() if requires: for r in requires: conanfile.with_require(r) if option_shared is not None: conanfile.with_option("shared", [True, False]) conanfile.with_default_option("shared", option_shared) self._cache_recipe(ref, conanfile) def recipe_conanfile(self, reference, conanfile): ref = RecipeReference.loads(reference) self._cache_recipe(ref, conanfile) def _cache_recipe(self, ref, test_conanfile): if not isinstance(ref, RecipeReference): ref = RecipeReference.loads(ref) ref.revision = "123" ref.timestamp = revision_timestamp_now() recipe_layout = self.cache.create_ref_layout(ref) save(recipe_layout.conanfile(), str(test_conanfile)) manifest = FileTreeManifest.create(recipe_layout.export()) manifest.save(recipe_layout.export()) def alias_cache(self, alias, target): ref = RecipeReference.loads(alias) conanfile = textwrap.dedent(""" from conan import ConanFile class Alias(ConanFile): alias = "%s" """ % target) self._cache_recipe(ref, conanfile) @staticmethod def recipe_consumer(reference=None, requires=None, build_requires=None): path = temp_folder() path = os.path.join(path, "conanfile.py") conanfile = GenConanfile() if reference: ref = RecipeReference.loads(reference) conanfile.with_name(ref.name).with_version(ref.version) if requires: for r in requires: conanfile.with_require(r) if build_requires: for r in build_requires: conanfile.with_build_requires(r) save(path, str(conanfile)) return path @staticmethod def consumer_conanfile(conanfile): path = temp_folder() path = os.path.join(path, "conanfile.py") save(path, str(conanfile)) return path def build_graph(self, content, profile_build_requires=None, install=True, options_build=None): path = temp_folder() path = os.path.join(path, "conanfile.py") save(path, str(content)) return self.build_consumer(path, profile_build_requires, install, options_build=options_build) def build_consumer(self, path, profile_build_requires=None, install=True, options_build=None): profile_host = Profile() profile_host.settings["os"] = "Linux" profile_build = Profile() profile_build.settings["os"] = "Windows" if profile_build_requires: profile_host.tool_requires = profile_build_requires if options_build: profile_build.options = Options(options_values=options_build) cache_settings = Settings(yaml.safe_load(default_settings_yml)) profile_host.process_settings(cache_settings) profile_build.process_settings(cache_settings) build_mode = ["*"] # Means build all conan_api = ConanAPI(cache_folder=self.cache_folder) deps_graph = conan_api.graph.load_graph_consumer(path, None, None, None, None, profile_host, profile_build, None, None, None) if install: deps_graph.report_graph_error() conan_api.graph.analyze_binaries(deps_graph, build_mode) conan_api.install.install_binaries(deps_graph) return deps_graph @staticmethod def _check_node(node, ref, deps=None, dependents=None, settings=None, options=None): dependents = dependents or [] deps = deps or [] conanfile = node.conanfile ref = RecipeReference.loads(str(ref)) assert node.ref == ref if conanfile: assert conanfile.name == ref.name assert len(node.edges) == len(deps) for d in node.neighbors(): assert d in deps dependants = node.inverse_neighbors() assert len(dependants) == len(dependents) for d in dependents: assert d in dependants if settings is not None: for k, v in settings.items(): assert conanfile.settings.get_safe(k) == v if options is not None: for k, v in options.items(): assert conanfile.options.get_safe(k) == v ================================================ FILE: test/integration/graph/core/graph_manager_test.py ================================================ import pytest from conan.internal.graph.graph_error import GraphMissingError, GraphLoopError, GraphConflictError from conan.errors import ConanException from test.integration.graph.core.graph_manager_base import GraphManagerTest from conan.test.utils.tools import GenConanfile def _check_transitive(node, transitive_deps): values = list(node.transitive_deps.values()) assert len(values) == len(transitive_deps), f"{node}: Number of deps don't match " \ f"\n{[r.require.ref for r in values]}" \ f"!=\n{transitive_deps}" for v1, v2 in zip(values, transitive_deps): assert v1.node is v2[0], f"{v1.node}!={v2[0]}" assert v1.require.headers is v2[1], f"{v1.node}!={v2[0]} headers" assert v1.require.libs is v2[2], f"{v1.node}!={v2[0]} libs" assert v1.require.build is v2[3], f"{v1.node}!={v2[0]} build" assert v1.require.run is v2[4], f"{v1.node}!={v2[0]} run" assert len(v2) <= 5 class TestLinear(GraphManagerTest): def test_basic(self): deps_graph = self.build_graph(GenConanfile("app", "0.1")) assert 1 == len(deps_graph.nodes) node = deps_graph.root self._check_node(node, "app/0.1") def test_dependency(self): # app -> libb0.1 self.recipe_cache("libb/0.1") consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 2 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[], dependents=[app]) def test_dependency_missing(self): # app -> libb0.1 (non existing) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer, install=False) # TODO: Better error handling assert type(deps_graph.error) is GraphMissingError assert 1 == len(deps_graph.nodes) app = deps_graph.root self._check_node(app, "app/0.1", deps=[]) def test_transitive(self): # app -> libb0.1 -> liba0.1 # By default if packages do not specify anything link=True is propagated run=None (unknown) self.recipe_cache("liba/0.1") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_transitive_propagate_link(self): # app -> libb0.1 -> liba0.1 # transitive_link=False will avoid propagating linkage requirement self.recipe_cache("liba/0.1") self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1", transitive_libs=False)) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, True, False, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_transitive_all_static(self): # app -> libb0.1 (static) -> liba0.1 (static) self.recipe_cache("liba/0.1", option_shared=False) self.recipe_cache("libb/0.1", ["liba/0.1"], option_shared=False) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst for r, t in libb.transitive_deps.items(): assert r.package_id_mode == "minor_mode" self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, False, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_transitive_all_static_transitive_headers(self): # app -> libb0.1 (static) -> liba0.1 (static) self.recipe_cache("liba/0.1", option_shared=False) libb = GenConanfile().with_requirement("liba/0.1", transitive_headers=True) libb.with_shared_option() self.recipe_conanfile("libb/0.1", libb) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_transitive_all_shared(self): # app -> libb0.1 (shared) -> liba0.1 (shared) self.recipe_cache("liba/0.1", option_shared=True) self.recipe_cache("libb/0.1", ["liba/0.1"], option_shared=True) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run # Default for app->liba is that it doesn't link, libb shared will isolate symbols by default _check_transitive(app, [(libb, True, True, False, True), (liba, False, False, False, True)]) _check_transitive(libb, [(liba, True, True, False, True)]) def test_transitive_all_shared_transitive_headers_libs(self): # app -> libb0.1 (shared) -> liba0.1 (shared) self.recipe_cache("liba/0.1", option_shared=True) libb = GenConanfile().with_requirement("liba/0.1", transitive_headers=True, transitive_libs=True) libb.with_shared_option(True) self.recipe_conanfile("libb/0.1", libb) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run # Default for app->liba is that it doesn't link, libb shared will isolate symbols by default _check_transitive(app, [(libb, True, True, False, True), (liba, True, True, False, True)]) _check_transitive(libb, [(liba, True, True, False, True)]) def test_middle_shared_up_static(self): # app -> libb0.1 (shared) -> liba0.1 (static) self.recipe_cache("liba/0.1", option_shared=False) self.recipe_cache("libb/0.1", ["liba/0.1"], option_shared=True) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, True), (liba, False, False, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_middle_shared_up_static_transitive_headers(self): # app -> libb0.1 (shared) -> liba0.1 (static) self.recipe_cache("liba/0.1", option_shared=False) libb = GenConanfile().with_requirement("liba/0.1", transitive_headers=True) libb.with_shared_option(True) self.recipe_conanfile("libb/0.1", libb) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, True), (liba, True, False, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_middle_static_up_shared(self): # app -> libb0.1 (static) -> liba0.1 (shared) self.recipe_cache("liba/0.1", option_shared=True) self.recipe_cache("libb/0.1", ["liba/0.1"], option_shared=False) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, False, True, False, True)]) _check_transitive(libb, [(liba, True, True, False, True)]) def test_middle_static_up_shared_transitive_headers(self): # app -> libb0.1 (static) -> liba0.1 (shared) self.recipe_cache("liba/0.1", option_shared=True) libb = GenConanfile().with_requirement("liba/0.1", transitive_headers=True) libb.with_shared_option(False) self.recipe_conanfile("libb/0.1", libb) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, True, True, False, True)]) _check_transitive(libb, [(liba, True, True, False, True)]) def test_private(self): # app -> libb0.1 -(private) -> liba0.1 self.recipe_cache("liba/0.1") libb = GenConanfile().with_requirement("liba/0.1", visible=False) self.recipe_conanfile("libb/0.1", libb) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_generic_library_without_shared_option(self): # app -> libb0.1 -> liba0.1 (library without shared option) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("library")) libb = GenConanfile().with_requirement("liba/0.1") self.recipe_conanfile("libb/0.1", libb) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) with pytest.raises(ConanException) as exc: self.build_consumer(consumer) assert "liba/0.1: Package type is 'library', but no 'shared' option declared" in str(exc) def test_build_script_requirement(self): # app -> libb0.1 -br-> liba0.1 (build-scripts) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("build-scripts")) self.recipe_conanfile("libb/0.1", GenConanfile().with_tool_requirement("liba/0.1")) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False)]) _check_transitive(libb, [(liba, False, False, True, True)]) @pytest.mark.parametrize("package_type", ["application", "shared-library", "static-library", "header-library", "build-scripts", None]) def test_generic_build_require_adjust_run_with_package_type(self, package_type): # app --br-> cmake (app) self.recipe_conanfile("cmake/0.1", GenConanfile().with_package_type(package_type)) # build require with run=None by default consumer = self.recipe_consumer("app/0.1", build_requires=["cmake/0.1"]) deps_graph = self.build_consumer(consumer) assert 2 == len(deps_graph.nodes) app = deps_graph.root cmake = app.edges[0].dst # node, headers, lib, build, run run = package_type in ("application", "shared-library", "build-scripts") _check_transitive(app, [(cmake, False, False, True, run)]) def test_direct_header_only(self): # app -> liba0.1 (header_only) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["liba/0.1"]) deps_graph = self.build_consumer(consumer) assert 2 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst self._check_node(app, "app/0.1", deps=[liba]) self._check_node(liba, "liba/0.1#123", dependents=[app]) # node, headers, lib, build, run _check_transitive(app, [(liba, True, False, False, False)]) def test_header_only(self): # app -> libb0.1 -> liba0.1 (header_only) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library")) libb = GenConanfile().with_requirement("liba/0.1") self.recipe_conanfile("libb/0.1", libb) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, False, False, False, False)]) _check_transitive(libb, [(liba, True, False, False, False)]) def test_header_only_with_transitives(self): # app -> liba0.1(header) -> libb0.1 (static) # \-----------> libc0.1 (shared) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("shared-library")) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library") .with_requires("libb/0.1", "libc/0.1")) consumer = self.recipe_consumer("app/0.1", ["liba/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst libb = liba.edges[0].dst libc = liba.edges[1].dst self._check_node(app, "app/0.1", deps=[liba]) self._check_node(liba, "liba/0.1#123", deps=[libb, libc], dependents=[app]) self._check_node(libb, "libb/0.1#123", dependents=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[liba]) # node, headers, lib, build, run _check_transitive(app, [(liba, True, False, False, False), (libb, True, True, False, False), (libc, True, True, False, True)]) _check_transitive(liba, [(libb, True, True, False, False), (libc, True, True, False, True)]) def test_multiple_header_only_with_transitives(self): # app -> libd0.1(header) -> liba0.1(header) -> libb0.1 (static) # \-----------> libc0.1 (shared) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("shared-library")) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library") .with_requires("libb/0.1", "libc/0.1")) self.recipe_conanfile("libd/0.1", GenConanfile().with_package_type("header-library") .with_requires("liba/0.1")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst liba = libd.edges[0].dst libb = liba.edges[0].dst libc = liba.edges[1].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", deps=[libb, libc], dependents=[libd]) self._check_node(libb, "libb/0.1#123", dependents=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[liba]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (liba, True, False, False, False), (libb, True, True, False, False), (libc, True, True, False, True)]) _check_transitive(libd, [(liba, True, False, False, False), (libb, True, True, False, False), (libc, True, True, False, True)]) _check_transitive(liba, [(libb, True, True, False, False), (libc, True, True, False, True)]) def test_static_multiple_header_only_with_transitives(self): # app -> libd0.1(static) -> liba0.1(header) -> libb0.1 (static) # \-----------> libc0.1 (shared) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("shared-library")) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library") .with_requires("libb/0.1", "libc/0.1")) self.recipe_conanfile("libd/0.1", GenConanfile().with_package_type("static-library") .with_requires("liba/0.1")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst liba = libd.edges[0].dst libb = liba.edges[0].dst libc = liba.edges[1].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", deps=[libb, libc], dependents=[libd]) self._check_node(libb, "libb/0.1#123", dependents=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[liba]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, True, False, False), (liba, False, False, False, False), (libb, False, True, False, False), (libc, False, True, False, True)]) _check_transitive(libd, [(liba, True, False, False, False), (libb, True, True, False, False), (libc, True, True, False, True)]) _check_transitive(liba, [(libb, True, True, False, False), (libc, True, True, False, True)]) def test_multiple_levels_transitive_headers(self): # app -> libcc0.1 -> libb0.1 -> liba0.1 self.recipe_cache("liba/0.1") self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library") .with_requirement("liba/0.1", transitive_headers=True)) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("static-library") .with_requirement("libb/0.1", transitive_headers=True)) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libc, True, True, False, False), (libb, True, True, False, False), (liba, True, True, False, False)]) class TestLinearFourLevels(GraphManagerTest): def test_default(self): # app -> libc/0.1 -> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libc, True, True, False, False), (libb, True, True, False, False), (liba, True, True, False, False)]) def test_negate_headers(self): # app -> libc/0.1 -> libb0.1 -(not headers)-> liba0.1 # So nobody depends on the headers downstream self.recipe_conanfile("liba/0.1", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1", headers=False)) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libc, [(libb, True, True, False, False), (liba, False, True, False, False)]) _check_transitive(app, [(libc, True, True, False, False), (libb, True, True, False, False), (liba, False, True, False, False)]) @pytest.mark.parametrize("library_type", ["static-library", "shared-library"]) def test_libraries_transitive_headers(self, library_type): # app -> libc/0.1 -> libb0.1 -> liba0.1 # All with transitive_headers, the final application shoud get all headers # https://github.com/conan-io/conan/issues/12504 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type(library_type)) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type(library_type) .with_requirement("liba/0.1", transitive_headers=True)) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type(library_type) .with_requirement("libb/0.1", transitive_headers=True)) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run if library_type == "shared-library": _check_transitive(app, [(libc, True, True, False, True), (libb, True, False, False, True), (liba, True, False, False, True)]) else: # Both static and unknown behave the same _check_transitive(app, [(libc, True, True, False, False), (libb, True, True, False, False), (liba, True, True, False, False)]) def test_negate_libs(self): # app -> libc/0.1 -> libb0.1 -> liba0.1 # even if all are static, we want to disable the propagation of one static lib downstream # because only the headers are used self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library") .with_requirement("liba/0.1", libs=False)) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("static-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, False, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, False, False, False, False)]) _check_transitive(app, [(libc, True, True, False, False), (libb, False, True, False, False), (liba, False, False, False, False)]) def test_disable_transitive_libs(self): # app -> libc/0.1 -> libb0.1 -> liba0.1 # even if all are static, we want to disable the propagation of one static lib downstream # Maybe we are re-archiving self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library") .with_requirement("liba/0.1", transitive_libs=False)) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("static-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, False, False, False, False)]) _check_transitive(app, [(libc, True, True, False, False), (libb, False, True, False, False), (liba, False, False, False, False)]) def test_shared_depends_static_libraries(self): self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) # Emulate a re-packaging, re-archiving static library self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("shared-library") .with_requirement("liba/0.1")) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("static-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libc, [(libb, True, True, False, True), (liba, False, False, False, False)]) _check_transitive(app, [(libc, True, True, False, False), (libb, False, True, False, True), (liba, False, False, False, False)]) def test_negate_run(self): # app -> libc/0.1 -> libb0.1 -> liba0.1 # even if all are shared, we want to disable the propagation of one shared lib downstream # because only the headers are used self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("shared-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("shared-library") .with_requirement("liba/0.1", run=False)) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("shared-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, True), (liba, False, False, False, False)]) _check_transitive(app, [(libc, True, True, False, True), (libb, False, False, False, True), (liba, False, False, False, False)]) def test_force_run(self): # app -> libc/0.1 -> libb0.1 -> liba0.1 # even if all are static, there is something in a static lib (files or whatever) that # is necessary at runtime self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library") .with_requirement("liba/0.1", run=True)) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("static-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, True, False, True)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, False, True, False, True)]) _check_transitive(app, [(libc, True, True, False, False), (libb, False, True, False, False), (liba, False, True, False, True)]) @pytest.mark.parametrize("run", [True, False]) def test_header_only_run(self, run): # app -> libc/0.1 -> libb0.1 -> liba0.1 # many header-onlys self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("header-library") .with_requirement("liba/0.1", run=run)) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("header-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, False, False, run)]) _check_transitive(libc, [(libb, True, False, False, False), (liba, True, False, False, run)]) _check_transitive(app, [(libc, True, False, False, False), (libb, True, False, False, False), (liba, True, False, False, run)]) def test_intermediate_header_only(self): # app -> libc/0.1 (static) -> libb0.1 (header) -> liba0.1 (static) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("header-library") .with_requirement("liba/0.1")) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("static-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(libb, True, False, False, False), (liba, True, True, False, False)]) _check_transitive(app, [(libc, True, True, False, False), (libb, False, False, False, False), (liba, False, True, False, False)]) def test_static_propagation_linear(self): # https://github.com/conan-io/conan/issues/16402 # app -> libc/0.1 (shared) -> libb0.1 (static) -> liba0.1 (static) # The propagation of traits is correct, it is only the "skip-binaries" not able to skip self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library") .with_requirement("liba/0.1")) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("shared-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, False, True, False, False)]) _check_transitive(app, [(libc, True, True, False, True), (libb, False, False, False, False), (liba, False, False, False, False)]) def test_incorrect_static_propagation_diamond(self): # https://github.com/conan-io/conan/issues/16402 # app -> libc/0.1 (shared) -> libb0.1 (static) -> liba0.1 (static) # \------------------------------------------------/ self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_package_type("static-library") .with_requirement("liba/0.1")) self.recipe_conanfile("libc/0.1", GenConanfile().with_package_type("shared-library") .with_requirement("libb/0.1")) consumer = self.recipe_consumer("app/0.1", ["libc/0.1", "liba/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc, liba]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb, app]) # node, headers, lib, build, run _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, False, True, False, False)]) _check_transitive(app, [(libc, True, True, False, True), (libb, False, False, False, False), (liba, True, True, False, False)]) class TestLinearFiveLevelsHeaders(GraphManagerTest): def test_all_header_only(self): # app -> libd/0.1 -> libc/0.1 -> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("header-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1") .with_package_type("header-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, False, False, False), (libb, True, False, False, False), (liba, True, False, False, False)]) def test_all_header_only_aggregating_libc(self): # libc is copying and pasting the others headers at build time, creating re-distribution # app -> libd/0.1 -> libc/0.1 -(transitive_headers=FALSE)-> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("header-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", transitive_headers=False) .with_package_type("header-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, False, False, False), (libb, False, False, False, False), (liba, False, False, False, False)]) def test_first_header_only(self): # app -> libd/0.1(header) -> libc/0.1 -> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1") .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, True, False, False), (libb, False, True, False, False), (liba, False, True, False, False)]) def test_first_header_only_transitive_headers_b(self): # app -> libd/0.1(header) -> libc/0.1 -(transitive_headers=T)-> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", transitive_headers=True) .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, True, False, False), (libb, True, True, False, False), (liba, False, True, False, False)]) def test_first_header_only_transitive_headers_b_a(self): # app -> libd/0.1(header) -> libc/0.1 -(transitive_headers=T)-> libb0.1 -(transitive_headers=T)-> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1", transitive_headers=True) .with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", transitive_headers=True) .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, True, False, False), (libb, True, True, False, False), (liba, True, True, False, False)]) def test_first_header_only_reject_libs(self): # Like Libc knows it only uses headers from libb # app -> libd/0.1(header) -> libc/0.1 -(libs=False)-> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", libs=False) .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, True, False, False), (libb, False, False, False, False), (liba, False, False, False, False)]) def test_d_b_header_only(self): # app -> libd/0.1(header) -> libc/0.1 -> libb0.1(header) -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("header-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1") .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, True, False, False), (libb, False, False, False, False), (liba, False, True, False, False)]) def test_d_b_header_only_transitive_headers_b(self): # app -> libd/0.1(header) -> libc/0.1 -(transitive_headers=T)-> libb0.1(header) -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("header-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", transitive_headers=True) .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("header-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, False, False, False), (libc, True, True, False, False), (libb, True, False, False, False), (liba, True, True, False, False)]) def test_visible_transitivity(self): # app -> libd/0.1 -> libc/0.1 -(visible=False)-> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", visible=False)) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, True, False, False), (libc, True, True, False, False)]) _check_transitive(libd, [(libc, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, True, True, False, False)]) def test_visible_build_transitivity(self): # app -> libd/0.1 -> libc/0.1 -(visible=True, build=True)-> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", build=True)) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, True, False, False), (libc, True, True, False, False), (libb, False, False, True, False)]) _check_transitive(libd, [(libc, True, True, False, False), (libb, False, False, True, False)]) _check_transitive(libc, [(libb, True, True, True, False)]) class TestLinearFiveLevelsLibraries(GraphManagerTest): def test_all_static(self): # app -> libd/0.1 -> libc/0.1 -> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1") .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("static-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, True, False, False), (libc, False, True, False, False), (libb, False, True, False, False), (liba, False, True, False, False)]) def test_libc_aggregating_static(self): # Lets think libc is re-linking its dependencies in a single .lib # app -> libd/0.1 -> libc/0.1 -(transitive_libs=False)-> libb0.1 -> liba0.1 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("static-library")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1") .with_package_type("static-library")) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("libb/0.1", transitive_libs=False) .with_package_type("static-library")) self.recipe_conanfile("libd/0.1", GenConanfile().with_requirement("libc/0.1") .with_package_type("static-library")) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = libd.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, True, False, False), (libc, False, True, False, False), (libb, False, False, False, False), (liba, False, False, False, False)]) class TestDiamond(GraphManagerTest): def test_diamond(self): # app -> libb0.1 -> liba0.1 # \-> libc0.1 ->/ self.recipe_cache("liba/0.1") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) _check_transitive(app, [(libb, True, True, False, False), (libc, True, True, False, False), (liba, True, True, False, False)]) @pytest.mark.parametrize("order", [True, False]) def test_diamond_additive(self, order): # app -> libb0.1 ---------> liba0.1 # \-> libc0.1 (run=True)->/ self.recipe_cache("liba/0.1") if order: self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("liba/0.1", run=True)) else: self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1", run=True)) self.recipe_cache("libc/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) _check_transitive(app, [(libb, True, True, False, False), (libc, True, True, False, False), (liba, True, True, False, True)]) def test_half_diamond(self): # app -----------> liba0.1 # \-> libc0.1 ->/ self.recipe_cache("liba/0.1") self.recipe_cache("libc/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["liba/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst libc = app.edges[1].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[liba, libc]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[app, libc]) # Order seems to be link order _check_transitive(app, [(libc, True, True, False, False), (liba, True, True, False, False)]) # Both requires of app are direct! https://github.com/conan-io/conan/pull/12388 for require in app.transitive_deps.keys(): assert require.direct is True def test_half_diamond_reverse(self): # same as above, just swap order of declaration # app -->libc0.1--> liba0.1 # \-------------->/ self.recipe_cache("liba/0.1") self.recipe_cache("libc/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libc/0.1", "liba/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst liba = app.edges[1].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[liba, libc]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[app, libc]) # Order seems to be link order, constant irrespective of declaration order, good _check_transitive(app, [(libc, True, True, False, False), (liba, True, True, False, False)]) # Both requires of app are direct! https://github.com/conan-io/conan/pull/12388 for require in app.transitive_deps.keys(): assert require.direct is True def test_shared_static(self): # app -> libb0.1 (shared) -> liba0.1 (static) # \-> libc0.1 (shared) ->/ self.recipe_cache("liba/0.1", option_shared=False) self.recipe_cache("libb/0.1", ["liba/0.1"], option_shared=True) self.recipe_cache("libc/0.1", ["liba/0.1"], option_shared=True) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst liba1 = libc.edges[0].dst assert liba is liba1 self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, True), (libc, True, True, False, True), (liba, False, False, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(liba, True, True, False, False)]) def test_private(self): # app -> libd0.1 -(private)-> libb0.1 -> liba0.1 # \ ---(private)-> libc0.1 --->/ self.recipe_cache("liba/0.1") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1"]) libd = GenConanfile().with_requirement("libb/0.1", visible=False) libd.with_requirement("libc/0.1", visible=False) self.recipe_conanfile("libd/0.1", libd) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libb = libd.edges[0].dst libc = libd.edges[1].dst liba = libb.edges[0].dst liba2 = libc.edges[0].dst assert liba is liba2 self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libb, libc], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libd]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[libd]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) # node, headers, lib, build, run _check_transitive(app, [(libd, True, True, False, False)]) _check_transitive(libd, [(libb, True, True, False, False), (libc, True, True, False, False), (liba, True, True, False, False)]) def test_shared_static_private(self): # app -> libb0.1 (shared) -(private)-> liba0.1 (static) # \-> libc0.1 (shared) -> liba0.2 (static) # This private allows to avoid the liba conflict self.recipe_cache("liba/0.1", option_shared=False) self.recipe_cache("liba/0.2", option_shared=False) libb = GenConanfile().with_requirement("liba/0.1", visible=False) libb.with_shared_option(True) self.recipe_conanfile("libb/0.1", libb) self.recipe_cache("libc/0.1", ["liba/0.2"], option_shared=True) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba1 = libb.edges[0].dst liba2 = libc.edges[0].dst assert liba1 is not liba2 self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba2], dependents=[app]) self._check_node(liba1, "liba/0.1#123", dependents=[libb]) self._check_node(liba2, "liba/0.2#123", dependents=[libc]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, True), (libc, True, True, False, True), (liba2, False, False, False, False)]) _check_transitive(libb, [(liba1, True, True, False, False)]) _check_transitive(libc, [(liba2, True, True, False, False)]) def test_diamond_conflict(self): # app -> libb0.1 -> liba0.1 # \-> libc0.1 -> liba0.2 self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.2"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba1 = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[], dependents=[app]) self._check_node(liba1, "liba/0.1#123", dependents=[libb]) def test_shared_conflict_shared(self): # app -> libb0.1 (shared) -> liba0.1 (shared) # \-> libc0.1 (shared) -> liba0.2 (shared) self.recipe_cache("liba/0.1", option_shared=True) self.recipe_cache("liba/0.2", option_shared=True) self.recipe_cache("libb/0.1", ["liba/0.1"], option_shared=True) self.recipe_cache("libc/0.1", ["liba/0.2"], option_shared=True) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba1 = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[], dependents=[app]) self._check_node(liba1, "liba/0.1#123", dependents=[libb]) def test_private_conflict(self): # app -> libd0.1 -(private)-> libb0.1 -> liba0.1 # \ ---(private)-> libc0.1 -> liba0.2 # # private requires do not avoid conflicts at the node level, only downstream self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.2"]) libd = GenConanfile().with_requirement("libb/0.1", visible=False) libd.with_requirement("libc/0.1", visible=False) self.recipe_conanfile("libd/0.1", libd) consumer = self.recipe_consumer("app/0.1", ["libd/0.1"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError assert 5 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libb = libd.edges[0].dst libc = libd.edges[1].dst liba1 = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libd]) self._check_node(libd, "libd/0.1#123", deps=[libb, libc], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[libd]) self._check_node(libc, "libc/0.1#123", deps=[], dependents=[libd]) self._check_node(liba1, "liba/0.1#123", dependents=[libb]) def test_diamond_transitive_private(self): # https://github.com/conan-io/conan/issues/13630 # libc0.1 ----------> liba0.1 --(private) -> zlib/1.0 # \-> --(libb---->/ self.recipe_cache("zlib/0.1") self.recipe_conanfile("liba/0.1", GenConanfile("liba", "0.1") .with_requirement("zlib/0.1", visible=False)) self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1", "libb/0.1"]) consumer = self.consumer_conanfile(GenConanfile("libc", "0.1") .with_requires("liba/0.1", "libb/0.1")) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) libc = deps_graph.root liba = libc.edges[0].dst libb = libc.edges[1].dst liba1 = libb.edges[0].dst zlib = liba.edges[0].dst assert liba is liba1 # TODO: No Revision??? Because of consumer? self._check_node(libc, "libc/0.1", deps=[liba, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc], deps=[zlib]) self._check_node(zlib, "zlib/0.1#123", dependents=[liba]) _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, True, True, False, False)]) def test_private_transitive_headers_no_conflict(self): # https://github.com/conan-io/conan/issues/15559 # app -->liba/0.1 -(private)-> spdlog/0.1(header-only) -> fmt/0.1 (header-only) # \ --------------------------------------------------> fmt/0.2 self.recipe_conanfile("fmt/0.1", GenConanfile("fmt", "0.1").with_package_type("header-library")) self.recipe_conanfile("fmt/0.2", GenConanfile("fmt", "0.2")) self.recipe_conanfile("spdlog/0.1", GenConanfile("spdlog", "0.1").with_package_type("header-library") .with_requires("fmt/0.1")) self.recipe_conanfile("liba/0.1", GenConanfile("liba", "0.2").with_requirement("spdlog/0.1", visible=False)) consumer = self.consumer_conanfile(GenConanfile("app", "0.1") .with_requires("liba/0.1", "fmt/0.2")) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst spdlog = liba.edges[0].dst fmt01 = spdlog.edges[0].dst fmt02 = app.edges[1].dst self._check_node(app, "app/0.1", deps=[liba, fmt02]) self._check_node(liba, "liba/0.1#123", deps=[spdlog], dependents=[app]) self._check_node(spdlog, "spdlog/0.1#123", deps=[fmt01], dependents=[liba]) self._check_node(fmt01, "fmt/0.1#123", deps=[], dependents=[spdlog]) self._check_node(fmt02, "fmt/0.2#123", dependents=[app]) # node, headers, lib, build, run _check_transitive(app, [(liba, True, True, False, False), (fmt02, True, True, False, False)]) class TestDiamondMultiple(GraphManagerTest): def test_consecutive_diamonds(self): # app -> libe0.1 -> libd0.1 -> libb0.1 -> liba0.1 # \-> libf0.1 ->/ \-> libc0.1 ->/ self.recipe_cache("liba/0.1") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1"]) self.recipe_cache("libd/0.1", ["libb/0.1", "libc/0.1"]) self.recipe_cache("libe/0.1", ["libd/0.1"]) self.recipe_cache("libf/0.1", ["libd/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libe/0.1", "libf/0.1"]) deps_graph = self.build_consumer(consumer) assert 7 == len(deps_graph.nodes) app = deps_graph.root libe = app.edges[0].dst libf = app.edges[1].dst libd = libe.edges[0].dst libb = libd.edges[0].dst libc = libd.edges[1].dst liba = libc.edges[0].dst self._check_node(app, "app/0.1", deps=[libe, libf]) self._check_node(libe, "libe/0.1#123", deps=[libd], dependents=[app]) self._check_node(libf, "libf/0.1#123", deps=[libd], dependents=[app]) self._check_node(libd, "libd/0.1#123", deps=[libb, libc], dependents=[libe, libf]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libd]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) _check_transitive(app, [(libe, True, True, False, False), (libf, True, True, False, False), (libd, True, True, False, False), (libb, True, True, False, False), (libc, True, True, False, False), (liba, True, True, False, False)]) def test_consecutive_diamonds_private(self): # app -> libe0.1 ---------> libd0.1 ---> libb0.1 ---> liba0.1 # \-> (private)->libf0.1 ->/ \-private-> libc0.1 ->/ self.recipe_cache("liba/0.1") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1"]) self._cache_recipe("libd/0.1", GenConanfile().with_require("libb/0.1") .with_requirement("libc/0.1", visible=False)) self.recipe_cache("libe/0.1", ["libd/0.1"]) self.recipe_cache("libf/0.1", ["libd/0.1"]) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libe/0.1") .with_requirement("libf/0.1", visible=False)) deps_graph = self.build_consumer(consumer) assert 7 == len(deps_graph.nodes) app = deps_graph.root libe = app.edges[0].dst libf = app.edges[1].dst libd = libe.edges[0].dst libb = libd.edges[0].dst libc = libd.edges[1].dst liba = libc.edges[0].dst self._check_node(app, "app/0.1", deps=[libe, libf]) self._check_node(libe, "libe/0.1#123", deps=[libd], dependents=[app]) self._check_node(libf, "libf/0.1#123", deps=[libd], dependents=[app]) self._check_node(libd, "libd/0.1#123", deps=[libb, libc], dependents=[libe, libf]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libd]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) # FIXME: In this case the order seems a bit broken _check_transitive(app, [(libe, True, True, False, False), (libf, True, True, False, False), (libd, True, True, False, False), (libb, True, True, False, False), (liba, True, True, False, False), ]) def test_parallel_diamond(self): # app -> libb0.1 -> liba0.1 # \-> libc0.1 ->/ # \-> libd0.1 ->/ self.recipe_cache("liba/0.1") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1"]) self.recipe_cache("libd/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1", "libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst libd = app.edges[2].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb, libc, libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(libd, "libd/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc, libd]) def test_nested_diamond(self): # app --------> libb0.1 -> liba0.1 # \--------> libc0.1 ->/ # \-> libd0.1 ->/ self.recipe_cache("liba/0.1") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1"]) self.recipe_cache("libd/0.1", ["libc/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1", "libd/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst libd = app.edges[2].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb, libc, libd]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app, libd]) self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) def test_multiple_transitive(self): # https://github.com/conanio/conan/issues/4720 # app -> libb0.1 -> libc0.1 -> libd0.1 # \--------------->/ / # \------------------------>/ self.recipe_cache("libd/0.1") self.recipe_cache("libc/0.1", ["libd/0.1"]) self.recipe_cache("libb/0.1", ["libc/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libd/0.1", "libc/0.1", "libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libc = app.edges[1].dst libb = app.edges[2].dst self._check_node(app, "app/0.1", deps=[libd, libc, libb]) self._check_node(libd, "libd/0.1#123", dependents=[app, libc]) self._check_node(libb, "libb/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libd], dependents=[app, libb]) def test_loop(self): # app -> libc0.1 -> libb0.1 -> liba0.1 ->| # \<-------------------------| self.recipe_cache("liba/0.1", ["libc/0.1"]) self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["libb/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer, install=False) # TODO: Better error modeling assert type(deps_graph.error) is GraphLoopError assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", deps=[], dependents=[libb]) class TestTransitiveOverridesGraph(GraphManagerTest): def test_diamond(self): # app -> libb0.1 -> liba0.2 (overriden to lib0.2) # \-> --------- ->/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1") .with_requirement("liba/0.2", force=True)) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = app.edges[1].dst liba2 = libb.edges[0].dst assert liba is liba2 # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libb, liba]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.2#123", dependents=[libb, app]) def test_diamond_conflict(self): # app -> libb0.1 -> liba0.2 (overriden to lib0.2) # \-> --------- ->/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "liba/0.2"]) deps_graph = self.build_consumer(consumer, install=False) assert deps_graph.error is not False assert type(deps_graph.error) is GraphConflictError assert 2 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[], dependents=[app]) def test_build_script_no_conflict(self): # app -> libb0.1 -> liba0.1 (build-scripts) # \-> libc0.1 -> liba0.2 self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("build-scripts")) self.recipe_conanfile("liba/0.2", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_tool_requirement("liba/0.1", run=False)) self.recipe_conanfile("libc/0.1", GenConanfile().with_requirement("liba/0.2")) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 5 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba1 = libb.edges[0].dst liba2 = libc.edges[0].dst self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba2], dependents=[app]) self._check_node(liba1, "liba/0.1#123", dependents=[libb]) self._check_node(liba2, "liba/0.2#123", dependents=[libc]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (libc, True, True, False, False), (liba2, True, True, False, False)]) _check_transitive(libb, [(liba1, False, False, True, False)]) _check_transitive(libc, [(liba2, True, True, False, False)]) def test_diamond_reverse_order(self): # foo ---------------------------------> dep1/2.0 # \ -> dep2/1.0--(dep1/1.0 overriden)-->/ self.recipe_cache("dep1/1.0") self.recipe_cache("dep1/2.0") self.recipe_cache("dep2/1.0", ["dep1/1.0"]) consumer = self.consumer_conanfile(GenConanfile("app", "0.1") .with_requirement("dep1/2.0", force=True) .with_requirement("dep2/1.0")) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root dep1 = app.edges[0].dst dep2 = app.edges[1].dst self._check_node(app, "app/0.1", deps=[dep1, dep2]) self._check_node(dep1, "dep1/2.0#123", deps=[], dependents=[app, dep2]) self._check_node(dep2, "dep2/1.0#123", deps=[dep1], dependents=[app]) def test_diamond_reverse_order_conflict(self): # foo ---------------------------------> dep1/2.0 # \ -> dep2/1.0--(dep1/1.0 overriden)-->/ self.recipe_cache("dep1/1.0") self.recipe_cache("dep1/2.0") self.recipe_cache("dep2/1.0", ["dep1/1.0"]) consumer = self.recipe_consumer("app/0.1", ["dep1/2.0", "dep2/1.0"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError assert 3 == len(deps_graph.nodes) app = deps_graph.root dep1 = app.edges[0].dst dep2 = app.edges[1].dst self._check_node(app, "app/0.1", deps=[dep1, dep2]) self._check_node(dep1, "dep1/2.0#123", deps=[], dependents=[app]) # dep2 no dependency, it was not resolved due to conflict self._check_node(dep2, "dep2/1.0#123", deps=[], dependents=[app]) def test_invisible_not_forced(self): # app -> libb0.1 -(visible=False)----> liba0.1 (NOT forced to lib0.2) # \-> -----(force not used)-------> liba0.2 self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1", visible=False)) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1") .with_requirement("liba/0.2", force=True)) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba2 = app.edges[1].dst liba = libb.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libb, liba2]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) self._check_node(liba2, "liba/0.2#123", dependents=[app]) class TestPureOverride(GraphManagerTest): def test_diamond(self): # app -> libb0.1 -> liba0.2 (overriden to lib0.2) # \-> ---(override)------ ->/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1") .with_requirement("liba/0.2", override=True)) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.2#123", dependents=[libb]) def test_discarded_override(self): # app ->---(override)------> liba0.2 consumer = self.consumer_conanfile(GenConanfile("app", "0.1") .with_requirement("liba/0.2", override=True)) deps_graph = self.build_consumer(consumer) assert 1 == len(deps_graph.nodes) app = deps_graph.root # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[]) def test_invisible_not_overriden(self): # app -> libb0.1 -(visible=False)----> liba0.1 (NOT overriden to lib0.2) # \-> -----(override not used)------->/ self.recipe_cache("liba/0.1") self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1", visible=False)) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1") .with_requirement("liba/0.2", override=True)) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) def test_nested_overrides(self): # app -> libc0.1 ------> libb0.1 ------> liba0.1 # \ \-> --(override liba0.2)---->/ # \-> ---(override liba0.3)------------>/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("liba/0.3") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_conanfile("libc/0.1", GenConanfile("libc", "0.1") .with_requirement("libb/0.1") .with_requirement("liba/0.2", override=True)) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libc/0.1") .with_requirement("liba/0.3", override=True)) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.3#123", dependents=[libb]) def test_override_solve_upstream_conflict(self): # app -> libc0.1 ------> libb0.1 ------> liba0.1 # \ \-> --(liba0.2 conflict)---->/ # \-> ---(override liba0.3)------------>/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("liba/0.3") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_conanfile("libc/0.1", GenConanfile("libc", "0.1") .with_requires("libb/0.1", "liba/0.2")) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libc/0.1") .with_requirement("liba/0.3", override=True)) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst # TODO: No Revision??? Because of consumer? self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb, liba], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.3#123", dependents=[libb, libc]) def test_override_not_used(self): # https://github.com/conan-io/conan/issues/13630 # libc0.1 ----------> liba0.1 --(override) -> zlib/1.0 # \-> --(libb---->/ self.recipe_conanfile("liba/0.1", GenConanfile("liba", "0.1") .with_requirement("zlib/0.1", override=True)) self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/0.1", "libb/0.1"]) consumer = self.consumer_conanfile(GenConanfile("libc", "0.1") .with_requires("liba/0.1", "libb/0.1")) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) libc = deps_graph.root liba = libc.edges[0].dst libb = libc.edges[1].dst liba1 = libb.edges[0].dst assert liba is liba1 # TODO: No Revision??? Because of consumer? self._check_node(libc, "libc/0.1", deps=[liba, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) _check_transitive(libb, [(liba, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, True, True, False, False)]) class TestPackageIDDeductions(GraphManagerTest): def test_static_dep_to_shared(self): # project -> app1 -> lib # \---- > app2 --/ self._cache_recipe("lib/0.1", GenConanfile()) self._cache_recipe("app1/0.1", GenConanfile().with_requirement("lib/0.1")) self._cache_recipe("app2/0.1", GenConanfile().with_requirement("lib/0.1")) deps_graph = self.build_graph(GenConanfile("project", "0.1") .with_requirement("app1/0.1", headers=False, libs=False, build=False, run=True) .with_requirement("app2/0.1", headers=False, libs=False, build=False, run=True) ) assert 4 == len(deps_graph.nodes) project = deps_graph.root app1 = project.edges[0].dst app2 = project.edges[1].dst lib = app1.edges[0].dst lib2 = app2.edges[0].dst assert lib is lib2 self._check_node(project, "project/0.1@", deps=[app1, app2], dependents=[]) self._check_node(app1, "app1/0.1#123", deps=[lib], dependents=[project]) self._check_node(app2, "app2/0.1#123", deps=[lib], dependents=[project]) self._check_node(lib, "lib/0.1#123", deps=[], dependents=[app1, app2]) # node, headers, lib, build, run _check_transitive(project, [(app1, False, False, False, True), (app2, False, False, False, True), (lib, False, False, False, False)]) class TestProjectApp(GraphManagerTest): """ Emulating a project that can gather multiple applications and other resources and build a consistent graph, in which dependencies are same versions """ def test_project_require_transitive(self): # project -> app1 -> lib # \---- > app2 --/ self._cache_recipe("lib/0.1", GenConanfile()) self._cache_recipe("app1/0.1", GenConanfile().with_requirement("lib/0.1")) self._cache_recipe("app2/0.1", GenConanfile().with_requirement("lib/0.1")) deps_graph = self.build_graph(GenConanfile("project", "0.1") .with_requirement("app1/0.1", headers=False, libs=False, build=False, run=True) .with_requirement("app2/0.1", headers=False, libs=False, build=False, run=True) ) assert 4 == len(deps_graph.nodes) project = deps_graph.root app1 = project.edges[0].dst app2 = project.edges[1].dst lib = app1.edges[0].dst lib2 = app2.edges[0].dst assert lib is lib2 self._check_node(project, "project/0.1@", deps=[app1, app2], dependents=[]) self._check_node(app1, "app1/0.1#123", deps=[lib], dependents=[project]) self._check_node(app2, "app2/0.1#123", deps=[lib], dependents=[project]) self._check_node(lib, "lib/0.1#123", deps=[], dependents=[app1, app2]) # node, headers, lib, build, run _check_transitive(project, [(app1, False, False, False, True), (app2, False, False, False, True), (lib, False, False, False, False)]) def test_project_require_transitive_conflict(self): # project -> app1 -> lib/0.1 # \---- > app2 -> lib/0.2 self._cache_recipe("lib/0.1", GenConanfile()) self._cache_recipe("lib/0.2", GenConanfile()) self._cache_recipe("app1/0.1", GenConanfile().with_requirement("lib/0.1")) self._cache_recipe("app2/0.1", GenConanfile().with_requirement("lib/0.2")) deps_graph = self.build_graph(GenConanfile("project", "0.1") .with_requirement("app1/0.1", headers=False, libs=False, build=False, run=True) .with_requirement("app2/0.1", headers=False, libs=False, build=False, run=True), install=False) assert type(deps_graph.error) is GraphConflictError def test_project_require_apps_transitive(self): # project -> app1 (app type) -> lib # \---- > app2 (app type) --/ self._cache_recipe("lib/0.1", GenConanfile()) self._cache_recipe("app1/0.1", GenConanfile().with_package_type("application"). with_requirement("lib/0.1")) self._cache_recipe("app2/0.1", GenConanfile().with_package_type("application"). with_requirement("lib/0.1")) deps_graph = self.build_graph(GenConanfile("project", "0.1").with_requires("app1/0.1", "app2/0.1")) assert 4 == len(deps_graph.nodes) project = deps_graph.root app1 = project.edges[0].dst app2 = project.edges[1].dst lib = app1.edges[0].dst lib2 = app2.edges[0].dst assert lib is lib2 self._check_node(project, "project/0.1@", deps=[app1, app2], dependents=[]) self._check_node(app1, "app1/0.1#123", deps=[lib], dependents=[project]) self._check_node(app2, "app2/0.1#123", deps=[lib], dependents=[project]) self._check_node(lib, "lib/0.1#123", deps=[], dependents=[app1, app2]) # node, headers, lib, build, run _check_transitive(project, [(app1, False, False, False, True), (app2, False, False, False, True), (lib, False, False, False, False)]) def test_project_require_apps_transitive_conflict(self): # project -> app1 (app type) -> lib/0.1 # \---- > app2 (app type) -> lib/0.2 self._cache_recipe("lib/0.1", GenConanfile()) self._cache_recipe("lib/0.2", GenConanfile()) self._cache_recipe("app1/0.1", GenConanfile().with_package_type("application"). with_requirement("lib/0.1")) self._cache_recipe("app2/0.1", GenConanfile().with_package_type("application"). with_requirement("lib/0.2")) deps_graph = self.build_graph(GenConanfile("project", "0.1").with_requires("app1/0.1", "app2/0.1"), install=False) assert type(deps_graph.error) is GraphConflictError def test_project_require_private(self): # project -(!visible)-> app1 -> lib1 # \----(!visible)- > app2 -> lib2 # This doesn't conflict on project, as lib1, lib2 do not include, link or public self._cache_recipe("lib/0.1", GenConanfile()) self._cache_recipe("lib/0.2", GenConanfile()) self._cache_recipe("app1/0.1", GenConanfile().with_requirement("lib/0.1")) self._cache_recipe("app2/0.1", GenConanfile().with_requirement("lib/0.2")) deps_graph = self.build_graph(GenConanfile("project", "0.1") .with_requirement("app1/0.1", headers=False, libs=False, build=False, run=True, visible=False) .with_requirement("app2/0.1", headers=False, libs=False, build=False, run=True, visible=False) ) assert 5 == len(deps_graph.nodes) project = deps_graph.root app1 = project.edges[0].dst app2 = project.edges[1].dst lib1 = app1.edges[0].dst lib2 = app2.edges[0].dst assert lib1 is not lib2 self._check_node(project, "project/0.1@", deps=[app1, app2], dependents=[]) self._check_node(app1, "app1/0.1#123", deps=[lib1], dependents=[project]) self._check_node(app2, "app2/0.1#123", deps=[lib2], dependents=[project]) self._check_node(lib1, "lib/0.1#123", deps=[], dependents=[app1]) self._check_node(lib2, "lib/0.2#123", deps=[], dependents=[app2]) # node, headers, lib, build, run _check_transitive(project, [(app1, False, False, False, True), (lib1, False, False, False, False), (app2, False, False, False, True), (lib2, False, False, False, False)]) ================================================ FILE: test/integration/graph/core/test_alias.py ================================================ from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.assets.genconanfile import GenConanfile from test.integration.graph.core.graph_manager_base import GraphManagerTest from test.integration.graph.core.graph_manager_test import _check_transitive from conan.test.utils.tools import TestClient DepsGraphBuilder.ALLOW_ALIAS = True class TestAlias(GraphManagerTest): def test_basic(self): # app -> liba/latest -(alias)-> liba/0.1 self.recipe_cache("liba/0.1") self.alias_cache("liba/latest", "liba/0.1") consumer = self.recipe_consumer("app/0.1", ["liba/(latest)"]) deps_graph = self.build_consumer(consumer) assert 2 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[app]) self._check_node(app, "app/0.1", deps=[liba]) def test_alias_diamond(self): # app -> ----------------------------------> liba/0.1 # \ -> libb/0.1 -> liba/latest -(alias) ----->/ self.recipe_cache("liba/0.1") self.alias_cache("liba/latest", "liba/0.1") self.recipe_cache("libb/0.1", requires=["liba/(latest)"]) consumer = self.recipe_consumer("app/0.1", ["liba/0.1", "libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst libb = app.edges[1].dst self._check_node(liba, "liba/0.1#123", dependents=[app, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(app, "app/0.1", deps=[liba, libb]) # node, include, link, build, run # It seems they are in link order _check_transitive(app, [(libb, True, True, False, False), (liba, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_two_alias_diamond(self): # https://github.com/conan-io/conan/issues/3353 # app -> liba/latest -(alias) --------------------------------------> liba/0.1 # \ -> libb/latest -(alias) -> libb/0.1 -> liba/latest -(alias) ----->/ self.recipe_cache("liba/0.1") self.alias_cache("liba/latest", "liba/0.1") self.recipe_cache("libb/0.1", requires=["liba/(latest)"]) self.alias_cache("libb/latest", "libb/0.1") consumer = self.recipe_consumer("app/0.1", ["liba/(latest)", "libb/(latest)"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst libb = app.edges[1].dst self._check_node(liba, "liba/0.1#123", dependents=[app, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(app, "app/0.1", deps=[liba, libb]) # node, include, link, build, run # Seems reverse order _check_transitive(app, [(libb, True, True, False, False), (liba, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_full_two_branches_diamond(self): # https://github.com/conan-io/conan/issues/3353 # app -> libb/latest -(alias) -> libb/0.1 -> liba/latest -(alias) ----> liba/0.1 # \ -> libc/latest -(alias) -> libc/0.1 -> liba/latest -(alias) ----->/ self.recipe_cache("liba/0.1") self.alias_cache("liba/latest", "liba/0.1") self.recipe_cache("libb/0.1", requires=["liba/(latest)"]) self.recipe_cache("libc/0.1", requires=["liba/(latest)"]) self.alias_cache("libb/latest", "libb/0.1") self.alias_cache("libc/latest", "libc/0.1") consumer = self.recipe_consumer("app/0.1", ["libb/(latest)", "libc/(latest)"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(app, "app/0.1", deps=[libb, libc]) # node, include, link, build, run _check_transitive(app, [(libb, True, True, False, False), (libc, True, True, False, False), (liba, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_alias_bug(self): # https://github.com/conan-io/conan/issues/2252 # app -> libb/0.1 -> liba/latest -(alias)->liba/0.1 # \ -> libc/0.1 ----/ self.recipe_cache("liba/0.1") self.alias_cache("liba/latest", "liba/0.1") self.recipe_cache("libb/0.1", requires=["liba/(latest)"]) self.recipe_cache("libc/0.1", requires=["liba/(latest)"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[liba], dependents=[app]) self._check_node(app, "app/0.1", deps=[libb, libc]) # node, include, link, build, run _check_transitive(app, [(libb, True, True, False, False), (libc, True, True, False, False), (liba, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_alias_tansitive(self): # app -> liba/giga -(alias)->-> liba/mega -(alias)-> liba/latest -(alias)->liba/0.1 self.recipe_cache("liba/0.1") self.alias_cache("liba/latest", "liba/0.1") self.alias_cache("liba/mega", "liba/(latest)") self.alias_cache("liba/giga", "liba/(mega)") consumer = self.recipe_consumer("app/0.1", ["liba/(giga)"]) deps_graph = self.build_consumer(consumer) assert 2 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[app]) self._check_node(app, "app/0.1", deps=[liba]) # node, include, link, build, run _check_transitive(app, [(liba, True, True, False, False)]) class TestAliasBuildRequires(GraphManagerTest): def test_non_conflicting_alias(self): # https://github.com/conan-io/conan/issues/5468 # libc ----> libb -------------------> liba/0.1 # \-(build)-> liba/latest -(alias)-> liba/0.2 self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.alias_cache("liba/latest", "liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"], build_requires=["liba/(latest)"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba_build = app.edges[1].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[libb]) self._check_node(liba_build, "liba/0.2#123", dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(app, "app/0.1", deps=[libb, liba_build]) def test_mixing_aliases_and_fix_versions(): # cd/1.0 -----------------------------> cb/latest -(alias)-> cb/1.0 -> ca/1.0 # \-----> cc/latest -(alias)-> cc/1.0 ->/ / # \------ ca/latest -(alias)------->/ client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("ca", "1.0")}) client.run("create . ") client.alias("ca/latest@", "ca/1.0@") client.save({"conanfile.py": GenConanfile("cb", "1.0").with_requirement("ca/1.0@")}) client.run("create . --name=cb --version=1.0") client.alias("cb/latest@", "cb/1.0@") client.save({"conanfile.py": GenConanfile("cc", "1.0") .with_requirement("cb/(latest)") .with_requirement("ca/(latest)")}) client.run("create . ") client.alias("cc/latest@", "cc/1.0@") client.save({"conanfile.py": GenConanfile("cd", "1.0") .with_requirement("cb/(latest)") .with_requirement("cc/(latest)")}) client.run("create . ") ================================================ FILE: test/integration/graph/core/test_auto_package_type.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient simple = """ from conan import ConanFile class Pkg(ConanFile): options = {"shared": [True, False], "header_only": [True, False]} """ pkg_type = """ from conan import ConanFile class Pkg(ConanFile): package_type = "library" options = {"shared": [True, False], "header_only": [True, False]} """ remove = """ from conan import ConanFile class Pkg(ConanFile): package_type = "library" options = {"shared": [True, False], "header_only": [True, False]} def configure(self): if self.options.header_only: self.options.rm_safe("shared") """ @pytest.mark.parametrize("conanfile", [simple, pkg_type, remove]) def test_auto_package_type(conanfile): c = TestClient(light=True) c.save({"conanfile.py": conanfile}) c.run("graph info . --filter package_type") assert "package_type: static-library" in c.out c.run("graph info . --filter package_type -o shared=True") assert "The package_type will have precedence over the options" not in c.out assert "package_type: shared-library" in c.out c.run("graph info . --filter package_type -o shared=True -o header_only=False") assert "package_type: shared-library" in c.out c.run("graph info . --filter package_type -o header_only=True") assert "package_type: header-library" in c.out c.run("graph info . --filter package_type -o header_only=True -o shared=False") assert "package_type: header-library" in c.out def test_package_type_and_header_library(): """ Show that forcing a package_type and header_only=True does not change the package_type""" tc = TestClient(light=True) tc.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): package_type = "static-library" options = {"header_only": [True, False]} """)}) tc.run("graph info . --filter package_type -o &:header_only=False") assert "package_type: static-library" in tc.out assert "The package_type will have precedence over the options" in tc.out tc.run("graph info . --filter package_type -o &:header_only=True") assert "package_type: static-library" in tc.out assert "The package_type will have precedence over the options" in tc.out ================================================ FILE: test/integration/graph/core/test_build_require_invalid.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID class TestInvalidConfiguration: """ ConanInvalidConfiguration without a binary fall backs, result in errors """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Conan(ConanFile): settings = "os" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Package does not work in Windows!") """) linux_package_id = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" invalid = "Invalid" @pytest.fixture(scope="class") def client(self): client = TestClient() client.save({"pkg/conanfile.py": self.conanfile}) client.run("create pkg --name=pkg --version=0.1 -s os=Linux") return client def test_invalid(self, client): conanfile_consumer = GenConanfile().with_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("install consumer -s os=Windows", assert_error=True) assert "pkg/0.1: Invalid: Package does not work in Windows!" in client.out def test_invalid_info(self, client): """ the conan info command does not raise, but it outputs info """ conanfile_consumer = GenConanfile().with_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("graph info consumer -s os=Windows") assert "binary: Invalid" in client.out def test_valid(self, client): conanfile_consumer = GenConanfile().with_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("install consumer -s os=Linux") client.assert_listed_binary({"pkg/0.1": (self.linux_package_id, "Cache")}) assert "pkg/0.1: Already installed!" in client.out def test_invalid_build_require(self, client): conanfile_consumer = GenConanfile().with_tool_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("install consumer -s:h os=Windows -s:b os=Windows", assert_error=True) assert "pkg/0.1: Invalid: Package does not work in Windows!" in client.out def test_valid_build_require_two_profiles(self, client): conanfile_consumer = GenConanfile().with_tool_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("install consumer -s:b os=Linux -s:h os=Windows") client.assert_listed_binary({"pkg/0.1": (self.linux_package_id, "Cache")}, build=True) assert "pkg/0.1: Already installed!" in client.out class TestErrorConfiguration(TestInvalidConfiguration): """ A configuration error is unsolvable, even if a binary exists """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Conan(ConanFile): settings = "os" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Package does not work in Windows!") def package_id(self): del self.info.settings.os """) linux_package_id = NO_SETTINGS_PACKAGE_ID invalid = "ConfigurationError" class TestErrorConfigurationCompatible(TestInvalidConfiguration): """ A configuration error is unsolvable, even if a binary exists """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Conan(ConanFile): settings = "os" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Package does not work in Windows!") def compatibility(self): if self.settings.os == "Windows": return [{"settings": [("os", "Linux")]}] """) linux_package_id = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" invalid = "ConfigurationError" class TestInvalidBuildPackageID: """ ConanInvalidBuildConfiguration will not block if setting is removed from package_id """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Conan(ConanFile): settings = "os" def validate_build(self): if self.settings.os == "Windows": raise ConanInvalidConfiguration("Package does not work in Windows!") def package_id(self): del self.info.settings.os """) linux_package_id = NO_SETTINGS_PACKAGE_ID windows_package_id = NO_SETTINGS_PACKAGE_ID @pytest.fixture(scope="class") def client(self): client = TestClient() client.save({"pkg/conanfile.py": self.conanfile}) client.run("create pkg --name=pkg --version=0.1 -s os=Linux") return client def test_valid(self, client): conanfile_consumer = GenConanfile().with_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("install consumer -s os=Windows") client.assert_listed_binary({"pkg/0.1": (self.linux_package_id, "Cache")}) assert "pkg/0.1: Already installed!" in client.out client.run("install consumer -s os=Linux") client.assert_listed_binary({"pkg/0.1": (self.linux_package_id, "Cache")}) assert "pkg/0.1: Already installed!" in client.out def test_invalid_try_build(self, client): conanfile_consumer = GenConanfile().with_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("install consumer -s os=Windows --build='*'", assert_error=True) # Only when trying to build, it will try to build the Windows one client.assert_listed_binary({"pkg/0.1": (self.windows_package_id, "Invalid")}) assert "Package does not work in Windows!" in client.out def test_valid_build_require_two_profiles(self, client): conanfile_consumer = GenConanfile().with_tool_requires("pkg/0.1").with_settings("os") client.save({"consumer/conanfile.py": conanfile_consumer}) client.run("install consumer -s:b os=Linux -s:h os=Windows") client.assert_listed_binary({"pkg/0.1": (self.linux_package_id, "Cache")}, build=True) assert "pkg/0.1: Already installed!" in client.out client.run("install consumer -s:b os=Windows -s:h os=Windows") client.assert_listed_binary({"pkg/0.1": (self.linux_package_id, "Cache")}, build=True) assert "pkg/0.1: Already installed!" in client.out class TestInvalidBuildCompatible(TestInvalidBuildPackageID): """ ConanInvalidBuildConfiguration will not block if compatible_packages fallback """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Conan(ConanFile): settings = "os" def validate_build(self): if self.settings.os == "Windows": raise ConanInvalidConfiguration("Package does not work in Windows!") def compatibility(self): if self.settings.os == "Windows": return [{"settings": [("os", "Linux")]}] """) linux_package_id = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" windows_package_id = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" ================================================ FILE: test/integration/graph/core/test_build_requires.py ================================================ import textwrap import pytest from conan.internal.graph.graph_error import GraphConflictError, GraphLoopError from conan.api.model import RecipeReference from test.integration.graph.core.graph_manager_base import GraphManagerTest from conan.test.utils.tools import GenConanfile, NO_SETTINGS_PACKAGE_ID, TestClient def _check_transitive(node, transitive_deps): values = list(node.transitive_deps.values()) assert len(values) == len(transitive_deps), f"{node}:{len(values)} != {len(transitive_deps)}" for v1, v2 in zip(values, transitive_deps): # asserts were difficult to debug if v1.node is not v2[0]: raise Exception(f"{v1.node}!={v2[0]}") if v1.require.headers is not v2[1]: raise Exception(f"{v1.node}!={v2[0]} headers") if v1.require.libs is not v2[2]: raise Exception(f"{v1.node}!={v2[0]} libs") if v1.require.build is not v2[3]: raise Exception(f"{v1.node}!={v2[0]} build") if v1.require.run is not v2[4]: raise Exception(f"{v1.node}!={v2[0]} run") if len(v2) >= 6: if v1.require.test is not v2[5]: raise Exception(f"{v1.node}!={v2[0]} test") class TestBuildRequiresGraph(GraphManagerTest): @pytest.mark.parametrize("build_require", ["recipe", "profile"]) def test_basic(self, build_require): # app -(br)-> cmake self._cache_recipe("cmake/0.1", GenConanfile()) if build_require == "recipe": profile_build_requires = None conanfile = GenConanfile("app", "0.1").with_tool_requires("cmake/0.1") else: profile_build_requires = {"*": [RecipeReference.loads("cmake/0.1")]} conanfile = GenConanfile("app", "0.1") deps_graph = self.build_graph(conanfile, profile_build_requires=profile_build_requires, install=False) # Build requires always apply to the consumer assert 2 == len(deps_graph.nodes) app = deps_graph.root cmake = app.edges[0].dst self._check_node(app, "app/0.1@", deps=[cmake], dependents=[]) self._check_node(cmake, "cmake/0.1#123", deps=[], dependents=[app]) def test_lib_build_require(self): # app -> lib -(br)-> cmake self._cache_recipe("cmake/0.1", GenConanfile()) self._cache_recipe("lib/0.1", GenConanfile().with_tool_requires("cmake/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1")) assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app]) self._check_node(cmake, "cmake/0.1#123", deps=[], dependents=[lib]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) _check_transitive(lib, [(cmake, False, False, True, True)]) @pytest.mark.parametrize("cmakelib_type", ["shared", "static", "notrun", "run"]) def test_build_require_transitive(self, cmakelib_type): # app -> lib -(br)-> cmake -> cmakelib (cmakelib_type) if cmakelib_type in ("notrun", "run"): # Unknown cmakelib = GenConanfile().with_settings("os") else: cmakelib = GenConanfile().with_settings("os").\ with_shared_option(cmakelib_type == "shared") run = True if cmakelib_type == "run" else None # Not necessary to specify self._cache_recipe("cmakelib/0.1", cmakelib) self._cache_recipe("cmake/0.1", GenConanfile().with_settings("os"). with_requirement("cmakelib/0.1", run=run)) self._cache_recipe("lib/0.1", GenConanfile().with_settings("os"). with_tool_requires("cmake/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_settings("os"). with_require("lib/0.1")) assert 4 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst cmakelib = cmake.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[], settings={"os": "Linux"}) self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app], settings={"os": "Linux"}) self._check_node(cmake, "cmake/0.1#123", deps=[cmakelib], dependents=[lib], settings={"os": "Windows"}) self._check_node(cmakelib, "cmakelib/0.1#123", deps=[], dependents=[cmake], settings={"os": "Windows"}) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) if cmakelib_type in ("static", "notrun"): _check_transitive(lib, [(cmake, False, False, True, True)]) else: _check_transitive(lib, [(cmake, False, False, True, True), (cmakelib, False, False, True, True)]) def test_build_require_bootstrap(self): # app -> lib -(br)-> cmake/2 -(br)-> cmake/1 self._cache_recipe("cmake/0.1", GenConanfile()) self._cache_recipe("cmake/0.2", GenConanfile().with_tool_requires("cmake/0.1")) self._cache_recipe("lib/0.1", GenConanfile().with_tool_requires("cmake/0.2")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1")) assert 4 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake2 = lib.edges[0].dst cmake1 = cmake2.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake2], dependents=[app]) self._check_node(cmake2, "cmake/0.2#123", deps=[cmake1], dependents=[lib]) self._check_node(cmake1, "cmake/0.1#123", deps=[], dependents=[cmake2]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) _check_transitive(lib, [(cmake2, False, False, True, True)]) _check_transitive(cmake2, [(cmake1, False, False, True, True)]) def test_build_require_private(self): # app -> lib -(br)-> cmake -(private)-> zlib self._cache_recipe("zlib/0.1", GenConanfile()) self._cache_recipe("cmake/0.1", GenConanfile().with_requirement("zlib/0.1", visible=False)) self._cache_recipe("lib/0.1", GenConanfile().with_tool_requires("cmake/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1")) assert 4 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst zlib = cmake.edges[0].dst self._check_node(app, "app/0.1", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app]) self._check_node(cmake, "cmake/0.1#123", deps=[zlib], dependents=[lib]) self._check_node(zlib, "zlib/0.1#123", deps=[], dependents=[cmake]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) _check_transitive(lib, [(cmake, False, False, True, True)]) _check_transitive(cmake, [(zlib, True, True, False, False)]) class TestBuildRequiresTransitivityDiamond(GraphManagerTest): def test_build_require_transitive_static(self): # app -> lib -(br)-> cmake -> zlib1 (static) # \--(br)-> mingw -> zlib2 (static) self._cache_recipe("zlib/0.1", GenConanfile().with_shared_option(False)) self._cache_recipe("zlib/0.2", GenConanfile().with_shared_option(False)) self._cache_recipe("cmake/0.1", GenConanfile().with_require("zlib/0.1")) self._cache_recipe("mingw/0.1", GenConanfile().with_require("zlib/0.2")) self._cache_recipe("lib/0.1", GenConanfile().with_tool_requires("cmake/0.1", "mingw/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1")) assert 6 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst mingw = lib.edges[1].dst zlib1 = cmake.edges[0].dst zlib2 = mingw.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake, mingw], dependents=[app]) self._check_node(cmake, "cmake/0.1#123", deps=[zlib1], dependents=[lib]) self._check_node(zlib1, "zlib/0.1#123", deps=[], dependents=[cmake]) self._check_node(mingw, "mingw/0.1#123", deps=[zlib2], dependents=[lib]) self._check_node(zlib2, "zlib/0.2#123", deps=[], dependents=[mingw]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) _check_transitive(lib, [(cmake, False, False, True, True), (mingw, False, False, True, True)]) def test_build_require_transitive_shared(self): # app -> lib -(br)-> cmake -> zlib1 (shared) # \--(br)-> mingw -> zlib2 (shared) -> SHOULD CONFLICT self._cache_recipe("zlib/0.1", GenConanfile().with_shared_option(True)) self._cache_recipe("zlib/0.2", GenConanfile().with_shared_option(True)) self._cache_recipe("cmake/0.1", GenConanfile().with_require("zlib/0.1")) self._cache_recipe("mingw/0.1", GenConanfile().with_require("zlib/0.2")) self._cache_recipe("lib/0.1", GenConanfile().with_tool_requires("cmake/0.1", "mingw/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1"), install=False) assert type(deps_graph.error) is GraphConflictError out = str(deps_graph.error) assert "Version conflict: Conflict between zlib/0.2 and zlib/0.1 in the graph." in out assert "Conflict originates from lib/0.1" in out assert 6 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst mingw = lib.edges[1].dst zlib1 = cmake.edges[0].dst zlib2 = mingw.edges[0].dst assert zlib1 is not zlib2 self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake, mingw], dependents=[app]) self._check_node(cmake, "cmake/0.1#123", deps=[zlib1], dependents=[lib]) self._check_node(zlib1, "zlib/0.1#123", deps=[], dependents=[cmake]) self._check_node(mingw, "mingw/0.1#123", deps=[zlib2], dependents=[lib]) self._check_node(zlib2, "zlib/0.2#123", deps=[], dependents=[mingw]) def test_build_require_conflict(self): # https://github.com/conan-io/conan/issues/4931 # cheetah -> gazelle -> grass/0.1 # \--(br)----------> grass/0.2 self._cache_recipe("grass/0.1", GenConanfile()) self._cache_recipe("grass/0.2", GenConanfile()) self._cache_recipe("gazelle/0.1", GenConanfile().with_require("grass/0.1")) deps_graph = self.build_graph(GenConanfile("cheetah", "0.1") .with_require("gazelle/0.1") .with_tool_requires("grass/0.2")) assert 4 == len(deps_graph.nodes) cheetah = deps_graph.root gazelle = cheetah.edges[0].dst grass2 = cheetah.edges[1].dst grass1 = gazelle.edges[0].dst self._check_node(cheetah, "cheetah/0.1", deps=[gazelle, grass2]) self._check_node(gazelle, "gazelle/0.1#123", deps=[grass1], dependents=[cheetah]) self._check_node(grass1, "grass/0.1#123", deps=[], dependents=[gazelle]) self._check_node(grass2, "grass/0.2#123", dependents=[cheetah]) class TestBuildRequiresVisible(GraphManagerTest): def test_visible_build(self): self._cache_recipe("liba/0.1", GenConanfile()) self._cache_recipe("libb/0.1", GenConanfile().with_requirement("liba/0.1", build=True)) self._cache_recipe("libc/0.1", GenConanfile().with_requirement("libb/0.1", visible=False)) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("libc/0.1")) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1@", deps=[libc], dependents=[]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", deps=[], dependents=[libb]) # node, include, link, build, run _check_transitive(app, [(libc, True, True, False, False)]) _check_transitive(libc, [(libb, True, True, False, False), (liba, False, False, True, False)]) # liba is build & visible! _check_transitive(libb, [(liba, True, True, True, False)]) class TestTestRequire(GraphManagerTest): def test_basic(self): # app -(tr)-> gtest self._cache_recipe("gtest/0.1", GenConanfile()) conanfile = GenConanfile("app", "0.1").with_test_requires("gtest/0.1") deps_graph = self.build_graph(conanfile) # Build requires always apply to the consumer assert 2 == len(deps_graph.nodes) app = deps_graph.root gtest = app.edges[0].dst self._check_node(app, "app/0.1@", deps=[gtest], dependents=[]) self._check_node(gtest, "gtest/0.1#123", deps=[], dependents=[app]) # node, include, link, build, run, test _check_transitive(app, [(gtest, True, True, False, False, True)]) def test_lib_build_require(self): # app -> lib -(tr)-> gtest self._cache_recipe("gtest/0.1", GenConanfile()) self._cache_recipe("lib/0.1", GenConanfile().with_test_requires("gtest/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1")) assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst gtest = lib.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[gtest], dependents=[app]) self._check_node(gtest, "gtest/0.1#123", deps=[], dependents=[lib]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) _check_transitive(lib, [(gtest, True, True, False, False)]) def test_lib_build_require_transitive(self): # app -> lib -(tr)-> gtest self._cache_recipe("gtest/0.1", GenConanfile()) self._cache_recipe("lib/0.1", GenConanfile().with_test_requires("gtest/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1")) assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst gtest = lib.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[gtest], dependents=[app]) self._check_node(gtest, "gtest/0.1#123", deps=[], dependents=[lib]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) _check_transitive(lib, [(gtest, True, True, False, False)]) @pytest.mark.parametrize("gtestlib_type", ["shared", "static", "notrun", "run"]) def test_test_require_transitive(self, gtestlib_type): # app -> lib -(tr)-> gtest -> gtestlib (gtestlib_type) if gtestlib_type in ("notrun", "run"): # Unknown gtestlib = GenConanfile().with_settings("os") else: gtestlib = GenConanfile().with_settings("os"). \ with_shared_option(gtestlib_type == "shared") run = True if gtestlib_type == "run" else None # Not necessary to specify self._cache_recipe("gtestlib/0.1", gtestlib) self._cache_recipe("gtest/0.1", GenConanfile().with_settings("os"). with_requirement("gtestlib/0.1", run=run)) self._cache_recipe("lib/0.1", GenConanfile().with_settings("os"). with_test_requires("gtest/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_settings("os"). with_require("lib/0.1")) assert 4 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst gtest = lib.edges[0].dst gtestlib = gtest.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[], settings={"os": "Linux"}) self._check_node(lib, "lib/0.1#123", deps=[gtest], dependents=[app], settings={"os": "Linux"}) self._check_node(gtest, "gtest/0.1#123", deps=[gtestlib], dependents=[lib], settings={"os": "Linux"}) self._check_node(gtestlib, "gtestlib/0.1#123", deps=[], dependents=[gtest], settings={"os": "Linux"}) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) # TODO: Check run=None if gtestlib_type in ("shared", "run"): _check_transitive(lib, [(gtest, True, True, False, False), (gtestlib, True, True, False, True)]) elif gtestlib_type == "static": _check_transitive(lib, [(gtest, True, True, False, False), (gtestlib, True, True, False, False)]) elif gtestlib_type == "notrun": _check_transitive(lib, [(gtest, True, True, False, False), (gtestlib, True, True, False, False)]) def test_trait_aggregated(self): # app -> lib -(tr)-> gtest -> zlib # \-------------------/ # If zlib is in the host context, a dependency for host, better test=False trait self._cache_recipe("zlib/0.1", GenConanfile()) self._cache_recipe("gtest/0.1", GenConanfile().with_requires("zlib/0.1")) self._cache_recipe("lib/0.1", GenConanfile().with_test_requires("gtest/0.1") .with_requires("zlib/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1")) assert 4 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst gtest = lib.edges[1].dst zlib = gtest.edges[0].dst zlib2 = lib.edges[0].dst assert zlib is zlib2 self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[zlib, gtest], dependents=[app]) self._check_node(gtest, "gtest/0.1#123", deps=[zlib], dependents=[lib]) self._check_node(zlib, "zlib/0.1#123", deps=[], dependents=[gtest, lib]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False), (zlib, True, True, False, False, False)]) _check_transitive(lib, [(gtest, True, True, False, False), (zlib, True, True, False, False, False)]) def test_test_require_loop(self): # https://github.com/conan-io/conan/issues/15412 self._cache_recipe("gtest/1.11", GenConanfile()) self._cache_recipe("abseil/1.0", GenConanfile().with_test_requires("gtest/[>=1 <1.14]")) self._cache_recipe("gtest/1.14", GenConanfile().with_requires("abseil/1.0")) deps_graph = self.build_graph(GenConanfile("opencv", "1.0").with_test_requires("gtest/1.14")) assert 4 == len(deps_graph.nodes) opencv = deps_graph.root gtest14 = opencv.edges[0].dst abseil = gtest14.edges[0].dst gtest11 = abseil.edges[0].dst self._check_node(opencv, "opencv/1.0@", deps=[gtest14], dependents=[]) self._check_node(gtest14, "gtest/1.14#123", deps=[abseil], dependents=[opencv]) self._check_node(abseil, "abseil/1.0#123", deps=[gtest11], dependents=[gtest14]) self._check_node(gtest11, "gtest/1.11#123", deps=[], dependents=[abseil]) class TestTestRequiresProblemsShared(GraphManagerTest): def _check_graph(self, deps_graph, reverse): assert 3 == len(deps_graph.nodes) lib_c = deps_graph.root if not reverse: lib_a = lib_c.edges[0].dst util = lib_a.edges[0].dst util2 = lib_c.edges[1].dst else: util = lib_c.edges[0].dst lib_a = lib_c.edges[1].dst util2 = lib_a.edges[0].dst assert util is util2 self._check_node(lib_c, "lib_c/0.1@", deps=[lib_a, util], dependents=[]) self._check_node(lib_a, "lib_a/0.1#123", deps=[util], dependents=[lib_c]) self._check_node(util, "util/0.1#123", deps=[], dependents=[lib_a, lib_c]) # node, include, link, build, run _check_transitive(lib_c, [(lib_a, True, True, False, True), (util, True, True, False, True)]) @pytest.mark.parametrize("reverse", [True, False]) def test_fixed_versions(self, reverse): # lib_c -(tr)-> lib_a -0.1--> util # \--------(tr)----0.1------/ # if versions exactly match, it shouldn't be an issue self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/0.1") .with_package_type("shared-library")) deps = ("lib_a/0.1", "util/0.1") if not reverse else ("util/0.1", "lib_a/0.1") deps_graph = self.build_graph(GenConanfile("lib_c", "0.1").with_test_requires(*deps)) self._check_graph(deps_graph, reverse) @pytest.mark.parametrize("reverse", [True, False]) def test_fixed_versions_conflict(self, reverse): # lib_c -(tr)-> lib_a -0.1--> util # \--------(tr)----0.2------/ # This should be a a conflict of versions self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("util/0.2", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/0.1") .with_package_type("shared-library")) deps = ("lib_a/0.1", "util/0.2") if not reverse else ("util/0.2", "lib_a/0.1") conanfile = GenConanfile("lib_c", "0.1").with_test_requires(*deps) deps_graph = self.build_graph(conanfile, install=False) assert type(deps_graph.error) is GraphConflictError @pytest.mark.parametrize("reverse", [True, False]) def test_fixed_versions_hybrid(self, reverse): # lib_c -----> lib_a--0.1--> util # \--------(tr)----0.1------/ # mixing requires + test_requires, should work self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("util/0.2", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/0.1") .with_package_type("shared-library")) conanfile = GenConanfile("lib_c", "0.1") if not reverse: conanfile = conanfile.with_requires("lib_a/0.1").with_test_requires("util/0.1") else: conanfile = conanfile.with_test_requires("lib_a/0.1").with_requires("util/0.1") deps_graph = self.build_graph(conanfile) self._check_graph(deps_graph, reverse=reverse) @pytest.mark.parametrize("reverse", [True, False]) def test_fixed_versions_hybrid_conflict(self, reverse): # lib_c -----> lib_a--0.1---> util # \--------(tr)----0.2------/ # Same as above, but mixing regular requires with test_requires self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("util/0.2", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/0.1") .with_package_type("shared-library")) conanfile = GenConanfile("lib_c", "0.1") if not reverse: conanfile = conanfile.with_requires("lib_a/0.1").with_test_requires("util/0.2") else: conanfile = conanfile.with_test_requires("lib_a/0.1").with_requires("util/0.2") deps_graph = self.build_graph(conanfile, install=False) assert type(deps_graph.error) is GraphConflictError @pytest.mark.parametrize("reverse", [True, False]) def test_version_ranges(self, reverse): # lib_c -(tr)-> lib_a -> util # \--------(tr)-------/ self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/[>=0.1 <1]") .with_package_type("shared-library")) deps = ("lib_a/[>=0]", "util/[>=0]") if not reverse else ("util/[>=0]", "lib_a/[>=0]") deps_graph = self.build_graph(GenConanfile("lib_c", "0.1").with_test_requires(*deps)) self._check_graph(deps_graph, reverse) @pytest.mark.parametrize("reverse", [True, False]) def test_version_ranges_conflict(self, reverse): # lib_c -(tr)-> lib_a -> util/0.1 # \--------(tr)------> util/1.0 self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("util/1.0", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/[>=0.1 <1]") .with_package_type("shared-library")) deps = ("lib_a/[>=0]", "util/[>=1]") if not reverse else ("util/[>=1]", "lib_a/[>=0]") deps_graph = self.build_graph(GenConanfile("lib_c", "0.1").with_test_requires(*deps), install=False) assert type(deps_graph.error) is GraphConflictError @pytest.mark.parametrize("reverse", [True, False]) def test_version_ranges_hybrid(self, reverse): # lib_c ---> lib_a -> util # \--------(tr)-------/ self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/[>=0.1 <1]") .with_package_type("shared-library")) conanfile = GenConanfile("lib_c", "0.1") if not reverse: conanfile = conanfile.with_requires("lib_a/[>=0.1]").with_test_requires("util/[>=0.1]") else: conanfile = conanfile.with_test_requires("lib_a/[>=0.1]").with_requires("util/[>=0.1]") deps_graph = self.build_graph(conanfile) self._check_graph(deps_graph, reverse) @pytest.mark.parametrize("reverse", [True, False]) def test_version_ranges_hybrid_conflict(self, reverse): # lib_c -(tr)-> lib_a -> util/0.1 # \--------(tr)------> util/1.0 self._cache_recipe("util/0.1", GenConanfile().with_package_type("shared-library")) self._cache_recipe("util/1.0", GenConanfile().with_package_type("shared-library")) self._cache_recipe("lib_a/0.1", GenConanfile().with_requires("util/[>=0.1 <1]") .with_package_type("shared-library")) conanfile = GenConanfile("lib_c", "0.1") if not reverse: conanfile = conanfile.with_requires("lib_a/[>=0.1]").with_test_requires("util/[>=1]") else: conanfile = conanfile.with_test_requires("lib_a/[>=0.1]").with_requires("util/[>=1]") deps_graph = self.build_graph(conanfile, install=False) assert type(deps_graph.error) is GraphConflictError class TestBuildRequiresPackageID(GraphManagerTest): def test_default_no_affect(self,): # app -> lib -(br)-> cmake self.recipe_conanfile("cmake/0.1", GenConanfile()) self.recipe_conanfile("lib/0.1", GenConanfile().with_tool_requires("cmake/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("lib/0.1")) # Build requires always apply to the consumer assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app]) assert lib.package_id == NO_SETTINGS_PACKAGE_ID self._check_node(cmake, "cmake/0.1#123", deps=[], dependents=[lib]) def test_minor_mode(self,): # app -> lib -(br)-> cmake self.recipe_conanfile("cmake/0.1", GenConanfile()) self.recipe_conanfile("lib/0.1", GenConanfile(). with_tool_requirement("cmake/[*]", package_id_mode="minor_mode")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("lib/0.1")) # Build requires always apply to the consumer assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app]) assert lib.package_id == "6b92478cba14dbdc06d4e991430d3c1c04959d4a" assert lib.package_id != NO_SETTINGS_PACKAGE_ID self._check_node(cmake, "cmake/0.1#123", deps=[], dependents=[lib]) # Change the dependency to next minor self.recipe_conanfile("cmake/0.2", GenConanfile()) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("lib/0.1")) # Build requires always apply to the consumer assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst assert lib.package_id == "2813db72897dd13aca2af071efe8ecb116f679ed" assert lib.package_id != NO_SETTINGS_PACKAGE_ID class TestPublicBuildRequires(GraphManagerTest): def test_simple(self): # app -> lib -(br public)-> cmake self.recipe_conanfile("cmake/0.1", GenConanfile()) self.recipe_conanfile("lib/0.1", GenConanfile() .with_tool_requirement("cmake/0.1", visible=True)) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("lib/0.1")) # Build requires always apply to the consumer assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst cmake = lib.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app]) self._check_node(cmake, "cmake/0.1#123", deps=[], dependents=[lib]) # node, include, link, build, run _check_transitive(lib, [(cmake, False, False, True, True)]) _check_transitive(app, [(lib, True, True, False, False), (cmake, False, False, True, True)]) def test_deep_dependency_tree(self): # app -> liba -> libb-(br public) -> sfun -> libsfun -> libx -> liby -> libz # -(normal req) -> libsfun -> libx -> liby -> libz self.recipe_conanfile("libz/0.1", GenConanfile()) self.recipe_conanfile("liby/0.1", GenConanfile().with_requirement("libz/0.1", run=True)) self.recipe_conanfile("libx/0.1", GenConanfile().with_requirement("liby/0.1", run=True)) self.recipe_conanfile("libsfun/0.1", GenConanfile().with_requirement("libx/0.1", run=True)) self.recipe_conanfile("sfun/0.1", GenConanfile().with_requirement("libsfun/0.1", run=True)) self.recipe_conanfile("libb/0.1", GenConanfile() .with_tool_requirement("sfun/0.1", visible=True) .with_requirement("libsfun/0.1", run=True)) self.recipe_conanfile("liba/0.1", GenConanfile().with_requirement("libb/0.1", run=True)) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requirement("liba/0.1", run=True)) # Build requires always apply to the consumer assert 8 + 4 == len(deps_graph.nodes) app = deps_graph.root liba = app.edges[0].dst libb = liba.edges[0].dst libsfun = libb.edges[0].dst libx = libsfun.edges[0].dst liby = libx.edges[0].dst libz = liby.edges[0].dst sfun = libb.edges[1].dst libsfun_build = sfun.edges[0].dst libx_build = libsfun_build.edges[0].dst liby_build = libx_build.edges[0].dst libz_build = liby_build.edges[0].dst # TODO non-build-requires self._check_node(app, "app/0.1@", deps=[liba], dependents=[]) self._check_node(liba, "liba/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[sfun, libsfun], dependents=[liba]) self._check_node(sfun, "sfun/0.1#123", deps=[libsfun_build], dependents=[libb]) self._check_node(libsfun_build, "libsfun/0.1#123", deps=[libx_build], dependents=[sfun]) self._check_node(libx_build, "libx/0.1#123", deps=[liby_build], dependents=[libsfun_build]) self._check_node(liby_build, "liby/0.1#123", deps=[libz_build], dependents=[libx_build]) self._check_node(libz_build, "libz/0.1#123", deps=[], dependents=[liby_build]) # node, include, link, build, run _check_transitive(liby_build, [(libz_build, True, True, False, True)]) _check_transitive(libx_build, [(liby_build, True, True, False, True), (libz_build, True, True, False, True)]) _check_transitive(libsfun_build, [(libx_build, True, True, False, True), (liby_build, True, True, False, True), (libz_build, True, True, False, True)]) _check_transitive(sfun, [(libsfun_build, True, True, False, True), (libx_build, True, True, False, True), (liby_build, True, True, False, True), (libz_build, True, True, False, True)]) _check_transitive(libb, [(libsfun, True, True, False, True), (libx, True, True, False, True), (liby, True, True, False, True), (libz, True, True, False, True), (sfun, False, False, True, True), (libsfun_build, False, False, True, True), (libx_build, False, False, True, True), (liby_build, False, False, True, True), (libz_build, False, False, True, True)]) _check_transitive(liba, [(libb, True, True, False, True), (libsfun, True, True, False, True), (libx, True, True, False, True), (liby, True, True, False, True), (libz, True, True, False, True), (sfun, False, False, True, True), (libsfun_build, False, False, True, True), (libx_build, False, False, True, True), (liby_build, False, False, True, True), (libz_build, False, False, True, True)]) _check_transitive(app, [(liba, True, True, False, True), (libb, True, True, False, True), (libsfun, True, True, False, True), (libx, True, True, False, True), (liby, True, True, False, True), (libz, True, True, False, True), (sfun, False, False, True, True), (libsfun_build, False, False, True, True), (libx_build, False, False, True, True), (liby_build, False, False, True, True), (libz_build, False, False, True, True)]) def test_conflict_diamond(self): # app -> libb -(br public)-> cmake/0.1 # \--> libc -(br public)-> cmake/0.2 self.recipe_conanfile("cmake/0.1", GenConanfile()) self.recipe_conanfile("cmake/0.2", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_tool_requirement("cmake/0.1", visible=True)) self.recipe_conanfile("libc/0.1", GenConanfile().with_tool_requirement("cmake/0.2", visible=True)) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("libb/0.1", "libc/0.1"), install=False) assert type(deps_graph.error) is GraphConflictError # Build requires always apply to the consumer assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst cmake1 = libb.edges[0].dst self._check_node(app, "app/0.1@", deps=[libb, libc], dependents=[]) self._check_node(libb, "libb/0.1#123", deps=[cmake1], dependents=[app]) self._check_node(cmake1, "cmake/0.1#123", deps=[], dependents=[libb]) def test_conflict_diamond_two_levels(self): # app -> libd -> libb -(br public)-> cmake/0.1 # \--> libe -> libc -(br public)-> cmake/0.2 self.recipe_conanfile("cmake/0.1", GenConanfile()) self.recipe_conanfile("cmake/0.2", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_tool_requirement("cmake/0.1", visible=True)) self.recipe_conanfile("libc/0.1", GenConanfile().with_tool_requirement("cmake/0.2", visible=True)) self.recipe_conanfile("libd/0.1", GenConanfile().with_requires("libb/0.1")) self.recipe_conanfile("libe/0.1", GenConanfile().with_requires("libc/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("libd/0.1", "libe/0.1"), install=False) assert type(deps_graph.error) is GraphConflictError # Build requires always apply to the consumer assert 6 == len(deps_graph.nodes) app = deps_graph.root libd = app.edges[0].dst libe = app.edges[1].dst libb = libd.edges[0].dst libc = libe.edges[0].dst cmake1 = libb.edges[0].dst self._check_node(app, "app/0.1@", deps=[libd, libe], dependents=[]) self._check_node(libd, "libd/0.1#123", deps=[libb], dependents=[app]) self._check_node(libe, "libe/0.1#123", deps=[libc], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[cmake1], dependents=[libd]) self._check_node(libc, "libc/0.1#123", deps=[], dependents=[libe]) self._check_node(cmake1, "cmake/0.1#123", deps=[], dependents=[libb]) def test_tool_requires(self): # app -> libb -(br public)-> protobuf/0.1 # \--------------> protobuf/0.2 self.recipe_conanfile("protobuf/0.1", GenConanfile()) self.recipe_conanfile("protobuf/0.2", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_tool_requirement("protobuf/0.1", visible=True) .with_require("protobuf/0.2")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("libb/0.1")) # Build requires always apply to the consumer assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst protobuf_host = libb.edges[0].dst protobuf_build = libb.edges[1].dst self._check_node(app, "app/0.1@", deps=[libb], dependents=[]) self._check_node(libb, "libb/0.1#123", deps=[protobuf_host, protobuf_build], dependents=[app]) self._check_node(protobuf_host, "protobuf/0.2#123", deps=[], dependents=[libb]) self._check_node(protobuf_build, "protobuf/0.1#123", deps=[], dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (protobuf_host, True, True, False, False), (protobuf_build, False, False, True, True)]) def test_tool_requires_override(self): # app -> libb -(br public)-> protobuf/0.1 # \--------------> protobuf/0.2 # \---(br, override)------> protobuf/0.2 self.recipe_conanfile("protobuf/0.1", GenConanfile()) self.recipe_conanfile("protobuf/0.2", GenConanfile()) self.recipe_conanfile("libb/0.1", GenConanfile().with_tool_requirement("protobuf/0.1", visible=True) .with_require("protobuf/0.2")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_requires("libb/0.1") .with_tool_requirement("protobuf/0.2", override=True)) # Build requires always apply to the consumer assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst protobuf_host = libb.edges[0].dst protobuf_build = libb.edges[1].dst self._check_node(app, "app/0.1@", deps=[libb], dependents=[]) self._check_node(libb, "libb/0.1#123", deps=[protobuf_host, protobuf_build], dependents=[app]) self._check_node(protobuf_host, "protobuf/0.2#123", deps=[], dependents=[libb]) self._check_node(protobuf_build, "protobuf/0.2#123", deps=[], dependents=[libb]) # node, headers, lib, build, run _check_transitive(app, [(libb, True, True, False, False), (protobuf_host, True, True, False, False), (protobuf_build, False, False, True, True)]) _check_transitive(libb, [(protobuf_host, True, True, False, False), (protobuf_build, False, False, True, True)]) def test_test_require(self): # app -(tr)-> gtest/0.1 # This test should survive in 2.0 tool_ref = RecipeReference.loads("gtest/0.1") self._cache_recipe(tool_ref, GenConanfile("gtest", "0.1")) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "app" version = "0.1" def build_requirements(self): self.test_requires("gtest/0.1") """) deps_graph = self.build_graph(conanfile) # Build requires always apply to the consumer assert 2 == len(deps_graph.nodes) app = deps_graph.root tool = app.edges[0].dst self._check_node(app, "app/0.1@", deps=[tool], dependents=[]) self._check_node(tool, "gtest/0.1#123", deps=[], dependents=[app]) class TestLoops(GraphManagerTest): def test_direct_loop_error(self): # app -(br)-> cmake/0.1 -(br itself)-> cmake/0.1.... # causing an infinite loop self._cache_recipe("cmake/0.1", GenConanfile().with_tool_requires("cmake/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_build_requires("cmake/0.1"), install=False) assert type(deps_graph.error) is GraphLoopError # Build requires always apply to the consumer assert 4 == len(deps_graph.nodes) app = deps_graph.root tool = app.edges[0].dst tool2 = tool.edges[0].dst tool3 = tool2.edges[0].dst self._check_node(app, "app/0.1", deps=[tool], dependents=[]) self._check_node(tool, "cmake/0.1#123", deps=[tool2], dependents=[app]) self._check_node(tool2, "cmake/0.1#123", deps=[tool3], dependents=[tool]) def test_indirect_loop_error(self): # app -(br)-> gtest/0.1 -(br)-> cmake/0.1 -(br)->gtest/0.1 .... # causing an infinite loop self._cache_recipe("gtest/0.1", GenConanfile().with_tool_requires("cmake/0.1")) self._cache_recipe("cmake/0.1", GenConanfile().with_test_requires("gtest/0.1")) deps_graph = self.build_graph(GenConanfile().with_build_requires("cmake/0.1"), install=False) assert type(deps_graph.error) is GraphLoopError # Build requires always apply to the consumer assert 6 == len(deps_graph.nodes) app = deps_graph.root cmake = app.edges[0].dst gtest = cmake.edges[0].dst cmake2 = gtest.edges[0].dst gtest2 = cmake2.edges[0].dst cmake3 = gtest2.edges[0].dst assert deps_graph.error.ancestor == cmake assert deps_graph.error.node == cmake3 def test_infinite_recursion_test(self): """Ensure that direct conflicts in a node are properly reported and Conan does not loop""" tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("app").with_package_type("application")}) tc.run("create . --version=1.0 --build-require") tc.run("create . --version=1.1 --build-require") tc.run("graph info --tool-requires=app/1.1 --tool-requires=app/1.0", assert_error=True) assert "Duplicated requirement: app/1.0" in tc.out def test_tool_requires(): """Testing temporary tool_requires attribute being "an alias" of build_require and introduced to provide a compatible recipe with 2.0. At 2.0, the meaning of a build require being a 'tool' will be a tool_require.""" client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=tool1 --version=1.0") client.run("create . --name=tool2 --version=1.0") client.run("create . --name=tool3 --version=1.0") client.run("create . --name=tool4 --version=1.0") consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "tool2/1.0" build_requires = "tool3/1.0" def build_requirements(self): self.tool_requires("tool1/1.0") self.build_requires("tool4/1.0") def generate(self): assert len(self.dependencies.build.values()) == 4 """) client.save({"conanfile.py": consumer}) client.run("create . --name=consumer --version=1.0") client.assert_listed_require({"tool1/1.0": "Cache", "tool2/1.0": "Cache", "tool3/1.0": "Cache", "tool4/1.0": "Cache"}, build=True) class TestDuplicateBuildRequires: """ what happens when you require and tool_require the same dependency """ # https://github.com/conan-io/conan/issues/11179 @pytest.fixture() def client(self): client = TestClient() msg = "self.output.info('This is the binary for OS={}'.format(self.info.settings.os))" msg2 = "self.output.info('This is in context={}'.format(self.context))" client.save({"conanfile.py": GenConanfile().with_settings("os").with_package_id(msg) .with_package_id(msg2)}) client.run("create . --name=tool1 --version=1.0 -s os=Windows") client.run("create . --name=tool2 --version=1.0 -s os=Windows") client.run("create . --name=tool3 --version=1.0 -s os=Windows") client.run("create . --name=tool4 --version=1.0 -s os=Windows") consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "consumer" version = "1.0" tool_requires = "tool2/1.0" build_requires = "tool3/1.0" def requirements(self): self.requires("tool4/1.0") def build_requirements(self): self.build_requires("tool4/1.0") self.tool_requires("tool1/1.0") def generate(self): host_deps = sorted([d.ref for d, _ in self.dependencies.host.items()]) build_deps = sorted([d.ref for d, _ in self.dependencies.build.items()]) self.output.info("HOST DEPS: {}".format(host_deps)) self.output.info("BUILD DEPS: {}".format(build_deps)) assert len(host_deps) == 1, host_deps assert len(build_deps) == 4, build_deps """) client.save({"conanfile.py": consumer}) return client def test_tool_requires_in_test_package(self, client): """Test that tool requires can be listed as build and host requirements""" test = textwrap.dedent("""\ from conan import ConanFile class Test(ConanFile): def build_requirements(self): self.tool_requires(self.tested_reference_str) def test(self): pass """) client.save({"test_package/conanfile.py": test}) client.run("create . -s:b os=Windows -s:h os=Linux --build-require") assert "This is the binary for OS=Linux" not in client.out assert "This is in context=host" not in client.out client.assert_listed_require({"consumer/1.0": "Cache"}, build=True) for tool in "tool1", "tool2", "tool3", "tool4": client.assert_listed_require({f"{tool}/1.0": "Cache"}, build=True) assert f"{tool}/1.0: This is the binary for OS=Windows" in client.out assert f"{tool}/1.0: This is in context=build" in client.out assert "consumer/1.0: HOST DEPS: [tool4/1.0]" in client.out assert "consumer/1.0: BUILD DEPS: [tool1/1.0, tool2/1.0, tool3/1.0, tool4/1.0]" in client.out def test_test_requires_in_test_package(self, client): """Test that tool requires can be listed as build and host requirements""" test = textwrap.dedent("""\ from conan import ConanFile class Test(ConanFile): def build_requirements(self): self.test_requires(self.tested_reference_str) def test(self): pass """) client.save({"test_package/conanfile.py": test}) client.run("create . -s:b os=Windows -s:h os=Linux --build=missing") for tool in "tool1", "tool2", "tool3", "tool4": client.assert_listed_require({f"{tool}/1.0": "Cache"}, build=True) assert f"{tool}/1.0: This is the binary for OS=Windows" in client.out assert f"{tool}/1.0: This is in context=build" in client.out client.assert_listed_require({"consumer/1.0": "Cache", "tool4/1.0": "Cache"}) client.assert_listed_binary({"tool4/1.0": ("9a4eb3c8701508aa9458b1a73d0633783ecc2270", "Build")}) assert "tool4/1.0: This is the binary for OS=Linux" in client.out assert "tool4/1.0: This is in context=host" in client.out assert "consumer/1.0: HOST DEPS: [tool4/1.0]" in client.out assert "consumer/1.0: BUILD DEPS: [tool1/1.0, tool2/1.0, tool3/1.0, tool4/1.0]" in client.out ================================================ FILE: test/integration/graph/core/test_options.py ================================================ from conan.test.assets.genconanfile import GenConanfile from test.integration.graph.core.graph_manager_base import GraphManagerTest from test.integration.graph.core.graph_manager_test import _check_transitive class TestOptions(GraphManagerTest): def test_basic(self): # app -> libb0.1 (lib shared=True) -> liba0.1 (default static) # By default if packages do not specify anything link=True is propagated run=None (unknown) self.recipe_cache("liba/0.1", option_shared=False) self.recipe_conanfile("libb/0.1", GenConanfile().with_requires("liba/0.1"). with_default_option("liba/*:shared", True)) consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb], options={"shared": "True"}) # node, include, link, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, True, True, False, True)]) _check_transitive(libb, [(liba, True, True, False, True)]) def test_app_override(self): # app (liba static)-> libb0.1 (liba shared) -> liba0.1 (default static) # By default if packages do not specify anything link=True is propagated run=None (unknown) self.recipe_cache("liba/0.1", option_shared=False) self.recipe_conanfile("libb/0.1", GenConanfile().with_requires("liba/0.1"). with_default_option("liba/*:shared", True)) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_requires("libb/0.1"). with_default_option("liba/*:shared", False)) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb], options={"shared": "False"}) # node, include, link, build, run _check_transitive(app, [(libb, True, True, False, False), (liba, True, True, False, False)]) _check_transitive(libb, [(liba, True, True, False, False)]) def test_diamond_no_conflict(self): # app -> libb0.1 ---------------> liba0.1 # \-> libc0.1 (liba shared) -> liba0.1 # Just the first expanded one will prevail self.recipe_cache("liba/0.1", option_shared=False) self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_conanfile("libc/0.1", GenConanfile().with_requires("liba/0.1"). with_default_option("liba*:shared", True)) # If we expand first the "libb", it will expand to "liba" as static (shared=False) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst liba2 = libc.edges[0].dst assert liba is liba2 self._check_node(app, "app/0.1", deps=[libc, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc], options={"shared": "False"}) # Changing the order, will make "liba" shared=True consumer = self.recipe_consumer("app/0.1", ["libc/0.1", "libb/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = app.edges[1].dst liba = libb.edges[0].dst liba2 = libc.edges[0].dst assert liba is liba2 self._check_node(app, "app/0.1", deps=[libc, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc], options={"shared": "True"}) def test_diamond_downstream_priority(self): # app(liba:shared) -> libb0.1 ---------------> liba0.1 # \--------------> libc0.1 (liba shared) -> liba0.1 self.recipe_cache("liba/0.1", option_shared=False) self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_conanfile("libc/0.1", GenConanfile().with_requires("liba/0.1"). with_default_option("liba*:shared", True)) for shared in True, False: consumer = self.consumer_conanfile(GenConanfile("app", "0.1") .with_requires("libb/0.1", "libc/0.1") .with_default_option("liba*:shared", shared)) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst liba2 = libc.edges[0].dst assert liba is liba2 self._check_node(app, "app/0.1", deps=[libc, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc], options={"shared": str(shared)}) # order doesn't matter consumer = self.consumer_conanfile(GenConanfile("app", "0.1") .with_requires("libc/0.1", "libb/0.1") .with_default_option("liba*:shared", shared)) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = app.edges[1].dst liba = libb.edges[0].dst liba2 = libc.edges[0].dst assert liba is liba2 self._check_node(app, "app/0.1", deps=[libc, libb]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app]) self._check_node(liba, "liba/0.1#123", dependents=[libb, libc], options={"shared": str(shared)}) class TestBuildRequireOptions(GraphManagerTest): def test_protobuf_different_options_profile(self): # app -> lib ------> protobuf -> zlib (shared) # \--(br)-> protobuf -> zlib (static) self._cache_recipe("zlib/0.1", GenConanfile().with_shared_option(True)) self._cache_recipe("protobuf/0.1", GenConanfile().with_require("zlib/0.1")) self._cache_recipe("lib/0.1", GenConanfile().with_requires("protobuf/0.1"). with_tool_requires("protobuf/0.1")) deps_graph = self.build_graph(GenConanfile("app", "0.1").with_require("lib/0.1"), options_build={"zlib*:shared": False}) assert 6 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst protobuf_host = lib.edges[0].dst protobuf_build = lib.edges[1].dst zlib_shared = protobuf_host.edges[0].dst zlib_static = protobuf_build.edges[0].dst self._check_node(app, "app/0.1@", deps=[lib], dependents=[]) self._check_node(lib, "lib/0.1#123", deps=[protobuf_host, protobuf_build], dependents=[app]) self._check_node(protobuf_host, "protobuf/0.1#123", deps=[zlib_shared], dependents=[lib]) self._check_node(protobuf_build, "protobuf/0.1#123", deps=[zlib_static], dependents=[lib]) self._check_node(zlib_shared, "zlib/0.1#123", deps=[], dependents=[protobuf_host]) self._check_node(zlib_static, "zlib/0.1#123", deps=[], dependents=[protobuf_build]) assert not zlib_static.conanfile.options.shared assert zlib_shared.conanfile.options.shared # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False), (protobuf_host, True, True, False, False), (zlib_shared, True, True, False, True)]) _check_transitive(lib, [(protobuf_host, True, True, False, False), (zlib_shared, True, True, False, True), (protobuf_build, False, False, True, True)]) ================================================ FILE: test/integration/graph/core/test_provides.py ================================================ import textwrap import pytest from conan.internal.graph.graph_error import GraphProvidesError from test.integration.graph.core.graph_manager_base import GraphManagerTest from test.integration.graph.core.graph_manager_test import _check_transitive from conan.test.utils.tools import GenConanfile, TestClient class TestProvidesTest(GraphManagerTest): def test_direct_conflict(self): # app (provides feature)-> libb0.1 (provides feature) self.recipe_conanfile("libb/0.1", GenConanfile().with_provides("feature")) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_provides("feature"). with_requires("libb/0.1")) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphProvidesError assert 2 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[], dependents=[app]) def test_transitive_conflict(self): # app (provides feature)-> libb0.1 -> libc/0.1 (provides feature) self.recipe_conanfile("libc/0.1", GenConanfile().with_provides("feature")) self.recipe_conanfile("libb/0.1", GenConanfile().with_requires("libc/0.1")) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_provides("feature"). with_requires("libb/0.1")) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphProvidesError assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libb]) self._check_node(libb, "libb/0.1#123", deps=[libc], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[], dependents=[libb]) @pytest.mark.parametrize("private", [True, False]) def test_branches_conflict(self, private): # app -> libb/0.1 (provides feature) # \ -> libc/0.1 (provides feature) self.recipe_conanfile("libc/0.1", GenConanfile().with_provides("feature")) self.recipe_conanfile("libb/0.1", GenConanfile().with_provides("feature")) if private: consumer = self.consumer_conanfile(GenConanfile("app", "0.1"). with_requirement("libb/0.1", visible=False). with_requirement("libc/0.1", visible=False)) else: consumer = self.consumer_conanfile(GenConanfile("app", "0.1"). with_requires("libb/0.1", "libc/0.1")) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphProvidesError assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[], dependents=[app]) def test_private_no_conflict(self): # app (provides libjpeg) -(private)-> br/v1 -(private)-> br_lib/v1(provides libjpeg) self.recipe_conanfile("br_lib/0.1", GenConanfile().with_provides("libjpeg")) self.recipe_conanfile("br/0.1", GenConanfile().with_requirement("br_lib/0.1", visible=False)) path = self.consumer_conanfile(GenConanfile("app", "0.1"). with_requirement("br/0.1", visible=False). with_provides("libjpeg")) deps_graph = self.build_consumer(path) assert 3 == len(deps_graph.nodes) app = deps_graph.root br = app.edges[0].dst br_lib = br.edges[0].dst self._check_node(app, "app/0.1", deps=[br]) self._check_node(br, "br/0.1#123", deps=[br_lib], dependents=[app]) self._check_node(br_lib, "br_lib/0.1#123", deps=[], dependents=[br]) # node, include, link, build, run _check_transitive(app, [(br, True, True, False, False)]) _check_transitive(br, [(br_lib, True, True, False, False)]) def test_diamond_conflict(self): # app -> libb0.1 -> liba0.1 # \-> libc0.1 -> libd0.2 (provides liba) self.recipe_cache("liba/0.1") self.recipe_conanfile("libd/0.2", GenConanfile().with_provides("liba")) self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["libd/0.2"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphProvidesError assert 5 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba1 = libb.edges[0].dst libd2 = libc.edges[0].dst self._check_node(app, "app/0.1", deps=[libb, libc]) self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app]) self._check_node(libc, "libc/0.1#123", deps=[libd2], dependents=[app]) self._check_node(liba1, "liba/0.1#123", dependents=[libb]) # TODO: Conflicted without revision self._check_node(libd2, "libd/0.2#123", dependents=[libc]) def test_loop(self): # app -> libc0.1 -> libb0.1 -> liba0.1 ->| # \<------(provides)---------| self.recipe_conanfile("liba/0.1", GenConanfile().with_provides("libc")) self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["libb/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphProvidesError assert 4 == len(deps_graph.nodes) app = deps_graph.root libc = app.edges[0].dst libb = libc.edges[0].dst liba = libb.edges[0].dst self._check_node(app, "app/0.1", deps=[libc]) self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app]) self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) self._check_node(liba, "liba/0.1#123", dependents=[libb]) class TestProvidesBuildRequire(GraphManagerTest): def test_build_require_no_conflict(self): # app (provides libjpeg) -(build)-> br/v1 -> br_lib/v1(provides libjpeg) self.recipe_conanfile("br_lib/0.1", GenConanfile().with_provides("libjpeg")) self.recipe_cache("br/0.1", ["br_lib/0.1"]) path = self.consumer_conanfile(GenConanfile("app", "0.1").with_tool_requires("br/0.1"). with_provides("libjpeg")) deps_graph = self.build_consumer(path) assert 3 == len(deps_graph.nodes) app = deps_graph.root br = app.edges[0].dst br_lib = br.edges[0].dst self._check_node(app, "app/0.1", deps=[br]) self._check_node(br, "br/0.1#123", deps=[br_lib], dependents=[app]) self._check_node(br_lib, "br_lib/0.1#123", deps=[], dependents=[br]) # node, include, link, build, run _check_transitive(app, [(br, False, False, True, True)]) _check_transitive(br, [(br_lib, True, True, False, False)]) def test_transitive_br_no_conflict(self): # app (provides libjpeg) -> lib/v1 -(br)-> br/v1(provides libjpeg) self.recipe_conanfile("br/0.1", GenConanfile().with_provides("libjpeg")) self.recipe_conanfile("lib/0.1", GenConanfile().with_tool_requires("br/0.1")) path = self.consumer_conanfile(GenConanfile("app", "0.1").with_requires("lib/0.1"). with_provides("libjpeg")) deps_graph = self.build_consumer(path) assert 3 == len(deps_graph.nodes) app = deps_graph.root lib = app.edges[0].dst br = lib.edges[0].dst self._check_node(app, "app/0.1", deps=[lib]) self._check_node(lib, "lib/0.1#123", deps=[br], dependents=[app]) self._check_node(br, "br/0.1#123", deps=[], dependents=[lib]) # node, include, link, build, run _check_transitive(app, [(lib, True, True, False, False)]) _check_transitive(lib, [(br, False, False, True, True)]) def test_transitive_test_require_conflict(self): # app (provides libjpeg) -(test)-> br/v1 -> br_lib/v1(provides libjpeg) self.recipe_conanfile("br_lib/0.1", GenConanfile().with_provides("libjpeg")) self.recipe_cache("br/0.1", ["br_lib/0.1"]) path = self.consumer_conanfile(GenConanfile("app", "0.1").with_test_requires("br/0.1"). with_provides("libjpeg")) deps_graph = self.build_consumer(path, install=False) assert type(deps_graph.error) is GraphProvidesError assert 3 == len(deps_graph.nodes) app = deps_graph.root br = app.edges[0].dst br_lib = br.edges[0].dst self._check_node(app, "app/0.1", deps=[br]) self._check_node(br, "br/0.1#123", deps=[br_lib], dependents=[app]) self._check_node(br_lib, "br_lib/0.1#123", deps=[], dependents=[br]) def test_two_br_conflict(self): # app -(build)-> br1/v1 (provides libjpeg) # \ -(build)-> br2/v1 (provides libjpeg) self.recipe_conanfile("br1/0.1", GenConanfile().with_provides("libjpeg")) self.recipe_conanfile("br2/0.1", GenConanfile().with_provides("libjpeg")) path = self.consumer_conanfile(GenConanfile("app", "0.1") .with_tool_requires("br1/0.1", "br2/0.1")) deps_graph = self.build_consumer(path, install=False) assert type(deps_graph.error) is GraphProvidesError assert 3 == len(deps_graph.nodes) app = deps_graph.root br1 = app.edges[0].dst br2 = app.edges[1].dst self._check_node(app, "app/0.1", deps=[br1, br2]) self._check_node(br1, "br1/0.1#123", deps=[], dependents=[app]) self._check_node(br2, "br2/0.1#123", deps=[], dependents=[app]) def test_conditional(): conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): requires = 'req/v1' options = {'conflict': [True, False]} default_options = {'conflict': False} def configure(self): if self.options.conflict: self.provides = 'libjpeg' def package_info(self): self.info.clear() """) t = TestClient(light=True) t.save({'requires.py': GenConanfile("req", "v1").with_provides("libjpeg"), 'app.py': conanfile}) t.run("create requires.py") t.run("install app.py --name=app --version=version") t.run("install app.py --name=app --version=version -o app/*:conflict=True", assert_error=True) assert "ERROR: Provide Conflict: Both 'app/version' and 'req/v1' provide 'libjpeg'" in t.out def test_self_build_require(): c = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.build import cross_building class Pkg(ConanFile): name = "grpc" version = "0.1" settings = "os" provides = "grpc-impl" def build_requirements(self): if cross_building(self): self.tool_requires("grpc/0.1") """) c.save({'conanfile.py': conanfile}) c.run("create . -s os=Windows -s:b os=Windows") c.assert_listed_binary({"grpc/0.1": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Build")}) c.run("create . -s os=Linux -s:b os=Windows --build=missing") c.assert_listed_binary({"grpc/0.1": ("9a4eb3c8701508aa9458b1a73d0633783ecc2270", "Build")}) c.assert_listed_binary({"grpc/0.1": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Cache")}, build=True) def test_name_provide_error_message(): tc = TestClient(light=True) tc.save({"libjepg/conanfile.py": GenConanfile("libjpeg", "0.1"), "mozjpeg/conanfile.py": GenConanfile("mozjpeg", "0.1").with_provides("libjpeg")}) tc.run("create libjepg") tc.run("create mozjpeg") tc.run("graph info --requires=mozjpeg/0.1 --requires=libjpeg/0.1", assert_error=True) # This used to report that None was provided, but now it reports the name of the provides assert ("ERROR: Provide Conflict: Both 'libjpeg/0.1' and " "'mozjpeg/0.1' provide '['libjpeg']'") in tc.out ================================================ FILE: test/integration/graph/core/test_version_ranges.py ================================================ from collections import OrderedDict import pytest from conan.api.model import Remote from conan.internal.graph.graph_error import GraphConflictError, GraphMissingError from conan.test.assets.genconanfile import GenConanfile from test.integration.graph.core.graph_manager_base import GraphManagerTest from conan.test.utils.tools import TestClient, TestServer, NO_SETTINGS_PACKAGE_ID class TestVersionRanges(GraphManagerTest): def test_transitive(self): # app -> libb[>0.1] self.recipe_cache("libb/0.1") self.recipe_cache("libb/0.2") consumer = self.recipe_consumer("app/0.1", ["libb/[>=0.0]"]) deps_graph = self.build_consumer(consumer) assert 2 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst self._check_node(libb, "libb/0.2#123", dependents=[app]) self._check_node(app, "app/0.1", deps=[libb]) def test_transitive_local_conditions(self): for v in ["0.1", "0.2", "0.3", "1.1", "1.1.2", "1.2.1", "2.1", "2.2.1"]: self.recipe_cache(f"libb/{v}") for expr, solution in [(">0.0", "2.2.1"), (">0.1 <1", "0.3"), (">0.1 <1||2.1", "2.1"), ("", "2.2.1"), ("~0", "0.3"), ("~1", "1.2.1"), ("~1.1", "1.1.2"), ("~2", "2.2.1"), ("~2.1", "2.1"), ]: consumer = self.recipe_consumer("app/0.1", [f"libb/[{expr}]"]) deps_graph = self.build_consumer(consumer) assert 2 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst self._check_node(libb, f"libb/{solution}#123", dependents=[app]) self._check_node(app, "app/0.1", deps=[libb]) def test_missing(self): # app -> libb[>0.1] (missing) consumer = self.recipe_consumer("app/0.1", ["libb/[>=0.0]"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphMissingError assert 1 == len(deps_graph.nodes) app = deps_graph.root self._check_node(app, "app/0.1", deps=[]) def test_userchannel_no_match(self): # app -> libb[>0.1] (missing) self.recipe_cache("libb/0.1@user/channel") consumer = self.recipe_consumer("app/0.1", ["libb/[>=0.0]"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphMissingError assert 1 == len(deps_graph.nodes) app = deps_graph.root self._check_node(app, "app/0.1", deps=[]) def test_required_userchannel_no_match(self): # app -> libb[>0.1] (missing) self.recipe_cache("libb/0.1") consumer = self.recipe_consumer("app/0.1", ["libb/[>=0.0]@user/channel"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphMissingError assert 1 == len(deps_graph.nodes) app = deps_graph.root self._check_node(app, "app/0.1", deps=[]) def test_transitive_out_range(self): # app -> libb[>0.1] (missing) self.recipe_cache("libb/0.1") consumer = self.recipe_consumer("app/0.1", ["libb/[>1.0]"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphMissingError assert 1 == len(deps_graph.nodes) app = deps_graph.root self._check_node(app, "app/0.1", deps=[]) class TestVersionRangesDiamond(GraphManagerTest): def test_transitive(self): # app -> libb/0.1 -(range >0)-> liba/0.2 # \ -> libc/0.1 -(range <1)---/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/[>=0.0]"]) self.recipe_cache("libc/0.1", ["liba/[<1.0]"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.2#123", dependents=[libb, libc], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, libc]) def test_transitive_interval(self): # app -> libb/0.1 -(range >0 <0.3)-> liba/0.2 # \ -> libc/0.1 -(range <1)--------/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("liba/0.3") self.recipe_cache("libb/0.1", ["liba/[>=0.0 <0.3]"]) self.recipe_cache("libc/0.1", ["liba/[<1.0]"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.2#123", dependents=[libb, libc], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, libc]) def test_transitive_fixed(self): # app -> libb/0.1 --------> liba/0.1 # \ -> libc/0.1 -(range <1)---/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) self.recipe_cache("libc/0.1", ["liba/[<1.0]"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer) assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[libb, libc], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, libc]) def test_transitive_conflict(self): # app -> libb/0.1 -(range >0)-> liba/0.2 # \ -> libc/0.1 -(range >1)---/ self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/[>=0.0]"]) self.recipe_cache("libc/0.1", ["liba/[>1.0]"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError assert 4 == len(deps_graph.nodes) app = deps_graph.root app.enabled_remotes = [Remote("foo", None)] libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[app], deps=[]) self._check_node(app, "app/0.1", deps=[libb, libc]) def test_transitive_fixed_conflict(self): # app -> libb/0.1 ---------> liba/0.2 # \ -> libc/0.1 -(range >1)---/ self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/[>=0.0]"]) self.recipe_cache("libc/0.1", ["liba/[>1.0]"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "libc/0.1"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError assert 4 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst libc = app.edges[1].dst liba = libb.edges[0].dst self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[app], deps=[]) self._check_node(app, "app/0.1", deps=[libb, libc]) class TestVersionRangesOverridesDiamond(GraphManagerTest): def test_transitive(self): # app -> libb/0.1 -(range >0)-> liba/0.2 # \ ---------------------------/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/[>=0.0]"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "liba/0.2"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.2#123", dependents=[libb, app], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, liba]) def test_transitive_overriden(self): # app -> libb/0.1 -(range >0)-> liba/0.1 # \ ---------liba/0.1-------------/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/[>=0.0]"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "liba/0.1"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[libb, app], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, liba]) def test_transitive_fixed(self): # app ---> libb/0.1 -----------> liba/0.1 # \ --------(range<1)----------/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "liba/[<1.0]"]) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.1#123", dependents=[libb, app], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, liba]) def test_transitive_fixed_conflict(self): # app ---> libb/0.1 -----------> liba/0.1 # \ --------(range>1)----------/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/1.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.recipe_consumer("app/0.1", ["libb/0.1", "liba/[>1.0]"]) deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError assert 2 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst def test_transitive_fixed_conflict_forced(self): # app ---> libb/0.1 -----------> liba/1.2 # \ --------(range>1)----------/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/1.2") self.recipe_cache("libb/0.1", ["liba/0.1"]) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1") .with_requirement("liba/[>1.0]", force=True)) deps_graph = self.build_consumer(consumer, install=False) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(liba, "liba/1.2#123", dependents=[libb, app], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, liba]) def test_two_ranges_overriden(self): # app -> libb/0.1 -(range >0)-> liba/0.1 # \ ---------liba/[<0.3>]-------------/ self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("liba/0.3") self.recipe_cache("libb/0.1", ["liba/[>=0.0]"]) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1") .with_requirement("liba/[<0.4]")) deps_graph = self.build_consumer(consumer) assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.3#123", dependents=[libb, app], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, liba]) def test_two_ranges_overriden_no_conflict(self): # app -> libb/0.1 -(range >0)-> liba/0.1 # \ ---------liba/[<0.3>]-------------/ # Conan learned to solve this conflict in 2.0.14 self.recipe_cache("liba/0.1") self.recipe_cache("liba/0.2") self.recipe_cache("liba/0.3") self.recipe_cache("libb/0.1", ["liba/[>=0.0]"]) consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1") .with_requirement("liba/[<0.3]")) deps_graph = self.build_consumer(consumer, install=False) # This is no longer a conflict, and Conan knows that liba/2.0 is a valid joint solution assert 3 == len(deps_graph.nodes) app = deps_graph.root libb = app.edges[0].dst liba = libb.edges[0].dst self._check_node(liba, "liba/0.2#123", dependents=[libb, app], deps=[]) self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba]) self._check_node(app, "app/0.1", deps=[libb, liba]) def test_mixed_user_channel(): # https://github.com/conan-io/conan/issues/7846 t = TestClient(default_server_user=True, light=True) t.save({"conanfile.py": GenConanfile()}) t.run("create . --name=pkg --version=1.0") t.run("create . --name=pkg --version=1.1") t.run("create . --name=pkg --version=2.0") t.run("create . --name=pkg --version=1.0 --user=user --channel=testing") t.run("create . --name=pkg --version=1.1 --user=user --channel=testing") t.run("create . --name=pkg --version=2.0 --user=user --channel=testing") t.run("upload * --confirm -r default") t.run("remove * -c") t.run('install --requires="pkg/[>0 <2]@"') t.assert_listed_require({"pkg/1.1": "Downloaded (default)"}) t.run('install --requires="pkg/[>0 <2]@user/testing"') t.assert_listed_require({"pkg/1.1@user/testing": "Downloaded (default)"}) def test_remote_version_ranges(): t = TestClient(default_server_user=True, light=True) t.save({"conanfile.py": GenConanfile()}) for v in ["0.1", "0.2", "0.3", "1.1", "1.1.2", "1.2.1", "2.1", "2.2.1"]: t.run(f"create . --name=dep --version={v}") t.run("upload * --confirm -r default") # TODO: Deprecate the comma separator for expressions for expr, solution in [(">0.0", "2.2.1"), (">0.1 <1", "0.3"), (">0.1 <1||2.1", "2.1"), ("", "2.2.1"), ("~0", "0.3"), ("~1", "1.2.1"), ("~1.1", "1.1.2"), ("~2", "2.2.1"), ("~2.1", "2.1"), ]: t.run("remove * -c") t.save({"conanfile.py": GenConanfile().with_requires(f"dep/[{expr}]")}) t.run("install .") assert str(t.out).count("Not found in local cache, looking in remotes") == 1 t.assert_listed_binary({f"dep/{solution}": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Download (default)")}) def test_different_user_channel_resolved_correctly(): server1 = TestServer() server2 = TestServer() servers = OrderedDict([("server1", server1), ("server2", server2)]) client = TestClient(servers=servers, inputs=2*["admin", "password"], light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0 --user=conan --channel=stable") client.run("create . --name=lib --version=1.0 --user=conan --channel=testing") client.run("upload lib/1.0@conan/stable -r=server1") client.run("upload lib/1.0@conan/testing -r=server2") client2 = TestClient(servers=servers, light=True) client2.run("install --requires=lib/[>=1.0]@conan/testing") assert f"lib/1.0@conan/testing: Retrieving package {NO_SETTINGS_PACKAGE_ID} " \ f"from remote 'server2' " in client2.out def test_unknown_options(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("lib", "2.0")}) c.run("create .") c.run("graph info --requires=lib/[>1.2,<1.4]", assert_error=True) assert '"<1.4" in version range ">1.2,<1.4" is not a valid option' in c.out c.run("graph info --requires=lib/[>1.2,unknown_conf]") assert 'WARN: Unrecognized version range option "unknown_conf" in ">1.2,unknown_conf"' in c.out @pytest.mark.parametrize("version_range,should_warn", [ [">=0.1, include_prereleases", False], [">=0.1, include_prerelease=True", True], [">=0.1, include_prerelease=False", True] ]) def test_bad_options_syntax(version_range, should_warn): """We don't error out on bad options, maybe we should, but for now this test ensures we don't change it without realizing""" tc = TestClient(light=True) tc.save({"lib/conanfile.py": GenConanfile("lib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires(f"lib/[{version_range}]")}) tc.run("export lib") tc.run("graph info app/conanfile.py") if should_warn: assert "its presence unconditionally enables prereleases" in tc.out else: assert "its presence unconditionally enables prereleases" not in tc.out def test_empty_version_ranger(): tc = TestClient(light=True) tc.save({"lib/conanfile.py": GenConanfile("lib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("lib/[]")}) tc.run("export lib") tc.run("graph info app") assert "lib/[]: lib/1.0" in tc.out assert "Empty version range usage is discouraged" in tc.out ================================================ FILE: test/integration/graph/require_override_test.py ================================================ from conan.test.utils.tools import TestClient, GenConanfile class TestRequireOverride: def test_override_user_channel(self): c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep1/0.1") .with_requires("dep2/0.1@us/chan"), "app/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_requirement("dep1/0.1@us/chan", override=True) .with_requirement("dep2/0.1", override=True)}) c.run("create dep --name=dep1 --version=0.1") c.run("create dep --name=dep1 --version=0.1 --user=us --channel=chan") c.run("create dep --name=dep2 --version=0.1") c.run("create dep --name=dep2 --version=0.1 --user=us --channel=chan") c.run("export pkg") c.run("graph info app") c.assert_overrides({"dep1/0.1": ['dep1/0.1@us/chan'], "dep2/0.1@us/chan": ['dep2/0.1']}) c.assert_listed_require({"dep1/0.1@us/chan": "Cache", "dep2/0.1": "Cache"}) def test_can_override_even_versions_with_build_metadata(self): # https://github.com/conan-io/conan/issues/5900 c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("lib")}) c.run("create . --version=1.0+abc") c.run("create . --version=1.0+xyz") c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_require("lib/1.0+abc")}) c.run("create .") c.save({"conanfile.py": GenConanfile().with_require("pkg/1.0") .with_requirement("lib/1.0+xyz", override=True)}) c.run("graph info .") c.assert_overrides({"lib/1.0+abc": ['lib/1.0+xyz']}) c.assert_listed_require({"lib/1.0+xyz": "Cache", "pkg/1.0": "Cache"}) ================================================ FILE: test/integration/graph/test_dependencies_visit.py ================================================ import textwrap import pytest from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_dependencies_visit(): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile().with_shared_option(True)}) client.run("create . --name=openssl --version=0.1") client.run("create . --name=openssl --version=0.2") client.save({"conanfile.py": GenConanfile().with_requires("openssl/0.2")}) client.run("create . --name=cmake --version=0.1") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "openssl/0.1" build_requires = "cmake/0.1" def generate(self): dep = self.dependencies["openssl"] self.output.info("DefRef: {}!!!".format(dep.ref.repr_notime())) self.output.info("DefPRef: {}!!!".format(dep.pref.repr_notime())) dep = self.dependencies.build["openssl"] self.output.info("DefRefBuild: {}!!!".format(dep.ref.repr_notime())) self.output.info("DefPRefBuild: {}!!!".format(dep.pref.repr_notime())) for r, d in self.dependencies.build.items(): self.output.info("DIRECTBUILD {}: {}".format(r.direct, d)) if "openssl" in self.dependencies: self.output.info("OpenSSL found in deps") if "cmake" in self.dependencies: self.output.info("cmake found in default deps") if "cmake" in self.dependencies.build: self.output.info("cmake found in deps.build") if "badlib" in self.dependencies: self.output.info("badlib found in deps") """) client.save({"conanfile.py": conanfile}) client.run("install .") refs = client.cache.get_latest_recipe_revision(RecipeReference.loads("openssl/0.1")) pkgs = client.cache.get_package_references(refs) prev1 = client.cache.get_latest_package_revision(pkgs[0]) assert f"DefRef: {repr(prev1.ref)}!!!" in client.out assert f"DefPRef: {prev1.repr_notime()}!!!" in client.out refs = client.cache.get_latest_recipe_revision(RecipeReference.loads("openssl/0.2")) pkgs = client.cache.get_package_references(refs) prev2 = client.cache.get_latest_package_revision(pkgs[0]) assert f"DefRefBuild: {repr(prev2.ref)}!!!" in client.out assert f"DefPRefBuild: {prev2.repr_notime()}!!!" in client.out assert "conanfile.py: DIRECTBUILD True: cmake/0.1" in client.out assert "conanfile.py: DIRECTBUILD False: openssl/0.2" in client.out assert "OpenSSL found in deps" in client.out assert "badlib found in deps" not in client.out assert "cmake found in default deps" not in client.out assert "cmake found in deps.build" in client.out def test_dependencies_visit_settings_options(): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile().with_settings("os"). with_option("shared", [True, False]).with_default_option("shared", False)}) client.run("create . --name=openssl --version=0.1 -s os=Linux") client.save({"conanfile.py": GenConanfile().with_requires("openssl/0.1")}) client.run("create . --name=pkg --version=0.1 -s os=Linux") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "pkg/0.1" def generate(self): dep = self.dependencies["openssl"] self.output.info("SETTINGS: {}!".format(dep.settings.os)) self.output.info("OPTIONS: shared={}!".format(dep.options.shared)) """) client.save({"conanfile.py": conanfile}) client.run("install . -s os=Linux") assert "conanfile.py: SETTINGS: Linux!" in client.out assert "conanfile.py: OPTIONS: shared=False!" in client.out asserts = [ ('print("=>{}".format(self.dependencies["zlib"].ref))', False, "=>zlib/0.2"), ('print("=>{}".format(self.dependencies.build.get("zlib").ref))', False, "=>zlib/0.1"), ('print("=>{}".format(self.dependencies.get("zlib", build=True).ref))', False, "=>zlib/0.1"), ('print("=>{}".format(self.dependencies.get("zlib", build=False).ref))', False, "=>zlib/0.2"), ('print("=>{}".format(self.dependencies.get("zlib", build=True, visible=False).ref))', False, "=>zlib/0.1"), ('self.dependencies.get("cmake", build=True)', True, 'There are more than one requires matching the specified filters: {\'build\': True}\n' '- cmake/0.1, Traits: build=True, headers=False, libs=False, run=False, visible=False\n' '- cmake/0.2, Traits: build=True, headers=False, libs=False, run=False, visible=False' ), ('self.dependencies["missing"]', True, "'missing' not found in the dependency set"), ('self.output.info("Missing in deps: " + str("missing" in self.dependencies))', False, "Missing in deps: False"), ('self.output.info("Zlib in deps: " + str("zlib" in self.dependencies))', False, "Zlib in deps: True"), ('self.output.info("Zlib in deps.build: " + str("zlib" in self.dependencies.build))', False, "Zlib in deps.build: True"), ] @pytest.mark.parametrize("generates_line, assert_error, output_text", asserts) def test_cmake_zlib(generates_line, assert_error, output_text): # app (br)---> cmake (0.1) -> zlib/0.1 # \ ----> zlib/0.2 # \ (br)---> cmake (0.2) client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=zlib --version=0.1") client.run("create . --name=zlib --version=0.2") client.save({"conanfile.py": GenConanfile().with_tool_requirement("zlib/0.1", visible=True)}) client.run("create . --name=cmake --version=0.1") client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=cmake --version=0.2") app_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def requirements(self): self.requires("zlib/0.2") self.requires("cmake/0.1", headers=False, libs=False, visible=False, build=True, run=False) self.requires("cmake/0.2", headers=False, libs=False, visible=False, build=True, run=False) def generate(self): {} """.format(generates_line)) client.save({"conanfile.py": app_conanfile}) client.run("create . --name=app --version=1.0", assert_error=assert_error) assert output_text in client.out def test_invisible_not_colliding_test_requires(): """ The test_requires are private, so the app can't see them, only the foo and bar :return: """ # app ---> foo/0.1 --test_require--> gtest/0.1 # \ ---> bar/0.1 --test_require--> gtest/0.2 client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=gtest --version=0.1") client.run("create . --name=gtest --version=0.2") client.save({"conanfile.py": GenConanfile().with_test_requires("gtest/0.1")}) client.run("create . --name=foo --version=0.1") client.save({"conanfile.py": GenConanfile().with_test_requires("gtest/0.2")}) client.run("create . --name=bar --version=0.1") app_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def requirements(self): self.requires("foo/0.1") self.requires("bar/0.1") def generate(self): print(self.dependencies.get("gtest")) """) client.save({"conanfile.py": app_conanfile}) client.run("create . --name=app --version=1.0", assert_error=True) assert "'gtest' not found in the dependency set" in client.out def test_dependencies_visit_build_requires_profile(): """ At validate() time, in Conan 1.X, the build-requires are not available yet, because first the binary package_id is computed, then the build-requires are resolved. It is necessary to avoid in Conan 1.X the caching of the ConanFile.dependencies, because at generate() time it will have all the graph info, including build-requires """ # https://github.com/conan-io/conan/issues/10304 client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("cmake", "0.1")}) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def validate(self): self.output.info("VALIDATE DEPS: {}!!!".format(len(self.dependencies.items()))) def generate(self): for req, dep in self.dependencies.items(): self.output.info("GENERATE REQUIRE: {}!!!".format(dep.ref)) dep = self.dependencies.build["cmake"] self.output.info("GENERATE CMAKE: {}!!!".format(dep.ref)) """) client.save({"conanfile.py": conanfile, "profile": "[tool_requires]\ncmake/0.1"}) client.run("install . -pr:b=default -pr:h=profile --build='*'") # Use 2 contexts # Validate time, build-requires available assert "conanfile.py: VALIDATE DEPS: 1!!!" in client.out # generate time, build-requires already available assert "conanfile.py: GENERATE REQUIRE: cmake/0.1!!!" in client.out assert "conanfile.py: GENERATE CMAKE: cmake/0.1!!!" in client.out def test_dependencies_package_type(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("lib", "0.1").with_package_type("static-library")}) c.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "lib/0.1" def generate(self): is_app = self.dependencies["lib"].package_type == "static-library" self.output.info(f"APP: {is_app}!!") assert is_app self.dependencies["lib"].package_type == "not-exist-type" """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "APP: True!!" in c.out assert "conanfile.py: Error in generate() method, line 9" in c.out assert "ValueError: 'not-exist-type' is not a valid PackageType" in c.out def test_dependency_interface(): """ Quick test for the ConanFileInterface exposed """ c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "1.0" homepage = "myhome" url = "myurl" license = "MIT" """) user = textwrap.dedent(""" from conan import ConanFile class User(ConanFile): requires = "dep/1.0" def generate(self): dep = self.dependencies["dep"] self.output.info("HOME: {}".format(dep.homepage)) self.output.info("URL: {}".format(dep.url)) self.output.info("LICENSE: {}".format(dep.license)) self.output.info("RECIPE FOLDER: {}".format(dep.recipe_folder)) self.output.info("CONANDATA: {}".format(dep.conan_data)) self.output.info("RECIPE: {}".format(dep.recipe)) """) c.save({"dep/conanfile.py": conanfile, "dep/conandata.yml": "", "user/conanfile.py": user}) c.run("create dep") c.run("install user") assert "conanfile.py: HOME: myhome" in c.out assert "conanfile.py: URL: myurl" in c.out assert "conanfile.py: LICENSE: MIT" in c.out assert "conanfile.py: RECIPE FOLDER:" in c.out assert "conanfile.py: CONANDATA: {}" in c.out assert "conanfile.py: RECIPE: Cache" in c.out def test_dependency_interface_validate(): """ In the validate() method, there is no access to dep.package_folder, because the packages are not there. validate() operates at the graph level, without binaries, before they are installed, and we want to keep it that way https://github.com/conan-io/conan/issues/11959 """ c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "1.0" homepage = "myhome" """) user = textwrap.dedent(""" import os from conan import ConanFile class User(ConanFile): requires = "dep/1.0" def validate(self): dep = self.dependencies["dep"] self.output.info("HOME: {}".format(dep.homepage)) self.output.info("PKG FOLDER: {}".format(dep.package_folder is None)) """) c.save({"dep/conanfile.py": conanfile, "user/conanfile.py": user}) c.run("create dep") c.run("install user") assert "conanfile.py: HOME: myhome" in c.out assert "conanfile.py: PKG FOLDER: True" in c.out def test_validate_visibility(): # https://github.com/conan-io/conan/issues/12027 c = TestClient(light=True) t1 = textwrap.dedent(""" from conan import ConanFile class t1Conan(ConanFile): name = "t1" version = "0.1" package_type = "static-library" def package_info(self): self.cpp_info.libs = ["mylib"] """) t2 = textwrap.dedent(""" from conan import ConanFile class t2Conan(ConanFile): name = "t2" version = "0.1" requires = "t1/0.1" package_type = "shared-library" def validate(self): self.output.info("VALID: {}".format(self.dependencies["t1"])) """) t3 = textwrap.dedent(""" from conan import ConanFile class t3Conan(ConanFile): name = "t3" version = "0.1" requires = "t2/0.1" package_type = "application" def validate(self): self.output.info("VALID: {}".format(self.dependencies["t1"])) self.output.info("VALID: {}".format(self.dependencies["t2"])) def generate(self): self.output.info("GENERATE: {}".format(self.dependencies["t1"])) self.output.info("GENERATE: {}".format(self.dependencies["t2"])) """) c.save({"t1/conanfile.py": t1, "t2/conanfile.py": t2, "t3/conanfile.py": t3}) c.run("create t1") c.run("create t2") c.run("install t3") assert "t2/0.1: VALID: t1/0.1" in c.out assert "conanfile.py (t3/0.1): VALID: t1/0.1" in c.out assert "conanfile.py (t3/0.1): VALID: t2/0.1" in c.out assert "conanfile.py (t3/0.1): GENERATE: t1/0.1" in c.out assert "conanfile.py (t3/0.1): GENERATE: t2/0.1" in c.out c.run("create t3") assert "t2/0.1: VALID: t1/0.1" in c.out assert "t3/0.1: VALID: t1/0.1" in c.out assert "t3/0.1: VALID: t2/0.1" in c.out assert "t3/0.1: GENERATE: t1/0.1" in c.out assert "t3/0.1: GENERATE: t2/0.1" in c.out ================================================ FILE: test/integration/graph/test_divergent_cppstd_build_host.py ================================================ import json import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_divergent_cppstd_build_host(): c = TestClient() conanfile = textwrap.dedent(""" [requires] waterfall/1.0 [tool_requires] rainbow/1.0 """) c.save({"waterfall/conanfile.py": GenConanfile("waterfall", "1.0").with_settings("compiler"), "rainbow/conanfile.py": GenConanfile("rainbow", "1.0").with_settings("compiler") .with_requires("waterfall/1.0"), "conanfile.txt": conanfile}) c.run("export waterfall") c.run("export rainbow") c.run(f"install . --build=missing -s compiler.cppstd=14 -s:b compiler.cppstd=17 --format=json", redirect_stdout="graph.json") graph = json.loads(c.load("graph.json")) # waterfall is twice in the graph: as a direct host dependency, and an indirect build dependency assert graph['graph']['nodes']['1']['ref'] == "waterfall/1.0#821e924dcef2f185dd651e6d434f9f95" assert graph['graph']['nodes']['1']['context'] == "host" assert graph['graph']['nodes']['3']['ref'] == "waterfall/1.0#821e924dcef2f185dd651e6d434f9f95" assert graph['graph']['nodes']['3']['context'] == "build" # is this the right behaviour? # without the compatibility plugin, we would require two different package_id in the same graph, # but because of compatibility plugin, Conan graph uses a "compatible" package_id in the # build context rather than the exact one assert graph['graph']['nodes']['1']['package_id'] == graph['graph']['nodes']['3']['package_id'] ================================================ FILE: test/integration/graph/test_platform_requires.py ================================================ import json import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestPlatformRequires: @pytest.mark.parametrize("revision", ["", "#myrev"]) def test_platform_requires(self, revision): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "profile": f"[platform_requires]\ndep/1.0{revision}"}) client.run("create . -pr=profile") assert f"dep/1.0{revision or '#platform'} - Platform" in client.out def test_platform_requires_non_matching(self): """ if what is specified in [platform_requires] doesn't match what the recipe requires, then the platform_requires will not be used, and the recipe will use its declared version """ client = TestClient(light=True) client.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "profile": "[platform_requires]\ndep/1.1"}) client.run("create dep") client.run("create . -pr=profile") assert "dep/1.0#6a99f55e933fb6feeb96df134c33af44 - Cache" in client.out @pytest.mark.parametrize("revision", ["", "#myrev"]) def test_platform_requires_range(self, revision): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/[>=1.0]"), "profile": f"[platform_requires]\ndep/1.1{revision}"}) client.run("create . -pr=profile") assert f"dep/1.1{revision or '#platform'} - Platform" in client.out def test_platform_requires_range_non_matching(self): """ if what is specified in [platform_requires] doesn't match what the recipe requires, then the platform_requires will not be used, and the recipe will use its declared version """ client = TestClient(light=True) client.save({"dep/conanfile.py": GenConanfile("dep", "1.1"), "conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/[>=1.0]"), "profile": "[platform_requires]\ndep/0.1"}) client.run("create dep") client.run("create . -pr=profile") assert "dep/1.1#af79f1e3973b7d174e7465038c3f5f36 - Cache" in client.out def test_platform_requires_no_host(self): """ platform_requires must not affect tool-requires """ client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("dep/1.0"), "profile": "[platform_requires]\ndep/1.0"}) client.run("create . -pr=profile", assert_error=True) assert "ERROR: Package 'dep/1.0' not resolved: No remote defined" in client.out def test_platform_requires_build_context(self): """ platform_requires must work in the build context too """ client = TestClient(light=True) client.save({"tool/conanfile.py": GenConanfile("tool", "1.0").with_requires("dep/1.0"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/1.0"), "profile": "[settings]\nos=Linux\n[platform_requires]\ndep/1.0"}) client.run("create tool -pr:b=profile --build-require") assert "dep/1.0#platform - Platform" in client.out client.run("create pkg -pr:b=profile") assert "dep/1.0#platform - Platform" in client.out def test_graph_info_platform_requires_range(self): """ graph info doesn't crash """ client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/[>=1.0]"), "profile": "[platform_requires]\ndep/1.1"}) client.run("graph info . -pr=profile") assert "dep/1.1#platform - Platform" in client.out def test_consumer_resolved_version(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/[>=1.0]" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {r.ref}") """) client.save({"conanfile.py": conanfile, "profile": "[platform_requires]\ndep/1.1"}) client.run("install . -pr=profile") assert "dep/1.1#platform - Platform" in client.out assert "conanfile.py: DEPENDENCY dep/1.1" in client.out @pytest.mark.parametrize("revision", ["", "#rev1"]) def test_consumer_resolved_revision(self, revision): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/1.1" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {repr(r.ref)}") """) client.save({"conanfile.py": conanfile, "profile": f"[platform_requires]\ndep/1.1{revision}"}) client.run("install . -pr=profile") assert "dep/1.1 - Platform" in client.out assert f"conanfile.py: DEPENDENCY dep/1.1{revision or '#platform'}" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/1.1#rev1" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {repr(r.ref)}") """) client.save({"conanfile.py": conanfile}) client.run("install . -pr=profile") assert "dep/1.1 - Platform" in client.out assert f"conanfile.py: DEPENDENCY dep/1.1{revision or '#platform'}" in client.out def test_consumer_unresolved_revision(self): """ if a recipe specifies an exact revision and so does the profile and it doesn't match, it is an error """ client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/1.1#rev2" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {repr(r.ref)}") """) client.save({"conanfile.py": conanfile, "profile": "[platform_requires]\ndep/1.1#rev1"}) client.run("install . -pr=profile", assert_error=True) assert "ERROR: Package 'dep/1.1' not resolved" in client.out def test_platform_requires_with_options(self): """ https://github.com/conan-io/conan/issues/15685 """ client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "profile": "[platform_requires]\ndep/1.0"}) client.run("create . -pr=profile -o *:myoption=True") assert "dep/1.0 - Platform" in client.out conanfile = GenConanfile("pkg", "1.0").with_requirement("dep/1.0", options={"dep/1.0:myoption": True}) client.save({"conanfile.py": conanfile, "profile": "[platform_requires]\ndep/1.0"}) client.run("create . -pr=profile") # This crashed before for non-existing options assert "dep/1.0 - Platform" in client.out class TestPlatformTestRequires: def test_platform_requires(self): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_test_requires("dep/1.0"), "profile": "[platform_requires]\ndep/1.0"}) client.run("create . -pr=profile") assert "dep/1.0 - Platform" in client.out class TestPlatformRequiresLock: @pytest.mark.parametrize("revision", ["", "#rev1"]) def test_platform_requires_range(self, revision): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/[>=1.0]"), "profile": f"[platform_requires]\ndep/1.1{revision}"}) c.run("lock create . -pr=profile") assert "dep/1.1 - Platform" in c.out lock = json.loads(c.load("conan.lock")) assert lock["requires"] == [f"dep/1.1{revision or '#platform'}"] c.run("install .", assert_error=True) assert "Package 'dep/1.1' not resolved: No remote defined" in c.out c.run("install . -pr=profile") assert f"dep/1.1{revision or '#platform'} - Platform" in c.out # if the profile points to another version it is an error, not in the lockfile c.save({"profile": f"[platform_requires]\ndep/1.2{revision}"}) c.run("install . -pr=profile", assert_error=True) assert f"ERROR: Requirement 'dep/1.2{revision or '#platform'}' not in lockfile" in c.out @pytest.mark.parametrize("platform_rev", [None, "myrev"]) @pytest.mark.parametrize("is_tool_platform", [True, False]) def test_platform_requires_lockfile(self, platform_rev, is_tool_platform): tc = TestClient(light=True) profile_rev = f"#{platform_rev}" if platform_rev else "" expected_rev = platform_rev if platform_rev else "platform" conanfile = GenConanfile("pkg", "1.0") if is_tool_platform: conanfile = conanfile.with_tool_requires("dep/1.0") else: conanfile = conanfile.with_requirement("dep/1.0") substitution = "platform_tool_requires" if is_tool_platform else "platform_requires" tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": conanfile, "profile": f"[{substitution}]\ndep/1.0{profile_rev}"}) tc.run("create dep") created_dep_ref = tc.created_layout().reference.ref # The platform revision is correctly captured in the lockfile tc.run("lock create --lockfile-out=platform.lock -pr=profile") assert f"dep/1.0#{expected_rev}" in tc.load("platform.lock") tc.run("lock create --lockfile-out=real.lock") assert str(created_dep_ref) in tc.load("real.lock") # Not using lockfiles when expanding the graph works as expected tc.run("install") tc.assert_listed_require({str(created_dep_ref): "Cache"}, build=is_tool_platform) tc.run("install -pr=profile") tc.assert_listed_require({f"dep/1.0#{expected_rev}": "Platform"}, build=is_tool_platform) # And also with lockfiles, the platform revision is correctly captured in the lockfile tc.run("install --lockfile=real.lock") tc.assert_listed_require({str(created_dep_ref): "Cache"}, build=is_tool_platform) tc.run("install -pr=profile --lockfile=platform.lock") tc.assert_listed_require({f"dep/1.0#{expected_rev}": "Platform"}, build=is_tool_platform) tc.run("install --lockfile=platform.lock", assert_error=True) assert "Package 'dep/1.0' not resolved" in tc.out tc.run("install -pr=profile --lockfile=real.lock", assert_error=True) assert f"Requirement 'dep/1.0#{expected_rev}' not in lockfile" in tc.out # Now, what happens if we just merge both lockfiles so we can switch on the fly? # Both can coexist because they have different revisions. # The platform requires does not have a timestamp, so it's never selected outside platform matches tc.run("lock merge --lockfile=real.lock --lockfile=platform.lock --lockfile-out=merged.lock") tc.run("install --lockfile=merged.lock") tc.assert_listed_require({str(created_dep_ref): "Cache"}, build=is_tool_platform) tc.run("install -pr=profile --lockfile=merged.lock") tc.assert_listed_require({f"dep/1.0#{expected_rev}": "Platform"}, build=is_tool_platform) # And now, having a lockfile which just has name/version requires = "build-requires" if is_tool_platform else "requires" tc.run(f"lock add --{requires}=dep/1.0 --lockfile-out=simple.lock") tc.run("install -pr=profile --lockfile=simple.lock") tc.assert_listed_require({f"dep/1.0#{expected_rev}": "Platform"}, build=is_tool_platform) class TestGenerators: def test_platform_requires_range(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "build_type" requires = "dep/[>=1.0]" generators = "CMakeDeps", # "PkgConfigDeps" """) client.save({"conanfile.py": conanfile, "profile": "[platform_requires]\ndep/1.1"}) client.run("install . -pr=profile") assert "dep/1.1 - Platform" in client.out assert not os.path.exists(os.path.join(client.current_folder, "dep-config.cmake")) assert not os.path.exists(os.path.join(client.current_folder, "dep.pc")) class TestPackageID: """ if a consumer depends on recipe revision or package_id what happens """ @pytest.mark.parametrize("package_id_mode", ["recipe_revision_mode", "full_package_mode"]) def test_package_id_modes(self, package_id_mode): """ this test validates that the computation of the downstream consumers package_id doesn't break even if it depends on fields not existing in upstream platform_requires, like package_id """ client = TestClient(light=True) client.save_home({"global.conf": f"core.package_id:default_unknown_mode={package_id_mode}"}) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "profile": "[platform_requires]\ndep/1.0"}) client.run("create . -pr=profile") assert "dep/1.0#platform - Platform" in client.out def test_package_id_explicit_revision(self): """ Changing the platform_requires revision affects consumers if package_revision_mode=recipe_revision """ client = TestClient(light=True) client.save_home({"global.conf": "core.package_id:default_unknown_mode=recipe_revision_mode"}) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "profile": "[platform_requires]\ndep/1.0#r1", "profile2": "[platform_requires]\ndep/1.0#r2"}) client.run("create . -pr=profile") assert "dep/1.0#r1 - Platform" in client.out assert "pkg/1.0#7ed9bbd2a7c3c4381438c163c93a9f21:" \ "d9d32dd13cfc9734becc01287570721cd048ba19 - Build" in client.out client.run("create . -pr=profile2") # pkg gets a new package_id because it is a different revision assert "dep/1.0#r2 - Platform" in client.out assert "pkg/1.0#7ed9bbd2a7c3c4381438c163c93a9f21:" \ "2f6bc9cf5015a7210181592d454f36687791a941 - Build" in client.out @pytest.mark.parametrize("revision", ["", "#myrev1"]) def test_package_id_full_package_mode(self, revision): """ platform_requires do not have settings or package_id, so it is ignored """ client = TestClient() client.save_home({"global.conf": "core.package_id:default_unknown_mode=full_package_mode"}) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "profile": f"[platform_requires]\ndep/1.0{revision}"}) client.run("create . -pr=profile -s os=Linux") assert f"dep/1.0{revision or '#platform'} - Platform" in client.out assert "pkg/1.0#7ed9bbd2a7c3c4381438c163c93a9f21:" \ "f2cfe57716d0a3320019f058edcd728d3379ab32 - Build" in client.out # The default revision is not taken into account at all assert f"pkg/1.0: requires: dep/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709" in client.out client.run("create . -pr=profile -s os=Windows") # pkg gets exactly same package_id, changing the settings, do not affect plaform package-id assert f"dep/1.0{revision or '#platform'} - Platform" in client.out assert "pkg/1.0#7ed9bbd2a7c3c4381438c163c93a9f21:" \ "f2cfe57716d0a3320019f058edcd728d3379ab32 - Build" in client.out @pytest.mark.parametrize("revision, package_id", [ ("", "f2cfe57716d0a3320019f058edcd728d3379ab32"), ("#myrev1", "bc8dc9f0bfb1e1d217f2ba7cccf545cdfdb382f6") ]) def test_package_id_full_mode(self, revision, package_id): """ platform_requires do not have settings or package_id, so it is ignored """ client = TestClient() client.save_home({"global.conf": "core.package_id:default_unknown_mode=full_mode"}) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0"), "profile": f"[platform_requires]\ndep/1.0{revision}"}) client.run("create . -pr=profile -s os=Linux") assert f"dep/1.0{revision or '#platform'} - Platform" in client.out assert "pkg/1.0#7ed9bbd2a7c3c4381438c163c93a9f21:" \ f"{package_id} - Build" in client.out # The default revision is not taken into account for package id calculation, # but if the user defines it, it is assert f"pkg/1.0: requires: dep/1.0{revision or ''}:da39a3ee5e6b4b0d3255bfef95601890afd80709" in client.out client.run("create . -pr=profile -s os=Windows") # pkg gets exactly same package_id, changing the settings, do not affect plaform package-id assert f"dep/1.0{revision or '#platform'} - Platform" in client.out assert "pkg/1.0#7ed9bbd2a7c3c4381438c163c93a9f21:" \ f"{package_id} - Build" in client.out ================================================ FILE: test/integration/graph/test_pure_runtime_dep.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_pure_runtime_dep(): c = TestClient() c.save({"tooldep/conanfile.py": GenConanfile("tooldep", "0.1").with_package_file("bin/mysh", "myshared") .with_package_type("shared-library"), "tool/conanfile.py": GenConanfile("tool", "0.1").with_requirement("tooldep/0.1") .with_package_file("bin/myexe", "exe"), "lib/conanfile.py": GenConanfile("lib", "0.1").with_requirement("tool/0.1", headers=False, libs=False, run=True), "consumer/conanfile.py": GenConanfile().with_settings("build_type", "os", "compiler", "arch") .with_requires("lib/0.1")}) c.run("create tooldep") c.run("create tool") c.run("create lib") c.run("install consumer -g CMakeDeps") ================================================ FILE: test/integration/graph/test_remote_resolution.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_build_requires_ranges(): # app -> pkga ----------> pkgb ------------> pkgc # \-cmake/[*] \ -cmake/1.0 \-cmake/[*] # The resolution of cmake/[*] is invariant, it will always resolved to cmake/0.5, not # one to cmake/0.5 and the next one to cmake/1.0 because in between there was an explicit # dependency to cmake/1.0 client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=cmake --version=0.5") client.run("create . --name=cmake --version=1.0") client.run("upload cmake/1.0* -c -r default") client.run("remove cmake/1.0* -c") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): {} tool_requires = "cmake/{}" def generate(self): for r, d in self.dependencies.items(): self.output.info("REQUIRE {{}}: {{}}".format(r.ref, d)) dep = self.dependencies.build.get("cmake") self.output.info("CMAKEVER: {{}}!!".format(dep.ref.version)) """) client.save({"pkgc/conanfile.py": conanfile.format("", "[*]"), "pkgb/conanfile.py": conanfile.format("requires = 'pkgc/1.0'", "1.0"), "pkga/conanfile.py": conanfile.format("requires = 'pkgb/1.0'", "[*]"), }) client.run("export pkgc --name=pkgc --version=1.0") client.run("export pkgb --name=pkgb --version=1.0") client.run("export pkga --name=pkga --version=1.0") client.run("install --requires=pkga/1.0@ --build=missing") assert "pkgc/1.0: REQUIRE cmake/0.5: cmake/0.5" in client.out assert "pkgc/1.0: CMAKEVER: 0.5!!" in client.out assert "pkgb/1.0: REQUIRE cmake/1.0: cmake/1.0" in client.out assert "pkgb/1.0: CMAKEVER: 1.0!!" in client.out assert "pkga/1.0: REQUIRE cmake/0.5: cmake/0.5" in client.out assert "pkga/1.0: CMAKEVER: 0.5!!" in client.out ================================================ FILE: test/integration/graph/test_repackaging.py ================================================ import re import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_repackage(): # consumer -> repackager -> liba # \------> libb client = TestClient(light=True) client.save({"conanfile.py": GenConanfile().with_package_file("liba.txt", "HelloA!")}) client.run("create . --name=liba --version=1.0") client.save({"conanfile.py": GenConanfile().with_package_file("libb.txt", "HelloB!")}) client.run("create . --name=libb --version=1.0 ") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): settings = "os" def requirements(self): self.requires("liba/1.0", headers=True, libs=True, visible=False, run=False) self.requires("libb/1.0", headers=True, libs=True, visible=False, run=False) def package(self): for r, d in self.dependencies.items(): copy(self, "*", src=d.package_folder, dst=os.path.join(self.package_folder, r.ref.name)) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=repackager --version=1.0") client.save({"conanfile.py": GenConanfile().with_requires("repackager/1.0")}, clean_first=True) client.run("install .") assert re.search(r"Skipped binaries(\s*)liba/1.0, libb/1.0", client.out) assert "repackager/1.0: Already installed!" in client.out def test_repackage_library_self(): c = TestClient() c.save({"conanfile.py": GenConanfile("liba", "1.0").with_package_file("a.txt", "A1.0!")}) c.run("create .") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): name = "liba" version = "2.0" def requirements(self): self.requires("liba/1.0", visible=False) def package(self): for _, dep in self.dependencies.items(): copy(self, "*", src=dep.package_folder, dst=self.package_folder) """) c.save({"conanfile.py": conanfile}) c.run("create .") c.run("install --requires=liba/2.0 --deployer=full_deploy") assert re.search(r"Skipped binaries(\s*)liba/1.0", c.out) assert "liba/2.0: Already installed!" in c.out assert c.load("full_deploy/host/liba/2.0/a.txt") == "A1.0!" def test_repackage_library_self_infinite_loop(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "liba" version = "1.0" def requirements(self): self.requires("liba/1.0", visible=False, headers=False, libs=False, run=False) """) c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) assert "There is a cycle/loop in the graph" in c.out def test_repackage_library_self_multiple(): c = TestClient() c.save({"1/conanfile.py": GenConanfile("liba", "1.0").with_package_file("a1.txt", "A1.0!"), "2/conanfile.py": GenConanfile("liba", "2.0").with_package_file("a2.txt", "A2.0!")}) c.run("create 1") c.run("create 2") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): name = "liba" version = "3.0" def requirements(self): # To avoid conflict among liba versions self.requires("liba/1.0", headers=False, libs=False, visible=False) self.requires("liba/2.0", visible=False) def package(self): for _, dep in self.dependencies.items(): copy(self, "*", src=dep.package_folder, dst=self.package_folder) """) c.save({"conanfile.py": conanfile}) c.run("create .") c.run("install --requires=liba/3.0 --deployer=full_deploy") assert re.search(r"Skipped binaries(\s*)liba/1.0, liba/2.0", c.out) assert "liba/3.0: Already installed!" in c.out assert c.load("full_deploy/host/liba/3.0/a1.txt") == "A1.0!" assert c.load("full_deploy/host/liba/3.0/a2.txt") == "A2.0!" def test_repackage_library_self_transitive(): c = TestClient() c.save({"a/conanfile.py": GenConanfile("liba", "1.0").with_package_file("a1.txt", "A1.0!"), "b/conanfile.py": GenConanfile("libb", "1.0").with_requires("liba/1.0")}) c.run("create a") c.run("create b") conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): name = "liba" version = "3.0" def requirements(self): # libs=True, otherwise transitive liba is skipped self.requires("libb/1.0", headers=False, libs=True, visible=False, run=False) def package(self): for _, dep in self.dependencies.items(): copy(self, "*", src=dep.package_folder, dst=self.package_folder) """) c.save({"conanfile.py": conanfile}) c.run("create .") c.run("install --requires=liba/3.0 --deployer=full_deploy") assert re.search(r"Skipped binaries(\s*)liba/1.0, libb/1.0", c.out) assert "liba/3.0: Already installed!" in c.out assert c.load("full_deploy/host/liba/3.0/a1.txt") == "A1.0!" ================================================ FILE: test/integration/graph/test_replace_requires.py ================================================ import json import textwrap import pytest from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("require, pattern, alternative, pkg", [ # PATTERN VERSIONS # override all dependencies to "dep" to a specific version,user and channel) # TODO: This is a version override, is this really wanted? ("dep/1.3", "dep/*", "dep/1.1", "dep/1.1"), ("dep/[>=1.0 <2]", "dep/*", "dep/1.1", "dep/1.1"), # override all dependencies to "dep" to the same version with other user, remove channel) ("dep/1.3", "dep/*", "dep/*@system", "dep/1.3@system"), ("dep/[>=1.0 <2]", "dep/*", "dep/*@system", "dep/1.1@system"), # override all dependencies to "dep" to the same version with other user, same channel) ("dep/1.3@comp/stable", "dep/*@*/*", "dep/*@system/*", "dep/1.3@system/stable"), ("dep/[>=1.0 <2]@comp/stable", "dep/*@*/*", "dep/*@system/*", "dep/1.1@system/stable"), # EXACT VERSIONS # replace exact dependency version for one in the system ("dep/1.1", "dep/1.1", "dep/1.1@system", "dep/1.1@system"), ("dep/[>=1.0 <2]", "dep/1.1", "dep/1.1@system", "dep/1.1@system"), ("dep/[>=1.0 <2]@comp", "dep/1.1@*", "dep/1.1@*/stable", "dep/1.1@comp/stable"), ("dep/1.1@comp", "dep/1.1@*", "dep/1.1@*/stable", "dep/1.1@comp/stable"), # PACKAGE ALTERNATIVES (zlib->zlibng) ("dep/1.0", "dep/*", "depng/*", "depng/1.0"), ("dep/[>=1.0 <2]", "dep/*", "depng/*", "depng/1.1"), ("dep/[>=1.0 <2]", "dep/1.1", "depng/1.2", "depng/1.2"), # NON MATCHING ("dep/1.3", "dep/1.1", "dep/1.1@system", "dep/1.3"), ("dep/1.3", "dep/*@comp", "dep/*@system", "dep/1.3"), ("dep/[>=1.0 <2]", "dep/2.1", "dep/2.1@system", "dep/1.1"), # PATTERN - PATTERN REPLACE ("dep/[>=1.3 <2]", "dep/*", "dep/[>=1.0 <1.9]", "dep/1.1"), # DIRECT REPLACE OF PINNED VERSIONS ("dep/1.3", "dep/1.3", "dep/1.5", "dep/1.5"), ]) @pytest.mark.parametrize("tool_require", [False, True]) class TestReplaceRequires: def test_alternative(self, tool_require, require, pattern, alternative, pkg): c = TestClient(light=True) conanfile = GenConanfile().with_tool_requires(require) if tool_require else \ GenConanfile().with_requires(require) profile_tag = "replace_requires" if not tool_require else "replace_tool_requires" c.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": conanfile, "profile": f"[{profile_tag}]\n{pattern}: {alternative}"}) ref = RecipeReference.loads(pkg) user = f"--user={ref.user}" if ref.user else "" channel = f"--channel={ref.channel}" if ref.channel else "" c.run(f"create dep --name={ref.name} --version={ref.version} {user} {channel}") rrev = c.exported_recipe_revision() c.run("profile show -pr=profile") assert profile_tag in c.out c.run("install pkg -pr=profile") assert profile_tag in c.out c.assert_listed_require({f"{pkg}#{rrev}": "Cache"}, build=tool_require) # Check lockfile c.run("lock create pkg -pr=profile") lock = c.load("pkg/conan.lock") assert f"{pkg}#{rrev}" in lock # c.run("create dep2 --version=1.2") # with lockfile c.run("install pkg -pr=profile") c.assert_listed_require({f"{pkg}#{rrev}": "Cache"}, build=tool_require) def test_diamond(self, tool_require, require, pattern, alternative, pkg): c = TestClient(light=True) conanfile = GenConanfile().with_tool_requires(require) if tool_require else \ GenConanfile().with_requires(require) profile_tag = "replace_requires" if not tool_require else "replace_tool_requires" c.save({"dep/conanfile.py": GenConanfile(), "libb/conanfile.py": conanfile, "libc/conanfile.py": conanfile, "app/conanfile.py": GenConanfile().with_requires("libb/0.1", "libc/0.1"), "profile": f"[{profile_tag}]\n{pattern}: {alternative}"}) ref = RecipeReference.loads(pkg) user = f"--user={ref.user}" if ref.user else "" channel = f"--channel={ref.channel}" if ref.channel else "" c.run(f"create dep --name={ref.name} --version={ref.version} {user} {channel}") rrev = c.exported_recipe_revision() c.run("export libb --name=libb --version=0.1") c.run("export libc --name=libc --version=0.1") c.run("install app -pr=profile", assert_error=True) assert "ERROR: Missing binary: libb/0.1" in c.out assert "ERROR: Missing binary: libc/0.1" in c.out c.run("install app -pr=profile --build=missing") c.assert_listed_require({f"{pkg}#{rrev}": "Cache"}, build=tool_require) # Check lockfile c.run("lock create app -pr=profile") lock = c.load("app/conan.lock") assert f"{pkg}#{rrev}" in lock # with lockfile c.run("install app -pr=profile") c.assert_listed_require({f"{pkg}#{rrev}": "Cache"}, build=tool_require) @pytest.mark.parametrize("pattern, replace", [ ("pkg", "pkg/0.1"), ("pkg/*", "pkg"), ("pkg/*:pid1", "pkg/0.1"), ("pkg/*:pid1", "pkg/0.1:pid2"), ("pkg/*", "pkg/0.1:pid2"), (":", ""), ("pkg/version:pid", ""), ("pkg/version:pid", ":") ]) def test_replace_requires_errors(pattern, replace): c = TestClient(light=True) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"), "app/conanfile.py": GenConanfile().with_requires("pkg/0.2"), "profile": f"[replace_requires]\n{pattern}: {replace}"}) c.run("create pkg") c.run("install app -pr=profile", assert_error=True) assert "ERROR: Error reading 'profile' profile: Error in [replace_xxx]" in c.out def test_replace_requires_invalid_requires_errors(): """ replacing for something incorrect not existing is not an error per-se, it is valid that a recipe requires("pkg/2.*"), and then it will fail because such package doesn't exist """ c = TestClient(light=True) c.save({"app/conanfile.py": GenConanfile().with_requires("pkg/0.2"), "profile": f"[replace_requires]\npkg/0.2: pkg/2.*"}) c.run("install app -pr=profile", assert_error=True) assert "pkg/0.2: pkg/2.*" in c.out # The replacement happens assert "ERROR: Package 'pkg/2.*' not resolved" in c.out def test_replace_requires_json_format(): c = TestClient(light=True) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.2"), "app/conanfile.py": GenConanfile().with_requires("pkg/0.1"), "profile": f"[replace_requires]\npkg/0.1: pkg/0.2"}) c.run("create pkg") c.run("install app -pr=profile --format=json") assert "pkg/0.1: pkg/0.2" in c.out # The replacement happens graph = json.loads(c.stdout) assert graph["graph"]["replaced_requires"] == {"pkg/0.1": "pkg/0.2"} assert graph["graph"]["nodes"]["0"]["dependencies"]["1"]["ref"] == "pkg/0.2" assert graph["graph"]["nodes"]["0"]["dependencies"]["1"]["require"] == "pkg/0.1" def test_replace_requires_test_requires(): c = TestClient(light=True) c.save({"gtest/conanfile.py": GenConanfile("gtest", "0.2"), "app/conanfile.py": GenConanfile().with_test_requires("gtest/0.1"), "profile": f"[replace_requires]\ngtest/0.1: gtest/0.2"}) c.run("create gtest") c.run("install app -pr=profile") assert "gtest/0.1: gtest/0.2" in c.out # The replacement happens # We test even replacing by itself, not great, but shouldn't crash @pytest.mark.parametrize("name, version", [("zlib", "0.1"), ("zlib", "0.2"), ("zlib-ng", "0.1")]) def test_replace_requires_consumer_references(name, version): c = TestClient() # IMPORTANT: The replacement package must be target-compatible dep = textwrap.dedent(f""" from conan import ConanFile class ZlibNG(ConanFile): name = "{name}" version = "{version}" def package_info(self): self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") """) conanfile = textwrap.dedent(""" from conan import ConanFile class App(ConanFile): name = "app" version = "0.1" settings = "build_type" requires = "zlib/0.1" generators = "CMakeDeps" def generate(self): self.output.info(f"DEP ZLIB generate: {self.dependencies['zlib'].ref.name}!") def build(self): self.output.info(f"DEP ZLIB build: {self.dependencies['zlib'].ref.name}!") def package_info(self): self.output.info(f"DEP ZLIB package_info: {self.dependencies['zlib'].ref.name}!") self.cpp_info.requires = ["zlib::zlib"] """) c.save({"dep/conanfile.py": dep, "app/conanfile.py": conanfile, "profile": f"[replace_requires]\nzlib/0.1: {name}/{version}"}) c.run("create dep") c.run("build app -pr=profile") assert f"zlib/0.1: {name}/{version}" in c.out assert f"conanfile.py (app/0.1): DEP ZLIB generate: {name}!" in c.out assert f"conanfile.py (app/0.1): DEP ZLIB build: {name}!" in c.out # Check generated CMake code. If the targets are NOT compatible, then the replacement # Cannot happen assert "find_package(ZLIB)" in c.out assert "target_link_libraries(... ZLIB::ZLIB)" in c.out cmake = c.load("app/ZLIBTargets.cmake") assert "add_library(ZLIB::ZLIB INTERFACE IMPORTED)" in cmake c.run("create app -pr=profile") assert f"zlib/0.1: {name}/{version}" in c.out assert f"app/0.1: DEP ZLIB generate: {name}!" in c.out assert f"app/0.1: DEP ZLIB build: {name}!" in c.out if name == "zlib-ng": # CMakeDeps can not be used to consume replaced requires for different packages # only CMakeConfigDeps has this capability c.run("install --requires=app/0.1 -pr=profile -g CMakeDeps", assert_error=True) def test_replace_requires_consumer_references_error_multiple(): # https://github.com/conan-io/conan/issues/17407 c = TestClient() # IMPORTANT: The replacement package must be target-compatible zlib = textwrap.dedent(""" from conan import ConanFile class ZlibNG(ConanFile): name = "zlib" version = "0.2" def package_info(self): self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") """) conanfile = textwrap.dedent(""" from conan import ConanFile class App(ConanFile): name = "app" version = "0.1" settings = "build_type" requires = "zlib/0.1", "bzip2/0.1" generators = "CMakeDeps" def generate(self): self.output.info(f"DEP ZLIB generate: {self.dependencies['zlib'].ref.name}!") self.output.info(f"DEP BZIP2 generate: {self.dependencies['bzip2'].ref.name}!") def build(self): self.output.info(f"DEP ZLIB build: {self.dependencies['zlib'].ref.name}!") self.output.info(f"DEP BZIP2 build: {self.dependencies['bzip2'].ref.name}!") def package_info(self): self.output.info(f"DEP ZLIB package_info: {self.dependencies['zlib'].ref.name}!") self.cpp_info.requires = ["zlib::zlib", "bzip2::bzip2"] """) c.save({"zlib/conanfile.py": zlib, "app/conanfile.py": conanfile, "profile": "[replace_requires]\nzlib/0.1: zlib/0.2\nbzip2/0.1: zlib/0.2"}) c.run("create zlib") c.run("build app -pr=profile") assert "zlib/0.1: zlib/0.2" in c.out assert "conanfile.py (app/0.1): DEP ZLIB generate: zlib!" in c.out assert "conanfile.py (app/0.1): DEP ZLIB build: zlib!" in c.out assert "conanfile.py (app/0.1): DEP BZIP2 generate: zlib!" in c.out assert "conanfile.py (app/0.1): DEP BZIP2 build: zlib!" in c.out # Check generated CMake code. If the targets are NOT compatible, then the replacement # Cannot happen assert "find_package(ZLIB)" in c.out assert "target_link_libraries(... ZLIB::ZLIB ZLIB::ZLIB)" in c.out cmake = c.load("app/ZLIBTargets.cmake") assert "add_library(ZLIB::ZLIB INTERFACE IMPORTED)" in cmake c.run("create app -pr=profile") assert "zlib/0.1: zlib/0.2" in c.out assert "app/0.1: DEP ZLIB generate: zlib!" in c.out assert "app/0.1: DEP ZLIB build: zlib!" in c.out def test_replace_requires_consumer_components_options(): c = TestClient() # IMPORTANT: The replacement package must be target-compatible zlib_ng = textwrap.dedent(""" from conan import ConanFile class ZlibNG(ConanFile): name = "zlib-ng" version = "0.1" options = {"compat": [False, True]} default_options = {"compat": False} def package_info(self): self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") if self.options.compat: self.cpp_info.components["myzlib"].set_property("cmake_target_name", "ZLIB::zmylib") """) conanfile = textwrap.dedent(""" from conan import ConanFile class App(ConanFile): name = "app" version = "0.1" settings = "build_type" requires = "zlib/0.1" generators = "CMakeDeps" def generate(self): self.output.info(f"DEP ZLIB generate: {self.dependencies['zlib'].ref.name}!") def build(self): self.output.info(f"DEP ZLIB build: {self.dependencies['zlib'].ref.name}!") def package_info(self): self.output.info(f"zlib in deps?: {'zlib' in self.dependencies}") self.output.info(f"zlib-ng in deps?: {'zlib-ng' in self.dependencies}") self.output.info(f"DEP ZLIB package_info: {self.dependencies['zlib'].ref.name}!") self.cpp_info.requires = ["zlib::myzlib"] """) profile = textwrap.dedent(""" [options] zlib-ng/*:compat=True [replace_requires] zlib/0.1: zlib-ng/0.1 """) c.save({"zlibng/conanfile.py": zlib_ng, "app/conanfile.py": conanfile, "profile": profile}) c.run("create zlibng -o *:compat=True") c.run("build app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out assert "conanfile.py (app/0.1): DEP ZLIB generate: zlib-ng!" in c.out assert "conanfile.py (app/0.1): DEP ZLIB build: zlib-ng!" in c.out # Check generated CMake code. If the targets are NOT compatible, then the replacement # Cannot happen assert "find_package(ZLIB)" in c.out assert "target_link_libraries(... ZLIB::ZLIB)" in c.out cmake = c.load("app/ZLIBTargets.cmake") assert "add_library(ZLIB::ZLIB INTERFACE IMPORTED)" in cmake cmake = c.load("app/ZLIB-Target-none.cmake") assert "set_property(TARGET ZLIB::ZLIB APPEND PROPERTY INTERFACE_LINK_LIBRARIES ZLIB::zmylib)" \ in cmake c.run("create app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out assert "app/0.1: DEP ZLIB generate: zlib-ng!" in c.out assert "app/0.1: DEP ZLIB build: zlib-ng!" in c.out assert "find_package(ZLIB)" in c.out assert "target_link_libraries(... ZLIB::ZLIB)" in c.out assert "zlib in deps?: True" in c.out assert "zlib-ng in deps?: False" in c.out def test_replace_requires_multiple(): conanfile = textwrap.dedent(""" from conan import ConanFile class EpoxyConan(ConanFile): name = "libepoxy" version = "0.1" def requirements(self): self.requires("opengl/system") self.requires("egl/system") def generate(self): for r, d in self.dependencies.items(): self.output.info(f"DEP: {r.ref.name}: {d.ref.name}") def package_info(self): self.cpp_info.requires.append("opengl::opengl") self.cpp_info.requires.append("egl::egl") """) profile = textwrap.dedent(""" [replace_requires] opengl/system: libgl/1.0 egl/system: libgl/1.0 """) c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "app/conanfile.py": conanfile, "profile": profile}) c.run("create dep --name=libgl --version=1.0") c.run("create app -pr=profile") # There are actually 2 dependencies, pointing to the same node assert "libepoxy/0.1: DEP: opengl: libgl" in c.out assert "libepoxy/0.1: DEP: egl: libgl" in c.out class TestReplaceRequiresTransitiveGenerators: """ Generators are incorrectly managing replace_requires # https://github.com/conan-io/conan/issues/17557 """ @pytest.mark.parametrize("diamond", [True, False]) def test_no_components(self, diamond): c = TestClient() zlib_ng = textwrap.dedent(""" from conan import ConanFile class ZlibNG(ConanFile): name = "zlib-ng" version = "0.1" package_type = "static-library" def package_info(self): self.cpp_info.libs = ["zlib"] self.cpp_info.type = "static-library" self.cpp_info.location = "lib/zlib.lib" self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") self.cpp_info.set_property("pkg_config_name", "ZLIB") """) openssl = textwrap.dedent(""" from conan import ConanFile class openssl(ConanFile): name = "openssl" version = "0.1" package_type = "static-library" requires = "zlib/0.1" def package_info(self): self.cpp_info.libs = ["crypto"] self.cpp_info.type = "static-library" self.cpp_info.location = "lib/crypto.lib" self.cpp_info.requires = ["zlib::zlib"] """) zlib = '"zlib/0.1"' if diamond else "" conanfile = textwrap.dedent(f""" from conan import ConanFile class App(ConanFile): name = "app" version = "0.1" settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] build_type = Release arch=x86_64 [replace_requires] zlib/0.1: zlib-ng/0.1 """) c.save({"zlibng/conanfile.py": zlib_ng, "openssl/conanfile.py": openssl, "app/conanfile.py": conanfile, "profile": profile}) c.run("create zlibng") c.run("create openssl -pr=profile") c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/ZLIB.pc") assert 'Libs: -L"${libdir}" -lzlib' in pc_content pc_content = c.load("app/openssl.pc") assert 'Requires: ZLIB' in pc_content cmake = c.load("app/ZLIB-Targets-release.cmake") assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake cmake = c.load("app/openssl-Targets-release.cmake") assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake assert "add_library(openssl::openssl STATIC IMPORTED)" in cmake assert "set_property(TARGET openssl::openssl APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ ' "$<$:ZLIB::ZLIB>")' in cmake # checking MSBuildDeps zlib_ng_props = c.load("app/conan_zlib-ng.props") assert 'Project="conan_zlib-ng_release_x64.props"' in zlib_ng_props props = c.load("app/conan_openssl_release_x64.props") assert "" in props @pytest.mark.parametrize("diamond", [True, False]) def test_openssl_components(self, diamond): c = TestClient() zlib_ng = textwrap.dedent(""" from conan import ConanFile class ZlibNG(ConanFile): name = "zlib-ng" version = "0.1" package_type = "static-library" def package_info(self): self.cpp_info.libs = ["zlib"] self.cpp_info.type = "static-library" self.cpp_info.location = "lib/zlib.lib" self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") self.cpp_info.set_property("pkg_config_name", "ZLIB") """) openssl = textwrap.dedent(""" from conan import ConanFile class openssl(ConanFile): name = "openssl" version = "0.1" package_type = "static-library" requires = "zlib/0.1" def package_info(self): self.cpp_info.components["crypto"].libs = ["crypto"] self.cpp_info.components["crypto"].type = "static-library" self.cpp_info.components["crypto"].location = "lib/crypto.lib" self.cpp_info.components["crypto"].requires = ["zlib::zlib"] """) zlib = '"zlib/0.1"' if diamond else "" conanfile = textwrap.dedent(f""" from conan import ConanFile class App(ConanFile): name = "app" version = "0.1" settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] build_type = Release arch=x86_64 [replace_requires] zlib/0.1: zlib-ng/0.1 """) c.save({"zlibng/conanfile.py": zlib_ng, "openssl/conanfile.py": openssl, "app/conanfile.py": conanfile, "profile": profile}) c.run("create zlibng") c.run("create openssl -pr=profile") c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/ZLIB.pc") assert 'Libs: -L"${libdir}" -lzlib' in pc_content pc_content = c.load("app/openssl-crypto.pc") assert 'Requires: ZLIB' in pc_content cmake = c.load("app/ZLIB-Targets-release.cmake") assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake cmake = c.load("app/openssl-Targets-release.cmake") assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake assert "add_library(openssl::crypto STATIC IMPORTED)" in cmake assert "set_property(TARGET openssl::crypto APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ ' "$<$:ZLIB::ZLIB>")' in cmake # checking MSBuildDeps zlib_ng_props = c.load("app/conan_zlib-ng.props") assert 'Project="conan_zlib-ng_release_x64.props"' in zlib_ng_props props = c.load("app/conan_openssl_crypto_release_x64.props") assert "" in props @pytest.mark.parametrize("diamond", [True, False]) @pytest.mark.parametrize("explicit_requires", [True, False]) def test_zlib_components(self, diamond, explicit_requires): c = TestClient() zlib_ng = textwrap.dedent(""" from conan import ConanFile class ZlibNG(ConanFile): name = "zlib-ng" version = "0.1" package_type = "static-library" def package_info(self): self.cpp_info.components["myzlib"].libs = ["zlib"] self.cpp_info.components["myzlib"].type = "static-library" self.cpp_info.components["myzlib"].location = "lib/zlib.lib" self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.components["myzlib"].set_property("pkg_config_name", "ZLIB") self.cpp_info.components["myzlib"].set_property("cmake_target_name", "ZLIB::ZLIB") """) openssl = textwrap.dedent(f""" from conan import ConanFile class openssl(ConanFile): name = "openssl" version = "0.1" package_type = "static-library" requires = "zlib/0.1" def package_info(self): self.cpp_info.libs = ["crypto"] self.cpp_info.type = "static-library" self.cpp_info.location = "lib/crypto.lib" if {explicit_requires}: self.cpp_info.requires = ["zlib::zlib"] """) zlib = '"zlib/0.1"' if diamond else "" conanfile = textwrap.dedent(f""" from conan import ConanFile class App(ConanFile): name = "app" version = "0.1" settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] build_type = Release arch = x86_64 [replace_requires] zlib/0.1: zlib-ng/0.1 """) c.save({"zlibng/conanfile.py": zlib_ng, "openssl/conanfile.py": openssl, "app/conanfile.py": conanfile, "profile": profile}) c.run("create zlibng") c.run("create openssl -pr=profile") c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/zlib-ng.pc") assert 'Requires: ZLIB' in pc_content pc_content = c.load("app/ZLIB.pc") assert 'Libs: -L"${libdir}" -lzlib' in pc_content pc_content = c.load("app/openssl.pc") assert 'Requires: zlib-ng' in pc_content cmake = c.load("app/ZLIB-Targets-release.cmake") assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake cmake = c.load("app/openssl-Targets-release.cmake") assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake assert "add_library(openssl::openssl STATIC IMPORTED)" in cmake # It should access the generic zlib-ng target assert "set_property(TARGET openssl::openssl APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ ' "$<$:zlib-ng::zlib-ng>")' in cmake # checking MSBuildDeps zlib_ng_props = c.load("app/conan_zlib-ng.props") assert "" in props @pytest.mark.parametrize("diamond", [True, False]) @pytest.mark.parametrize("package_requires", [False, True]) def test_both_components(self, diamond, package_requires): c = TestClient() zlib_ng = textwrap.dedent(""" from conan import ConanFile class ZlibNG(ConanFile): name = "zlib-ng" version = "0.1" package_type = "static-library" def package_info(self): self.cpp_info.components["myzlib"].libs = ["zlib"] self.cpp_info.components["myzlib"].type = "static-library" self.cpp_info.components["myzlib"].location = "lib/zlib.lib" self.cpp_info.set_property("cmake_file_name", "ZLIB") self.cpp_info.components["myzlib"].set_property("pkg_config_name", "ZLIB") self.cpp_info.components["myzlib"].set_property("cmake_target_name", "ZLIB::ZLIB") """) openssl = textwrap.dedent(f""" from conan import ConanFile class openssl(ConanFile): name = "openssl" version = "0.1" package_type = "static-library" requires = "zlib/0.1" def package_info(self): self.cpp_info.components["crypto"].libs = ["crypto"] self.cpp_info.components["crypto"].type = "static-library" self.cpp_info.components["crypto"].location = "lib/crypto.lib" if {package_requires}: self.cpp_info.components["crypto"].requires = ["zlib::zlib"] else: self.cpp_info.components["crypto"].requires = ["zlib::myzlib"] """) zlib = '"zlib/0.1"' if diamond else "" conanfile = textwrap.dedent(f""" from conan import ConanFile class App(ConanFile): name = "app" version = "0.1" settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] build_type = Release arch = x86_64 [replace_requires] zlib/0.1: zlib-ng/0.1 """) c.save({"zlibng/conanfile.py": zlib_ng, "openssl/conanfile.py": openssl, "app/conanfile.py": conanfile, "profile": profile}) c.run("create zlibng") c.run("create openssl -pr=profile") c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/zlib-ng.pc") assert 'Requires: ZLIB' in pc_content pc_content = c.load("app/ZLIB.pc") assert 'Libs: -L"${libdir}" -lzlib' in pc_content pc_content = c.load("app/openssl-crypto.pc") assert f'Requires: {"zlib-ng" if package_requires else "ZLIB"}' in pc_content cmake = c.load("app/ZLIB-Targets-release.cmake") assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake cmake = c.load("app/openssl-Targets-release.cmake") assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake assert "add_library(openssl::crypto STATIC IMPORTED)" in cmake if package_requires: # The generic package requirement uses the package name zlib-ng assert "set_property(TARGET openssl::crypto APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ ' "$<$:zlib-ng::zlib-ng>")' in cmake else: assert "set_property(TARGET openssl::crypto APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ ' "$<$:ZLIB::ZLIB>")' in cmake # checking MSBuildDeps zlib_ng_props = c.load("app/conan_zlib-ng.props") assert "" in props else: assert "" in props @pytest.mark.parametrize("require, pattern, alternative, expected", [ # Version range as pattern # PINNED REQUIRE VERSION ("dep/1.0", "dep/[>=1.0 <2]", "dep/1.3", "dep/1.3"), ("dep/1.0", "dep/[>=1.5 <2]", "dep/1.3", False), # RANGE REQUIRE VERSION ("dep/[>=1.2 <2]", "dep/[>=1.0 <2]", "dep/1.3", "dep/1.3"), ("dep/[>=1.0 <1.5]", "dep/[>=1.2 <2]", "dep/1.3", "dep/1.3"), ("dep/[>=1.0 <1.5]", "dep/[>=1.5 <2]", "dep/1.3", False) ] ) def test_replace_requires_ranges(require, pattern, alternative, expected): c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile("dep"), "app/conanfile.py": GenConanfile().with_requires(require), "profile": f"[replace_requires]\n{pattern}: {alternative}"}) c.run("create dep --version=1.0") c.run("create dep --version=1.3") c.run("graph info app -pr=profile") if expected: assert "Replaced requires" in c.out assert f"{require}: {expected}" in c.out else: assert "Replaced requires" not in c.out def test_host_version_replace(): profile = textwrap.dedent(""" include(default) [replace_requires] pkg/*: pkg/0.1@user/channel """) tc = TestClient(light=True) tc.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"), "conanfile.py": GenConanfile() .with_requires("pkg/0.1@user/channel") .with_tool_requires("pkg/"), "profile": profile}) tc.run("create pkg") tc.run("create pkg --user=user --channel=channel") # We did not track the user/channel, we resolve the version but keep the original user/channel tc.run("install -pr=profile") tc.assert_listed_require({"pkg/0.1@user/channel#485dad6cb11e2fa99d9afbe44a57a164": "Cache"}) tc.assert_listed_require({"pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164": "Cache"}, build=True) # If we want to also match user/channel # Solution 1: Also replace the tool_requires in your profile to use same user/channel tool_profile = profile + "\n[replace_tool_requires]\npkg/*: pkg/@user/channel" tc.save({"tool_profile": tool_profile}) tc.run("install -pr=tool_profile") tc.assert_listed_require({"pkg/0.1@user/channel#485dad6cb11e2fa99d9afbe44a57a164": "Cache"}) tc.assert_listed_require({"pkg/0.1@user/channel#485dad6cb11e2fa99d9afbe44a57a164": "Cache"}, build=True) # Solution 2: Directly in the requirement tc.save({"conanfile.py": GenConanfile() .with_requires("pkg/0.1@user/channel") .with_tool_requires("pkg/@user/channel")}) tc.run("install -pr=profile") tc.assert_listed_require({"pkg/0.1@user/channel#485dad6cb11e2fa99d9afbe44a57a164": "Cache"}) tc.assert_listed_require({"pkg/0.1@user/channel#485dad6cb11e2fa99d9afbe44a57a164": "Cache"}, build=True) ================================================ FILE: test/integration/graph/test_require_same_pkg_versions.py ================================================ import re import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_require_different_versions(): """ this test demostrates that it is possible to tool_require different versions of the same thing, deactivating run=False (as long as their executables are not called the same) https://github.com/conan-io/conan/issues/13521 """ c = TestClient() gcc = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "gcc" def package(self): echo = f"@echo off\necho MYGCC={self.version}!!" save(self, os.path.join(self.package_folder, "bin", f"mygcc{self.version}.bat"), echo) save(self, os.path.join(self.package_folder, "bin", f"mygcc{self.version}.sh"), echo) os.chmod(os.path.join(self.package_folder, "bin", f"mygcc{self.version}.sh"), 0o777) """) wine = textwrap.dedent(""" import os, platform from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): name = "wine" version = "1.0" def build_requirements(self): self.tool_requires("gcc/1.0", run=False) self.tool_requires("gcc/2.0", run=False) def generate(self): gcc1 = self.dependencies.build["gcc/1.0"] assert gcc1.ref.version == "1.0" gcc2 = self.dependencies.build["gcc/2.0"] assert gcc2.ref.version == "2.0" def build(self): ext = "bat" if platform.system() == "Windows" else "sh" self.run(f"mygcc1.0.{ext}") self.run(f"mygcc2.0.{ext}") """) c.save({"gcc/conanfile.py": gcc, "wine/conanfile.py": wine}) c.run("create gcc --version=1.0") c.run("create gcc --version=2.0") c.run("build wine --lockfile-out=conan.lock") assert "gcc/1.0#3d6110b9e2b90074160fa33b6f0ea968 - Cache" in c.out assert "gcc/2.0#3d6110b9e2b90074160fa33b6f0ea968 - Cache" in c.out assert "MYGCC=1.0!!" in c.out assert "MYGCC=2.0!!" in c.out lock = c.load("wine/conan.lock") assert "gcc/1.0#3d6110b9e2b90074160fa33b6f0ea968" in lock assert "gcc/2.0#3d6110b9e2b90074160fa33b6f0ea968" in lock def test_require_different_versions_profile_override(): """ same as above but what if the profile is the one overriding the version? """ c = TestClient() gcc = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "gcc" def package(self): echo = f"@echo off\necho MYGCC={self.version}!!" save(self, os.path.join(self.package_folder, "bin", f"mygcc{self.version}.bat"), echo) save(self, os.path.join(self.package_folder, "bin", f"mygcc{self.version}.sh"), echo) os.chmod(os.path.join(self.package_folder, "bin", f"mygcc{self.version}.sh"), 0o777) """) wine = textwrap.dedent(""" import os, platform from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): name = "wine" version = "1.0" def build_requirements(self): self.tool_requires("gcc/1.0", run=False) def build(self): ext = "bat" if platform.system() == "Windows" else "sh" self.run(f"mygcc1.0.{ext}") self.run(f"mygcc2.0.{ext}") """) c.save({"gcc/conanfile.py": gcc, "wine/conanfile.py": wine, "profile": "[tool_requires]\ngcc/2.0"}) c.run("create gcc --version=1.0") c.run("create gcc --version=2.0") c.run("build wine -pr=profile --lockfile-out=conan.lock") assert "gcc/1.0#3d6110b9e2b90074160fa33b6f0ea968 - Cache" in c.out assert "gcc/2.0#3d6110b9e2b90074160fa33b6f0ea968 - Cache" in c.out assert "MYGCC=1.0!!" in c.out assert "MYGCC=2.0!!" in c.out lock = c.load("wine/conan.lock") assert "gcc/1.0#3d6110b9e2b90074160fa33b6f0ea968" in lock assert "gcc/2.0#3d6110b9e2b90074160fa33b6f0ea968" in lock def test_require_different_versions_profile_override_build_script(): """ build-scripts by default do the right thing, because they have run=True (they could be runnable shell scripts) """ c = TestClient(light=True) buildscripts = GenConanfile("buildscripts").with_package_type("build-scripts") wine = GenConanfile("wine", "1.0").with_tool_requirement("buildscripts/1.0") c.save({"buildscripts/conanfile.py": buildscripts, "wine/conanfile.py": wine, "profile": "[tool_requires]\nbuildscripts/2.0"}) c.run("create buildscripts --version=2.0") c.run("build wine -pr=profile --lockfile-out=conan.lock") assert "buildscripts/1.0" not in c.out assert "buildscripts/2.0#fced952ee7aba96f858b70c7d6c9c8d2 - Cache" in c.out lock = c.load("wine/conan.lock") assert "buildscripts/1.0" not in lock assert "buildscripts/2.0#fced952ee7aba96f858b70c7d6c9c8d2" in lock def test_require_different_options(): """ this test demostrates that it is possible to tool_require different options of the same thing, deactivating run=False (as long as their executables are not called the same) https://github.com/conan-io/conan/issues/13521 """ c = TestClient() gcc = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "gcc" version = "1.0" options = {"myoption": [1, 2]} def package(self): echo = f"@echo off\necho MYGCC={self.options.myoption}!!" save(self, os.path.join(self.package_folder, "bin", f"mygcc{self.options.myoption}.bat"), echo) save(self, os.path.join(self.package_folder, "bin", f"mygcc{self.options.myoption}.sh"), echo) os.chmod(os.path.join(self.package_folder, "bin", f"mygcc{self.options.myoption}.sh"), 0o777) """) wine = textwrap.dedent(""" import os, platform from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): name = "wine" version = "1.0" def build_requirements(self): self.tool_requires("gcc/1.0", run=False, options={"myoption": 1}) self.tool_requires("gcc/1.0", run=False, options={"myoption": 2}) def generate(self): gcc1 = self.dependencies.build.get("gcc", options={"myoption": 1}) assert gcc1.options.myoption == "1" gcc2 = self.dependencies.build.get("gcc", options={"myoption": 2}) assert gcc2.options.myoption == "2" def build(self): ext = "bat" if platform.system() == "Windows" else "sh" self.run(f"mygcc1.{ext}") self.run(f"mygcc2.{ext}") """) c.save({"gcc/conanfile.py": gcc, "wine/conanfile.py": wine}) c.run("create gcc -o myoption=1") c.run("create gcc -o myoption=2") c.run("build wine --lockfile-out=conan.lock") assert "gcc/1.0#616ce3babcecef39a27806c1a5f4b4ff - Cache" in c.out assert "MYGCC=1!!" in c.out assert "MYGCC=2!!" in c.out lock = c.load("wine/conan.lock") # Testing it doesn't crash or anything like that assert "gcc/1.0#616ce3babcecef39a27806c1a5f4b4ff" in lock def test_require_different_versions_transitive(): """ https://github.com/conan-io/conan/issues/18086 """ c = TestClient(default_server_user=True, path_with_spaces=False) qemu = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "myqemu" package_type = "application" def package(self): echo = f"@echo off\necho RUNNING {self.name}/{self.version}!!" save(self, os.path.join(self.package_folder, "bin", f"{self.name}.bat"), echo) save(self, os.path.join(self.package_folder, "bin", f"{self.name}.sh"), echo) os.chmod(os.path.join(self.package_folder, "bin", f"{self.name}.sh"), 0o777) """) mytool = textwrap.dedent(r""" import os, platform from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): version = "1.0" package_type = "application" def requirements(self): version = "1.0" if self.name == "snippy" else "2.0" self.requires(f"myqemu/{version}", visible=False, no_skip=True) def package(self): c = f'call "%1/myqemu.bat"' if platform.system() == "Windows" else f'"$1/myqemu.sh"' echo = f"@echo off\necho RUNNING {self.name}/{self.version}!!\n{c}" save(self, os.path.join(self.package_folder, "bin", f"{self.name}.bat"), echo) save(self, os.path.join(self.package_folder, "bin", f"{self.name}"), echo) os.chmod(os.path.join(self.package_folder, "bin", f"{self.name}"), 0o777) def package_info(self): pf = self.dependencies["myqemu"].cpp_info.bindir.replace("\\", "/") self.conf_info.define_path(f"user.myorg:{self.name}_qemu", pf) """) consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "consumer" version = "1.0" def build_requirements(self): self.tool_requires("snippy/1.0") self.tool_requires("valgrind/1.0") def build(self): qemu_snippy = self.conf.get("user.myorg:snippy_qemu") qemu_valgrind = self.conf.get("user.myorg:valgrind_qemu") self.run(f"valgrind {qemu_valgrind}") self.run(f'snippy {qemu_snippy}') """) c.save({"qemu/conanfile.py": qemu, "tool/conanfile.py": mytool, "consumer/conanfile.py": consumer}) c.run("create qemu --version=1.0") c.run("create qemu --version=2.0") c.run("create tool --name=snippy") c.run("create tool --name=valgrind") c.run("build consumer") assert "RUNNING valgrind/1.0!!" in c.out assert "RUNNING myqemu/2.0!!" in c.out assert "RUNNING snippy/1.0!!" in c.out assert "RUNNING myqemu/1.0!!" in c.out c.run("upload * -r=default -c") # The "tools.graph:skip_binaries" shouldn't affect the result, it is never skipped for skip in ("-c tools.graph:skip_binaries=True", "-c tools.graph:skip_binaries=False", ""): c.run("remove * -c") # Re-downloads and it works c.run(f"build consumer {skip}") assert "RUNNING valgrind/1.0!!" in c.out assert "RUNNING myqemu/2.0!!" in c.out assert "RUNNING snippy/1.0!!" in c.out assert "RUNNING myqemu/1.0!!" in c.out c.run("create consumer") c.run("upload consumer/1.0 -r=default -c") c.run("remove * -c") c.run("install --requires=consumer/1.0") assert re.search(r"Skipped binaries(\s*)myqemu/1.0, myqemu/2.0, snippy/1.0, valgrind/1.0", c.out) class TestTransitiveBuild: # project --(tool-requires)--> wrapper_a -(requires)-> gcc/X # \-----(tool-requires)--> wrapper_b -(requires)-> gcc/Y gcc = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "gcc" package_type = "application" def package(self): echo = f"@echo off\necho MYGCC={self.version}!!" save(self, os.path.join(self.package_folder, "bin", "mygcc.bat"), echo) save(self, os.path.join(self.package_folder, "bin", "mygcc.sh"), echo) os.chmod(os.path.join(self.package_folder, "bin", "mygcc.sh"), 0o777) """) project = textwrap.dedent(""" import platform from conan import ConanFile class Pkg(ConanFile): def build_requirements(self): self.tool_requires("wrappera/1.0") self.tool_requires("wrapperb/1.0") def build(self): ext = "bat" if platform.system() == "Windows" else "sh" """) def test_require_different_versions_transitive_noconflict(self): """ Same dependency to gcc/1.0, no conflict """ c = TestClient() project = r"self.run(f'mygcc.{ext}')" c.save({"gcc/conanfile.py": self.gcc, "wrappera/conanfile.py": GenConanfile("wrappera", "1.0").with_requires("gcc/1.0"), "wrapperb/conanfile.py": GenConanfile("wrapperb", "1.0").with_requires("gcc/1.0"), "project/conanfile.py": self.project + textwrap.indent(project, " ")}) c.run("create gcc --version=1.0") c.run("create wrappera") c.run("create wrapperb") c.run("build project") assert "MYGCC=1.0!!" in c.out def test_require_different_versions_transitive_conflict(self): c = TestClient() project = textwrap.dedent(r""" self.run(f'wrappera.{ext}') self.run(f'wrapperb.{ext}') """) c.save({"gcc/conanfile.py": self.gcc, "wrappera/conanfile.py": GenConanfile("wrappera", "1.0").with_requires("gcc/1.0"), "wrapperb/conanfile.py": GenConanfile("wrapperb", "1.0").with_requires("gcc/2.0"), "project/conanfile.py": self.project + textwrap.indent(project, " ")}) c.run("create gcc --version=1.0") c.run("create gcc --version=2.0") c.run("create wrappera") c.run("create wrapperb") c.run("install project", assert_error=True) assert "Version conflict: Conflict between gcc/2.0 and gcc/1.0 in the graph" in c.out # disabling ``run=False`` trait c.save({"wrapa/conanfile.py": GenConanfile("wrappera", "1.0").with_requirement("gcc/1.0", run=False), "wrapb/conanfile.py": GenConanfile("wrapperb", "1.0").with_requirement("gcc/2.0", run=False)}) c.run("create wrapa") c.run("create wrapb") c.run("install project") # Now it works! # If the run is not propagated, the consumer must do it explicitly to differentiate: project = textwrap.dedent(r""" import os, platform from conan import ConanFile from conan.tools.files import save, chdir class Pkg(ConanFile): name = "project" version = "1.0" def build_requirements(self): self.tool_requires("wrappera/1.0") self.tool_requires("wrapperb/1.0") self.tool_requires("gcc/1.0", run=False) self.tool_requires("gcc/2.0", run=False) def build(self): ext = "bat" if platform.system() == "Windows" else "sh" path_a = self.dependencies.build["gcc/1.0"].cpp_info.bindir path_b = self.dependencies.build["gcc/2.0"].cpp_info.bindir self.run(f'"{path_a}/mygcc.{ext}"') self.run(f'"{path_b}/mygcc.{ext}"') """) c.save({"project/conanfile.py": project}) c.run("build project") assert "MYGCC=1.0!!" in c.out assert "MYGCC=2.0!!" in c.out def test_require_different_versions_transitive_vendored(self): c = TestClient() wrapper = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "{name}" version = "1.0" package_type = "application" def requirements(self): self.requires("gcc/{gcc_version}", visible=False, run=False) def package(self): copy(self, "*", src=self.dependencies["gcc"].cpp_info.bindir, dst=os.path.join(self.package_folder, "bin")) echo_bat = f'@echo off\necho {name}={{self.version}}!!\ncall "%~dp0/mygcc.bat"' # Trick for cwd in Linux echo_sh = '@echo off\necho {name}=1.0!!\n"${{0%/*}}/mygcc.sh\"' save(self, os.path.join(self.package_folder, "bin", "{name}.bat"), echo_bat) save(self, os.path.join(self.package_folder, "bin", "{name}.sh"), echo_sh) os.chmod(os.path.join(self.package_folder, "bin", "{name}.sh"), 0o777) """) project = textwrap.dedent(r""" self.run(f'wrappera.{ext}') self.run(f'wrapperb.{ext}') """) c.save({"gcc/conanfile.py": self.gcc, "wrappera/conanfile.py": wrapper.format(name="wrappera", gcc_version="1.0"), "wrapperb/conanfile.py": wrapper.format(name="wrapperb", gcc_version="2.0"), "project/conanfile.py": self.project + textwrap.indent(project, " ")}) c.run("create gcc --version=1.0") c.run("create gcc --version=2.0") c.run("create wrappera") c.run("create wrapperb") c.run("build project") assert "wrappera=1.0!!" in c.out assert "MYGCC=1.0!!" in c.out assert "wrapperb=1.0!!" in c.out assert "MYGCC=2.0!!" in c.out ================================================ FILE: test/integration/graph/test_skip_binaries.py ================================================ import re import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID def test_private_skip(): # app -> pkg -(private)-> dep client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=dep --version=1.0") client.save({"conanfile.py": GenConanfile().with_requirement("dep/1.0", visible=False)}) client.run("create . --name=pkg --version=1.0") client.run("remove dep/1.0:* -c") # Dep binary is removed not used at all client.save({"conanfile.py": GenConanfile().with_requires("pkg/1.0")}) client.run("create . --name=app --version=1.0 -v") client.assert_listed_binary({"dep/1.0": (NO_SETTINGS_PACKAGE_ID, "Skip")}) def test_private_no_skip(): # app -> pkg -(private)-> dep client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=dep --version=1.0") client.save({"conanfile.py": GenConanfile().with_requirement("dep/1.0", visible=False)}) client.run("create . --name=pkg --version=1.0") # But if we want to build pkg, no skip client.run("create . --name=app --version=1.0 --build=app/* --build=pkg/*") client.assert_listed_binary({"dep/1.0": (NO_SETTINGS_PACKAGE_ID, "Cache")}) client.run("remove dep/1.0:* -c") # Dep binary is removed not used at all client.run("create . --name=app --version=1.0 --build=app/* --build=pkg/*", assert_error=True) client.assert_listed_binary({"dep/1.0": (NO_SETTINGS_PACKAGE_ID, "Missing")}) def test_consumer_no_skip(): # app -(private)-> pkg -> dep client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=dep --version=1.0") client.save({"conanfile.py": GenConanfile().with_requires("dep/1.0")}) client.run("create . --name=pkg --version=1.0") package_id = client.created_package_id("pkg/1.0") client.save({"conanfile.py": GenConanfile().with_requirement("pkg/1.0", visible=False)}) client.run("install . ") client.assert_listed_binary({f"dep/1.0": (NO_SETTINGS_PACKAGE_ID, "Cache")}) client.assert_listed_binary({f"pkg/1.0": (package_id, "Cache")}) def test_shared_link_static_skip(): # app -> pkg (shared) -> dep (static) client = TestClient(light=True) client.save({"conanfile.py": GenConanfile().with_shared_option(False)}) client.run("create . --name=dep --version=1.0") package_id = client.created_package_id("dep/1.0") client.save({"conanfile.py": GenConanfile().with_requirement("dep/1.0"). with_shared_option(True)}) client.run("create . --name=pkg --version=1.0") client.run("remove dep/1.0:* -c") # Dep binary is removed not used at all client.save({"conanfile.py": GenConanfile().with_requires("pkg/1.0")}) client.run("create . --name=app --version=1.0 -v") client.assert_listed_binary({"dep/1.0": (package_id, "Skip")}) def test_test_requires(): # Using a test_requires can be skipped if it is not necessary to build its consumer # app -> pkg (static) -(test_requires)-> gtest (static) client = TestClient(light=True) client.save({"conanfile.py": GenConanfile().with_shared_option(False)}) client.run("create . --name=gtest --version=1.0") package_id = client.created_package_id("gtest/1.0") client.save({"conanfile.py": GenConanfile().with_test_requires("gtest/1.0"). with_shared_option(False)}) client.run("create . --name=pkg --version=1.0") client.run("remove gtest/1.0:* -c") # Dep binary is removed not used at all client.save({"conanfile.py": GenConanfile().with_requires("pkg/1.0")}) # Checking list of skipped binaries client.run("create . --name=app --version=1.0") assert re.search(r"Skipped binaries(\s*)gtest/1.0", client.out) # Showing the complete information about the skipped binary client.run("create . --name=app --version=1.0 -v") client.assert_listed_binary({"gtest/1.0": (package_id, "Skip")}, test=True) def test_build_scripts_no_skip(): c = TestClient(light=True) c.save({"scripts/conanfile.py": GenConanfile("script", "0.1").with_package_type("build-scripts"), "app/conanfile.py": GenConanfile().with_tool_requires("script/0.1")}) c.run("create scripts") c.assert_listed_binary({"script/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}, build=True) c.run("install app") c.assert_listed_binary({"script/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}, build=True) def test_list_skip_printing(): """ make sure that when a package is required in the graph, it is not marked as SKIP, just because some other part of the graph is skipping it. In this case, a tool_require might be necessary for some packages building from soures, but not for others """ c = TestClient(light=True) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "pkga/conanfile.py": GenConanfile("pkga", "0.1").with_tool_requires("tool/0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1") .with_tool_requires("tool/0.1"), "app/conanfile.py": GenConanfile().with_requires("pkgb/0.1")}) c.run("create tool") c.run("create pkga") c.run("create pkgb") c.run("remove pkga:* -c") c.run("install app --build=missing") c.assert_listed_binary({"tool/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}, build=True) def test_conf_skip(): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=maths --version=1.0") client.run("create . --name=ai --version=1.0") client.save({"conanfile.py": GenConanfile().with_requirement("maths/1.0", visible=False)}) client.run("create . --name=liba --version=1.0") client.save({"conanfile.py": GenConanfile().with_requirement("ai/1.0", visible=False)}) client.run("create . --name=libb --version=1.0") client.save({"conanfile.py": GenConanfile().with_requires("liba/1.0", "libb/1.0")}) client.run("create . --name=app --version=0.0 -v") client.assert_listed_binary({"maths/1.0": (NO_SETTINGS_PACKAGE_ID, "Skip")}) client.assert_listed_binary({"ai/1.0": (NO_SETTINGS_PACKAGE_ID, "Skip")}) client.run("create . --name=app --version=1.0 -v -c *:tools.graph:skip_binaries=False") client.assert_listed_binary({"maths/1.0": (NO_SETTINGS_PACKAGE_ID, "Cache")}) client.assert_listed_binary({"ai/1.0": (NO_SETTINGS_PACKAGE_ID, "Cache")}) client.run("create . --name=app --version=2.0 -v -c maths/*:tools.graph:skip_binaries=False") client.assert_listed_binary({"maths/1.0": (NO_SETTINGS_PACKAGE_ID, "Cache")}) client.assert_listed_binary({"ai/1.0": (NO_SETTINGS_PACKAGE_ID, "Skip")}) client.run("create . --name=app --version=3.0 -v -c *:tools.graph:skip_binaries=True") client.assert_listed_binary({"maths/1.0": (NO_SETTINGS_PACKAGE_ID, "Skip")}) client.assert_listed_binary({"ai/1.0": (NO_SETTINGS_PACKAGE_ID, "Skip")}) def test_skipped_intermediate_header(): # app -> libc/0.1 (static) -> libb0.1 (header) -> liba0.1 (static) # This libb0.1 cannot be skipped because it is necessary its lib-config.cmake for transitivity c = TestClient() c.save({"liba/conanfile.py": GenConanfile("liba", "0.1").with_package_type("static-library") .with_package_info(cpp_info={"libs": ["liba"]}), "libb/conanfile.py": GenConanfile("libb", "0.1").with_package_type("header-library") .with_requires("liba/0.1"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_package_type("static-library") .with_requires("libb/0.1"), "app/conanfile.py": GenConanfile("app", "0.1").with_requires("libc/0.1") .with_settings("build_type")}) c.run("create liba") c.run("create libb") c.run("create libc") c.run("install app -g CMakeDeps") c.assert_listed_binary({"liba/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libb/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libc/0.1": ("0f9f8919daed27aacd18c33199957e8882a87fd7", "Cache")}) libc_data = c.load("app/libc-release-data.cmake") assert "list(APPEND libc_FIND_DEPENDENCY_NAMES libb)" in libc_data libb_data = c.load("app/libb-release-data.cmake") # libb brings no headers nor libraries assert "set(libb_INCLUDE_DIRS_RELEASE )" in libb_data assert "set(libb_LIBS_RELEASE )" in libb_data liba_data = c.load("app/liba-release-data.cmake") # liba brings only libraries assert "set(liba_INCLUDE_DIRS_RELEASE )" in liba_data assert "set(liba_LIBS_RELEASE liba)" in liba_data def test_skip_visible_build(): # https://github.com/conan-io/conan/issues/15346 c = TestClient(light=True) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requirement("liba/0.1", build=True), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requirement("libb/0.1", visible=False), "app/conanfile.py": GenConanfile("app", "0.1").with_requires("libc/0.1")}) c.run("create liba") c.run("create libb") c.run("create libc") c.run("install app --format=json") assert re.search(r"Skipped binaries(\s*)libb/0.1, liba/0.1", c.out) def test_skip_tool_requires_context(): c = TestClient() cmake = textwrap.dedent(""" from conan import ConanFile class CMake(ConanFile): name = "cmake" version = "1.0" def package_info(self): self.buildenv_info.define("MYVAR", "MYVALUE") """) gtest = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class gtest(ConanFile): name = "gtest" version = "1.0" settings = "os" package_type = "static-library" def build(self): env = load(self, "conanbuildenv.sh") self.output.info(f"MYENV: {env}") """) c.save({"cmake/conanfile.py": cmake, "gtest/conanfile.py": gtest, "lib/conanfile.py": GenConanfile("lib", "1.0").with_package_type("static-library") .with_test_requires("gtest/1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_settings("os") .with_requires("lib/1.0"), "profile": "[tool_requires]\ncmake/[>=1.0]"}) c.run("create cmake") c.run("create gtest -s:a os=Linux") c.run("create lib -s:a os=Linux") c.run("remove gtest:* -c") c.run("install app -s:a os=Linux -pr=profile -c=tools.graph:skip_binaries=False --build=missing") assert 'export MYVAR="MYVALUE"' in c.out assert "gtest/1.0: Package '9a4eb3c8701508aa9458b1a73d0633783ecc2270' built" in c.out def test_skip_intermediate_header(): # https://github.com/conan-io/conan/issues/16402 # Libb cannot be skipped in any case, because there is a link order libc->liba necessary # app -> libc/0.1 (static) -> libb0.1 (header) -> liba0.1 (static) # \------------------------------------------------/ # libb c = TestClient(light=True) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1").with_package_type("static-library"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requirement("liba/0.1") .with_package_type("header-library"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requirement("libb/0.1") .with_package_type("static-library"), "app/conanfile.py": GenConanfile("app", "0.1").with_package_type("application") .with_requires("libc/0.1", "liba/0.1")}) c.run("create liba") c.run("create libb") c.run("create libc") c.run("install app") assert "Skipped binaries" not in c.out assert "libb/0.1: Already installed!" in c.out assert "liba/0.1: Already installed!" in c.out assert "libc/0.1: Already installed!" in c.out def test_skip_intermediate_static(): # https://github.com/conan-io/conan/issues/16402 # In this case, libb can be completely skipped, because there is no linkage relationship at all # app -> libc/0.1 (shared) -> libb0.1 (static) -> liba0.1 (static) # \------------------------------------------------/ # libb c = TestClient(light=True) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1").with_package_type("static-library"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requirement("liba/0.1") .with_package_type("static-library"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requirement("libb/0.1") .with_package_type("shared-library"), "app/conanfile.py": GenConanfile("app", "0.1").with_package_type("application") .with_requires("libc/0.1", "liba/0.1")}) c.run("create liba") c.run("create libb") c.run("create libc") c.run("remove libb:* -c") # binary not necessary, can be skipped c.run("install app") assert re.search(r"Skipped binaries(\s*)libb/0.1", c.out) assert "libb/0.1: Already installed!" not in c.out assert "liba/0.1: Already installed!" in c.out assert "libc/0.1: Already installed!" in c.out def test_skip_intermediate_static_complex(): # https://github.com/conan-io/conan/issues/16402 # /----- libh(static)--libi(header)---libj(header)----------\ # app -> libe(shared)->libd(static) -> libc(static) -> libb(static) -> liba(static) # \---------libf(static) --libg(header)---------------------------------/ # libd and libc can be skipped c = TestClient(light=True) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1").with_package_type("static-library"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requirement("liba/0.1") .with_package_type("static-library"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requirement("libb/0.1") .with_package_type("static-library"), "libd/conanfile.py": GenConanfile("libd", "0.1").with_requirement("libc/0.1") .with_package_type("static-library"), "libe/conanfile.py": GenConanfile("libe", "0.1").with_requirement("libd/0.1") .with_package_type("shared-library"), "libg/conanfile.py": GenConanfile("libg", "0.1").with_requirement("liba/0.1") .with_package_type("header-library"), "libf/conanfile.py": GenConanfile("libf", "0.1").with_requirement("libg/0.1") .with_package_type("static-library"), "libj/conanfile.py": GenConanfile("libj", "0.1").with_requirement("libb/0.1") .with_package_type("header-library"), "libi/conanfile.py": GenConanfile("libi", "0.1").with_requirement("libj/0.1") .with_package_type("header-library"), "libh/conanfile.py": GenConanfile("libh", "0.1").with_requirement("libi/0.1") .with_package_type("static-library"), "app/conanfile.py": GenConanfile("app", "0.1").with_package_type("application") .with_requires("libh/0.1", "libe/0.1", "libf/0.1") }) for lib in ("a", "b", "c", "d", "e", "j", "i", "h", "g", "f"): c.run(f"create lib{lib}") c.run("remove libd:* -c") # binary not necessary, can be skipped c.run("remove libc:* -c") # binary not necessary, can be skipped c.run("install app") assert re.search(r"Skipped binaries(\s*)libc/0.1, libd/0.1", c.out) assert "libd/0.1: Already installed!" not in c.out assert "libc/0.1: Already installed!" not in c.out for lib in ("a", "b", "e", "f", "g", "h", "i", "j"): assert f"lib{lib}/0.1: Already installed!" in c.out for lib in ("c", "d"): assert f"lib{lib}/0.1: Already installed!" not in c.out ================================================ FILE: test/integration/graph/test_skip_build.py ================================================ import re import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_graph_skip_build_test(): # app -> pkg -(test)-> gtest # \---(tool)-> cmake c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" test_requires = "gtest/1.0" tool_requires = "cmake/1.0" """) c.save({"gtest/conanfile.py": GenConanfile("gtest", "1.0"), "cmake/conanfile.py": GenConanfile("cmake", "1.0"), "pkg/conanfile.py": pkg, "app/conanfile.py": GenConanfile("app", "1.0").with_requires("pkg/1.0")}) c.run("create gtest") c.run("create cmake") c.run("create pkg") c.run("create app -c tools.graph:skip_build=True -c tools.graph:skip_test=True") assert "cmake" not in c.out assert "gtest" not in c.out c.run("create app -c tools.graph:skip_test=True") assert "WARN: experimental: Usage of 'tools.graph:skip_test'" in c.out assert "WARN: tools.graph:skip_test set, but tools.build:skip_test is not" in c.out assert "cmake" in c.out assert "gtest" not in c.out c.run("create app -c tools.graph:skip_build=True") assert "cmake" not in c.out assert "gtest" in c.out c.run("install app") assert "cmake" in c.out assert "gtest" in c.out c.run("install app -c tools.graph:skip_build=True -c tools.graph:skip_test=True") assert "cmake" not in c.out assert "gtest" not in c.out c.run("install app -c tools.graph:skip_build=True --build=pkg/*", assert_error=True) assert "ERROR: Package pkg/1.0 skipped its test/tool requires with tools.graph:skip_build, " \ "but was marked to be built " in c.out def test_skip(): # https://github.com/conan-io/conan/issues/13439 c = TestClient() global_conf = textwrap.dedent(""" tools.graph:skip_test=True tools.build:skip_test=True """) c.save_home({"global.conf": global_conf}) c.save({"pkga/conanfile.py": GenConanfile("pkga", "1.0.0"), "pkgb/conanfile.py": GenConanfile("pkgb", "1.0.0").with_test_requires("pkga/1.0.0"), "pkgc/conanfile.py": GenConanfile("pkgc", "1.0.0").with_test_requires("pkgb/1.0.0")}) c.run("create pkga") c.run("create pkgb") # Always skipped c.run("install pkgc") # correct, pkga and pkgb are not in the output at all, they have been skipped assert "pkga" not in c.out assert "pkgb" not in c.out # not skipping test-requires c.run("install pkgc -c tools.graph:skip_test=False") # correct, pkga and pkgb are not skipped now assert "pkga" in c.out assert "pkgb" in c.out # but pkga binary is not really necessary assert re.search(r"Skipped binaries(\s*)pkga/1.0.0", c.out) # skipping all but the current one c.run("install pkgc -c &:tools.graph:skip_test=False") # correct, only pkga is skipped now assert "pkga" not in c.out assert "pkgb" in c.out ================================================ FILE: test/integration/graph/test_subgraph_reports.py ================================================ import json import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.api.model import RecipeReference from conan.internal.util.files import load def _metadata(c, ref): pref = c.get_latest_package_reference(RecipeReference.loads(ref)) return c.get_latest_pkg_layout(pref).metadata() def test_subgraph_reports(): c = TestClient() subgraph_hook = textwrap.dedent("""\ import os, json from conan.tools.files import save from conan.internal.model.lockfile import Lockfile def post_package(conanfile): subgraph = conanfile.subgraph save(conanfile, os.path.join(conanfile.package_metadata_folder, f"conangraph.json"), json.dumps(subgraph.serialize(), indent=2)) save(conanfile, os.path.join(conanfile.package_metadata_folder, f"conan.lock"), Lockfile(subgraph).dumps()) """) c.save_home({"extensions/hooks/subgraph_hook/hook_subgraph.py": subgraph_hook}) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requirement("dep/0.1"), "app/conanfile.py": GenConanfile("app", "0.1").with_requirement("pkg/0.1")}) c.run("export dep") c.run("export pkg") # app -> pkg -> dep c.run("create app --build=missing --format=json") app_graph = json.loads(load(os.path.join(_metadata(c, "app/0.1"), "conangraph.json"))) pkg_graph = json.loads(load(os.path.join(_metadata(c, "pkg/0.1"), "conangraph.json"))) dep_graph = json.loads(load(os.path.join(_metadata(c, "dep/0.1"), "conangraph.json"))) app_lock = json.loads(load(os.path.join(_metadata(c, "app/0.1"), "conan.lock"))) pkg_lock = json.loads(load(os.path.join(_metadata(c, "pkg/0.1"), "conan.lock"))) dep_lock = json.loads(load(os.path.join(_metadata(c, "dep/0.1"), "conan.lock"))) assert len(app_graph["nodes"]) == len(app_lock["requires"]) assert len(pkg_graph["nodes"]) == len(pkg_lock["requires"]) assert len(dep_graph["nodes"]) == len(dep_lock["requires"]) ================================================ FILE: test/integration/graph/test_system_tools.py ================================================ import json import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestToolRequires: @pytest.mark.parametrize("revision", ["", "#myrev"]) def test_system_tool_require(self, revision): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/1.0"), "profile": f"[platform_tool_requires]\ntool/1.0{revision}"}) client.run("create . -pr=profile") assert f"tool/1.0{revision or '#platform'} - Platform" in client.out def test_system_tool_require_non_matching(self): """ if what is specified in [system_tool_require] doesn't match what the recipe requires, then the system_tool_require will not be used, and the recipe will use its declared version """ client = TestClient(light=True) client.save({"tool/conanfile.py": GenConanfile("tool", "1.0"), "conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/1.0"), "profile": "[system_tools]\ntool/1.1"}) client.run("create tool") client.run("create . -pr=profile") assert "WARN: Profile [system_tools] is deprecated" in client.out assert "tool/1.0#60ed6e65eae112df86da7f6d790887fd - Cache" in client.out @pytest.mark.parametrize("revision", ["", "#myrev"]) def test_system_tool_require_range(self, revision): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/[>=1.0]"), "profile": f"[platform_tool_requires]\ntool/1.1{revision}"}) client.run("create . -pr=profile") assert f"tool/1.1{revision or '#platform'} - Platform" in client.out def test_system_tool_require_range_non_matching(self): """ if what is specified in [system_tool_require] doesn't match what the recipe requires, then the system_tool_require will not be used, and the recipe will use its declared version """ client = TestClient(light=True) client.save({"tool/conanfile.py": GenConanfile("tool", "1.1"), "conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/[>=1.0]"), "profile": "[platform_tool_requires]\ntool/0.1"}) client.run("create tool") client.run("create . -pr=profile") assert "tool/1.1#888bda2348dd2ddcf5960d0af63b08f7 - Cache" in client.out def test_system_tool_require_no_host(self): """ system_tools must not affect host context """ client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("tool/1.0"), "profile": "[platform_tool_requires]\ntool/1.0"}) client.run("create . -pr=profile", assert_error=True) assert "ERROR: Package 'tool/1.0' not resolved: No remote defined" in client.out def test_graph_info_system_tool_require_range(self): """ graph info doesn't crash """ client = TestClient(light=True) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/[>=1.0]"), "profile": "[platform_tool_requires]\ntool/1.1"}) client.run("graph info . -pr=profile") assert "tool/1.1 - Platform" in client.out def test_consumer_resolved_version(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "tool/[>=1.0]" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {r.ref}") """) client.save({"conanfile.py": conanfile, "profile": "[platform_tool_requires]\ntool/1.1"}) client.run("install . -pr=profile") assert "tool/1.1#platform - Platform" in client.out assert "conanfile.py: DEPENDENCY tool/1.1" in client.out def test_consumer_resolved_revision(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "tool/1.1" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {repr(r.ref)}") """) client.save({"conanfile.py": conanfile, "profile": "[platform_tool_requires]\ntool/1.1#rev1"}) client.run("install . -pr=profile") assert "tool/1.1 - Platform" in client.out assert "conanfile.py: DEPENDENCY tool/1.1#rev1" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "tool/1.1#rev1" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {repr(r.ref)}") """) client.save({"conanfile.py": conanfile}) client.run("install . -pr=profile") assert "tool/1.1#rev1 - Platform" in client.out assert "conanfile.py: DEPENDENCY tool/1.1#rev1" in client.out def test_consumer_unresolved_revision(self): """ if a recipe specifies an exact revision and so does the profiñe and it doesn't match, it is an error """ client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): tool_requires = "tool/1.1#rev2" def generate(self): for r, _ in self.dependencies.items(): self.output.info(f"DEPENDENCY {repr(r.ref)}") """) client.save({"conanfile.py": conanfile, "profile": "[platform_tool_requires]\ntool/1.1#rev1"}) client.run("install . -pr=profile", assert_error=True) assert "ERROR: Package 'tool/1.1' not resolved" in client.out def test_require_build_context(self): """ https://github.com/conan-io/conan/issues/15659 app -> libcurl --(tool-require)--> libtool -> automake -> autoconf \\--(br)-> automake -> autoconf """ c = TestClient(light=True) c.save({"autoconf/conanfile.py": GenConanfile("autoconf", "0.1"), "automake/conanfile.py": GenConanfile("automake", "0.1").with_requires("autoconf/0.1"), "libtool/conanfile.py": GenConanfile("libtool", "0.1").with_requires("automake/0.1") .with_tool_requires("automake/0.1"), "libcurl/conanfile.py": GenConanfile("libcurl", "0.1").with_tool_requires("libtool/0.1"), "app/conanfile.py": GenConanfile().with_requires("libcurl/0.1"), "profile_build": "[settings]\nos=Linux\n[platform_tool_requires]\nautomake/0.1"}) c.run("create autoconf") c.run("create automake") c.run("export libtool") c.run("export libcurl") c.run("install app -pr:b=profile_build --build=missing") assert "Install finished successfully" in c.out def test_platform_requires_error(self): """ https://github.com/conan-io/conan/issues/19745 """ c = TestClient(light=True) profile = textwrap.dedent("""\ include(default) [platform_tool_requires] cmake/[*] """) c.save({"conanfile.py": GenConanfile("catch2").with_tool_requires("cmake/[*]"), "profile": profile}) c.run("create --version=3.6.0 -pr=profile", assert_error=True) assert "AssertionError" not in c.out assert ("ERROR: Error reading 'profile' profile: Profile [platform_requires]/" "[platform_tool_requires] must be exact versions, " "not version ranges: [cmake/[*]]") in c.out class TestToolRequiresLock: @pytest.mark.parametrize("revision", ["", "#myrev"]) def test_system_tool_require_range(self, revision): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/[>=1.0]"), "profile": f"[platform_tool_requires]\ntool/1.1{revision}"}) c.run("lock create . -pr=profile") assert "tool/1.1 - Platform" in c.out lock = json.loads(c.load("conan.lock")) assert lock["build_requires"] == [f"tool/1.1{revision or '#platform'}"] c.run("install .", assert_error=True) assert "Package 'tool/1.1' not resolved: No remote defined" in c.out c.run("install . -pr=profile") assert f"tool/1.1{revision or '#platform'} - Platform" in c.out # even if we create a version within the range, it will error if not matching the profile c.save({"tool/conanfile.py": GenConanfile("tool", "1.1")}) c.run("create tool") # if the profile points to another version it is an error, not in the lockfile c.save({"profile": f"[platform_tool_requires]\ntool/1.2{revision}"}) c.run("install . -pr=profile", assert_error=True) assert f"ERROR: Requirement 'tool/1.2{revision or '#platform'}' not in lockfile" in c.out # if we relax the lockfile, we can still resolve to the platform_tool_requires # specified by the profile c.run("install . -pr=profile --lockfile-partial") assert f"tool/1.2{revision or '#platform'} - Platform" in c.out class TestGenerators: def test_system_tool_require_range(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps from conan.tools.gnu import PkgConfigDeps class Pkg(ConanFile): settings = "build_type" tool_requires = "tool/[>=1.0]" def generate(self): deps = CMakeDeps(self) deps.build_context_activated = ["tool"] deps.generate() deps = PkgConfigDeps(self) deps.build_context_activated = ["tool"] deps.generate() """) client.save({"conanfile.py": conanfile, "profile": "[platform_tool_requires]\ntool/1.1"}) client.run("install . -pr=profile") assert "tool/1.1 - Platform" in client.out assert not os.path.exists(os.path.join(client.current_folder, "tool-config.cmake")) assert not os.path.exists(os.path.join(client.current_folder, "tool.pc")) class TestPackageID: """ if a consumer depends on recipe revision or package_id what happens """ @pytest.mark.parametrize("package_id_mode", ["recipe_revision_mode", "full_package_mode"]) def test_package_id_modes(self, package_id_mode): """ this test validates that the computation of the downstream consumers package_id doesnt break even if it depends on fields not existing in upstream system_tool_require, like revision or package_id """ client = TestClient(light=True) client.save_home({"global.conf": f"core.package_id:default_build_mode={package_id_mode}"}) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("dep/1.0"), "profile": "[platform_tool_requires]\ndep/1.0"}) client.run("create . -pr=profile") assert "dep/1.0 - Platform" in client.out def test_package_id_explicit_revision(self): """ Changing the system_tool_require revision affects consumers if package_revision_mode=recipe_revision """ client = TestClient(light=True) client.save_home({"global.conf": "core.package_id:default_build_mode=recipe_revision_mode"}) client.save({"conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("dep/1.0"), "profile": "[platform_tool_requires]\ndep/1.0#r1", "profile2": "[platform_tool_requires]\ndep/1.0#r2"}) client.run("create . -pr=profile") assert "dep/1.0#r1 - Platform" in client.out assert "pkg/1.0#27a56f09310cf1237629bae4104fe5bd:" \ "ea0e320d94b4b70fcb3efbabf9ab871542f8f696 - Build" in client.out client.run("create . -pr=profile2") # pkg gets a new package_id because it is a different revision assert "dep/1.0#r2 - Platform" in client.out assert "pkg/1.0#27a56f09310cf1237629bae4104fe5bd:" \ "334882884da082740e5a002a0b6fdb509a280159 - Build" in client.out ================================================ FILE: test/integration/graph/test_test_requires.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestTestRequiresDiamond: def test_test_requires_linear(self): c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requires("zlib/1.0"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_test_requires("gtest/1.0") }) c.run("create zlib") c.run("create gtest") c.run("install engine") c.assert_listed_require({"gtest/1.0": "Cache", "zlib/1.0": "Cache"}, test=True) def test_test_requires_half_diamond(self): c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requires("zlib/1.0"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("zlib/1.0") .with_test_requires("gtest/1.0") }) c.run("create zlib") c.run("create gtest") c.run("install engine") c.assert_listed_require({"zlib/1.0": "Cache"}) c.assert_listed_require({"gtest/1.0": "Cache"}, test=True) def test_test_requires_half_diamond_change_order(self): engine = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def requirements(self): # Best practice is to declare test_requires in the build_requirements() method, # but this ensures it is also possible to declare them in requirements(), # as has historically been allowed. self.test_requires("gtest/1.0") self.requires("zlib/1.0") """) c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requires("zlib/1.0"), "engine/conanfile.py": engine }) c.run("create zlib") c.run("create gtest") c.run("install engine") c.assert_listed_require({"zlib/1.0": "Cache"}) c.assert_listed_require({"gtest/1.0": "Cache"}, test=True) def test_test_requires_diamond(self): c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requires("zlib/1.0"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("zlib/1.0"), "game/conanfile.py": GenConanfile().with_requires("engine/1.0") .with_test_requires("gtest/1.0") }) c.run("create zlib") c.run("create gtest") c.run("create engine") c.run("install game") c.assert_listed_require({"zlib/1.0": "Cache", "engine/1.0": "Cache"}) c.assert_listed_require({"gtest/1.0": "Cache"}, test=True) def test_test_requires_diamond_change_order(self): c = TestClient(light=True) game = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def build_requirements(self): self.test_requires("gtest/1.0") def requirements(self): self.requires("engine/1.0") """) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requires("zlib/1.0"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("zlib/1.0"), "game/conanfile.py": game }) c.run("create zlib") c.run("create gtest") c.run("create engine") c.run("install game") c.assert_listed_require({"zlib/1.0": "Cache", "engine/1.0": "Cache"}) c.assert_listed_require({"gtest/1.0": "Cache"}, test=True) def test_test_requires_conflict_force(self): c = TestClient(light=True) game = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def build_requirements(self): self.test_requires("gtest/1.0", force=True) self.test_requires("rapidcheck/1.0") """) c.save({"gtest/conanfile.py": GenConanfile("gtest"), "rapidcheck/conanfile.py": GenConanfile("rapidcheck", "1.0").with_requires("gtest/1.1"), "game/conanfile.py": game }) c.run("create gtest --version=1.0") c.run("create gtest --version=1.1") c.run("create rapidcheck") c.run("install game") c.assert_listed_require({"gtest/1.0": "Cache"}, test=True) c.assert_overrides({"gtest/1.1": ["gtest/1.0"]}) def test_require_options(): c = TestClient(light=True) gtest = textwrap.dedent(""" from conan import ConanFile class Gtest(ConanFile): name = "gtest" version = "1.0" options = {"myoption": [1, 2, 3]} default_options = {"myoption": 1} def package_info(self): self.output.info(f"MYOPTION: {self.options.myoption}") """) engine = textwrap.dedent(""" from conan import ConanFile class Engine(ConanFile): name = "engine" version = "1.0" def build_requirements(self): self.test_requires("gtest/1.0", options={"myoption": "2"}) """) c.save({"gtest/conanfile.py": gtest, "engine/conanfile.py": engine}) c.run("create gtest") c.run("create gtest -o gtest*:myoption=2") c.run("create gtest -o gtest*:myoption=3") c.run("create engine") assert "gtest/1.0: MYOPTION: 2" in c.out c.run("create engine -o gtest*:myoption=3") assert "gtest/1.0: MYOPTION: 3" in c.out def test_requires_components(): """ this test used to fail with "gtest" not required by components It is important to have at least 1 external ``requires`` because with no requires at all it doesn't fail. https://github.com/conan-io/conan/issues/13187 """ c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MyLib(ConanFile): name = "mylib" version = "0.1" requires = "openssl/1.1" test_requires = "gtest/1.0" def package_info(self): self.cpp_info.components["mylib"].requires = ["openssl::openssl"] """) c.save({"gtest/conanfile.py": GenConanfile("gtest", "1.0"), "openssl/conanfile.py": GenConanfile("openssl", "1.1"), "pkg/conanfile.py": conanfile}) c.run("create gtest") c.run("create openssl") c.run("create pkg") # This NO LONGER FAILS c.assert_listed_require({"gtest/1.0": "Cache"}, test=True) def test_requires_transitive_diamond_components(): """ libc -----> libb ----> liba |-(test-requires)----/ https://github.com/conan-io/conan/issues/13892 """ c = TestClient(light=True) libc = textwrap.dedent(""" from conan import ConanFile class LibC(ConanFile): name = "libc" version = "0.1" requires = "libb/1.0" test_requires = "liba/1.0" def package_info(self): self.cpp_info.components["comp"].libs = ["libc"] self.cpp_info.components["comp"].requires.append("libb::libb") """) c.save({"liba/conanfile.py": GenConanfile("liba", "1.0"), "libb/conanfile.py": GenConanfile("libb", "1.0").with_requires("liba/1.0"), "libc/conanfile.py": libc}) c.run("create liba") c.run("create libb") c.run("create libc") # This used to crash due to component not defined to liba assert "libc/0.1: Created package" in c.out def test_requires_transitive_diamond_components_order(): """ like the above, but in different order libc --(test-requires)---> liba |-----> libb -----------/ libc->liba => test=False, direct=True, is_test=True direct_dependencies (components check) => False https://github.com/conan-io/conan/issues/17164 """ c = TestClient(light=True) libc = textwrap.dedent(""" from conan import ConanFile class LibC(ConanFile): name = "libc" version = "0.1" def build_requirements(self): self.test_requires("liba/1.0") def requirements(self): self.requires("libb/1.0") def package_info(self): self.cpp_info.components["comp"].libs = ["libc"] self.cpp_info.components["comp"].requires.append("libb::libb") """) c.save({"liba/conanfile.py": GenConanfile("liba", "1.0"), "libb/conanfile.py": GenConanfile("libb", "1.0").with_requires("liba/1.0"), "libc/conanfile.py": libc}) c.run("create liba") c.run("create libb") c.run("create libc") # This used to crash due to component not defined to liba assert "libc/0.1: Created package" in c.out def test_wrong_requirement_test_requires(): """ https://github.com/conan-io/conan/issues/17312 app --------> etas --------------> enum \\-->hwinfo-/---(test_requires)---/ \\-----------------------------/ app->enum => test=False, direct=True, is_test=True direct_dependencies (components check) => True """ c = TestClient(light=True) app = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "app" version = "0.1" requires = "etas/0.1", "enum/0.1", "hwinfo/0.1" def generate(self): for r, d in self.dependencies.items(): assert not (r.direct and r.is_test) def package_info(self): self.cpp_info.requires = ["etas::etas", "enum::enum", "hwinfo::hwinfo"] """) files = { "enum/conanfile.py": GenConanfile("enum", "0.1"), "etas/conanfile.py": GenConanfile("etas", "0.1").with_requirement("enum/0.1"), "hwinfo/conanfile.py": GenConanfile("hwinfo", "0.1").with_requirement("etas/0.1") .with_test_requires("enum/0.1"), "app/conanfile.py": app, } c.save(files) c.run("create enum") c.run("create etas") c.run("create hwinfo") # The assert in generate() doesnt fail c.run("install app") # the package_info() doesn't fail c.run("create app") def test_test_requires_options(): """ the default_options = {} values also propagate to ``test_requires()`` """ c = TestClient(light=True) c.save({"test/conanfile.py": GenConanfile("test", "0.1").with_option("myoption", [1, 2, 3]), "consumer/conanfile.py": GenConanfile().with_test_requires("test/0.1") .with_default_option("test/*:myoption", "2")}) c.run("create test -o myoption=2") c.assert_listed_binary({"test/0.1": ("a3cb1345b8297bfdffea4ef4bb1b2694c54d1d69", "Build")}) c.run("install consumer") c.assert_listed_require({"test/0.1": "Cache"}, test=True) c.assert_listed_binary({"test/0.1": ("a3cb1345b8297bfdffea4ef4bb1b2694c54d1d69", "Cache")}, test=True) def test_invisible_requires_options(): """ the default_options = {} values also propagate to ``requires(visible=False)`` """ c = TestClient(light=True) c.save({"test/conanfile.py": GenConanfile("test", "0.1").with_option("myoption", [1, 2, 3]), "consumer/conanfile.py": GenConanfile().with_requirement("test/0.1", visible=False) .with_default_option("test/*:myoption", "2")}) c.run("create test -o myoption=2") c.assert_listed_binary({"test/0.1": ("a3cb1345b8297bfdffea4ef4bb1b2694c54d1d69", "Build")}) c.run("install consumer") c.assert_listed_require({"test/0.1": "Cache"}) c.assert_listed_binary({"test/0.1": ("a3cb1345b8297bfdffea4ef4bb1b2694c54d1d69", "Cache")}) ================================================ FILE: test/integration/graph/test_validate_build.py ================================================ import json import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_basic_validate_build_test(): t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class myConan(ConanFile): name = "foo" version = "1.0" settings = "os", "arch", "compiler" def validate_build(self): if self.settings.compiler == "gcc": raise ConanInvalidConfiguration("This doesn't build in GCC") def package_id(self): del self.info.settings.compiler """) settings_gcc = "-s compiler=gcc -s compiler.libcxx=libstdc++11 -s compiler.version=11" settings_clang = "-s compiler=clang -s compiler.libcxx=libc++ -s compiler.version=8" t.save({"conanfile.py": conanfile}) t.run(f"create . {settings_gcc}", assert_error=True) assert "foo/1.0: Cannot build for this configuration: This doesn't build in GCC" in t.out t.run(f"create . {settings_clang}") # Now with GCC again, but now we have the binary, we don't need to build, so it doesn't fail t.run(f"create . {settings_gcc} --build missing") assert "foo/1.0: Already installed!" in t.out # But if I force the build... it will fail t.run(f"create . {settings_gcc} ", assert_error=True) assert "foo/1.0: Cannot build for this configuration: This doesn't build in GCC" in t.out # What happens with a conan info? t.run(f"graph info --requires=foo/1.0 {settings_gcc} --format=json", redirect_stdout="myjson") myjson = json.loads(t.load("myjson"))["graph"]["nodes"] assert myjson["1"]["invalid_build"] == "This doesn't build in GCC" t.run(f"graph info --requires=foo/1.0 {settings_clang} --format=json", redirect_stdout="myjson") myjson = json.loads(t.load("myjson"))["graph"]["nodes"] assert myjson["1"]["invalid_build"] is False def test_with_options_validate_build_test(): t = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class myConan(ConanFile): name = "foo" version = "1.0" options = {"my_option": [True, False]} default_options = {"my_option": True} def validate_build(self): if not self.options.my_option: raise ConanInvalidConfiguration("This doesn't build with False option") """) t.save({"conanfile.py": conanfile}) t.run("export .") consumer = GenConanfile().with_require("foo/1.0").with_name("consumer").with_version("1.0") t.save({"consumer.py": consumer}) t.run("create consumer.py --build missing -o foo/*:my_option=False", assert_error=True) assert "foo/1.0: Cannot build for this configuration: This doesn't build " \ "with False option" in t.out t.run("create consumer.py --build missing -o foo/*:my_option=True") def test_basic_validate_build_command_build(): """ the "conan build" command should fail for a validate_build() too https://github.com/conan-io/conan/issues/12571 """ t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class myConan(ConanFile): settings = "os" def validate_build(self): if self.settings.os == "Windows": raise ConanInvalidConfiguration("This doesn't build in Windows") """) t.save({"conanfile.py": conanfile}) t.run(f"build . -s os=Windows", assert_error=True) assert "ERROR: conanfile.py: Cannot build for this configuration: " \ "This doesn't build in Windows" in t.out t.run("build . -s os=Linux") # It doesn't fail ================================================ FILE: test/integration/graph/ux/__init__.py ================================================ ================================================ FILE: test/integration/graph/ux/loop_detection_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient, GenConanfile class TestLoopDetection: def test_transitive_loop(self): client = TestClient(light=True) client.save({ 'pkg1.py': GenConanfile().with_require('pkg2/0.1@lasote/stable'), 'pkg2.py': GenConanfile().with_require('pkg3/0.1@lasote/stable'), 'pkg3.py': GenConanfile().with_require('pkg1/0.1@lasote/stable'), }) client.run('export pkg1.py --name=pkg1 --version=0.1 --user=lasote --channel=stable') client.run('export pkg2.py --name=pkg2 --version=0.1 --user=lasote --channel=stable') client.run('export pkg3.py --name=pkg3 --version=0.1 --user=lasote --channel=stable') client.run("install --requires=pkg3/0.1@lasote/stable --build='*'", assert_error=True) # TODO: Complete with better diagnostics assert "ERROR: There is a cycle/loop in the graph" in client.out def test_self_loop(self): client = TestClient(light=True) client.save({'pkg1.py': GenConanfile().with_require('pkg1/0.1@lasote/stable'), }) client.run('export pkg1.py --name=pkg1 --version=0.1 --user=lasote --channel=stable') client.run("install --requires=pkg1/0.1@lasote/stable --build='*'", assert_error=True) assert "ERROR: There is a cycle/loop in the graph" in client.out def test_install_order_infinite_loop(): c = TestClient(light=True) c.save({"fmt/conanfile.py": GenConanfile("fmt", "1.0"), "tool/conanfile.py": GenConanfile("tool", "1.0").with_requires("fmt/1.0"), "tool_profile": "[tool_requires]\n!tool/*: tool/1.0"}) c.run("export fmt") c.run("export tool") c.run("install tool -pr:h=tool_profile -b=missing", assert_error=True) assert "ERROR: There is a loop in the graph" in c.out assert "fmt/1.0 (Build) -> ['tool/1.0']" in c.out assert "tool/1.0 (Build) -> ['fmt/1.0']" in c.out # Graph build-order fails in the same way c.run("graph build-order tool -pr:h=tool_profile -b=missing", assert_error=True) assert "ERROR: There is a loop in the graph" in c.out assert "fmt/1.0 (Build) -> ['tool/1.0']" in c.out assert "tool/1.0 (Build) -> ['fmt/1.0']" in c.out c.run("graph build-order tool -pr:h=tool_profile --order-by=configuration -b=missing", assert_error=True) assert "ERROR: There is a loop in the graph" in c.out assert "fmt/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709 (Build) -> " \ "['tool/1.0:044d18636d2b7da86d3aa46a2aabf1400db525b1']" in c.out assert "tool/1.0:044d18636d2b7da86d3aa46a2aabf1400db525b1 (Build) -> " \ "['fmt/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709']" in c.out def test_build_require_undetected_loop(): c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class DemoRecipe(ConanFile): name = "test" version = "1.0" def requirements(self): self.requires("cmake/3.31.6", build=True) def build_requirements(self): self.tool_requires("cmake/3.31.6", options={ "bad": True # this doesn't need to be a valid option }) def generate(self): self.output.info(f"NUM DEPS: {len(self.dependencies.items())}") """) c.save({"cmake/conanfile.py": GenConanfile("cmake", "3.31.6").with_shared_option(True), "app/conanfile.py": conanfile}) c.run("create cmake") c.run("install app") # It doesn't hang, and it sees correctly just 1 dependency assert "conanfile.py (test/1.0): NUM DEPS: 1" in c.out ================================================ FILE: test/integration/graph/version_ranges/__init__.py ================================================ ================================================ FILE: test/integration/graph/version_ranges/test_version_range_conf.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_version_range_conf_nonexplicit_expression(): tc = TestClient(light=True) tc.save({"base/conanfile.py": GenConanfile("base")}) tc.run("create base/conanfile.py --version=1.5.1") tc.run("create base/conanfile.py --version=2.5.0-pre") tc.save({"v1/conanfile.py": GenConanfile("pkg", "1.0").with_requires("base/[>1 <2]"), "v2/conanfile.py": GenConanfile("pkg", "2.0").with_requires("base/[>2 <3]")}) tc.save_home({"global.conf": "core.version_ranges:resolve_prereleases=False"}) tc.run("create v1/conanfile.py") assert "base/[>1 <2]: base/1.5.1" in tc.out tc.run("create v2/conanfile.py", assert_error=True) assert "Package 'base/[>2 <3]' not resolved" in tc.out tc.save_home({"global.conf": "core.version_ranges:resolve_prereleases=True"}) tc.run("create v1/conanfile.py") assert "base/[>1 <2]: base/1.5.1" in tc.out tc.run("create v2/conanfile.py") assert "base/[>2 <3]: base/2.5.0-pre" in tc.out tc.save_home({"global.conf": "core.version_ranges:resolve_prereleases=None"}) tc.run("create v1/conanfile.py") assert "base/[>1 <2]: base/1.5.1" in tc.out tc.run("create v2/conanfile.py", assert_error=True) assert "Package 'base/[>2 <3]' not resolved" in tc.out def test_version_range_conf_explicit_expression(): tc = TestClient(light=True) tc.save({"base/conanfile.py": GenConanfile("base")}) tc.run("create base/conanfile.py --version=1.5.1") tc.run("create base/conanfile.py --version=2.5.0-pre") tc.save({"v1/conanfile.py": GenConanfile("pkg", "1.0").with_requires("base/[>1 <2, include_prerelease]"), "v2/conanfile.py": GenConanfile("pkg", "2.0").with_requires("base/[>2 <3, include_prerelease]")}) tc.save_home({"global.conf": "core.version_ranges:resolve_prereleases=False"}) tc.run("create v1/conanfile.py") assert "base/[>1 <2, include_prerelease]: base/1.5.1" in tc.out tc.run("create v2/conanfile.py", assert_error=True) assert "Package 'base/[>2 <3, include_prerelease]' not resolved" in tc.out tc.save_home({"global.conf": "core.version_ranges:resolve_prereleases=True"}) tc.run("create v1/conanfile.py") assert "base/[>1 <2, include_prerelease]: base/1.5.1" in tc.out tc.run("create v2/conanfile.py") assert "base/[>2 <3, include_prerelease]: base/2.5.0-pre" in tc.out tc.save_home({"global.conf": "core.version_ranges:resolve_prereleases=None"}) tc.run("create v1/conanfile.py") assert "base/[>1 <2, include_prerelease]: base/1.5.1" in tc.out tc.run("create v2/conanfile.py") assert "base/[>2 <3, include_prerelease]: base/2.5.0-pre" in tc.out ================================================ FILE: test/integration/graph/version_ranges/version_range_override_test.py ================================================ import json import pytest from conan.test.utils.tools import TestClient, GenConanfile class TestVersionRangeOverride: @pytest.fixture(autouse=True) def _setup(self): self.t = TestClient(light=True) self.t.save({"libb/conanfile.py": GenConanfile(), "libc/conanfile.py": GenConanfile().with_require("libb/[<=2.0]@user/channel")}) self.t.run("export libb --name=libb --version=1.0 --user=user --channel=channel") self.t.run("export libb --name=libb --version=2.0 --user=user --channel=channel") self.t.run("export libb --name=libb --version=3.0 --user=user --channel=channel") self.t.run("export libc --name=libc --version=1.0 --user=user --channel=channel") def test(self): # Use the version range self.t.save({"conanfile.py": GenConanfile().with_require("libc/1.0@user/channel")}) self.t.run("graph info . --filter requires") assert "libb/2.0@user/channel" in self.t.out def test_override_with_fixed_version(self): # Override upstream version range with a fixed version self.t.save({"conanfile.py": GenConanfile().with_requirement("libb/3.0@user/channel", override=True) .with_require("libc/1.0@user/channel")}) self.t.run("graph info . --filter requires") self.t.assert_overrides({'libb/[<=2.0]@user/channel': ['libb/3.0@user/channel']}) assert "libb/3.0@user/channel#" in self.t.out def test_override_using_version_range(self): # Override upstream version range with a different (narrower) version range self.t.save({"conanfile.py": GenConanfile().with_requirement("libb/[<2.x]@user/channel", override=True) .with_require("libc/1.0@user/channel")}) self.t.run("graph info . --filter requires") self.t.assert_overrides({'libb/[<=2.0]@user/channel': ['libb/[<2.x]@user/channel']}) assert "libb/2.0@user/channel" in self.t.out def test_override_version_range_outside(self): # Override upstream version range with a different (non intersecting) version range self.t.save({"conanfile.py": GenConanfile().with_requirement("libb/[>2.x]@user/channel", override=True) .with_require("libc/1.0@user/channel")}) self.t.run("graph info . --filter requires") self.t.assert_overrides({'libb/[<=2.0]@user/channel': ['libb/[>2.x]@user/channel']}) assert "libb/3.0@user/channel" in self.t.out class TestVersionRangeOverrideFail: def test_override(self): """ pkga -> ros_perception -> ros_core \\-----> pkgb -----------/ """ # https://github.com/conan-io/conan/issues/8071 t = TestClient(light=True) t.save({"conanfile.py": GenConanfile()}) t.run("create . --name=ros_core --version=1.1.4 --user=3rdparty --channel=unstable") t.run("create . --name=ros_core --version=pr-53 --user=3rdparty --channel=snapshot") t.save({"conanfile.py": GenConanfile().with_requires("ros_core/1.1.4@3rdparty/unstable")}) t.run("create . --name=ros_perception --version=1.1.4 --user=3rdparty --channel=unstable") t.run("create . --name=ros_perception --version=pr-53 --user=3rdparty --channel=snapshot") t.save({"conanfile.py": GenConanfile().with_requires("ros_core/[~1.1]@3rdparty/unstable")}) t.run("create . --name=pkgb --version=0.1 --user=common --channel=unstable") t.save({"conanfile.py": GenConanfile("pkga", "0.1").with_requires( "ros_perception/[~1.1]@3rdparty/unstable", "pkgb/[~0]@common/unstable")}) t.run("create . ") assert "ros_core/1.1.4@3rdparty/unstable" in t.out assert "ros_perception/1.1.4@3rdparty/unstable" in t.out assert "snapshot" not in t.out t.save({"conanfile.py": GenConanfile("pkga", "0.1") .with_require("pkgb/[~0]@common/unstable") .with_require("ros_perception/pr-53@3rdparty/snapshot") .with_requirement("ros_core/pr-53@3rdparty/snapshot", override=True)}) t.run("create . --build=missing --build=pkga") assert "ros_core/pr-53@3rdparty/snapshot" in t.out assert "ros_perception/pr-53@3rdparty/snapshot" in t.out # Override only the upstream without overriding the direct one t.save({"conanfile.py": GenConanfile("pkga", "0.1") .with_require("pkgb/[~0]@common/unstable") .with_require("ros_perception/[~1.1]@3rdparty/unstable") .with_requirement("ros_core/pr-53@3rdparty/snapshot", force=True)}) t.run("create . --build=missing --build=pkga") assert "ros_core/pr-53@3rdparty/snapshot" in t.out assert "ros_perception/1.1.4@3rdparty/unstable" in t.out # Check information got by graph info t.run("graph info . --format json") info = json.loads(t.stdout) expected_overrides = { "ros_core/[~1.1]@3rdparty/unstable": [ "ros_core/pr-53@3rdparty/snapshot" ], "ros_core/1.1.4@3rdparty/unstable": [ "ros_core/pr-53@3rdparty/snapshot" ] } assert info['graph']["overrides"] == expected_overrides expected_resolved_ranges = { "pkgb/[~0]@common/unstable": "pkgb/0.1@common/unstable", "ros_perception/[~1.1]@3rdparty/unstable": "ros_perception/1.1.4@3rdparty/unstable" } assert info['graph']["resolved_ranges"] == expected_resolved_ranges ================================================ FILE: test/integration/graph/version_ranges/version_ranges_cached_test.py ================================================ from collections import OrderedDict import pytest from unittest.mock import patch from conan.internal.rest.remote_manager import RemoteManager from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer class TestVersionRangesCache: @pytest.fixture(autouse=True) def _setup(self): self.counters = {"server0": 0, "server1": 0} def _mocked_search_recipes(self, remote, pattern, ignorecase=True): packages = { "server0": [RecipeReference.loads("liba/1.0.0"), RecipeReference.loads("liba/1.1.0")], "server1": [RecipeReference.loads("liba/2.0.0"), RecipeReference.loads("liba/2.1.0")] } self.counters[remote.name] = self.counters[remote.name] + 1 return packages[remote.name] def test_version_ranges_cached(self): servers = OrderedDict() for index in range(2): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) client = TestClient(light=True, servers=servers, inputs=["user", "password", "user", "password"]) # server0 does not satisfy range # server1 does for minor in range(2): client.save({"conanfile.py": GenConanfile("liba", f"1.{minor}.0")}) client.run("create .") client.run(f"upload liba/1.{minor}.0 -r server0 -c") for minor in range(2): client.save({"conanfile.py": GenConanfile("liba", f"2.{minor}.0")}) client.run("create .") client.run(f"upload liba/2.{minor}.0 -r server1 -c") client.run("remove * -c") client.save({"conanfile.py": GenConanfile("libb", "1.0").with_require("liba/[>=2.0]")}) client.run("create .") client.save({"conanfile.py": GenConanfile("libc", "1.0").with_require("liba/[>=2.0]")}) client.run("create .") client.save({"conanfile.py": GenConanfile("consumer", "1.0") .with_requires("libb/1.0", "libc/1.0")}) # should call only once to server0 self.counters["server0"] = 0 self.counters["server1"] = 0 with patch.object(RemoteManager, "search_recipes", new=self._mocked_search_recipes): client.run("create . --update") assert self.counters["server0"] == 1 assert self.counters["server1"] == 1 class TestVersionRangesDiamond: def test_caching_errors(self): # https://github.com/conan-io/conan/issues/6110 c = TestClient(light=True, default_server_user=True) c.save({"conanfile.py": GenConanfile("zlib")}) c.run("create . --version=1.0") c.run("create . --version=1.1") c.save({"conanfile.py": GenConanfile("poco", "1.0").with_requires("zlib/1.0")}) c.run("create .") c.save({"conanfile.py": GenConanfile("boost", "1.0").with_requires("zlib/[*]")}) c.run("create .") c.run("upload * -c -r=default") c.run("remove * -c") c.save({"conanfile.py": GenConanfile().with_requires("poco/1.0", "boost/1.0")}) c.run("install .") # Ok, no conflict c.run("install . --update") # Fails due to conflict def test_prefer_cache_version(): """ the latest version whenever it is https://github.com/conan-io/conan/issues/6544 """ c = TestClient(light=True, default_server_user=True) c.save({"pkg/conanfile.py": GenConanfile("pkg"), "consumer/conanfile.py": GenConanfile().with_requires("pkg/[*]")}) c.run("create pkg --version=1.0") c.run("upload * -c -r=default") c.run("install consumer") c.run("create pkg --version=1.1") c.run("install consumer --update") assert "pkg/1.1" in c.out assert "pkg/1.0" not in c.out ================================================ FILE: test/integration/graph/version_ranges/version_ranges_diamond_test.py ================================================ from collections import OrderedDict from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer class TestVersionRangesUpdatingTest: def test_update_remote(self): # https://github.com/conan-io/conan/issues/5333 client = TestClient(light=True, default_server_user=True) client.save({"conanfile.py": GenConanfile("boost")}) client.run("create . --version=1.69.0") client.run("create . --version=1.70.0") client.run("upload * -r=default --confirm") client.run("remove * -c") client.save({"conanfile.txt": "[requires]\nboost/[*]"}, clean_first=True) client.run("install .") assert "boost/1.70" in client.out assert "boost/1.69" not in client.out client.run("install .") assert "boost/1.70" in client.out assert "boost/1.69" not in client.out client.run("install . --update") assert "boost/1.70" in client.out assert "boost/1.69" not in client.out def test_update(self): client = TestClient(light=True, default_server_user=True) client.save({"pkg/conanfile.py": GenConanfile("pkg"), "app/conanfile.py": GenConanfile().with_requirement("pkg/[~1]")}) client.run("create pkg --version=1.1") client.run("create pkg --version=1.2") client.run("upload * -r=default --confirm") client.run("remove pkg/1.2* -c") client.run("install app") # Resolves to local package assert "pkg/1.1" in client.out assert "pkg/1.2" not in client.out client.run("install app --update") # Resolves to remote package assert "pkg/1.1" not in client.out assert "pkg/1.2" in client.out # newer in cache that in remotes and updating, should resolve the cache one client.run("create pkg --version=1.3") client.run("install app --update") assert "pkg/1.2" not in client.out assert "pkg/1.3" in client.out client.run("remove pkg/1.3* -c") # removes remote client.run("remove pkg* -r=default -c") # Resolves to local package client.run("install app") assert "pkg/1.1" not in client.out assert "pkg/1.2" in client.out client.run("install app --update") assert "pkg/1.1" not in client.out assert "pkg/1.2" in client.out class TestVersionRangesMultiRemote: def test_multi_remote(self): servers = OrderedDict() servers["default"] = TestServer() servers["other"] = TestServer() client = TestClient(light=True, servers=servers, inputs=2*["admin", "password"]) client.save({"hello0/conanfile.py": GenConanfile("hello0"), "hello1/conanfile.py": GenConanfile("hello1").with_requires("hello0/[*]")}) client.run("export hello0 --version=0.1") client.run("export hello0 --version=0.2") client.run("upload * -r=default -c") client.run("export hello0 --version=0.3") client.run("upload hello0/0.3 -r=other -c") client.run('remove "hello0/*" -c') client.run("install hello1 --build missing -r=default") assert "hello0/0.2" in client.out assert "hello0/0.3" not in client.out client.run("remove hello0/* -c") client.run("install hello1 --build missing -r=other") assert "hello0/0.2" not in client.out assert "hello0/0.3" in client.out ================================================ FILE: test/integration/layout/__init__.py ================================================ ================================================ FILE: test/integration/layout/devflow_test.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient from conan.internal.util.files import load, mkdir conanfile = ''' from conan import ConanFile from conan.tools.files import save, load from conan.tools.files import copy import os class ConanFileToolsTest(ConanFile): name = "pkg" version = "0.1" exports_sources = "*" def layout(self): self.folders.build = "../build" self.folders.source = "." def build(self): self.output.info("Source files: %s" % load(self, os.path.join(self.source_folder, "file.h"))) save(self, "myartifact.lib", "artifact contents!") save(self, "subdir/myartifact2.lib", "artifact2 contents!") def package(self): copy(self, "*.h", self.source_folder, self.package_folder) copy(self, "*.lib", self.build_folder, self.package_folder) ''' class TestDevInSourceFlow: @staticmethod def _assert_pkg(folder): assert sorted(['file.h', 'myartifact.lib', 'subdir', 'conaninfo.txt', 'conanmanifest.txt']) == sorted(os.listdir(folder)) assert load(os.path.join(folder, "myartifact.lib")) == "artifact contents!" assert load(os.path.join(folder, "subdir/myartifact2.lib")) == "artifact2 contents!" def test_parallel_folders(self): client = TestClient(light=True) repo_folder = os.path.join(client.current_folder, "recipe") build_folder = os.path.join(client.current_folder, "build") mkdir(repo_folder) mkdir(build_folder) client.current_folder = repo_folder # equivalent to git clone recipe client.save({"conanfile.py": conanfile, "file.h": "file_h_contents!"}) client.current_folder = build_folder client.run("install ../recipe") client.run("build ../recipe") client.current_folder = repo_folder client.run("export . --user=lasote --channel=testing") client.run("export-pkg . --name=pkg --version=0.1 --user=lasote --channel=testing") cache_package_folder = client.created_layout().package() self._assert_pkg(cache_package_folder) def test_insource_build(self): client = TestClient(light=True) repo_folder = client.current_folder package_folder = os.path.join(client.current_folder, "pkg") mkdir(package_folder) client.save({"conanfile.py": conanfile, "file.h": "file_h_contents!"}) client.run("install .") client.run("build .") client.current_folder = repo_folder client.run("export . --user=lasote --channel=testing") client.run("export-pkg . --name=pkg --version=0.1 --user=lasote --channel=testing") cache_package_folder = client.created_layout().package() self._assert_pkg(cache_package_folder) def test_child_build(self): client = TestClient(light=True) build_folder = os.path.join(client.current_folder, "build") mkdir(build_folder) package_folder = os.path.join(build_folder, "package") mkdir(package_folder) client.save({"conanfile.py": conanfile, "file.h": "file_h_contents!"}) client.current_folder = build_folder client.run("install ..") client.run("build ..") client.run("export-pkg .. --name=pkg --version=0.1 --user=lasote --channel=testing") cache_package_folder = client.created_layout().package() self._assert_pkg(cache_package_folder) conanfile_out = ''' from conan import ConanFile from conan.tools.files import save, load from conan.tools.files import copy import os class ConanFileToolsTest(ConanFile): name = "pkg" version = "0.1" def source(self): save(self, os.path.join(self.source_folder, "file.h"), "file_h_contents!") def build(self): self.output.info("Source files: %s" % load(self, os.path.join(self.source_folder, "file.h"))) save(self, "myartifact.lib", "artifact contents!") def package(self): copy(self, "*.h", self.source_folder, self.package_folder) copy(self, "*.lib", self.build_folder, self.package_folder) ''' class TestDevOutSourceFlow: @staticmethod def _assert_pkg(folder): assert sorted(['file.h', 'myartifact.lib', 'conaninfo.txt', 'conanmanifest.txt']) == \ sorted(os.listdir(folder)) def test_parallel_folders(self): client = TestClient(light=True) repo_folder = os.path.join(client.current_folder, "recipe") src_folder = os.path.join(client.current_folder, "src") build_folder = os.path.join(client.current_folder, "build") mkdir(repo_folder) mkdir(src_folder) mkdir(build_folder) client.current_folder = repo_folder # equivalent to git clone recipe conanfile_final = conanfile_out + """ def layout(self): self.folders.build = "../build" self.folders.source = "../src" """ client.save({"conanfile.py": conanfile_final}) client.current_folder = build_folder client.run("install ../recipe") client.current_folder = src_folder # FIXME: Source layout not working client.run("source ../recipe") client.current_folder = build_folder client.run("build ../recipe") client.current_folder = repo_folder client.run("export . --user=lasote --channel=testing") client.run("export-pkg . --name=pkg --version=0.1 --user=lasote --channel=testing") cache_package_folder = client.created_layout().package() self._assert_pkg(cache_package_folder) def test_insource_build(self): client = TestClient(light=True) repo_folder = client.current_folder client.save({"conanfile.py": conanfile_out}) client.run("install .") client.run("source .") client.run("build . ") client.current_folder = repo_folder client.run("export . --user=lasote --channel=testing") client.run("export-pkg . --name=pkg --version=0.1 --user=lasote --channel=testing") cache_package_folder = client.created_layout().package() self._assert_pkg(cache_package_folder) def test_child_build(self): client = TestClient(light=True) repo_folder = client.current_folder build_folder = os.path.join(client.current_folder, "build") mkdir(build_folder) conanfile_final = conanfile_out + """ def layout(self): self.folders.build = "build" """ client.save({"conanfile.py": conanfile_final}) client.current_folder = build_folder client.run("install ..") client.current_folder = repo_folder # FIXME: Source layout not working client.run("source .") client.current_folder = build_folder client.run("build ..") client.current_folder = repo_folder client.run("export-pkg . --name=pkg --version=0.1 --user=lasote --channel=testing") cache_package_folder = client.created_layout().package() self._assert_pkg(cache_package_folder) def test_error_build_file(): c = TestClient(light=True) conanfile_error = textwrap.dedent(r""" from conan import ConanFile class Test(ConanFile): def layout(self): self.folders.build = "build" """) c.save({"conanfile.py": conanfile_error, "build": "# Some existing BUILD file like bazel"}) c.run("build . ", assert_error=True) assert "ERROR: conanfile.py: Failed to create build folder, there is already a file" in c.out ================================================ FILE: test/integration/layout/export_folder_variable_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient class TestExportFoldersAvailability: def test_export_sources_folder_availability_local_methods(self): conanfile = textwrap.dedent(''' import os from conan import ConanFile class ConanLib(ConanFile): def layout(self): self.folders.source = "MY_SOURCE" def generate(self): assert os.path.exists(self.export_sources_folder) def export(self): assert self.export_sources_folder is None def export_sources(self): assert os.path.exists(self.export_sources_folder) def source(self): assert os.path.exists(self.export_sources_folder) def build(self): assert os.path.exists(self.export_sources_folder) def package(self): assert os.path.exists(self.export_sources_folder) ''') client = TestClient() client.save({"conanfile.py": conanfile}) client.run("export . --name foo --version 1.0") client.run("install .") client.run("source .") client.run("build .") def test_export_folder_availability_local_methods(self): conanfile = textwrap.dedent(''' import os from conan import ConanFile class ConanLib(ConanFile): def layout(self): self.folders.source = "MY_SOURCE" def generate(self): assert os.path.exists(self.export_sources_folder) def export_sources(self): # We need it available for the post_export hook so it is available assert os.path.exists(self.export_folder) def export(self): assert os.path.exists(self.export_folder) def source(self): assert os.path.exists(self.export_sources_folder) def build(self): assert os.path.exists(self.export_sources_folder) def package(self): assert os.path.exists(self.export_sources_folder) ''') client = TestClient() client.save({"conanfile.py": conanfile}) client.run("export . --name foo --version 1.0") client.run("install .") client.run("source .") client.run("build .") def test_export_folder_availability_create(self): conanfile = textwrap.dedent(''' import os from conan import ConanFile class ConanLib(ConanFile): def layout(self): self.folders.source = "MY_SOURCE" def generate(self): assert self.export_folder is None def export(self): assert os.path.exists(self.export_folder) def export_sources(self): # We need it available for the post_export hook so it is available assert os.path.exists(self.export_folder) def source(self): assert self.export_folder is None def build(self): assert self.export_folder is None def package(self): assert self.export_folder is None ''') client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name foo --version 1.0") def test_export_sources_folder_availability_create(self): conanfile = textwrap.dedent(''' import os from conan import ConanFile class ConanLib(ConanFile): def layout(self): self.folders.source = "MY_SOURCE" def generate(self): assert os.path.exists(self.export_sources_folder) def export(self): assert self.export_sources_folder is None def export_sources(self): assert os.path.exists(self.export_sources_folder) def source(self): assert os.path.exists(self.export_sources_folder) def build(self): assert os.path.exists(self.export_sources_folder) def package(self): assert os.path.exists(self.export_sources_folder) ''') client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name foo --version 1.0") ================================================ FILE: test/integration/layout/test_cmake_build_folder.py ================================================ import os import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient @pytest.mark.parametrize("cmd", ["install", "build"]) def test_cmake_layout_build_folder(cmd): """ testing the tools.cmake.cmake_layout:build_folder config for both build and install commands """ c = TestClient() abs_build = temp_folder() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def layout(self): cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config """) c.save({"conanfile.py": conanfile}) c.run(f'{cmd} . -c tools.cmake.cmake_layout:build_folder="{abs_build}"') assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake")) assert not os.path.exists(os.path.join(c.current_folder, "build")) # Make sure that a non-existing folder will not fail and will be created new_folder = os.path.join(temp_folder(), "my build") c.run(f'{cmd} . -c tools.cmake.cmake_layout:build_folder="{new_folder}"') assert os.path.exists(os.path.join(new_folder, "Release", "generators", "conan_toolchain.cmake")) assert not os.path.exists(os.path.join(c.current_folder, "build")) # Just in case we check that local build folder would be created if no arg is provided c.run('build . ') assert os.path.exists(os.path.join(c.current_folder, "build")) @pytest.mark.parametrize("cmd", ["install", "build"]) def test_cmake_layout_build_folder_relative(cmd): """ Same as above, but with a relative folder, which is relative to the conanfile """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def layout(self): cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config """) c.save({"pkg/conanfile.py": conanfile}) c.run(f'{cmd} pkg -c tools.cmake.cmake_layout:build_folder=mybuild') abs_build = os.path.join(c.current_folder, "pkg", "mybuild") assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake")) assert not os.path.exists(os.path.join(c.current_folder, "pkg", "build")) # relative path, pointing to a sibling folder c.run(f'{cmd} pkg -c tools.cmake.cmake_layout:build_folder=../mybuild') abs_build = os.path.join(c.current_folder, "mybuild") assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake")) assert not os.path.exists(os.path.join(c.current_folder, "build")) # Just in case we check that local build folder would be created if no arg is provided c.run('build pkg ') assert os.path.exists(os.path.join(c.current_folder, "pkg", "build")) def test_test_cmake_layout_build_folder_test_package(): """ relocate the test_package temporary build folders to elsewhere """ c = TestClient() abs_build = temp_folder() test_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self, generator="Ninja") def test(self): pass """) c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "test_package/conanfile.py": test_conanfile}) c.run(f'create . -c tools.cmake.cmake_layout:test_folder="{abs_build}" ' '-c tools.cmake.cmake_layout:build_folder_vars=[]') # Even if build_folder_vars=[] the "Release" folder is added always assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake")) assert not os.path.exists(os.path.join(c.current_folder, "test_package", "build")) c.run(f'create . -c tools.cmake.cmake_layout:build_folder_vars=[]') assert os.path.exists(os.path.join(c.current_folder, "test_package", "build")) def test_test_cmake_layout_build_folder_test_package_temp(): """ using always the same test_package build_folder will cause collisions. We need a mechanism to relocate, still provide unique folders for each build """ c = TestClient() test_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def test(self): pass """) profile = textwrap.dedent(""" include(default) [conf] tools.cmake.cmake_layout:test_folder=$TMP tools.cmake.cmake_layout:build_folder_vars=[] """) c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "test_package/conanfile.py": test_conanfile, "profile": profile}) c.run(f'create . -pr=profile') build_folder = re.search(r"Test package build folder: (\S+)", str(c.out)).group(1) assert os.path.exists(os.path.join(build_folder, "generators", "conan_toolchain.cmake")) assert not os.path.exists(os.path.join(c.current_folder, "test_package", "build")) c.run(f'test test_package pkg/0.1 -pr=profile') build_folder = re.search(r"Test package build folder: (\S+)", str(c.out)).group(1) assert os.path.exists(os.path.join(build_folder, "generators", "conan_toolchain.cmake")) assert not os.path.exists(os.path.join(c.current_folder, "test_package", "build")) c.run(f'create . -c tools.cmake.cmake_layout:build_folder_vars=[]') assert os.path.exists(os.path.join(c.current_folder, "test_package", "build")) def test_cmake_layout_build_folder_editable(): """ testing how it works with editables. Layout code pkga # editable add . pkgb # editable add . app # install -c build_folder="../../mybuild -c build_folder_vars="['self.name']" pkga-release-data.cmake pkga_PACKAGE_FOLDER_RELEASE = /abs/path/to/code/pkga pkga_INCLUDE_DIRS_RELEASE = ${pkga_PACKAGE_FOLDER_RELEASE}/include pkga_LIB_DIRS_RELEASE = /abs/path/to/mybuild/pkga/Release mybuild pkga/Release/ pkgb/Release/ """ c = TestClient() base_folder = temp_folder() c.current_folder = os.path.join(base_folder, "code").replace("\\", "/") project_build_folder = os.path.join(base_folder, "mybuild").replace("\\", "/") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): name = "{name}" version = "0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def layout(self): cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config """) c.save({"pkga/conanfile.py": conanfile.format(name="pkga"), "pkgb/conanfile.py": conanfile.format(name="pkgb"), "app/conanfile.py": GenConanfile().with_requires("pkga/0.1", "pkgb/0.1") .with_settings("build_type") .with_generator("CMakeDeps")}) c.run("editable add pkga") c.run("editable add pkgb") conf = f'-c tools.cmake.cmake_layout:build_folder="../../mybuild" ' \ '-c tools.cmake.cmake_layout:build_folder_vars="[\'self.name\']"' c.run(f'install app {conf} --build=editable') data = c.load("app/pkga-release-data.cmake") # The package is the ``code/pkga`` folder assert f'pkga_PACKAGE_FOLDER_RELEASE "{c.current_folder}/pkga"' in data # This is an absolute path, not relative, as it is not inside the package assert f'set(pkga_LIB_DIRS_RELEASE "{project_build_folder}/pkga/Release' in data data = c.load("app/pkgb-release-data.cmake") assert f'set(pkgb_LIB_DIRS_RELEASE "{project_build_folder}/pkgb/Release' in data assert os.path.exists(os.path.join(project_build_folder, "pkga", "Release", "generators", "conan_toolchain.cmake")) assert os.path.exists(os.path.join(project_build_folder, "pkgb", "Release", "generators", "conan_toolchain.cmake")) def test_cmake_layout_editable_output_folder(): """ testing how it works with editables, but --output-folder code pkga # editable add . --output-folder = ../mybuild pkgb # editable add . --output-folder = ../mybuild app # install -c build_folder_vars="['self.name']" pkga-release-data.cmake pkga_PACKAGE_FOLDER_RELEASE = /abs/path/to/mybuild pkga_INCLUDE_DIRS_RELEASE = /abs/path/to/code/pkga/include pkga_LIB_DIRS_RELEASE = pkga_PACKAGE_FOLDER_RELEASE/build/pkga/Release mybuild build pkga/Release/ pkgb/Release/ """ c = TestClient() base_folder = temp_folder() c.current_folder = os.path.join(base_folder, "code") project_build_folder = os.path.join(base_folder, "mybuild").replace("\\", "/") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): name = "{name}" version = "0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def layout(self): cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config """) c.save({"pkga/conanfile.py": conanfile.format(name="pkga"), "pkgb/conanfile.py": conanfile.format(name="pkgb"), "app/conanfile.py": GenConanfile().with_requires("pkga/0.1", "pkgb/0.1") .with_settings("build_type") .with_generator("CMakeDeps")}) c.run(f'editable add pkga --output-folder="../mybuild"') c.run(f'editable add pkgb --output-folder="../mybuild"') conf = f'-c tools.cmake.cmake_layout:build_folder_vars="[\'self.name\']"' c.run(f'install app {conf} --build=editable') data = c.load("app/pkga-release-data.cmake") assert f'set(pkga_PACKAGE_FOLDER_RELEASE "{project_build_folder}")' in data # Thse folders are relative to the package assert 'pkga_LIB_DIRS_RELEASE "${pkga_PACKAGE_FOLDER_RELEASE}/build/pkga/Release' in data data = c.load("app/pkgb-release-data.cmake") assert 'pkgb_LIB_DIRS_RELEASE "${pkgb_PACKAGE_FOLDER_RELEASE}/build/pkgb/Release' in data assert os.path.exists(os.path.join(project_build_folder, "build", "pkga", "Release", "generators", "conan_toolchain.cmake")) assert os.path.exists(os.path.join(project_build_folder, "build", "pkgb", "Release", "generators", "conan_toolchain.cmake")) ================================================ FILE: test/integration/layout/test_layout_generate.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_layout_generate(): """ Trying to leverage the layout knowledge at generate() time and put some files in the bindirs folders where executables will be built, like some dll from other dependencies. https://github.com/conan-io/conan/issues/12003 """ c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, load class Pkg(ConanFile): name = "pkg" version = "1.0" def layout(self): self.folders.build = "mybuild" self.folders.generators = "mybuild/generators" self.cpp.build.bindirs = ["bin"] self.cpp.build.libdirs = ["lib"] self.cpp.build.includedirs = ["include"] def generate(self): f = os.path.join(self.build_folder, self.cpp.build.bindir, "myfile.txt") save(self, f, "mybin!!!") f = os.path.join(self.build_folder, self.cpp.build.libdir, "myfile.txt") save(self, f, "mylib!!!") f = os.path.join(self.build_folder, self.cpp.build.includedir, "myfile.txt") save(self, f, "myinclude!!!") def build(self): self.output.info(load(self, os.path.join(self.cpp.build.libdir, "myfile.txt"))) self.output.info(load(self, os.path.join(self.cpp.build.bindir, "myfile.txt"))) self.output.info(load(self, os.path.join(self.cpp.build.includedir, "myfile.txt"))) """) c.save({"conanfile.py": conanfile}) c.run("create . ") assert "pkg/1.0: mybin!!!" in c.out assert "pkg/1.0: mylib!!!" in c.out assert "pkg/1.0: myinclude!!!" in c.out c.run("install .") assert os.path.exists(os.path.join(c.current_folder, "mybuild", "bin", "myfile.txt")) assert os.path.exists(os.path.join(c.current_folder, "mybuild", "lib", "myfile.txt")) assert os.path.exists(os.path.join(c.current_folder, "mybuild", "include", "myfile.txt")) def test_generate_source_folder(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def layout(self): cmake_layout(self) def generate(self): self.output.info("PKG_CONFIG_PATH {}!".format(os.path.exists(self.source_folder))) """) c.save({"conanfile.py": conanfile}) c.run("install .") assert "PKG_CONFIG_PATH True!" in c.out def test_generate_source_folder_test_package(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def generate(self): self.output.info("PKG_CONFIG_PATH {}!".format(os.path.exists(self.source_folder))) def test(self): pass """) c.save({"conanfile.py": GenConanfile("pkg", "1.0"), "test_package/conanfile.py": conanfile}) c.run("create .") assert "PKG_CONFIG_PATH True!" in c.out def test_generate_build_folder_test_package(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def generate(self): self.output.info(f"build_folder in test_package: {bool(self.build_folder)}") def test(self): pass """) c.save({"conanfile.py": GenConanfile("pkg", "1.0"), "test_package/conanfile.py": conanfile}) c.run("create .") assert f"build_folder in test_package: True" in c.out class TestCustomTestPackage: def test_custom_test_package(self): c = TestClient(light=True) conanfile = GenConanfile("pkg", "0.1").with_class_attribute('test_package_folder="mytest"') c.save({"conanfile.py": conanfile, "mytest/conanfile.py": GenConanfile().with_test("self.output.info('MYTEST!')"), "mytest2/conanfile.py": GenConanfile().with_test("self.output.info('MYTEST2!')")}) c.run("create .") assert "MYTEST!" in c.out c.run("create . -tf=mytest2") assert "MYTEST2!" in c.out def test_custom_test_package_subfolder(self): c = TestClient(light=True) conanfile = GenConanfile("pkg", "0.1").with_class_attribute('test_package_folder="my/test"') c.save({"pkg/conanfile.py": conanfile, "pkg/my/test/conanfile.py": GenConanfile().with_test("self.output.info('MYTEST!')")}) c.run("create pkg") assert "MYTEST!" in c.out def test_custom_test_package_sibling(self): c = TestClient(light=True) conanfile = GenConanfile("pkg", "0.1").with_class_attribute( 'test_package_folder="../my/test"') c.save({"pkg/conan/conanfile.py": conanfile, "pkg/my/test/conanfile.py": GenConanfile().with_test("self.output.info('MYTEST!')")}) c.run("create pkg/conan") assert "MYTEST!" in c.out def test_custom_test_package_export_pkg(self): c = TestClient(light=True) conanfile = GenConanfile("pkg", "0.1").with_class_attribute('test_package_folder="mytest"') c.save({"pkg/conanfile.py": conanfile, "pkg/mytest/conanfile.py": GenConanfile().with_test("self.output.info('MYTEST!')")}) c.run("export-pkg pkg") assert "MYTEST!" in c.out ================================================ FILE: test/integration/layout/test_layout_paths.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_editable_layout_paths(): # https://github.com/conan-io/conan/issues/12521 # https://github.com/conan-io/conan/issues/12839 c = TestClient() dep = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): self.cpp.source.includedirs = ["include"] """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type", "arch") .with_requires("dep/0.1") .with_generator("CMakeDeps") .with_generator("PkgConfigDeps") .with_generator("XcodeDeps")}) c.run("editable add dep") c.run("install pkg -s arch=x86_64") # It doesn't crash anymore assert "dep/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709 - Editable" in c.out data = c.load(f"pkg/dep-release-x86_64-data.cmake") assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/include")' in data pc = c.load("pkg/dep.pc") assert "includedir=${prefix}/include" in pc xcode = c.load("pkg/conan_dep_dep_release_x86_64.xcconfig") dep_path = os.path.join(c.current_folder, "dep") assert f"PACKAGE_ROOT_dep[config=Release][arch=x86_64][sdk=*] = {dep_path}" in xcode def test_layout_paths_normalized(): # make sure the paths doesn't end with trailing ".", and they are identical to the cwd c = TestClient() dep = textwrap.dedent(""" import os from conan import ConanFile class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): self.folders.source = "." self.folders.build = "." self.folders.generators = "." def build(self): assert os.getcwd() == self.source_folder assert os.getcwd() == self.build_folder assert os.getcwd() == self.generators_folder """) c.save({"conanfile.py": dep}) c.run("build .") # It doesn't assert in the build() ================================================ FILE: test/integration/layout/test_legacy_cpp_info_and_layout.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient # TODO: This test does not make sense for Conan v2. Please, remove/skip it in that case. @pytest.mark.parametrize("declare_layout", [True, False]) def test_legacy_deps_cpp_info_deps_version_using_or_not_layout(declare_layout): conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "1.0" {} def package_info(self): self.cpp_info.libs = ["hello"] """.format("def layout(self):pass" if declare_layout else "")) test_conanfile = textwrap.dedent(""" from conan import ConanFile class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def generate(self): self.output.info(self.dependencies["hello"].ref.version) def test(self): pass """) client = TestClient() client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) client.run("create .") assert "hello/1.0 (test package): 1.0" in client.out ================================================ FILE: test/integration/lockfile/__init__.py ================================================ ================================================ FILE: test/integration/lockfile/test_ci.py ================================================ import json import textwrap import pytest from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load, copy import os class Pkg(ConanFile): settings = "os" {requires} exports_sources = "myfile.txt" def generate(self): # Simulate an "imports" for dep in self.dependencies.values(): dest_folder = os.path.join(self.build_folder, dep.ref.name) copy(self, "myfile.txt", dep.package_folder, dest_folder) def package(self): # Copy the ones from the dependencies copied = copy(self, "*myfile.txt", self.build_folder, self.package_folder, keep_path=True) # Copy the exported one copied = copy(self, "myfile.txt", self.source_folder, self.package_folder) assert len(copied) == 1 def package_info(self): self.output.info("SELF OS: %s!!" % self.settings.os) self.output.info("SELF FILE: %s" % load(self, os.path.join(self.package_folder, "myfile.txt"))) for d in os.listdir(self.package_folder): p = os.path.join(self.package_folder, d, "myfile.txt") if os.path.isfile(p): self.output.info("DEP FILE %s: %s" % (d, load(self, p))) """) pkgawin_01_id = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" pkganix_01_id = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" pkgb_01_id = "65becc9bdccee92972f365ae4f742dd4b046d1e0" pkgb_012_id = "c753def4818cdc538183046f6149134eb4be7a32" pkgc_01_id = "2a23b96aea3b4787fcd1816182e8a403349b0815" pkgapp_01_id = "f8e4cc2232dff5983eeb2e7403b9c2dc755be44f" @pytest.fixture() def client_setup(): c = TestClient(light=True) pkb_requirements = """ def requirements(self): if self.settings.os == "Windows": self.requires("pkgawin/[>0.0 <1.0]") else: self.requires("pkganix/[>0.0 <1.0]") """ files = { "pkga/conanfile.py": conanfile.format(requires=""), "pkga/myfile.txt": "HelloA", "pkgj/conanfile.py": conanfile.format(requires=""), "pkgj/myfile.txt": "HelloJ", "pkgb/conanfile.py": conanfile.format(requires=pkb_requirements), "pkgb/myfile.txt": "HelloB", "pkgc/conanfile.py": conanfile.format(requires='requires="pkgb/[>0.0 <1.0]"'), "pkgc/myfile.txt": "HelloC", "app1/conanfile.py": conanfile.format(requires='requires="pkgc/[>0.0 <1.0]"'), "app1/myfile.txt": "App1", } c.save(files) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") c.run("create pkga --name=pkganix --version=0.1 -s os=Linux") c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows") c.run("create pkgc --name=pkgc --version=0.1 -s os=Windows") c.run("create app1 --name=app1 --version=0.1 -s os=Windows") assert "app1/0.1: SELF FILE: App1" in c.out assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: HelloB" in c.out assert "app1/0.1: DEP FILE pkgc: HelloC" in c.out return c def test_single_config_centralized(client_setup): """ app1 -> pkgc/0.1 -> pkgb/0.1 -> pkgawin/0.1 or pkganix/0.1 all version-ranges [>0 <1.0] lock app1.lock to lock graph including pkgawin/0.1 and pkganix/0.1 changes in pkgawin/0.2 and pkganix/0.2 are excluded by lockfile a change in pkgb produces a new pkgb/0.2 that we want to test if works in app1 lockfile the app1 can be built in a single node, including all necessary dependencies the final lockfile will include pkgb/0.2, and not pkgb/0.1 """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!", "pkgb/myfile.txt": "ByeB World!!", "pkgc/myfile.txt": "ByeC World!!"}) c.run("export pkga --name=pkgawin --version=0.2") # this will never be used c.run("export pkgc --name=pkgc --version=0.2") # this will never be used # Test that pkgb/0.2 works c.run("create pkgb --name=pkgb --version=0.2 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock") assert "pkgb/0.2: DEP FILE pkgawin: HelloA" in c.out # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--lockfile-out=app1_b_integrated.lock " "--build=missing -s os=Windows") c.assert_listed_binary({"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.2": (pkgb_01_id, "Cache"), "pkgc/0.1": (pkgc_01_id, "Build"), "app1/0.1": (pkgapp_01_id, "Build")}) assert "pkgb/0.2" in c.out assert "pkgb/0.1" not in c.out assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # All good! We can get rid of the now unused pkgb/0.1 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_b_integrated.lock " "--lockfile-out=app1_clean.lock -s os=Windows --lockfile-clean") app1_clean = c.load("app1_clean.lock") assert "pkgb/0.2" in app1_clean assert "pkgb/0.1" not in app1_clean def test_single_config_centralized_out_range(client_setup): """ same scenario as "test_single_config_centralized()" But pkgb/0.1 change version is bumped to pkgb/1.0, which doesn't fit in the consumers version range, so it is not used. Nothing to build in the app1, and the final lockfile doesn't change at all """ c = client_setup c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.2 -s os=Windows") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/1.0 works (but it is out of valid range!) c.run("create pkgb --name=pkgb --version=1.0 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock") assert "pkgb/1.0: DEP FILE pkgawin: HelloA" in c.out # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--lockfile-out=app1_b_integrated.lock " "--build=missing -s os=Windows") # Nothing changed, the change is outside the range, app1 not affected!! c.assert_listed_binary({"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.1": (pkgb_01_id, "Cache"), "pkgc/0.1": ("3b0c170cc929d4a1916489ce2dbb881fdad07f2e", "Cache"), "app1/0.1": ("3b0234ea72056ce9a0eb06584b4be6d73089e0e2", "Cache")}) assert "pkgb/0.2" not in c.out assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: HelloB" in c.out # All good! We can get rid of the now unused pkgb/1.0 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_b_integrated.lock " "--lockfile-out=app1_clean.lock -s os=Windows --lockfile-clean") app1_clean = c.load("app1_clean.lock") assert "pkgb/0.1" in app1_clean assert "pkgb/1.0" not in app1_clean def test_single_config_centralized_change_dep(client_setup): """ same scenario as "test_single_config_centralized()". But pkgb/0.1 change version is bumped to pkgb/0.2, and changes dependency from pkgA=>pkgJ """ c = client_setup c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.2 -s os=Windows") # Build new package alternative J c.run("create pkgj --name=pkgj --version=0.1 -s os=Windows") # Do a change in B, this is the change that we want to test c.save({"pkgb/conanfile.py": conanfile.format(requires='requires="pkgj/[>0.0 <1.0]"'), "pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.2 works c.run("create pkgb --name=pkgb --version=0.2 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock --lockfile-partial") assert "pkgb/0.2: DEP FILE pkgj: HelloJ" in c.out # Build new package alternative J, it won't be included, already locked in this create c.run("create pkgj --name=pkgj --version=0.2 -s os=Windows") # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--lockfile-out=app1_b_integrated.lock " "--build=missing -s os=Windows") assert "pkga/" not in c.out c.assert_listed_binary({"pkgj/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.2": ("79caa65bc5877c4ada84a2b454775f47a5045d59", "Cache"), "pkgc/0.1": ("67e4e9b17f41a4c71ff449eb29eb716b8f83767b", "Build"), "app1/0.1": ("f7eb3b81ac34ddecd04301afad031ee078c5ab3c", "Build")}) assert "app1/0.1: DEP FILE pkgj: HelloJ" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # All good! We can get rid of the now unused pkgb/1.0 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_b_integrated.lock " "--lockfile-out=app1_clean.lock -s os=Windows --lockfile-clean") app1_clean = c.load("app1_clean.lock") assert "pkgj/0.1" in app1_clean assert "pkgb/0.2" in app1_clean assert "pkgb/0.1" not in app1_clean def test_multi_config_centralized(client_setup): """ same scenario as above, but now we want to manage 2 configurations Windows & Linux When pkgB is changed, it is built for both, and produces app1_win.lock and app2_linux.lock With those, app1 can be built in a single node for both configurations. After building app1, the 2 lockfiles can be cleaned (removing the old pkgb/0.1, leaving pkgb/0.2 in the lock) The 2 final lockfiles can be "merged" in a single one for next iteration """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") c.run("lock create --requires=app1/0.1@ --lockfile=app1.lock --lockfile-out=app1.lock " "-s os=Linux") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.2 -s os=Windows") c.run("create pkga --name=pkganix --version=0.2 -s os=Linux") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.2 works c.run("create pkgb --name=pkgb --version=0.2 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_win.lock") assert "pkgb/0.2: DEP FILE pkgawin: HelloA" in c.out c.run("create pkgb --name=pkgb --version=0.2 -s os=Linux " "--lockfile=app1.lock --lockfile-out=app1_nix.lock") assert "pkgb/0.2: DEP FILE pkganix: HelloA" in c.out # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_win.lock --lockfile-out=app1_win.lock " "--build=missing -s os=Windows") c.assert_listed_binary({"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.2": (pkgb_01_id, "Cache"), "pkgc/0.1": (pkgc_01_id, "Build"), "app1/0.1": (pkgapp_01_id, "Build")}) assert "pkgb/0.2" in c.out assert "pkgb/0.1" not in c.out assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_nix.lock --lockfile-out=app1_nix.lock " "--build=missing -s os=Linux") c.assert_listed_binary({"pkganix/0.1": (pkganix_01_id, "Cache"), "pkgb/0.2": (pkgb_012_id, "Cache"), "pkgc/0.1": ("2a4113733176ffbce61bbcb4dd0e76ecc162439c", "Build"), "app1/0.1": ("6c198f82674d988b90eed54b52a494a5bbf09c41", "Build")}) assert "pkgb/0.2" in c.out assert "pkgb/0.1" not in c.out assert "app1/0.1: DEP FILE pkganix: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # All good! We can get rid of the now unused pkgb/0.1 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_win.lock " "--lockfile-out=app1_win.lock -s os=Windows --lockfile-clean") app1_clean = c.load("app1_win.lock") assert "pkgawin/0.1" in app1_clean assert "pkgb/0.2" in app1_clean assert "pkgb/0.1" not in app1_clean assert "pkgawin/0.2" not in app1_clean assert "pkganix/0.2" not in app1_clean c.run("lock create --requires=app1/0.1@ --lockfile=app1_nix.lock " "--lockfile-out=app1_nix.lock -s os=Linux --lockfile-clean") app1_clean = c.load("app1_nix.lock") assert "pkganix/0.1" in app1_clean assert "pkgb/0.2" in app1_clean assert "pkgb/0.1" not in app1_clean assert "pkgawin/0.2" not in app1_clean assert "pkganix/0.2" not in app1_clean # Finally, merge the 2 clean lockfiles, for keeping just 1 for next iteration c.run("lock merge --lockfile=app1_win.lock --lockfile=app1_nix.lock " "--lockfile-out=app1_final.lock") app1_clean = c.load("app1_final.lock") assert "pkgawin/0.1" in app1_clean assert "pkganix/0.1" in app1_clean assert "pkgb/0.2" in app1_clean assert "pkgb/0.1" not in app1_clean assert "pkgawin/0.2" not in app1_clean assert "pkganix/0.2" not in app1_clean def test_single_config_decentralized(client_setup): """ same scenario as "test_single_config_centralized()", but distributing the build in different build servers, using the "build-order" """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.2 -s os=Windows") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.2 works c.run("create pkgb --name=pkgb --version=0.2 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock") assert "pkgb/0.2: DEP FILE pkgawin: HelloA" in c.out # Now lets build the application, to see everything ok c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--build=missing --format=json -s os=Windows", redirect_stdout="build_order.json") json_file = c.load("build_order.json") to_build = json.loads(json_file) level0 = to_build[0] assert len(level0) == 1 pkgawin = level0[0] assert pkgawin["ref"] == "pkgawin/0.1#2f297d19d9ee4827caf97071de449a54" assert pkgawin["packages"][0][0]["binary"] == "Cache" level1 = to_build[1] assert len(level1) == 1 pkgb = level1[0] assert pkgb["ref"] == "pkgb/0.2#476b31358d78b3f04c68c4770bd6a79c" assert pkgb["packages"][0][0]["binary"] == "Cache" for level in to_build: for elem in level: ref = RecipeReference.loads(elem["ref"]) for package in elem["packages"][0]: # assumes no dependencies between packages binary = package["binary"] package_id = package["package_id"] if binary != "Build": continue build_args = package["build_args"] c.run(f"install {build_args} --lockfile=app1_b_changed.lock -s os=Windows") c.assert_listed_binary( {str(ref): (package_id, "Build"), "pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.2": (pkgb_01_id, "Cache")}) assert "pkgb/0.2" in c.out assert "pkgb/0.1" not in c.out assert "DEP FILE pkgawin: HelloA" in c.out assert "DEP FILE pkgb: ByeB World!!" in c.out # Just to make sure that the for-loops have been executed assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out def test_multi_config_decentralized(client_setup): """ same scenario as "test_multi_config_centralized()", but distributing the build in different build servers, using the "build-order" """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") c.run("lock create --requires=app1/0.1@ --lockfile=app1.lock --lockfile-out=app1.lock " "-s os=Linux") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.2 -s os=Windows") c.run("create pkga --name=pkganix --version=0.2 -s os=Linux") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.2 works c.run("create pkgb --name=pkgb --version=0.2 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_win.lock") assert "pkgb/0.2: DEP FILE pkgawin: HelloA" in c.out c.run("create pkgb --name=pkgb --version=0.2 -s os=Linux " "--lockfile=app1.lock --lockfile-out=app1_nix.lock") assert "pkgb/0.2: DEP FILE pkganix: HelloA" in c.out # Now lets build the application, to see everything ok, for all the configs c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_win.lock " "--build=missing --format=json -s os=Windows", redirect_stdout="app1_win.json") c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_nix.lock " "--build=missing --format=json -s os=Linux", redirect_stdout="app1_nix.json") c.run("graph build-order-merge --file=app1_win.json --file=app1_nix.json" " --format=json", redirect_stdout="build_order.json") json_file = c.load("build_order.json") to_build = json.loads(json_file) level0 = to_build[0] assert len(level0) == 2 pkgawin = level0[0] assert pkgawin["ref"] == "pkgawin/0.1#2f297d19d9ee4827caf97071de449a54" assert pkgawin["packages"][0][0]["binary"] == "Cache" pkgawin = level0[1] assert pkgawin["ref"] == "pkganix/0.1#2f297d19d9ee4827caf97071de449a54" assert pkgawin["packages"][0][0]["binary"] == "Cache" level1 = to_build[1] assert len(level1) == 1 pkgb = level1[0] assert pkgb["ref"] == "pkgb/0.2#476b31358d78b3f04c68c4770bd6a79c" assert pkgb["packages"][0][0]["binary"] == "Cache" for level in to_build: for elem in level: ref = elem["ref"] ref_without_rev = ref.split("#")[0] for package in elem["packages"][0]: # Assumes no dependencies between packages binary = package["binary"] package_id = package["package_id"] if binary != "Build": continue # TODO: The options are completely missing filenames = package["filenames"] lockfile = filenames[0] + ".lock" the_os = "Windows" if "win" in lockfile else "Linux" c.run("install --requires=%s --build=%s --lockfile=%s -s os=%s" % (ref, ref, lockfile, the_os)) c.assert_listed_binary({ref_without_rev: (package_id, "Build")}) if the_os == "Windows": c.assert_listed_binary( {"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.2": (pkgb_01_id, "Cache")}) assert "pkgb/0.2" in c.out assert "pkgb/0.1" not in c.out assert "DEP FILE pkgawin: HelloA" in c.out assert "DEP FILE pkgb: ByeB World!!" in c.out else: c.assert_listed_binary( {"pkganix/0.1": (pkganix_01_id, "Cache"), "pkgb/0.2": (pkgb_012_id, "Cache")}) assert "pkgb/0.2" in c.out assert "pkgb/0.1" not in c.out assert "DEP FILE pkganix: HelloA" in c.out assert "DEP FILE pkgb: ByeB World!!" in c.out # Just to make sure that the for-loops have been executed assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out def test_single_config_decentralized_overrides(): r""" same scenario as "test_single_config_centralized()", but distributing the build in different build servers, using the "build-order" Now with overrides pkga -> toola/1.0 -> toolb/1.0 -> toolc/1.0 \------override-----> toolc/2.0 pkgb -> toola/2.0 -> toolb/1.0 -> toolc/1.0 \------override-----> toolc/3.0 pkgc -> toola/3.0 -> toolb/1.0 -> toolc/1.0 """ c = TestClient() c.save({"toolc/conanfile.py": GenConanfile("toolc"), "toolb/conanfile.py": GenConanfile("toolb").with_requires("toolc/1.0"), "toola/conanfile.py": GenConanfile("toola", "1.0").with_requirement("toolb/1.0") .with_requirement("toolc/2.0", override=True), "toola2/conanfile.py": GenConanfile("toola", "2.0").with_requirement("toolb/1.0") .with_requirement("toolc/3.0", override=True), "toola3/conanfile.py": GenConanfile("toola", "3.0").with_requirement("toolb/1.0"), "pkga/conanfile.py": GenConanfile("pkga", "1.0").with_tool_requires("toola/1.0"), "pkgb/conanfile.py": GenConanfile("pkgb", "1.0").with_requires("pkga/1.0") .with_tool_requires("toola/2.0"), "pkgc/conanfile.py": GenConanfile("pkgc", "1.0").with_requires("pkgb/1.0") .with_tool_requires("toola/3.0"), }) c.run("export toolc --version=1.0") c.run("export toolc --version=2.0") c.run("export toolc --version=3.0") c.run("export toolb --version=1.0") c.run("export toola") c.run("export toola2") c.run("export toola3") c.run("export pkga") c.run("export pkgb") c.run("lock create pkgc") lock = json.loads(c.load("pkgc/conan.lock")) requires = "\n".join(lock["build_requires"]) assert "toolc/3.0" in requires assert "toolc/2.0" in requires assert "toolc/1.0" in requires assert len(lock["overrides"]) == 1 assert set(lock["overrides"]["toolc/1.0"]) == {"toolc/3.0", "toolc/2.0", None} c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing") to_build = json.loads(c.stdout) for level in to_build: for elem in level: for package in elem["packages"][0]: # assumes no dependencies between packages binary = package["binary"] assert binary == "Build" # All nodes in this case have to be built build_args = package["build_args"] c.run(f"install {build_args} --lockfile=pkgc/conan.lock") c.run("install pkgc --lockfile=pkgc/conan.lock") # All works, all binaries exist now assert "pkga/1.0: Already installed!" in c.out assert "pkgb/1.0: Already installed!" in c.out ================================================ FILE: test/integration/lockfile/test_ci_overrides.py ================================================ import json import pytest from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_graph_build_order_override_error(): """ libc -> libb -> liba -> zlib/1.2 |--------------/ |-----override------> zlib/1.3 """ c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib"), "liba/conanfile.py": GenConanfile("liba", "0.1").with_requires("zlib/1.0"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requires("liba/0.1", "zlib/2.0"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requirement("libb/0.1") .with_requirement("zlib/3.0", override=True) }) c.run("export zlib --version=2.0") c.run("export zlib --version=3.0") c.run("export liba") c.run("export libb") c.run("export libc") c.run("graph info --requires=libc/0.1 --lockfile-out=output.lock") c.run("graph build-order --requires=libc/0.1 --lockfile=output.lock --order-by=configuration " "--build=missing --format=json") to_build = json.loads(c.stdout) for level in to_build["order"]: for package in level: binary = package["binary"] assert binary == "Build" # All nodes in this case have to be built build_args = package["build_args"] c.run(f"install {build_args} --lockfile=output.lock") ref = RecipeReference.loads(package["ref"]) assert f"{ref}: Building from source" c.run("install --requires=libc/0.1 --lockfile=output.lock") # All works, all binaries exist now assert "zlib/3.0: Already installed!" in c.out assert "liba/0.1: Already installed!" in c.out assert "libb/0.1: Already installed!" in c.out assert "libc/0.1: Already installed!" in c.out @pytest.mark.parametrize("replace_pattern", ["*", "3.0"]) def test_graph_build_order_override_replace_requires(replace_pattern): """ libc -> libb -> liba -> zlib/1.2 |--------------/ |-----override------> zlib/1.3 replace_requires zlib -> zlib/system """ c = TestClient(light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib"), "liba/conanfile.py": GenConanfile("liba", "0.1").with_requires("zlib/1.0"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requires("liba/0.1", "zlib/2.0"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requirement("libb/0.1") .with_requirement("zlib/3.0", override=True), "profile": f"[replace_requires]\nzlib/{replace_pattern}: zlib/system" }) c.run("export zlib --version=2.0") c.run("export zlib --version=3.0") c.run("export zlib --version=system") c.run("export liba") c.run("export libb") c.run("export libc") c.run("lock create --requires=libc/0.1 --lockfile-out=output.lock -pr=profile") c.run("graph build-order --requires=libc/0.1 --lockfile=output.lock --order-by=configuration " "--build=missing -pr=profile --format=json") to_build = json.loads(c.stdout) for level in to_build["order"]: for package in level: binary = package["binary"] assert binary == "Build" # All nodes in this case have to be built build_args = package["build_args"] c.run(f"install {build_args} --lockfile=output.lock -pr=profile") ref = RecipeReference.loads(package["ref"]) assert f"{ref}: Building from source" c.run("install --requires=libc/0.1 --lockfile=output.lock -pr=profile") # All works, all binaries exist now assert "zlib/system: Already installed!" in c.out assert "liba/0.1: Already installed!" in c.out assert "libb/0.1: Already installed!" in c.out assert "libc/0.1: Already installed!" in c.out def test_single_config_decentralized_overrides(): r""" same scenario as "test_single_config_centralized()", but distributing the build in different build servers, using the "build-order" Now with overrides pkga -> toola/1.0 -> toolb/1.0 -> toolc/1.0 \------override-----> toolc/2.0 pkgb -> toola/2.0 -> toolb/1.0 -> toolc/1.0 \------override-----> toolc/3.0 pkgc -> toola/3.0 -> toolb/1.0 -> toolc/1.0 """ c = TestClient(light=True) c.save({"toolc/conanfile.py": GenConanfile("toolc"), "toolb/conanfile.py": GenConanfile("toolb").with_requires("toolc/1.0"), "toola/conanfile.py": GenConanfile("toola", "1.0").with_requirement("toolb/1.0") .with_requirement("toolc/2.0", override=True), "toola2/conanfile.py": GenConanfile("toola", "2.0").with_requirement("toolb/1.0") .with_requirement("toolc/3.0", override=True), "toola3/conanfile.py": GenConanfile("toola", "3.0").with_requirement("toolb/1.0"), "pkga/conanfile.py": GenConanfile("pkga", "1.0").with_tool_requires("toola/1.0"), "pkgb/conanfile.py": GenConanfile("pkgb", "1.0").with_requires("pkga/1.0") .with_tool_requires("toola/2.0"), "pkgc/conanfile.py": GenConanfile("pkgc", "1.0").with_requires("pkgb/1.0") .with_tool_requires("toola/3.0"), }) c.run("export toolc --version=1.0") c.run("export toolc --version=2.0") c.run("export toolc --version=3.0") c.run("export toolb --version=1.0") c.run("export toola") c.run("export toola2") c.run("export toola3") c.run("export pkga") c.run("export pkgb") c.run("lock create pkgc") lock = json.loads(c.load("pkgc/conan.lock")) requires = "\n".join(lock["build_requires"]) assert "toolc/3.0" in requires assert "toolc/2.0" in requires assert "toolc/1.0" in requires assert len(lock["overrides"]) == 1 assert set(lock["overrides"]["toolc/1.0"]) == {"toolc/3.0", "toolc/2.0", None} c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing") to_build = json.loads(c.stdout) for level in to_build: for elem in level: for package in elem["packages"][0]: # assumes no dependencies between packages binary = package["binary"] assert binary == "Build" # All nodes in this case have to be built build_args = package["build_args"] c.run(f"install {build_args} --lockfile=pkgc/conan.lock") c.run("install pkgc --lockfile=pkgc/conan.lock") # All works, all binaries exist now assert "pkga/1.0: Already installed!" in c.out assert "pkgb/1.0: Already installed!" in c.out def test_single_config_decentralized_overrides_nested(): r""" same scenario as "test_single_config_centralized()", but distributing the build in different build servers, using the "build-order" Now with overrides pkga -> toola/1.0 -> libb/1.0 -> libc/1.0 -> libd/1.0 -> libe/1.0 -> libf/1.0 \ \-----------override---------> libf/2.0 \--------------------override--------------------------> libf/3.0 """ c = TestClient(light=True) c.save({"libf/conanfile.py": GenConanfile("libf"), "libe/conanfile.py": GenConanfile("libe", "1.0").with_requires("libf/1.0"), "libd/conanfile.py": GenConanfile("libd", "1.0").with_requires("libe/1.0"), "libc/conanfile.py": GenConanfile("libc", "1.0").with_requirement("libd/1.0") .with_requirement("libf/2.0", override=True), "libb/conanfile.py": GenConanfile("libb", "1.0").with_requires("libc/1.0"), "toola/conanfile.py": GenConanfile("toola", "1.0").with_requirement("libb/1.0") .with_requirement("libf/3.0", override=True), "pkga/conanfile.py": GenConanfile("pkga", "1.0").with_tool_requires("toola/1.0"), }) c.run("export libf --version=3.0") c.run("export libe") c.run("export libd") c.run("export libc") c.run("export libb") c.run("export toola") c.run("lock create pkga") lock = json.loads(c.load("pkga/conan.lock")) assert lock["overrides"] == {"libf/1.0": ["libf/3.0"], "libf/2.0": ["libf/3.0"]} c.run("graph build-order pkga --lockfile=pkga/conan.lock --format=json --build=missing") to_build = json.loads(c.stdout) for level in to_build: for elem in level: ref = elem["ref"] if "libc" in ref: pass for package in elem["packages"][0]: # assumes no dependencies between packages binary = package["binary"] assert binary == "Build" # All nodes in this case have to be built build_args = package["build_args"] c.run(f"install {build_args} --lockfile=pkga/conan.lock") c.run("install pkga --lockfile=pkga/conan.lock") # All works, all binaries exist now assert "Install finished successfully" in c.out @pytest.mark.parametrize("forced", [False, True]) def test_single_config_decentralized_overrides_multi(forced): r""" same scenario as "test_single_config_centralized()", but distributing the build in different build servers, using the "build-order" Now with overrides pkga -> toola/1.0 -> libb/1.0 -> libc/1.0 -> libd/1.0 -> libe/1.0 -> libf/1.0 | \ \-----------override--------> libf/2.0 | \--------------------override-------------------------> libf/3.0 pkgb -> toola/1.1 -> libb/1.0 -> libc/1.0 -> libd/1.0 -> libe/1.0 -> libf/1.0 | \ \-----------override--------> libf/2.0 | \--------------------override-------------------------> libf/4.0 pkgc -> toola/1.2 -> libb/1.0 -> libc/1.0 -> libd/1.0 -> libe/1.0 -> libf/1.0 \-----------override--------> libf/2.0 """ override, force = (True, False) if not forced else (False, True) c = TestClient(light=True) c.save({"libf/conanfile.py": GenConanfile("libf"), "libe/conanfile.py": GenConanfile("libe", "1.0").with_requires("libf/1.0"), "libd/conanfile.py": GenConanfile("libd", "1.0").with_requires("libe/1.0"), "libc/conanfile.py": GenConanfile("libc", "1.0").with_requirement("libd/1.0") .with_requirement("libf/2.0", override=override, force=force), "libb/conanfile.py": GenConanfile("libb", "1.0").with_requires("libc/1.0"), "toola/conanfile.py": GenConanfile("toola", "1.0").with_requirement("libb/1.0") .with_requirement("libf/3.0", override=override, force=force), "toola1/conanfile.py": GenConanfile("toola", "1.1").with_requirement("libb/1.0") .with_requirement("libf/4.0", override=override, force=force), "toola2/conanfile.py": GenConanfile("toola", "1.2").with_requirement("libb/1.0"), "pkga/conanfile.py": GenConanfile("pkga", "1.0").with_tool_requires("toola/1.0"), "pkgb/conanfile.py": GenConanfile("pkgb", "1.0").with_requires("pkga/1.0") .with_tool_requires("toola/1.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "1.0").with_requires("pkgb/1.0") .with_tool_requires("toola/1.2"), }) c.run("export libf --version=2.0") c.run("export libf --version=3.0") c.run("export libf --version=4.0") c.run("export libe") c.run("export libd") c.run("export libc") c.run("export libb") c.run("export toola") c.run("export toola1") c.run("export toola2") c.run("export pkga") c.run("export pkgb") c.run("lock create pkgc") lock = json.loads(c.load("pkgc/conan.lock")) assert len(lock["overrides"]) == 2 assert set(lock["overrides"]["libf/1.0"]) == {"libf/4.0", "libf/2.0", "libf/3.0"} if forced: # When forced, there is one libf/2.0 that is not overriden assert set(lock["overrides"]["libf/2.0"]) == {"libf/4.0", "libf/3.0", None} else: assert set(lock["overrides"]["libf/2.0"]) == {"libf/4.0", "libf/3.0"} c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing") to_build = json.loads(c.stdout) for level in to_build: for elem in level: ref = elem["ref"] if "libc" in ref: pass for package in elem["packages"][0]: # assumes no dependencies between packages binary = package["binary"] assert binary == "Build" # All nodes in this case have to be built build_args = package["build_args"] c.run(f"install {build_args} --lockfile=pkgc/conan.lock") c.run("install pkgc --lockfile=pkgc/conan.lock") # All works, all binaries exist now assert "pkga/1.0: Already installed!" in c.out assert "pkgb/1.0: Already installed!" in c.out @pytest.mark.parametrize("replace_pattern", ["*", "1.0", "2.0", "3.0", "4.0"]) @pytest.mark.parametrize("forced", [False, True]) def test_single_config_decentralized_overrides_multi_replace_requires(replace_pattern, forced): r""" same scenario as "test_single_config_centralized()", but distributing the build in different build servers, using the "build-order" Now with overrides pkga -> toola/1.0 -> libb/1.0 -> libc/1.0 -> libd/1.0 -> libe/1.0 -> libf/1.0 | \ \-----------override--------> libf/2.0 | \--------------------override-------------------------> libf/3.0 pkgb -> toola/1.1 -> libb/1.0 -> libc/1.0 -> libd/1.0 -> libe/1.0 -> libf/1.0 | \ \-----------override--------> libf/2.0 | \--------------------override-------------------------> libf/4.0 pkgc -> toola/1.2 -> libb/1.0 -> libc/1.0 -> libd/1.0 -> libe/1.0 -> libf/1.0 \-----------override--------> libf/2.0 """ override, force = (True, False) if not forced else (False, True) c = TestClient(light=True) c.save({"libf/conanfile.py": GenConanfile("libf"), "libe/conanfile.py": GenConanfile("libe", "1.0").with_requires("libf/1.0"), "libd/conanfile.py": GenConanfile("libd", "1.0").with_requires("libe/1.0"), "libc/conanfile.py": GenConanfile("libc", "1.0").with_requirement("libd/1.0") .with_requirement("libf/2.0", override=override, force=force), "libb/conanfile.py": GenConanfile("libb", "1.0").with_requires("libc/1.0"), "toola/conanfile.py": GenConanfile("toola", "1.0").with_requirement("libb/1.0") .with_requirement("libf/3.0", override=override, force=force), "toola1/conanfile.py": GenConanfile("toola", "1.1").with_requirement("libb/1.0") .with_requirement("libf/4.0", override=override, force=force), "toola2/conanfile.py": GenConanfile("toola", "1.2").with_requirement("libb/1.0"), "pkga/conanfile.py": GenConanfile("pkga", "1.0").with_tool_requires("toola/1.0"), "pkgb/conanfile.py": GenConanfile("pkgb", "1.0").with_requires("pkga/1.0") .with_tool_requires("toola/1.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "1.0").with_requires("pkgb/1.0") .with_tool_requires("toola/1.2"), "profile": f"include(default)\n[replace_requires]\nlibf/{replace_pattern}: libf/system" }) c.run("export libf --version=2.0") c.run("export libf --version=3.0") c.run("export libf --version=4.0") c.run("export libf --version=system") c.run("export libe") c.run("export libd") c.run("export libc") c.run("export libb") c.run("export toola") c.run("export toola1") c.run("export toola2") c.run("export pkga") c.run("export pkgb") c.run("lock create pkgc -pr:b=profile") # overrides will be different everytime, just checking that things can be built c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json -pr:b=profile " "--build=missing") to_build = json.loads(c.stdout) for level in to_build: for elem in level: ref = elem["ref"] if "libc" in ref: pass for package in elem["packages"][0]: # assumes no dependencies between packages binary = package["binary"] assert binary == "Build" # All nodes in this case have to be built build_args = package["build_args"] c.run(f"install {build_args} --lockfile=pkgc/conan.lock -pr:b=profile") c.run("install pkgc --lockfile=pkgc/conan.lock -pr:b=profile") # All works, all binaries exist now assert "pkga/1.0: Already installed!" in c.out assert "pkgb/1.0: Already installed!" in c.out if replace_pattern == "1.0": # These will overriden by downstream assert "libf/system#7fb6d926dabeb955bcea1cafedf953c8 - Cache" not in c.out else: assert "libf/system#7fb6d926dabeb955bcea1cafedf953c8 - Cache" in c.out ================================================ FILE: test/integration/lockfile/test_ci_revisions.py ================================================ import json import textwrap import pytest from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load, copy import os class Pkg(ConanFile): settings = "os" {requires} exports_sources = "myfile.txt" def generate(self): # Simulate an "imports" for dep in self.dependencies.values(): dest_folder = os.path.join(self.build_folder, dep.ref.name) copy(self, "myfile.txt", dep.package_folder, dest_folder) def package(self): # Copy the ones from the dependencies copied = copy(self, "*myfile.txt", self.build_folder, self.package_folder, keep_path=True) # Copy the exported one copied = copy(self, "myfile.txt", self.source_folder, self.package_folder) assert len(copied) == 1 def package_info(self): self.output.info("SELF OS: %s!!" % self.settings.os) self.output.info("SELF FILE: %s" % load(self, os.path.join(self.package_folder, "myfile.txt"))) for d in os.listdir(self.package_folder): p = os.path.join(self.package_folder, d, "myfile.txt") if os.path.isfile(p): self.output.info("DEP FILE %s: %s" % (d, load(self, p))) """) pkgawin_01_id = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" pkganix_01_id = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" pkgb_01_id = "cee4c64978063c49773213b9c8f6b631c612a00b" pkgb_012_id = "6b6343eeb8ffeb498b809bca9853fceb7dd0c078" pkgc_01_id = "1bc0312cddbd3c076e666b84af9cc5ac3d263719" pkgapp_01_id = "dda63a9bddbbe704d4858d67156d5dad0361dc19" @pytest.fixture() def client_setup(): c = TestClient(light=True) c.save_home({"global.conf": "core.package_id:default_unknown_mode=recipe_revision_mode"}) pkb_requirements = """ def requirements(self): if self.settings.os == "Windows": self.requires("pkgawin/0.1") else: self.requires("pkganix/0.1") """ files = { "pkga/conanfile.py": conanfile.format(requires=""), "pkga/myfile.txt": "HelloA", "pkgj/conanfile.py": conanfile.format(requires=""), "pkgj/myfile.txt": "HelloJ", "pkgb/conanfile.py": conanfile.format(requires=pkb_requirements), "pkgb/myfile.txt": "HelloB", "pkgc/conanfile.py": conanfile.format(requires='requires="pkgb/0.1"'), "pkgc/myfile.txt": "HelloC", "app1/conanfile.py": conanfile.format(requires='requires="pkgc/0.1"'), "app1/myfile.txt": "App1", } c.save(files) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") c.run("create pkga --name=pkganix --version=0.1 -s os=Linux") c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows") c.run("create pkgc --name=pkgc --version=0.1 -s os=Windows") c.run("create app1 --name=app1 --version=0.1 -s os=Windows") assert "app1/0.1: SELF FILE: App1" in c.out assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: HelloB" in c.out assert "app1/0.1: DEP FILE pkgc: HelloC" in c.out return c def test_single_config_centralized(client_setup): """ app1 -> pkgc/0.1 -> pkgb/0.1 -> pkgawin/0.1 or pkganix/0.1 all versions are "0.1" exact (without pinning revision) lock app1.lock to lock graph including pkgawin/0.1#rev1 and pkganix/0.1#rev1 changes in pkgawin/0.1#rev2 and pkganix/0.1#rev2 are excluded by lockfile a change in pkgb produces a new pkgb/0.1#rev2 that we want to test if works in app1 lockfile the app1 can be built in a single node, including all necessary dependencies the final lockfile will include pkgb/0.1#rev2, and not pkgb/0.1#rev1 """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") app1_lock = c.load("app1.lock") assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" in app1_lock # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.1 new revision works c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock") assert "pkgb/0.1: DEP FILE pkgawin: HelloA" in c.out # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--lockfile-out=app1_b_integrated.lock " "--build=missing -s os=Windows") c.assert_listed_binary({"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.1": (pkgb_01_id, "Cache"), "pkgc/0.1": (pkgc_01_id, "Build"), "app1/0.1": (pkgapp_01_id, "Build")}) assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # All good! We can get rid of the now unused pkgb/0.1 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_b_integrated.lock " "--lockfile-out=app1_clean.lock -s os=Windows --lockfile-clean") app1_clean = c.load("app1_clean.lock") assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" not in app1_clean assert "pkgb/0.1#bb12977c3353d7633b34d55a926fe58c" in app1_clean def test_single_config_centralized_out_range(client_setup): """ same scenario as "test_single_config_centralized()" but pkgc pin the exact revision of pkgb/0.1#rev1 But pkgb/0.1 change produces pkgb/0.1#rev2, which doesn't match the pinned revisions rev1 Nothing to build in the app1, and the final lockfile doesn't change at all """ # Out of range in revisions means a pinned revision, new revision will not match c = client_setup c.save({"pkgc/conanfile.py": conanfile.format(requires='requires="pkgb/0.1#d03e920d532beeeb198cd886095bcca1"')}) c.run("create pkgc --name=pkgc --version=0.1 -s os=Windows") c.run("create app1 --name=app1 --version=0.1 -s os=Windows --build=missing") c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") app1_lock = c.load("app1.lock") assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" in app1_lock # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/1.0 works (but it is not matching the pinned revision) c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock") assert "pkgb/0.1: DEP FILE pkgawin: HelloA" in c.out app1_b_changed = c.load("app1_b_changed.lock") assert "pkgb/0.1#bb12977c3353d7633b34d55a926fe58c" in app1_b_changed assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" in app1_b_changed # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--lockfile-out=app1_b_integrated.lock " "--build=missing -s os=Windows") # Nothing changed, the change is outside the range, app1 not affected!! c.assert_listed_binary({"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.1": (pkgb_01_id, "Cache"), "pkgc/0.1": ("7dc09fd93c15a010373b013c3a44fc94fc9d3226", "Cache"), "app1/0.1": ("96324a4bf6d4bba4f697919a435eca6d746c2d18", "Cache")}) assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: HelloB" in c.out # All good! We can get rid of the now unused pkgb/1.0 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_b_integrated.lock " "--lockfile-out=app1_clean.lock -s os=Windows --lockfile-clean") app1_clean = c.load("app1_clean.lock") assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" in app1_clean assert "pkgb/0.1#504bc7152c72b49c99a6f16733fb2ff6" not in app1_clean def test_single_config_centralized_change_dep(client_setup): """ same scenario as "test_single_config_centralized()". But pkgb/0.1 change producing pkgb/0.1#rev2, and changes dependency from pkgA=>pkgJ """ c = client_setup c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") # Build new package alternative J c.run("create pkgj --name=pkgj --version=0.1 -s os=Windows") # Do a change in B, this is the change that we want to test, remove pkgA, replace with pkgJ c.save({"pkgb/conanfile.py": conanfile.format(requires='requires="pkgj/0.1"'), "pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.1 new revision works c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows --lockfile-partial " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock") assert "pkgb/0.1: DEP FILE pkgj: HelloJ" in c.out # Build new package alternative J, it won't be included, already locked in this create c.run("create pkgj --name=pkgj --version=0.1 -s os=Windows") # Now lets build the application, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--lockfile-out=app1_b_integrated.lock " "--build=missing -s os=Windows") assert "pkga/" not in c.out c.assert_listed_binary({"pkgj/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.1": ("6142fb85ccd4e94afad85a8d01a87234eefa5600", "Cache"), "pkgc/0.1": ("93cfcbc8109eedf4211558258ff5a844fdb62cca", "Build"), "app1/0.1": ("eb241e40d370e1e1b0fd516aff6ffff72de1e37d", "Build")}) assert "app1/0.1: DEP FILE pkgj: HelloJ" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # All good! We can get rid of the now unused pkgb/1.0 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_b_integrated.lock " "--lockfile-out=app1_clean.lock -s os=Windows --lockfile-clean") app1_clean = c.load("app1_clean.lock") assert "pkgj/0.1" in app1_clean assert "pkgb/0.1" in app1_clean assert "pkga" not in app1_clean def test_multi_config_centralized(client_setup): """ same scenario as above, but now we want to manage 2 configurations Windows & Linux When pkgB is changed, it is built for both, and produces app1_win.lock and app2_linux.lock With those, app1 can be built in a single node for both configurations. After building app1, the 2 lockfiles can be cleaned (removing the old pkgb/0.1#rev1, leaving pkgb/0.1#rev2 in the lock) The 2 final lockfiles can be "merged" in a single one for next iteration """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") c.run("lock create --requires=app1/0.1@ --lockfile=app1.lock --lockfile-out=app1.lock " "-s os=Linux") app1_lock = c.load("app1.lock") assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" in app1_lock assert "pkgawin/0.1#2f297d19d9ee4827caf97071de449a54" in app1_lock assert "pkganix/0.1#2f297d19d9ee4827caf97071de449a54" in app1_lock # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") c.run("create pkga --name=pkganix --version=0.1 -s os=Linux") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.1 works c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_win.lock") assert "pkgb/0.1: DEP FILE pkgawin: HelloA" in c.out c.run("create pkgb --name=pkgb --version=0.1 -s os=Linux " "--lockfile=app1.lock --lockfile-out=app1_nix.lock") assert "pkgb/0.1: DEP FILE pkganix: HelloA" in c.out # Now lets build the application in Windows, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_win.lock --lockfile-out=app1_win.lock " "--build=missing -s os=Windows") c.assert_listed_binary({"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.1": (pkgb_01_id, "Cache"), "pkgc/0.1": (pkgc_01_id, "Build"), "app1/0.1": (pkgapp_01_id, "Build")}) assert "app1/0.1: DEP FILE pkgawin: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # Now lets build the application in Linux, to see everything ok c.run("install --requires=app1/0.1@ --lockfile=app1_nix.lock --lockfile-out=app1_nix.lock " "--build=missing -s os=Linux") c.assert_listed_binary({"pkganix/0.1": (pkganix_01_id, "Cache"), "pkgb/0.1": (pkgb_012_id, "Cache"), "pkgc/0.1": ("62b3834a578b45bb303925e1e9cfe0dd9908486e", "Build"), "app1/0.1": ("6a589308a14c21c9082620be4a63240017665e38", "Build")}) assert "app1/0.1: DEP FILE pkganix: HelloA" in c.out assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out # All good! We can get rid of the now unused pkgb/0.1 version in the lockfile c.run("lock create --requires=app1/0.1@ --lockfile=app1_win.lock " "--lockfile-out=app1_win.lock -s os=Windows --lockfile-clean") c.run("lock create --requires=app1/0.1@ --lockfile=app1_nix.lock " "--lockfile-out=app1_nix.lock -s os=Linux --lockfile-clean") # Finally, merge the 2 clean lockfiles, for keeping just 1 for next iteration c.run("lock merge --lockfile=app1_win.lock --lockfile=app1_nix.lock " "--lockfile-out=app1_final.lock") app1_clean = c.load("app1_final.lock") assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" not in app1_clean assert "pkgawin/0.1#2f297d19d9ee4827caf97071de449a54" in app1_clean assert "pkganix/0.1#2f297d19d9ee4827caf97071de449a54" in app1_clean def test_single_config_decentralized(client_setup): """ same scenario as "test_single_config_centralized()", but distributing the build in different build servers, using the "build-order" """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") app1_lock = c.load("app1.lock") assert "pkgb/0.1#d03e920d532beeeb198cd886095bcca1" in app1_lock # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.1 works c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_b_changed.lock") assert "pkgb/0.1: DEP FILE pkgawin: HelloA" in c.out # Now lets build the application, to see everything ok c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_b_changed.lock " "--build=missing --format=json -s os=Windows", redirect_stdout="build_order.json") json_file = c.load("build_order.json") to_build = json.loads(json_file) level0 = to_build[0] assert len(level0) == 1 pkgawin = level0[0] assert pkgawin["ref"] == "pkgawin/0.1#2f297d19d9ee4827caf97071de449a54" assert pkgawin["packages"][0][0]["binary"] == "Cache" level1 = to_build[1] assert len(level1) == 1 pkgb = level1[0] assert pkgb["ref"] == "pkgb/0.1#bb12977c3353d7633b34d55a926fe58c" assert pkgb["packages"][0][0]["binary"] == "Cache" for level in to_build: for elem in level: ref = RecipeReference.loads(elem["ref"]) for package in elem["packages"][0]: # Assumes no dependencies between packages binary = package["binary"] package_id = package["package_id"] if binary != "Build": continue # TODO: The options are completely missing c.run( "install --requires=%s@ --build=%s@ --lockfile=app1_b_changed.lock -s os=Windows" % (ref, ref)) c.assert_listed_binary( {str(ref): (package_id, "Build"), "pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.1": (pkgb_01_id, "Cache")}) assert "DEP FILE pkgawin: HelloA" in c.out assert "DEP FILE pkgb: ByeB World!!" in c.out # Just to make sure that the for-loops have been executed assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out def test_multi_config_decentralized(client_setup): """ same scenario as "test_multi_config_centralized()", but distributing the build in different build servers, using the "build-order" """ c = client_setup # capture the initial lockfile of our product c.run("lock create --requires=app1/0.1@ --lockfile-out=app1.lock -s os=Windows") c.run("lock create --requires=app1/0.1@ --lockfile=app1.lock --lockfile-out=app1.lock " "-s os=Linux") # Do an unrelated change in A, should not be used, this is not the change we are testing c.save({"pkga/myfile.txt": "ByeA World!!"}) c.run("create pkga --name=pkgawin --version=0.1 -s os=Windows") c.run("create pkga --name=pkganix --version=0.1 -s os=Linux") # Do a change in B, this is the change that we want to test c.save({"pkgb/myfile.txt": "ByeB World!!"}) # Test that pkgb/0.1 new revision works c.run("create pkgb --name=pkgb --version=0.1 -s os=Windows " "--lockfile=app1.lock --lockfile-out=app1_win.lock") assert "pkgb/0.1: DEP FILE pkgawin: HelloA" in c.out c.run("create pkgb --name=pkgb --version=0.1 -s os=Linux " "--lockfile=app1.lock --lockfile-out=app1_nix.lock") assert "pkgb/0.1: DEP FILE pkganix: HelloA" in c.out # Now lets build the application, to see everything ok, for all the configs c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_win.lock " "--build=missing --format=json -s os=Windows", redirect_stdout="app1_win.json") c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_nix.lock " "--build=missing --format=json -s os=Linux", redirect_stdout="app1_nix.json") c.run("graph build-order-merge --file=app1_win.json --file=app1_nix.json" " --format=json", redirect_stdout="build_order.json") json_file = c.load("build_order.json") to_build = json.loads(json_file) level0 = to_build[0] assert len(level0) == 2 pkgawin = level0[0] assert pkgawin["ref"] == "pkgawin/0.1#2f297d19d9ee4827caf97071de449a54" assert pkgawin["packages"][0][0]["binary"] == "Cache" pkgawin = level0[1] assert pkgawin["ref"] == "pkganix/0.1#2f297d19d9ee4827caf97071de449a54" assert pkgawin["packages"][0][0]["binary"] == "Cache" level1 = to_build[1] assert len(level1) == 1 pkgb = level1[0] assert pkgb["ref"] == "pkgb/0.1#bb12977c3353d7633b34d55a926fe58c" assert pkgb["packages"][0][0]["binary"] == "Cache" for level in to_build: for elem in level: ref = elem["ref"] ref_without_rev = ref.split("#")[0] if "@" not in ref: ref = ref.replace("#", "@#") for package in elem["packages"][0]: # Assumes no dependencies between packages binary = package["binary"] package_id = package["package_id"] if binary != "Build": continue # TODO: The options are completely missing filenames = package["filenames"] lockfile = filenames[0] + ".lock" the_os = "Windows" if "win" in lockfile else "Linux" c.run("install --requires=%s --build=%s --lockfile=%s -s os=%s" % (ref, ref, lockfile, the_os)) c.assert_listed_binary({str(ref_without_rev): (package_id, "Build")}) if the_os == "Windows": c.assert_listed_binary( {"pkgawin/0.1": (pkgawin_01_id, "Cache"), "pkgb/0.1": (pkgb_01_id, "Cache")}) assert "DEP FILE pkgawin: HelloA" in c.out assert "DEP FILE pkgb: ByeB World!!" in c.out else: c.assert_listed_binary( {"pkganix/0.1": (pkganix_01_id, "Cache"), "pkgb/0.1": (pkgb_012_id, "Cache")}) assert "DEP FILE pkganix: HelloA" in c.out assert "DEP FILE pkgb: ByeB World!!" in c.out # Just to make sure that the for-loops have been executed assert "app1/0.1: DEP FILE pkgb: ByeB World!!" in c.out ================================================ FILE: test/integration/lockfile/test_compatibility.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_lockfile_compatibility(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "build_type" def compatibility(self): if self.settings.build_type == "Release": return [ {"settings": [("build_type", None)]}, ] """) c.save({"conanfile.py": conanfile, "profile": ""}) c.run("create . -pr=profile") c.save({"conanfile.py": GenConanfile().with_requires("pkg/1.0")}) c.run("install .") assert "pkg/1.0: Main binary package 'efa83b160a55b033c4ea706ddb980cd708e3ba1b' missing" in c.out assert "Found compatible package 'da39a3ee5e6b4b0d3255bfef95601890afd80709'" in c.out c.run("lock create conanfile.py") assert "pkg/1.0: Main binary package 'efa83b160a55b033c4ea706ddb980cd708e3ba1b' missing" in c.out assert "Found compatible package 'da39a3ee5e6b4b0d3255bfef95601890afd80709'" in c.out c.run("lock create conanfile.py --lockfile=conan.lock") assert "pkg/1.0: Main binary package 'efa83b160a55b033c4ea706ddb980cd708e3ba1b' missing" in c.out assert "Found compatible package 'da39a3ee5e6b4b0d3255bfef95601890afd80709'" in c.out c.run("install . --lockfile=conan.lock") c.assert_listed_binary({"pkg/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}) ================================================ FILE: test/integration/lockfile/test_graph_overrides.py ================================================ import json import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) def test_overrides_half_diamond(override, force): r""" pkgc -----> pkgb/0.1 --> pkga/0.1 \--(override/force)-->pkga/0.2 """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkga/0.2", override=override, force=force) }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkgb") c.run("lock create pkgc") lock = json.loads(c.load("pkgc/conan.lock")) requires = "\n".join(lock["requires"]) assert "pkga/0.2" in requires assert "pkga/0.1" not in requires c.run("graph info pkgc --lockfile=pkgc/conan.lock --format=json") dependencies = json.loads(c.stdout)["graph"]["nodes"]["0"]["dependencies"] assert "pkga/0.2" in str(dependencies) assert "pkga/0.1" not in str(dependencies) # apply the lockfile to pkgb, should it lock to pkga/0.2 c.run("graph info pkgb --lockfile=pkgc/conan.lock --format=json") dependencies = json.loads(c.stdout)["graph"]["nodes"]["0"]["dependencies"] assert "pkga/0.2" == dependencies["1"]["ref"] assert "pkga/0.1" == dependencies["1"]["require"] @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) def test_overrides_half_diamond_ranges(override, force): r""" pkgc -----> pkgb/0.1 --> pkga/[>0.1 <0.2] \--(override/force)-->pkga/0.2 """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/[>=0.1 <0.2]"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkga/0.2", override=override, force=force) }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkgb") assert "pkga/0.2" not in c.out assert "pkga/0.1" in c.out c.run("lock create pkgc") lock = c.load("pkgc/conan.lock") assert "pkga/0.2" in lock assert "pkga/0.1" not in lock c.run("graph info pkgc --lockfile=pkgc/conan.lock") assert "pkga/0.2" in c.out assert "pkga/0.1" not in c.out c.run("graph info pkgb --lockfile=pkgc/conan.lock") # should work @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) def test_overrides_half_diamond_ranges_inverted(override, force): r""" the override is defining the lower bound of the range pkgc -----> pkgb/0.1 --> pkga/[>=0.1] \--(override/force)-->pkga/0.1 """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/[>=0.1]"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkga/0.1", override=override, force=force) }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkgb") assert "pkga/0.2" in c.out assert "pkga/0.1" not in c.out c.run("lock create pkgc") lock = c.load("pkgc/conan.lock") assert "pkga/0.1" in lock assert "pkga/0.2" not in lock c.run("graph info pkgc --lockfile=pkgc/conan.lock") assert "pkga/0.1" in c.out assert "pkga/0.2" not in c.out c.run("graph info pkgb --lockfile=pkgc/conan.lock") # should work @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) def test_overrides_diamond(override, force): r""" pkgd -----> pkgb/0.1 --> pkga/0.1 \------> pkgc/0.1 --> pkga/0.2 \--(override/force)-->pkga/0.3 """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkga/0.2"), "pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkgc/0.1") .with_requirement("pkga/0.3", override=override, force=force) }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkga --version=0.3") c.run("create pkgb") c.run("create pkgc") c.run("lock create pkgd") lock = json.loads(c.load("pkgd/conan.lock")) requires = "\n".join(lock["requires"]) assert "pkga/0.3" in requires assert "pkga/0.2" not in requires assert "pkga/0.1" not in requires c.run("graph info pkgd --lockfile=pkgd/conan.lock --format=json") json_graph = json.loads(c.stdout) deps = json_graph["graph"]["nodes"]["0"]["dependencies"] assert "pkga/0.3" in str(deps) assert "pkga/0.2" not in str(deps) assert "pkga/0.1" not in str(deps) # Redundant assert, but checking "overrides" summary overrides = json_graph['graph']["overrides"] assert len(overrides) == 2 assert overrides['pkga/0.1'] == ['pkga/0.3'] assert overrides['pkga/0.2'] == ['pkga/0.3'] # apply the lockfile to pkgb, should it lock to pkga/0.3 c.run("graph info pkgb --lockfile=pkgd/conan.lock --format=json") json_graph = json.loads(c.stdout) deps = json_graph["graph"]["nodes"]["0"]["dependencies"] assert "pkga/0.3" == deps["1"]["ref"] assert "pkga/0.1" == deps["1"]["require"] # Redundant assert, but checking "overrides" summary overrides = json_graph['graph']["overrides"] assert len(overrides) == 1 assert overrides["pkga/0.1"] == ["pkga/0.3"] @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) def test_overrides_diamond_ranges(override, force): r""" pkgd -----> pkgb/0.1 --> pkga/[>=0.1 <0.2] \------> pkgc/0.1 --> pkga/[>=0.2 <0.3] \--(override/force)-->pkga/0.3 """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/[>=0.1 <0.2]"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkga/[>=0.2 <0.3]"), "pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkgc/0.1") .with_requirement("pkga/0.3", override=override, force=force) }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkga --version=0.3") c.run("create pkgb") c.run("create pkgc") c.run("lock create pkgd") lock = json.loads(c.load("pkgd/conan.lock")) requires = "\n".join(lock["requires"]) assert "pkga/0.3" in requires assert "pkga/0.2" not in requires assert "pkga/0.1" not in requires c.run("graph info pkgd --lockfile=pkgd/conan.lock --format=json") dependencies = json.loads(c.stdout)["graph"]["nodes"]["0"]["dependencies"] assert "pkga/0.3" in str(dependencies) assert "pkga/0.2" not in str(dependencies) assert "pkga/0.1" not in str(dependencies) # apply the lockfile to pkgb, should it lock to pkga/0.3 c.run("graph info pkgb --lockfile=pkgd/conan.lock --format=json") dependencies = json.loads(c.stdout)["graph"]["nodes"]["0"]["dependencies"] assert "pkga/0.3" in str(dependencies) assert "pkga/0.2" not in str(dependencies) assert "pkga/0.1" not in str(dependencies) @pytest.mark.parametrize("override1, force1", [(True, False), (False, True)]) @pytest.mark.parametrize("override2, force2", [(True, False), (False, True)]) def test_overrides_multiple(override1, force1, override2, force2): r""" pkgd/0.1 -> pkgc/0.1 -> pkgb/0.1 -> pkga/0.1 \ \--override---------> pkga/0.2 \---override-------------------> pkga/0.3 """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkga/0.2", override=override1, force=force1), "pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgc/0.1") .with_requirement("pkga/0.3", override=override2, force=force2) }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkga --version=0.3") c.run("create pkgb") c.run("create pkgc --build=missing") c.run("lock create pkgd") lock = json.loads(c.load("pkgd/conan.lock")) requires = "\n".join(lock["requires"]) assert "pkga/0.3" in requires assert "pkga/0.2" not in requires assert "pkga/0.1" not in requires c.run("graph info pkgd --lockfile=pkgd/conan.lock") assert "pkga/0.3" in c.out assert "pkga/0.2#" not in c.out assert "pkga/0.1#" not in c.out # appears in override information c.run("graph info pkgb --lockfile=pkgd/conan.lock") # should work def test_graph_different_overrides(): r""" pkga -> toola/0.1 -> toolb/0.1 -> toolc/0.1 \------override-----> toolc/0.2 pkgb -> toola/0.2 -> toolb/0.2 -> toolc/0.1 \------override-----> toolc/0.3 pkgc -> toola/0.3 -> toolb/0.3 -> toolc/0.1 """ c = TestClient(light=True) c.save({"toolc/conanfile.py": GenConanfile("toolc"), "toolb/conanfile.py": GenConanfile("toolb").with_requires("toolc/0.1"), "toola/conanfile.py": GenConanfile("toola", "0.1").with_requirement("toolb/0.1") .with_requirement("toolc/0.2", override=True), "toola2/conanfile.py": GenConanfile("toola", "0.2").with_requirement("toolb/0.2") .with_requirement("toolc/0.3", override=True), "toola3/conanfile.py": GenConanfile("toola", "0.3").with_requirement("toolb/0.3"), "pkga/conanfile.py": GenConanfile("pkga", "0.1").with_tool_requires("toola/0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1") .with_tool_requires("toola/0.2"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkgb/0.1") .with_tool_requires("toola/0.3"), }) c.run("create toolc --version=0.1") c.run("create toolc --version=0.2") c.run("create toolc --version=0.3") c.run("create toolb --version=0.1") c.run("create toolb --version=0.2") c.run("create toolb --version=0.3") c.run("create toola --build=missing") c.run("create toola2 --build=missing") c.run("create toola3 --build=missing") c.run("create pkga") c.run("create pkgb") c.run("lock create pkgc") lock = json.loads(c.load("pkgc/conan.lock")) requires = "\n".join(lock["build_requires"]) assert "toolc/0.3" in requires assert "toolc/0.2" in requires assert "toolc/0.1" in requires c.run("graph info toolb --build-require --version=0.1 --lockfile=pkgc/conan.lock --format=json") # defaults to the non overriden c.assert_listed_require({"toolc/0.1": "Cache"}, build=True) # TODO: Solve it with build-order or manual overrides for the other packages @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) def test_introduced_conflict(override, force): """ Using --lockfile-partial we can evaluate and introduce a new conflict pkgd -----> pkgb/[*] --> pkga/[>=0.1 <0.2] """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb").with_requires("pkga/[>=0.1 <0.2]"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkga/[>=0.2 <0.3]"), "pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgb/[*]") }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkga --version=0.3") c.run("create pkgb --version=0.1") c.run("create pkgc") c.run("lock create pkgd") lock = json.loads(c.load("pkgd/conan.lock")) requires = "\n".join(lock["requires"]) assert "pkga/0.1" in requires assert "pkga/0.2" not in requires assert "pkga/0.3" not in requires # This will not be used thanks to the lockfile c.run("create pkgb --version=0.2") """ This change in pkgd introduce a conflict Using --lockfile-partial we can evaluate and introduce a new conflict pkgd -----> pkgb/[*] --> pkga/[>=0.1 <0.2] |-------> pkgc/0.1 --> pkga/[>=0.2 <0.3] """ c.save({"pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkgc/0.1") }) c.run("graph info pkgd --lockfile=pkgd/conan.lock --lockfile-partial", assert_error=True) assert "Version conflict: Conflict between pkga/[>=0.2 <0.3] and pkga/0.1 in the graph" in c.out # Resolve the conflict with an override or force c.save({"pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkgc/0.1") .with_requirement("pkga/0.3", override=override, force=force) }) c.run("graph info pkgd --lockfile=pkgd/conan.lock --lockfile-partial " "--lockfile-out=pkgd/conan2.lock --lockfile-clean") assert "pkgb/0.2" not in c.out assert "pkgb/0.1" in c.out lock = json.loads(c.load("pkgd/conan2.lock")) requires = "\n".join(lock["requires"]) assert "pkga/0.3" in requires assert "pkga/0.1" not in requires assert "pkga/0.2" not in requires assert "pkgb/0.2" not in requires def test_command_line_lockfile_overrides(): """ --lockfile-overrides cannot be abused to inject new overrides, only existing ones """ c = TestClient(light=True) c.save({ "pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkgb/0.1"), }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkgb") c.run('install pkgc --lockfile-overrides="{\'pkga/0.1\': [\'pkga/0.2\']}"', assert_error=True) assert "Cannot define overrides without a lockfile" in c.out c.run('lock create pkgc') c.run('install pkgc --lockfile-overrides="{\'pkga/0.1\': [\'pkga/0.2\']}"') # From https://github.com/conan-io/conan/issues/19738 it is simply ignored # Not a hard "protection" error, still users can't inject a dependency to pkga/0.2 if not # in the lockfile already assert "pka/0.2" not in c.out assert "pkga/0.1" in c.out def test_consecutive_installs(): c = TestClient(light=True) c.save({ "pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkgb/0.1") .with_requirement("pkga/0.2", override=True), }) c.run("export pkga --version=0.1") c.run("export pkga --version=0.2") c.run("export pkgb") c.run("install pkgc --build=missing --lockfile-out=conan.lock") c.assert_overrides({"pkga/0.1": ["pkga/0.2"]}) # This used to crash when overrides were not managed c.run("install pkgc --build=missing --lockfile=conan.lock --lockfile-out=conan.lock") c.assert_overrides({"pkga/0.1": ["pkga/0.2"]}) class TestOverrideContextError: # https://github.com/conan-io/conan/issues/19738 def test_error_lockfile_override_build_require(self): # The override that comes from the host context is breaking the build locking c = TestClient(light=True) c.save({"abseil/conanfile.py": GenConanfile("abseil"), "protobuf/conanfile.py": GenConanfile("protobuf", "0.1").with_requires("abseil/[*]"), "app/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("protobuf/0.1") .with_requirement("abseil/0.1", override=True) .with_tool_requires("protobuf/0.1") }) c.run("create abseil --version=0.1") c.run("create abseil --version=0.2") c.run("create protobuf") c.run("lock create app") c.run("install app --build=missing --lockfile=app/conan.lock") # It doesnt fail def test_error_lockfile_override_build_require_build(self): # Same as the above, but now the override is in the "build" context, affecting the # host one that shouldn't be overriden c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/[*]"), "toolb/conanfile.py": GenConanfile("toolb", "0.1").with_requirement("pkgb/0.1") .with_requirement("pkga/0.1", override=True), "app/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgb/0.1") .with_tool_requires("toolb/0.1") }) c.run("create pkga --version=0.1") c.run("create pkga --version=0.2") c.run("create pkgb") c.run("create toolb --build=missing") c.run("lock create app") c.run("install app --lockfile=app/conan.lock") # It doesnt fail ================================================ FILE: test/integration/lockfile/test_lock_alias.py ================================================ import os import pytest from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient DepsGraphBuilder.ALLOW_ALIAS = True @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_deps_ranges(requires): """ conanfile.txt locking it dependencies (with version ranges) using alias """ client = TestClient(light=True) client.save({"pkg/conanfile.py": GenConanfile("pkg"), "consumer/conanfile.txt": f"[{requires}]\npkg/(latest)"}) client.run("create pkg --version=0.1") client.run("create pkg --version=0.2") with client.chdir("alias"): client.run("new alias -d name=pkg -d version=latest -d target=0.1") client.run("export .") client.run("lock create consumer/conanfile.txt") assert "pkg/0.1" in client.out assert '"pkg/latest": "pkg/0.1"' in client.load("consumer/conan.lock") # Change the alias with client.chdir("alias"): client.run("new alias -d name=pkg -d version=latest -d target=0.2 -f") client.run("export .") client.run("install consumer/conanfile.txt") # use conan.lock by default assert "pkg/0.1" in client.out assert "pkg/0.2" not in client.out os.remove(os.path.join(client.current_folder, "consumer/conan.lock")) client.run("install consumer/conanfile.txt") assert "pkg/0.2" in client.out assert "pkg/0.1" not in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_deps_ranges_lock_revisions(requires): """ conanfile.txt locking it dependencies (with version ranges) """ client = TestClient(light=True) client.save({"pkg/conanfile.py": GenConanfile("pkg"), "consumer/conanfile.txt": f"[{requires}]\npkg/(latest)"}) client.run("create pkg --version=0.1") client.assert_listed_require({"pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26": "Cache"}) client.run("create pkg --version=0.2") with client.chdir("alias"): client.run("new alias -d name=pkg -d version=latest -d target=0.1") client.run("export .") client.run("lock create consumer/conanfile.txt") assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" in client.out assert '"pkg/latest": "pkg/0.1"' in client.load("consumer/conan.lock") # Create a new revision client.save({"pkg/conanfile.py": GenConanfile("pkg").with_class_attribute("potato=42")}) client.run("create pkg --version=0.1") client.assert_listed_require({"pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b": "Cache"}) client.run("install consumer/conanfile.txt") # use conan.lock by default assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" in client.out assert "pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b" not in client.out os.remove(os.path.join(client.current_folder, "consumer/conan.lock")) client.run("install consumer/conanfile.txt") assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" not in client.out assert "pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b" in client.out ================================================ FILE: test/integration/lockfile/test_lock_build_requires.py ================================================ import json import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_lock_build_tool_requires(): c = TestClient(light=True) c.save({"common/conanfile.py": GenConanfile("common", "1.0").with_settings("os"), "tool/conanfile.py": GenConanfile("tool", "1.0").with_settings("os") .with_requires("common/1.0"), "lib/conanfile.py": GenConanfile("lib", "1.0").with_settings("os") .with_requires("tool/1.0"), "consumer/conanfile.py": GenConanfile("consumer", "1.0").with_settings("os") .with_requires("lib/1.0") .with_build_requires("tool/1.0")}) c.run("export common") c.run("export tool") c.run("export lib") # cross compile Linux->Windows c.run("lock create consumer/conanfile.py -s:h os=Linux -s:b os=Windows --build=*") c.run("install --tool-requires=tool/1.0 --build=missing --lockfile=consumer/conan.lock " "-s:h os=Linux -s:b os=Windows") c.assert_listed_binary({"tool/1.0": ("78ba71aef65089d6e3244756171f9f37d5a76223", "Build"), "common/1.0": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Build")}, build=True) def test_lock_buildrequires_create(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("tool", "0.1")}) c.run("create . --build-require --lockfile-out=conan.lock") lock = json.loads(c.load("conan.lock")) assert "tool/0.1#2d65f1b4af1ce59028f96adbfe7ed5a2" in lock["build_requires"][0] def test_lock_buildrequires_export(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("tool", "0.1")}) c.run("export . --build-require --lockfile-out=conan.lock") lock = json.loads(c.load("conan.lock")) assert "tool/0.1#2d65f1b4af1ce59028f96adbfe7ed5a2" in lock["build_requires"][0] def test_lock_buildrequires_create_transitive(): c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "tool/conanfile.py": GenConanfile("tool", "0.1").with_requires("dep/0.1")}) c.run("create dep") c.run("create tool --build-require --lockfile-out=conan.lock") lock = json.loads(c.load("conan.lock")) assert "tool/0.1#e4f0da4d9097c4da0725ea25b8bf83c8" in lock["build_requires"][0] assert "dep/0.1#f8c2264d0b32a4c33f251fe2944bb642" in lock["build_requires"][1] def test_lock_create_build_require_transitive(): """ cross compiling from Windows to Linux """ c = TestClient(light=True) dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" settings = "os" def package_id(self): self.output.info(f"MYOS:{self.info.settings.os}!!") self.output.info(f"TARGET:{self.settings_target.os}!!") """) tool = textwrap.dedent(""" from conan import ConanFile class Tool(ConanFile): name = "tool" version = "0.1" requires = "dep/[*]" settings = "os" def generate(self): self.output.info(f"MYOS-GEN:{self.info.settings.os}!!") self.output.info(f"TARGET-GEN:{self.settings_target.os}!!") def package_id(self): self.output.info(f"MYOS:{self.info.settings.os}!!") self.output.info(f"TARGET:{self.settings_target.os}!!") """) c.save({"dep/conanfile.py": dep, "tool/conanfile.py": tool}) c.run("create dep --build-require --version=0.1 -s:b os=Windows -s:h os=Linux") assert "dep/0.1: MYOS:Windows!!" in c.out assert "dep/0.1: TARGET:Linux!!" in c.out # The lockfile should contain dep in build-requires c.run("lock create tool --build-require -s:b os=Windows -s:h os=Linux") assert "dep/0.1: MYOS:Windows!!" in c.out assert "dep/0.1: TARGET:Linux!!" in c.out lock = json.loads(c.load("tool/conan.lock")) assert "dep/0.1" in lock["build_requires"][0] # Now try to apply it in graph info, even if a new 0.2 verion si there c.run("create dep --build-require --version=0.2 -s:b os=Windows -s:h os=Linux") assert "dep/0.2: MYOS:Windows!!" in c.out assert "dep/0.2: TARGET:Linux!!" in c.out c.run("graph info tool --build-require -s:b os=Windows -s:h os=Linux") c.assert_listed_require({"dep/0.1": "Cache"}, build=True) assert "dep/0.1: MYOS:Windows!!" in c.out assert "dep/0.1: TARGET:Linux!!" in c.out assert "conanfile.py (tool/0.1): MYOS:Windows!!" in c.out assert "conanfile.py (tool/0.1): TARGET:Linux!!" in c.out assert "context: build" in c.out assert "context: host" not in c.out c.run("install tool --build-require -s:b os=Windows -s:h os=Linux") c.assert_listed_require({"dep/0.1": "Cache"}, build=True) assert "dep/0.1: MYOS:Windows!!" in c.out assert "dep/0.1: TARGET:Linux!!" in c.out assert "conanfile.py (tool/0.1): MYOS:Windows!!" in c.out assert "conanfile.py (tool/0.1): TARGET:Linux!!" in c.out assert "conanfile.py (tool/0.1): MYOS-GEN:Windows!!" in c.out assert "conanfile.py (tool/0.1): TARGET-GEN:Linux!!" in c.out class TestTransitiveBuildRequires: @pytest.fixture() def client(self): # https://github.com/conan-io/conan/issues/13899 client = TestClient(light=True) client.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "cmake/conanfile.py": GenConanfile("cmake", "1.0").with_requires("zlib/1.0"), "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_build_requires("cmake/1.0"), "consumer/conanfile.py": GenConanfile().with_requires("pkg/[>=1.0]"), }) client.run("export zlib") client.run("export cmake") client.run("export pkg") client.run("lock create consumer/conanfile.py -pr:b=default --build=* " "--lockfile-out=conan.lock") return client def test_transitive_build_require(self, client): # This used to crash, not anymore with the fix client.run("install consumer/conanfile.py --build=missing --lockfile=conan.lock") assert "zlib/1.0: Created package" in client.out assert "cmake/1.0: Created package" in client.out assert "pkg/1.0: Created package" in client.out def test_transitive_build_require_intermediate(self, client): # This used to crash, not anymore with the fix client.run("install pkg/conanfile.py --build=missing --lockfile=conan.lock") assert "zlib/1.0: Created package" in client.out assert "cmake/1.0: Created package" in client.out ================================================ FILE: test/integration/lockfile/test_lock_merge.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_merge_alias(requires): """ basic lockfile merging including alias """ c = TestClient() app = textwrap.dedent(f""" from conan import ConanFile class App(ConanFile): settings = "build_type" def requirements(self): if self.settings.build_type == "Debug": self.{requires}("pkg/(alias_debug)") else: self.{requires}("pkg/(alias_release)") """) c.save({"pkg/conanfile.py": GenConanfile("pkg"), "alias_release/conanfile.py": GenConanfile("pkg", "alias_release").with_class_attribute( "alias = 'pkg/0.1'"), "alias_debug/conanfile.py": GenConanfile("pkg", "alias_debug").with_class_attribute( "alias = 'pkg/0.2'"), "app/conanfile.py": app}) c.run("create pkg --version=0.1") c.run("create pkg --version=0.2") c.run("export alias_release") c.run("export alias_debug") c.run("lock create app -s build_type=Release --lockfile-out=release.lock") c.run("lock create app -s build_type=Debug --lockfile-out=debug.lock") c.run("lock merge --lockfile=release.lock --lockfile=debug.lock --lockfile-out=conan.lock") # Update alias, won't be used c.save({"alias_release/conanfile.py": GenConanfile("pkg", "alias_release").with_class_attribute( "alias = 'pkg/0.3'"), "alias_debug/conanfile.py": GenConanfile("pkg", "alias_debug").with_class_attribute( "alias = 'pkg/0.4'")}) c.run("export alias_release") c.run("export alias_debug") # Merged one can resolve both aliased without issues c.run("install app -s build_type=Release --lockfile=conan.lock") is_build_requires = requires == "tool_requires" c.assert_listed_require({"pkg/0.1": "Cache"}, build=is_build_requires) c.run("install app -s build_type=Debug --lockfile=conan.lock") c.assert_listed_require({"pkg/0.2": "Cache"}, build=is_build_requires) # without lockfiles it would be pointing to the new (unexistent) ones c.run("install app -s build_type=Release", assert_error=True) assert "ERROR: Package 'pkg/0.3' not resolved" in c.out c.run("install app -s build_type=Debug", assert_error=True) assert "ERROR: Package 'pkg/0.4' not resolved" in c.out ================================================ FILE: test/integration/lockfile/test_lock_packages.py ================================================ import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID from conan.test.utils.env import environment_update @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_lock_packages(requires): """ Check that package revisions can be locked too NOTE: They are still not used! only to check that it is possible to store them And that the lockfile is still usable """ client = TestClient(light=True) client.save({"pkg/conanfile.py": GenConanfile().with_package_file("file.txt", env_var="MYVAR"), "consumer/conanfile.txt": f"[{requires}]\npkg/[>0.0]"}) with environment_update({"MYVAR": "MYVALUE"}): client.run("create pkg --name=pkg --version=0.1") prev = client.created_package_revision("pkg/0.1") client.run("lock create consumer/conanfile.txt --lockfile-packages") assert "ERROR: The --lockfile-packages arg is private and shouldn't be used" in client.out assert "pkg/0.1#" in client.out lock = client.load("consumer/conan.lock") assert NO_SETTINGS_PACKAGE_ID in lock with environment_update({"MYVAR": "MYVALUE2"}): client.run("create pkg --name=pkg --version=0.1") prev2 = client.created_package_revision("pkg/0.1") assert prev2 != prev client.run("install consumer/conanfile.txt") assert prev in client.out assert prev2 not in client.out os.remove(os.path.join(client.current_folder, "consumer/conan.lock")) client.run("install consumer/conanfile.txt") assert prev2 in client.out assert prev not in client.out ================================================ FILE: test/integration/lockfile/test_lock_pyrequires.py ================================================ import json import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import load def test_transitive_py_requires(): # https://github.com/conan-io/conan/issues/5529 client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class PackageInfo(ConanFile): python_requires = "dep/[>0.0]@user/channel" """) consumer = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "pkg/0.1@user/channel" """) client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) client.run("export dep --name=dep --version=0.1 --user=user --channel=channel") client.run("export pkg --name=pkg --version=0.1 --user=user --channel=channel") client.run("lock create consumer/conanfile.py") client.run("export dep --name=dep --version=0.2 --user=user --channel=channel") client.run("install consumer/conanfile.py") assert "dep/0.1@user/channel" in client.out assert "dep/0.2" not in client.out os.remove(os.path.join(client.current_folder, "consumer/conan.lock")) client.run("install consumer/conanfile.py") assert "dep/0.2@user/channel" in client.out assert "dep/0.1" not in client.out def test_transitive_matching_ranges(): client = TestClient(light=True) tool = textwrap.dedent(""" from conan import ConanFile class PackageInfo(ConanFile): python_requires = "dep/{}" """) pkg = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "tool/{}" def configure(self): for k, p in self.python_requires.items(): self.output.info("%s: %s!!" % (k, p.ref)) """) client.save({"dep/conanfile.py": GenConanfile(), "tool1/conanfile.py": tool.format("[<0.2]"), "tool2/conanfile.py": tool.format("[>0.0]"), "pkga/conanfile.py": pkg.format("[<0.2]"), "pkgb/conanfile.py": pkg.format("[>0.0]"), "app/conanfile.py": GenConanfile().with_requires("pkga/[*]", "pkgb/[*]")}) client.run("export dep --name=dep --version=0.1") client.run("export dep --name=dep --version=0.2") client.run("export tool1 --name=tool --version=0.1") client.run("export tool2 --name=tool --version=0.2") client.run("create pkga --name=pkga --version=0.1") client.run("create pkgb --name=pkgb --version=0.1") client.run("lock create app/conanfile.py --lockfile-out=app.lock") client.run("export dep --name=dep --version=0.2") client.run("export tool2 --name=tool --version=0.3") client.run("create pkga --name=pkga --version=0.2") client.run("create pkgb --name=pkgb --version=0.2") client.run("install app/conanfile.py --lockfile=app.lock") assert "pkga/0.1: tool: tool/0.1!!" in client.out assert "pkga/0.1: dep: dep/0.1!!" in client.out assert "pkgb/0.1: tool: tool/0.2!!" in client.out assert "pkgb/0.1: dep: dep/0.2!!" in client.out client.run("install app/conanfile.py") assert "pkga/0.2: tool: tool/0.1!!" in client.out assert "pkga/0.2: dep: dep/0.1!!" in client.out assert "pkgb/0.2: tool: tool/0.3!!" in client.out assert "pkgb/0.2: dep: dep/0.2!!" in client.out def test_lock_pyrequires_prereleases(): tc = TestClient(light=True) tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0-0.1"), "app/conanfile.py": GenConanfile("app", "1.0").with_python_requires("dep/[>=0]")}) tc.run("export dep") dep_rrev = tc.exported_recipe_revision() tc.run("lock create app --lockfile-out=app.lock", assert_error=True) # Makes sense, prereleases are not active assert ("Version range '>=0' from requirement 'dep/[>=0]' required by " "'python_requires' could not be resolved") in tc.out tc.save_home({"global.conf": "core.version_ranges:resolve_prereleases=True"}) # This used to crash even with the conf activated tc.run("lock create app --lockfile-out=app.lock") data = json.loads(load(os.path.join(tc.current_folder, "app.lock"))) assert data["python_requires"][0].startswith(f"dep/1.0-0.1#{dep_rrev}") def test_lock_pyrequires_create(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("tool", "0.1").with_package_type("python-require")}) c.run("create . --lockfile-out=conan.lock") lock = json.loads(c.load("conan.lock")) assert "tool/0.1#7835890f1e86b3f7fc6d76b4e3c43cb1" in lock["python_requires"][0] def test_lock_pyrequires_export(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("tool", "0.1").with_package_type("python-require")}) c.run("export . --lockfile-out=conan.lock") lock = json.loads(c.load("conan.lock")) assert "tool/0.1#7835890f1e86b3f7fc6d76b4e3c43cb1" in lock["python_requires"][0] def test_lock_pyrequires_export_transitive(): c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_package_type("python-require"), "tool/conanfile.py": GenConanfile("tool", "0.1").with_package_type("python-require") .with_python_requires("dep/0.1")}) c.run("export dep") c.run("export tool --lockfile-out=conan.lock") lock = json.loads(c.load("conan.lock")) assert "tool/0.1#c0008d3fcf07f7a690dd16bf6c171cec" in lock["python_requires"][0] assert "dep/0.1#5d31586a2a4355d68898875dc591009a" in lock["python_requires"][1] def test_lock_export_transitive_pyrequire(): c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_package_type("python-require"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("dep/0.1")}) c.run("export dep") c.run("export pkg --lockfile-out=conan.lock") lock = json.loads(c.load("conan.lock")) assert "pkg/0.1#dfd6bc1becb3915043a671111860baee" in lock["requires"][0] assert "dep/0.1#5d31586a2a4355d68898875dc591009a" in lock["python_requires"][0] def test_pyrequires_test_package_lockfile_error(): # https://github.com/conan-io/conan/issues/19340 c = TestClient(light=True) c.save({"utils/conanfile.py": GenConanfile("utils").with_package_type("python-require"), "bar/conanfile.py": GenConanfile("bar", "8.0").with_python_requires("utils/[^1]"), "bar/test_package/conanfile.py": GenConanfile().with_test("pass") .with_python_requires("utils/[^1]"), "foo/conanfile.py": GenConanfile("foo", "1.0").with_requires("bar/[*]") .with_python_requires("utils/1.7.1")}) c.run("create utils --version=1.7.1") c.run("create utils --version=1.8.0") c.run("create bar --lockfile-out=bar.lock") c.run("create foo", assert_error=True) assert "Missing prebuilt package for 'bar/8.0'" in c.out c.run("create foo --lockfile=bar.lock --lockfile-partial --lockfile-out=foo.lock") c.assert_listed_require({"utils/1.7.1": "Cache", "utils/1.8.0": "Cache"}, python=True) lock = json.loads(c.load("foo.lock")) assert "utils/1.8.0" in lock["python_requires"][0] assert "utils/1.7.1" in lock["python_requires"][1] # The lockfile works, because the fixed version is an older one, and the lock can have # both, the range resolves to 1.8.0, and the fixed resolves to 1.7.1 # This lockfile can be used with same results c.run("create foo --lockfile=foo.lock") c.assert_listed_require({"utils/1.7.1": "Cache", "utils/1.8.0": "Cache"}, python=True) def test_pyrequires_test_package_lockfile_error_forward(): # https://github.com/conan-io/conan/issues/19340 c = TestClient(light=True) c.save({"utils/conanfile.py": GenConanfile("utils").with_package_type("python-require"), "bar/conanfile.py": GenConanfile("bar", "8.0").with_python_requires("utils/[^1]"), "bar/test_package/conanfile.py": GenConanfile().with_test("pass") .with_python_requires("utils/[^1]"), "foo/conanfile.py": GenConanfile("foo", "1.0").with_requires("bar/[*]") .with_python_requires("utils/1.9.0")}) # bar binary is built with utils/1.8.0 c.run("create utils --version=1.8.0") c.run("create bar --lockfile-out=bar.lock") c.assert_listed_require({"utils/1.8.0": "Cache"}, python=True) lock = json.loads(c.load("bar.lock")) assert "utils/1.8.0" in lock["python_requires"][0] # now a new utils/1.9.0 is out, so the regular consumption of bar will fail with missing binary # this will happen even if foo doesn't declare python-requires c.run("create utils --version=1.9.0") c.run("create foo", assert_error=True) assert "Missing prebuilt package for 'bar/8.0'" in c.out c.assert_listed_require({"utils/1.9.0": "Cache"}, python=True) # If we try to force bar strict dependencies with lockfile, it will fail, as expected c.run("create foo --lockfile=bar.lock", assert_error=True) assert "Requirement 'utils/1.9.0' not in lockfile 'python_requires'" in c.out # we can relax the lockfile, and it will be able to resolve, # but it will still fail with missing binary for bar c.run("create foo --lockfile=bar.lock --lockfile-partial", assert_error=True) assert "Missing prebuilt package for 'bar/8.0'" in c.out c.run("create foo --lockfile=bar.lock --lockfile-partial --build=missing " "--lockfile-out=foo.lock") c.assert_listed_require({"utils/1.9.0": "Cache"}, python=True) assert "utils/1.8.0" not in c.out # This build is reproducible with this lockfile c.run("create foo --lockfile=foo.lock") c.assert_listed_require({"utils/1.9.0": "Cache"}, python=True) assert "utils/1.8.0" not in c.out ================================================ FILE: test/integration/lockfile/test_lock_pyrequires_revisions.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_transitive_py_requires_revisions(): # https://github.com/conan-io/conan/issues/5529 client = TestClient(light=True) python_req = textwrap.dedent(""" from conan import ConanFile some_var = {} class PackageInfo(ConanFile): pass """) conanfile = textwrap.dedent(""" from conan import ConanFile class PackageInfo(ConanFile): python_requires = "dep/0.1@user/channel" """) consumer = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "pkg/0.1@user/channel" def generate(self): self.output.info("VAR={}!!!".format(self.python_requires["dep"].module.some_var)) """) client.save({"dep/conanfile.py": python_req.format("42"), "pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) client.run("export dep --name=dep --version=0.1 --user=user --channel=channel") client.run("export pkg --name=pkg --version=0.1 --user=user --channel=channel") client.run("lock create consumer/conanfile.py") client.save({"dep/conanfile.py": python_req.format("123")}) client.run("export dep --name=dep --version=0.1 --user=user --channel=channel") client.run("install consumer/conanfile.py") assert "conanfile.py: VAR=42!!!" in client.out os.remove(os.path.join(client.current_folder, "consumer/conan.lock")) client.run("install consumer/conanfile.py") assert "conanfile.py: VAR=123!!!" in client.out def test_transitive_matching_revisions(): client = TestClient(light=True) dep = textwrap.dedent(""" from conan import ConanFile some_var = {} class PackageInfo(ConanFile): pass """) tool = textwrap.dedent(""" from conan import ConanFile class PackageInfo(ConanFile): python_requires = "dep/{}" def package_id(self): self.output.info("VAR={{}}!!!".format(self.python_requires["dep"].module.some_var)) """) pkg = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "{}/0.1" def package_id(self): self.output.info("VAR={{}}!!!".format(self.python_requires["dep"].module.some_var)) """) client.save({"dep/conanfile.py": dep.format(42), "toola/conanfile.py": tool.format("0.1"), "toolb/conanfile.py": tool.format("0.2"), "pkga/conanfile.py": pkg.format("toola"), "pkgb/conanfile.py": pkg.format("toolb"), "app/conanfile.py": GenConanfile().with_requires("pkga/0.1", "pkgb/0.1")}) client.run("export dep --name=dep --version=0.1") client.run("export dep --name=dep --version=0.2") client.run("export toola --name=toola --version=0.1") client.run("export toolb --name=toolb --version=0.1") client.run("create pkga --name=pkga --version=0.1") client.run("create pkgb --name=pkgb --version=0.1") client.run("lock create app/conanfile.py --lockfile-out=app.lock") client.save({"dep/conanfile.py": dep.format(123)}) client.run("export dep --name=dep --version=0.1") client.run("export dep --name=dep --version=0.2") client.run("install app/conanfile.py --lockfile=app.lock") assert "pkga/0.1: VAR=42!!!" in client.out assert "pkgb/0.1: VAR=42!!!" in client.out assert "VAR=123" not in client.out client.run("install app/conanfile.py") assert "pkga/0.1: VAR=123!!!" in client.out assert "pkgb/0.1: VAR=123!!!" in client.out assert "VAR=42" not in client.out ================================================ FILE: test/integration/lockfile/test_lock_requires.py ================================================ import json import os import textwrap import time import pytest from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_deps_ranges(requires): """ conanfile.txt locking it dependencies (with version ranges) """ client = TestClient(light=True) client.save({"pkg/conanfile.py": GenConanfile(), "consumer/conanfile.txt": f"[{requires}]\npkg/[>0.0]@user/testing"}) client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") client.run("lock create consumer/conanfile.txt") assert "pkg/0.1@user/testing#" in client.out client.run("create pkg --name=pkg --version=0.2 --user=user --channel=testing") client.run("install consumer/conanfile.txt") assert "pkg/0.1@user/testing#" in client.out assert "pkg/0.2" not in client.out os.remove(os.path.join(client.current_folder, "consumer/conan.lock")) client.run("install consumer/conanfile.txt") assert "pkg/0.2@user/testing#" in client.out assert "pkg/0.1" not in client.out @pytest.mark.parametrize("command", ["install", "create", "graph info", "export-pkg"]) def test_lockfile_out(command): # Check that lockfile out is generated for different commands c = TestClient(light=True) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/[*]")}) c.run("create dep") c.run(f"{command} pkg --lockfile-out=conan.lock") lock = c.load("conan.lock") assert "dep/0.1" in lock def test_lockfile_out_export(): # Check that lockfile out is generated for "conan export" c = TestClient(light=True) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export pkg --lockfile-out=conan.lock") lock = c.load("conan.lock") assert "pkg/0.1" in lock @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_deps_ranges_transitive(requires): """ conanfile.txt locking it dependencies and its transitive dependencies (with version ranges) """ client = TestClient(light=True) client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": GenConanfile().with_requires("dep/[>0.0]@user/testing"), "consumer/conanfile.txt": f"[{requires}]\npkg/[>0.0]@user/testing"}) client.run("create dep --name=dep --version=0.1 --user=user --channel=testing") client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") client.run("lock create consumer/conanfile.txt") assert "dep/0.1@user/testing#" in client.out assert "pkg/0.1@user/testing#" in client.out client.run("create dep --name=dep --version=0.2 --user=user --channel=testing") client.run("install consumer/conanfile.txt") assert "dep/0.1@user/testing#" in client.out assert "dep/0.2" not in client.out os.remove(os.path.join(client.current_folder, "consumer/conan.lock")) client.run("install consumer/conanfile.txt", assert_error=True) assert "dep/0.2@user/testing#" in client.out assert "dep/0.1" not in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_strict(requires): """ conanfile.txt locking it dependencies (with version ranges) """ client = TestClient(light=True) client.save({"pkg/conanfile.py": GenConanfile(), "consumer/conanfile.txt": f"[{requires}]\npkg/[>0.0]@user/testing"}) client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") client.run("lock create consumer/conanfile.txt") assert "pkg/0.1@user/testing#" in client.out client.run("create pkg --name=pkg --version=0.2 --user=user --channel=testing") client.run("create pkg --name=pkg --version=1.2 --user=user --channel=testing") # Not strict mode works client.save({"consumer/conanfile.txt": f"[{requires}]\npkg/[>1.0]@user/testing"}) client.run("install consumer/conanfile.txt", assert_error=True) kind = "build_requires" if requires == "tool_requires" else "requires" assert f"Requirement 'pkg/[>1.0]@user/testing' not in lockfile '{kind}'" in client.out client.run("install consumer/conanfile.txt --lockfile-partial") assert "pkg/1.2@user/testing" in client.out assert "pkg/1.2" not in client.load("consumer/conan.lock") # test it is possible to capture new changes too, when not strict, mutating the lockfile client.run("install consumer/conanfile.txt --lockfile-partial --lockfile-out=conan.lock") assert "pkg/1.2@user/testing" in client.out lock = client.load("conan.lock") assert "pkg/1.2" in lock assert "pkg/0.1" in lock # both versions are locked now # clean legacy versions client.run("lock create consumer/conanfile.txt --lockfile-out=conan.lock --lockfile-clean") lock = client.load("conan.lock") assert "pkg/1.2" in lock assert "pkg/0.1" not in lock @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conditional_os(requires): """ conanfile.txt can lock conditional dependencies (conditional on OS for example), with consecutive calls to "conan lock create", augmenting the lockfile """ client = TestClient(light=True) pkg_conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): if self.settings.os == "Windows": self.requires("win/[>0.0]") else: self.requires("nix/[>0.0]") """) client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": pkg_conanfile, "consumer/conanfile.txt": f"[{requires}]\npkg/0.1"}) client.run("create dep --name=win --version=0.1") client.run("create dep --name=nix --version=0.1") client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("lock create consumer/conanfile.txt --lockfile-out=consumer.lock -s os=Windows" " -s:b os=Windows") assert "win/0.1#" in client.out assert "pkg/0.1#" in client.out client.run("lock create consumer/conanfile.txt --lockfile=consumer.lock " "--lockfile-out=consumer.lock -s os=Linux -s:b os=Linux") assert "nix/0.1#" in client.out assert "pkg/0.1#" in client.out # New dependencies will not be used if using the lockfile client.run("create dep --name=win --version=0.2") client.run("create dep --name=nix --version=0.2") client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("install consumer --lockfile=consumer.lock -s os=Windows -s:b os=Windows") assert "win/0.1#" in client.out assert "win/0.2" not in client.out client.run("install consumer -s os=Windows -s:b os=Windows") assert "win/0.2#" in client.out assert "win/0.1" not in client.out assert "nix/0.1" not in client.out client.run("install consumer --lockfile=consumer.lock -s os=Linux -s:b os=Linux") assert "nix/0.1#" in client.out assert "nix/0.2" not in client.out client.run("install consumer -s os=Linux -s:b os=Linux") assert "nix/0.2#" in client.out assert "nix/0.1" not in client.out assert "win/" not in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conditional_same_package(requires): # What happens when a conditional requires different versions of the same package? client = TestClient(light=True) pkg_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): if self.settings.os == "Windows": self.requires("dep/0.1") else: self.requires("dep/0.2") """) client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": pkg_conanfile, "consumer/conanfile.txt": f"[{requires}]\npkg/0.1"}) client.run("create dep --name=dep --version=0.1") client.run("create dep --name=dep --version=0.2") client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("lock create consumer/conanfile.txt --lockfile-out=conan.lock -s os=Windows" " -s:b os=Windows") assert "dep/0.1#" in client.out assert "dep/0.2" not in client.out client.run("lock create consumer/conanfile.txt --lockfile=conan.lock " "--lockfile-out=conan.lock -s os=Linux -s:b os=Linux") assert "dep/0.2#" in client.out assert "dep/0.1" not in client.out client.run("install consumer --lockfile=conan.lock --lockfile-out=win.lock -s os=Windows" " -s:b os=Windows") assert "dep/0.1#" in client.out assert "dep/0.2" not in client.out client.run("install consumer --lockfile=conan.lock --lockfile-out=linux.lock -s os=Linux" " -s:b os=Linux") assert "dep/0.2#" in client.out assert "dep/0.1" not in client.out @pytest.mark.parametrize("requires", ["requires", "build_requires"]) def test_conditional_incompatible_range(requires): client = TestClient(light=True) pkg_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): if self.settings.os == "Windows": self.requires("dep/[<1.0]") else: self.requires("dep/[>=1.0]") """) client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": pkg_conanfile, "consumer/conanfile.txt": "[requires]\npkg/0.1"}) client.run("create dep --name=dep --version=0.1") client.run("create dep --name=dep --version=1.1") client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("lock create consumer/conanfile.txt --lockfile-out=conan.lock -s os=Windows" " -s:b os=Windows") assert "dep/0.1#" in client.out assert "dep/1.1" not in client.out # The previous lock was locking dep/0.1. This new lock will not use dep/0.1 as it is outside # of its range, can't lock to it and will depend on dep/1.1. Both dep/0.1 for Windows and # dep/1.1 for Linux now coexist in the lock client.run("lock create consumer/conanfile.txt --lockfile=conan.lock " "--lockfile-out=conan.lock -s os=Linux -s:b os=Linux") assert "dep/1.1#" in client.out assert "dep/0.1" not in client.out lock = client.load("conan.lock") assert "dep/0.1" in lock assert "dep/1.1" in lock # These will not be used, lock will avoid them client.run("create dep --name=dep --version=0.2") client.run("create dep --name=dep --version=1.2") client.run("install consumer --lockfile=conan.lock --lockfile-out=win.lock -s os=Windows" " -s:b os=Windows") assert "dep/0.1#" in client.out assert "dep/1.1" not in client.out client.run("install consumer --lockfile=conan.lock --lockfile-out=linux.lock -s os=Linux" " -s:b os=Linux") assert "dep/1.1#" in client.out assert "dep/0.1" not in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conditional_compatible_range(requires): client = TestClient(light=True) pkg_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): if self.settings.os == "Windows": self.requires("dep/[<0.2]") else: self.requires("dep/[>0.0]") """) client.save({"dep/conanfile.py": GenConanfile(), "pkg/conanfile.py": pkg_conanfile, "consumer/conanfile.txt": f"[{requires}]\npkg/0.1"}) client.run("create dep --name=dep --version=0.1") client.run("create dep --name=dep --version=0.2") client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("lock create consumer/conanfile.txt --lockfile-out=conan.lock -s os=Linux" " -s:b os=Linux") assert "dep/0.2#" in client.out assert "dep/0.1" not in client.out client.run("lock create consumer/conanfile.txt --lockfile=conan.lock " "--lockfile-out=conan.lock -s os=Windows -s:b os=Windows") assert "dep/0.1#" in client.out assert "dep/0.2" not in client.out # These will not be used, lock will avoid them client.run("create dep --name=dep --version=0.1.1") client.run("create dep --name=dep --version=0.3") client.run("install consumer --lockfile=conan.lock --lockfile-out=win.lock -s os=Windows" " -s:b os=Windows") assert "dep/0.1#" in client.out assert "dep/0.2" not in client.out assert "dep/0.1.1" not in client.out client.run("install consumer --lockfile=conan.lock --lockfile-out=linux.lock -s os=Linux " " -s:b os=Linux") assert "dep/0.2#" in client.out assert "dep/0.1" not in client.out assert "dep/0.3" not in client.out def test_partial_lockfile(): """ make sure that a partial lockfile can be applied anywhere downstream without issues, as lockfiles by default are not strict """ c = TestClient(light=True) c.save({"pkga/conanfile.py": GenConanfile("pkga"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/[*]"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkgb/[*]"), "app/conanfile.py": GenConanfile("app", "0.1").with_requires("pkgc/[*]")}) c.run("create pkga --version=0.1") c.run("lock create pkgb --lockfile-out=b.lock") c.run("create pkga --version=0.2") c.run("create pkgb --lockfile=b.lock") assert "pkga/0.1" in c.out assert "pkga/0.2" not in c.out c.run("install pkgc --lockfile=b.lock --lockfile-partial") assert "pkga/0.1" in c.out assert "pkga/0.2" not in c.out c.run("create pkgc --lockfile=b.lock --lockfile-partial") assert "pkga/0.1" in c.out assert "pkga/0.2" not in c.out c.run("create app --lockfile=b.lock --lockfile-partial") assert "pkga/0.1" in c.out assert "pkga/0.2" not in c.out c.run("create app --lockfile=b.lock", assert_error=True) assert "ERROR: Requirement 'pkgc/[*]' not in lockfile" in c.out def test_ux_defaults(): # Make sure the when explicit ``--lockfile`` argument, the file must exist, even if is conan.lock c = TestClient(light=True) c.save({"conanfile.txt": ""}) c.run("install . --lockfile=conan.lock", assert_error=True) assert "ERROR: Lockfile doesn't exist" in c.out class TestLockTestPackage: @pytest.fixture() def client(self): c = TestClient() test_package = textwrap.dedent(""" from conan import ConanFile class TestPackageConan(ConanFile): settings = "os", "compiler", "arch", "build_type" def build_requirements(self): self.tool_requires("cmake/[*]") def requirements(self): self.requires(self.tested_reference_str) def test(self): print("package tested") """) c.save({"cmake/conanfile.py": GenConanfile("cmake"), "dep/conanfile.py": GenConanfile("dep"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("dep/[*]"), "app/test_package/conanfile.py": test_package}) c.run("create cmake --version=1.0") c.run("create dep --version=1.0") return c def test_lock_tool_requires_test(self, client): # https://github.com/conan-io/conan/issues/11763 c = client with c.chdir("app"): c.run("lock create") lock = c.load("conan.lock") assert "cmake/1.0" not in lock assert "dep/1.0" in lock c.run("lock create test_package --lockfile=conan.lock --lockfile-out=conan.lock") lock = c.load("conan.lock") assert "cmake/1.0" in lock assert "dep/1.0" in lock c.run("create cmake --version=2.0") c.run("create dep --version=2.0") with c.chdir("app"): c.run("create . --lockfile=conan.lock") assert "cmake/1.0" in c.out assert "dep/1.0" in c.out assert "cmake/2.0" not in c.out assert "dep/2.0" not in c.out assert "package tested" in c.out def test_partial_approach(self, client): """ do not include it in the lockfile, but apply it partially, so the tool_require is free, will freely upgrade """ c = client # https://github.com/conan-io/conan/issues/11763 with c.chdir("app"): c.run("lock create .") lock = c.load("conan.lock") assert "cmake/1.0" not in lock assert "dep/1.0" in lock c.run("create cmake --version=2.0") c.run("create dep --version=2.0") with c.chdir("app"): c.run("create . --lockfile=conan.lock --lockfile-partial") assert "cmake/2.0" in c.out # because it is in test_package and not locked assert "dep/1.0" in c.out assert "cmake/1.0" not in c.out assert "dep/2.0" not in c.out assert "package tested" in c.out # or to be more guaranteed with c.chdir("app"): c.run("create . --lockfile=conan.lock -tf=\"\"") assert "cmake" not in c.out assert "dep/1.0" in c.out assert "dep/2.0" not in c.out assert "package tested" not in c.out c.run("test test_package app/1.0 --lockfile=conan.lock --lockfile-partial") assert "cmake/1.0" not in c.out assert "cmake/2.0" in c.out assert "dep/1.0" in c.out assert "dep/2.0" not in c.out assert "package tested" in c.out def test_create_lock_tool_requires_test(self, client): """ same as above, but the full lockfile including the "test_package" can be obtained with a "conan test" """ c = client with c.chdir("app"): c.run("create . --lockfile-out=conan.lock -tf=") lock = c.load("conan.lock") assert "cmake/1.0" not in lock assert "dep/1.0" in lock c.run("test test_package app/1.0 --lockfile-partial --lockfile=conan.lock " "--lockfile-out=conan.lock") lock = c.load("conan.lock") assert "cmake/1.0" in lock assert "dep/1.0" in lock c.run("create cmake --version=2.0") c.run("create dep --version=2.0") with c.chdir("app"): c.run("create . --lockfile=conan.lock") assert "cmake/1.0" in c.out assert "dep/1.0" in c.out assert "cmake/2.0" not in c.out assert "dep/2.0" not in c.out assert "package tested" in c.out def test_test_package_lockfile(self): c = TestClient(light=True) test = textwrap.dedent(""" from conan import ConanFile class TestBasicConanfile(ConanFile): def requirements(self): self.requires(self.tested_reference_str) self.requires("pkga/1.0") def test(self): pass """) c.save({"pkga/conanfile.py": GenConanfile("pkga", "1.0"), "pkgb/conanfile.py": GenConanfile("pkgb", "1.0"), "pkgb/test_package/conanfile.py": test}) c.run("create pkga") c.run("lock create pkgb") # alternative 1, relax lockfile c.run("create pkgb --lockfile-partial") # alternative 2, do not run test_package with same lockfile c.run("create pkgb --test-folder=") # the test_package can be tested later, so the lockfile-partial only affects the test_package c.run("test pkgb/test_package pkgb/1.0 --lockfile=pkgb/conan.lock --lockfile-partial") assert "Using lockfile:" in c.out # alternative 3, create the lockfile in the test_package c.run("lock create pkgb/test_package --lockfile=pkgb/conan.lock " "--lockfile-out=pkgb/test_package/conan.lock") lockfile = c.load("pkgb/test_package/conan.lock") assert "pkga/1.0" in lockfile c.run("test pkgb/test_package pkgb/1.0 --lockfile=pkgb/test_package/conan.lock " "--lockfile-partial") # alternative 4, add the test_package dependencies to the main lockfile c.run("lock create pkgb/test_package --lockfile=pkgb/conan.lock " "--lockfile-out=pkgb/conan.lock") lockfile = c.load("pkgb/conan.lock") assert "pkga/1.0" in lockfile c.run("create pkgb --lockfile=pkgb/conan.lock") class TestErrorDuplicates: def test_error_duplicates(self): """ the problem is having 2 different, almost identical requires that will point to the same thing, with different traits and not colliding. Lockfiles do a ``require.ref`` update and that alters some dictionaries iteration, producing an infinite loop and blocking """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("dep/0.1#f8c2264d0b32a4c33f251fe2944bb642", headers=False, libs=False, visible=False) self.requires("dep/0.1", headers=True, libs=False, visible=False) """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": pkg}) c.run("create dep --lockfile-out=conan.lock") c.run("create pkg", assert_error=True) assert "Duplicated requirement: dep/0.1" in c.out c.run("create pkg --lockfile=conan.lock", assert_error=True) assert "Duplicated requirement: dep/0.1" in c.out def test_error_duplicates_reverse(self): """ Same as above, but order requires changed """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("dep/0.1", headers=True, libs=False, visible=False) self.requires("dep/0.1#f8c2264d0b32a4c33f251fe2944bb642", headers=False, libs=False, visible=False) """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": pkg}) c.run("create dep --lockfile-out=conan.lock") c.run("create pkg", assert_error=True) assert "Duplicated requirement: dep/0.1" in c.out c.run("create pkg --lockfile=conan.lock", assert_error=True) assert "Duplicated requirement: dep/0.1" in c.out def test_error_duplicates_revisions(self): """ 2 different revisions can be added without conflict, if they are not visible and not other conflicting traits """ c = TestClient(light=True) pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def requirements(self): self.requires("dep/0.1#f8c2264d0b32a4c33f251fe2944bb642", headers=False, libs=False, visible=False) self.requires("dep/0.1#7b91e6100797b8b012eb3cdc5544800b", headers=True, libs=False, visible=False) """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "dep2/conanfile.py": GenConanfile("dep", "0.1").with_class_attribute("potato=42"), "pkg/conanfile.py": pkg}) c.run("create dep --lockfile-out=conan.lock") c.run("create dep2 --lockfile=conan.lock --lockfile-out=conan.lock") c.run("create pkg") assert "dep/0.1#f8c2264d0b32a4c33f251fe2944bb642 - Cache" in c.out assert "dep/0.1#7b91e6100797b8b012eb3cdc5544800b - Cache" in c.out c.run("create pkg --lockfile=conan.lock") assert "dep/0.1#f8c2264d0b32a4c33f251fe2944bb642 - Cache" in c.out assert "dep/0.1#7b91e6100797b8b012eb3cdc5544800b - Cache" in c.out def test_revision_timestamp(): """ https://github.com/conan-io/conan/issues/14108 """ c = TestClient(default_server_user=True) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1")}) # revision 0 c.run("export pkg") c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("_my=1")}) # revision 1 c.run("export pkg") rrev = c.exported_recipe_revision() c.run("upload *#* -r=default -c") c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("_my=2")}) # revision 2 time.sleep(1) c.run("export pkg") latest_rrev = c.exported_recipe_revision() c.run("upload * -r=default -c") c.run("remove * -c") c.run("list *#* -r=default --format=json") list_json = json.loads(c.stdout) server_timestamp = list_json["default"]["pkg/0.1"]["revisions"][latest_rrev]["timestamp"] time.sleep(2) ref = RecipeReference.loads(f"pkg/0.1#{rrev}") # we force the lock to include the 2nd revision c.save({"conanfile.txt": f"[requires]\n{repr(ref)}"}, clean_first=True) c.run("lock create .") lock = c.load("conan.lock") lock = json.loads(lock) locked_ref = RecipeReference.loads(lock["requires"][0]) assert locked_ref == ref assert locked_ref.timestamp and locked_ref.timestamp != server_timestamp class TestLockfileUpdate: """ Check that --update works """ @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_deps_ranges(self, requires): """ conanfile.txt locking it dependencies (with version ranges) """ c = TestClient(default_server_user=True) c.save({"pkg/conanfile.py": GenConanfile("pkg"), "consumer/conanfile.txt": f"[{requires}]\npkg/[>0.0]"}) c.run("create pkg --version=0.1") c.run("create pkg --version=0.2") c.run("upload pkg/0.2 -r=default -c") c.run("remove pkg/0.2 -c") c.run("list *") assert "pkg/0.2" not in c.out c.run("lock create consumer/conanfile.txt --update") assert "pkg/0.1" not in c.out assert "pkg/0.2" in c.out lock = c.load("consumer/conan.lock") assert "pkg/0.1" not in lock assert "pkg/0.2" in lock def test_error_test_explicit(): # https://github.com/conan-io/conan/issues/14833 client = TestClient(light=True) test = GenConanfile().with_test("pass") client.save({"conanfile.py": GenConanfile("pkg", "0.1"), "test_package/conanfile.py": test}) client.run("lock create conanfile.py --lockfile-out=my.lock") client.run("create . --lockfile=my.lock") def test_lock_error_create(): # https://github.com/conan-io/conan/issues/15801 c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_package_type("build-scripts")}) c.run("lock create . -u --lockfile-out=none.lock") lock = json.loads(c.load("none.lock")) assert lock["requires"] == [] assert lock["build_requires"] == [] c.run("create . --lockfile=none.lock --lockfile-out=none_updated.lock") # It doesn't crash, it used to lock = json.loads(c.load("none_updated.lock")) assert lock["requires"] == [] assert len(lock["build_requires"]) == 1 assert "pkg/0.1#4e9dba5c3041ba4c87724486afdb7eb4" in lock["build_requires"][0] def test_lock_error(): # https://github.com/conan-io/conan/issues/17363 c = TestClient() transitive_dep = textwrap.dedent(""" # recipes/transitive_dep/conanfile.py from conan import ConanFile class TransitiveDepConan(ConanFile): name = "transitive_dep" version = "2.0.0" settings = "build_type" """) build_tool = textwrap.dedent(""" # recipes/build_tool/conanfile.py from conan import ConanFile class BuildToolConan(ConanFile): name = "build_tool" version = "1.0.0" settings = "build_type" def requirements(self): self.requires("transitive_dep/2.0.0") """) build_tool_test = textwrap.dedent(""" # recipes/build_tool/test_package/conanfile.py from conan import ConanFile class TestPackageConan(ConanFile): settings = "build_type" def requirements(self): self.requires(self.tested_reference_str) def test(self): pass """) runtime_dep = textwrap.dedent(""" # recipes/runtime_dep/conanfile.py from conan import ConanFile class RuntimeDepConan(ConanFile): name = "runtime_dep" version = "1.2.3" settings = "build_type" def build_requirements(self): self.tool_requires("build_tool/1.0.0") """) consumer = textwrap.dedent(""" # recipes/consumer/conanfile.py from conan import ConanFile class ConsumerConan(ConanFile): name = "consumer" version = "0.0.1" settings = "build_type" def requirements(self): self.requires("runtime_dep/1.2.3") """) c.save({"recipes/transitive_dep/conanfile.py": transitive_dep, "recipes/build_tool/conanfile.py": build_tool, "recipes/build_tool/test_package/conanfile.py": build_tool_test, "recipes/runtime_dep/conanfile.py": runtime_dep, "recipes/consumer/conanfile.py": consumer, }) c.run("export recipes/transitive_dep") c.run("export recipes/build_tool") c.run("export recipes/runtime_dep") settings = "-s:b build_type=Release -s:h build_type=Debug" c.run(f"lock create recipes/consumer --no-remote {settings}") c.run(f"graph build-order recipes/consumer {settings} --build=missing --order-by=configuration " "--reduce --format=json", redirect_stdout="build_order.json") assert "Using lockfile:" in c.out ref = "transitive_dep/2.0.0" c.run(f"install --tool-requires={ref} {settings} --build={ref} " "--lockfile=recipes/consumer/conan.lock ") # The test of ``trantisive_dep`` doesn't have the test_package ref = "build_tool/1.0.0" c.run(f"install --tool-requires={ref} {settings} --build={ref} " "--lockfile=recipes/consumer/conan.lock ") ref = "runtime_dep/1.2.3" c.run(f"install --requires={ref} {settings} --build={ref} " "--lockfile=recipes/consumer/conan.lock ") ================================================ FILE: test/integration/lockfile/test_lock_requires_revisions.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_deps_revisions(requires): """ conanfile.txt locking it dependencies (with revisions) """ client = TestClient(light=True) client.save({"pkg/conanfile.py": GenConanfile().with_package_id("self.output.info('REV1!!!!')"), "consumer/conanfile.txt": f"[{requires}]\npkg/0.1@user/testing"}) client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") assert "REV1!!!" in client.out client.run("lock create consumer/conanfile.txt --lockfile-out=consumer.lock") assert "pkg/0.1@user/testing#" in client.out client.save({"pkg/conanfile.py": GenConanfile().with_package_id("self.output.info('REV2!!!!')")}) client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") assert "REV2!!!" in client.out client.run("install consumer/conanfile.txt --lockfile=consumer.lock") assert "REV1!!!" in client.out assert "REV2!!!" not in client.out client.run("install consumer/conanfile.txt") assert "REV2!!!" in client.out assert "REV1!!!" not in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) @pytest.mark.parametrize("req_version", ["0.1", "[>=0.0]"]) def test_conanfile_txt_deps_revisions_transitive(requires, req_version): """ conanfile.txt locking it dependencies and its transitive dependencies (with revisions) """ client = TestClient(light=True) client.save({"dep/conanfile.py": GenConanfile().with_package_id("self.output.info('REV1!!!!')"), "pkg/conanfile.py": GenConanfile().with_requires(f"dep/{req_version}@user/testing"), "consumer/conanfile.txt": f"[{requires}]\npkg/{req_version}@user/testing"}) client.run("create dep --name=dep --version=0.1 --user=user --channel=testing") assert "REV1!!!" in client.out client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") client.run("lock create consumer/conanfile.txt --lockfile-out=consumer.lock") assert "dep/0.1@user/testing#" in client.out assert "pkg/0.1@user/testing#" in client.out client.save({"dep/conanfile.py": GenConanfile().with_package_id("self.output.info('REV2!!!!')")}) client.run("create dep --name=dep --version=0.1 --user=user --channel=testing") assert "REV2!!!" in client.out client.run("install consumer/conanfile.txt --lockfile=consumer.lock") assert "REV1!!!" in client.out assert "REV2!!!" not in client.out client.run("list dep/0.1@user/testing#*") client.run("install consumer/conanfile.txt") assert "REV2!!!" in client.out assert "REV1!!!" not in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_strict_revisions(requires): """ conanfile.txt locking it dependencies (with version ranges) """ client = TestClient(light=True) client.save({"pkg/conanfile.py": GenConanfile().with_package_id("self.output.info('REV1!!!!')"), "consumer/conanfile.txt": f"[{requires}]\npkg/0.1@user/testing"}) client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") client.run("lock create consumer/conanfile.txt") assert "pkg/0.1@user/testing#" in client.out client.save({"pkg/conanfile.py": GenConanfile().with_package_id("self.output.info('REV2!!!!')")}) client.run("create pkg --name=pkg --version=0.1 --user=user --channel=testing") rrev = client.exported_recipe_revision() # Not strict mode works client.save({"consumer/conanfile.txt": f"[{requires}]\npkg/0.1@user/testing#{rrev}"}) client.run("install consumer/conanfile.txt", assert_error=True) assert f"Requirement 'pkg/0.1@user/testing#{rrev}' not in lockfile" in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conditional_os(requires): """ conanfile.txt can lock conditional dependencies (conditional on OS for example), with consecutive calls to "conan lock create", augmenting the lockfile """ client = TestClient(light=True) pkg_conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): if self.settings.os == "Windows": self.requires("win/0.1") else: self.requires("nix/0.1") """) client.save({"dep/conanfile.py": GenConanfile().with_package_id("self.output.info('REV1!!!!')"), "pkg/conanfile.py": pkg_conanfile, "consumer/conanfile.txt": f"[{requires}]\npkg/0.1"}) client.run("create dep --name=win --version=0.1") client.run("create dep --name=nix --version=0.1") client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("lock create consumer/conanfile.txt --lockfile-out=consumer.lock -s os=Windows" " -s:b os=Windows") assert "win/0.1#" in client.out assert "pkg/0.1#" in client.out client.run("lock create consumer/conanfile.txt --lockfile=consumer.lock " "--lockfile-out=consumer.lock -s os=Linux -s:b os=Linux") assert "nix/0.1#" in client.out assert "pkg/0.1#" in client.out # New dependencies will not be used if using the lockfile client.save({"dep/conanfile.py": GenConanfile().with_package_id("self.output.info('REV2!!!!')")}) client.run("create dep --name=win --version=0.1") client.run("create dep --name=nix --version=0.1") client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("install consumer --lockfile=consumer.lock -s os=Windows -s:b os=Windows") assert "REV1!!!" in client.out assert "REV2!!!" not in client.out assert "nix/0.1" not in client.out client.run("install consumer -s os=Windows -s:b os=Windows") assert "REV2!!!" in client.out assert "REV1!!!" not in client.out assert "nix/0.1" not in client.out client.run("install consumer --lockfile=consumer.lock -s os=Linux -s:b os=Linux") assert "REV1!!!" in client.out assert "REV2!!!" not in client.out assert "win/0.1" not in client.out client.run("install consumer -s os=Linux -s:b os=Linux") assert "REV2!!!" in client.out assert "REV1!!!" not in client.out assert "win/0.1" not in client.out @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conditional_same_package_revisions(requires): # What happens when a conditional requires different versions of the same package? client = TestClient(light=True) pkg_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def requirements(self): if self.settings.os == "Windows": self.requires("dep/0.1#{}") else: self.requires("dep/0.1#{}") """) client.save({"dep1/conanfile.py": GenConanfile().with_package_id("self.output.info('REV1!!!!')"), "dep2/conanfile.py": GenConanfile().with_package_id("self.output.info('REV2!!!!')"), "pkg/conanfile.py": pkg_conanfile, "consumer/conanfile.txt": f"[{requires}]\npkg/0.1"}) client.run("create dep1 --name=dep --version=0.1") rrev1 = client.exported_recipe_revision() client.run("create dep2 --name=dep --version=0.1") rrev2 = client.exported_recipe_revision() client.save({"pkg/conanfile.py": pkg_conanfile.format(rrev1, rrev2)}) client.run("create pkg --name=pkg --version=0.1 -s os=Windows") client.run("create pkg --name=pkg --version=0.1 -s os=Linux") client.run("lock create consumer/conanfile.txt --lockfile-out=conan.lock -s os=Windows" " -s:b os=Windows") assert "REV1!!!" in client.out assert "REV2!!!" not in client.out client.run("lock create consumer/conanfile.txt --lockfile=conan.lock " "--lockfile-out=conan.lock -s os=Linux -s:b os=Linux") assert "REV2!!!" in client.out assert "REV1!!!" not in client.out client.run("install consumer --lockfile=conan.lock --lockfile-out=win.lock -s os=Windows" " -s:b os=Windows") assert "REV1!!!" in client.out assert "REV2!!!" not in client.out client.run("install consumer --lockfile=conan.lock --lockfile-out=linux.lock -s os=Linux" " -s:b os=Linux") assert "REV2!!!" in client.out assert "REV1!!!" not in client.out ================================================ FILE: test/integration/lockfile/test_options.py ================================================ import json import textwrap from conan.test.utils.tools import TestClient def test_options(): """ lockfiles no longer contains option or any other configuration information. Instead the ``graph build-order`` applying a lockfile will return the necessary options to build it in order """ client = TestClient(light=True) ffmpeg = textwrap.dedent(""" from conan import ConanFile class FfmpegConan(ConanFile): options = {"variation": ["standard", "nano"]} default_options = {"variation": "standard"} def build(self): self.output.info("Variation %s!!" % self.options.variation) """) variant = textwrap.dedent(""" from conan import ConanFile class Meta(ConanFile): requires = "ffmpeg/1.0" default_options = {"ffmpeg/1.0:variation": "nano"} """) client.save({"ffmepg/conanfile.py": ffmpeg, "variant/conanfile.py": variant}) client.run("export ffmepg --name=ffmpeg --version=1.0") client.run("export variant --name=nano --version=1.0") client.run("lock create --requires=nano/1.0@ --build=*") client.run("graph build-order --requires=nano/1.0@ " "--lockfile-out=conan.lock --build=missing " "--format=json", redirect_stdout="build_order.json") json_file = client.load("build_order.json") to_build = json.loads(json_file) ffmpeg = to_build[0][0] ref = ffmpeg["ref"] options = " ".join(f"-o {option}" for option in ffmpeg["packages"][0][0]["options"]) cmd = "install --requires={} --build={} {}".format(ref, ref, options) client.run(cmd) assert "ffmpeg/1.0: Variation nano!!" in client.out ================================================ FILE: test/integration/lockfile/test_user_overrides.py ================================================ import json import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_user_overrides(): """ Show that it is possible to add things to lockfiles, to pre-lock things explicitly from user side """ c = TestClient(light=True) c.save({"math/conanfile.py": GenConanfile("math"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("math/[*]"), "game/conanfile.py": GenConanfile("game", "1.0").with_requires("engine/[*]")}) c.run("export math --version=1.0") c.run("export math --version=1.1") c.run("export math --version=1.2") c.run("export engine") c.run("graph info game") assert "math/1.2" in c.out assert "math/1.0" not in c.out c.run("lock add --requires=math/1.0 --requires=unrelated/2.0") c.run("graph info game --lockfile=conan.lock --lockfile-out=new.lock --lockfile-partial") assert "math/1.0" in c.out assert "math/1.2" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert "math/1.0#8e1a7a5ce869d8c54ae3d33468fd657" in new_lock # Repeat for 1.1 c.run("lock add --requires=math/1.1 --requires=unrelated/2.0") c.run("graph info game --lockfile=conan.lock --lockfile-partial --lockfile-out=new.lock") assert "math/1.1" in c.out assert "math/1.0" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert "math/1.1#8e1a7a5ce869d8c54ae3d33468fd657" in new_lock def test_user_build_overrides(): """ Test that it is possible to lock also build-requries """ c = TestClient(light=True) c.save({"cmake/conanfile.py": GenConanfile("cmake"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_build_requires("cmake/[*]")}) c.run("export cmake --version=1.0") c.run("export cmake --version=1.1") c.run("export cmake --version=1.2") c.run("graph info engine") assert "cmake/1.2" in c.out assert "cmake/1.0" not in c.out c.run("lock add --build-requires=cmake/1.0") c.run("graph info engine --lockfile=conan.lock --lockfile-out=new.lock --lockfile-partial") assert "cmake/1.0" in c.out assert "cmake/1.2" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert "cmake/1.0" in new_lock # Repeat for 1.1 c.run("lock add --build-requires=cmake/1.1 --lockfile-out=conan.lock") c.run("graph info engine --lockfile=conan.lock --lockfile-out=new.lock --lockfile-partial") assert "cmake/1.1" in c.out assert "cmake/1.0" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert "cmake/1.1" in new_lock def test_user_python_overrides(): """ Test that it is possible to lock also python-requries """ c = TestClient(light=True) c.save({"pytool/conanfile.py": GenConanfile("pytool"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_python_requires("pytool/[*]")}) c.run("export pytool --version=1.0") c.run("export pytool --version=1.1") c.run("export pytool --version=1.2") c.run("graph info engine") assert "pytool/1.2" in c.out assert "pytool/1.0" not in c.out c.run("lock add --python-requires=pytool/1.0 --lockfile-out=conan.lock") c.run("graph info engine --lockfile=conan.lock --lockfile-out=new.lock") assert "pytool/1.0" in c.out assert "pytool/1.2" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert "pytool/1.0" in new_lock # Repeat for 1.1 c.run("lock add --python-requires=pytool/1.1 --lockfile-out=conan.lock") c.run("graph info engine --lockfile=conan.lock --lockfile-out=new.lock") assert "pytool/1.1" in c.out assert "pytool/1.0" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert "pytool/1.1" in new_lock def test_config_overrides(): """ Test that it is possible to lock also config-requires """ c = TestClient(light=True) c.run("lock add --config-requires=config/1.0") assert json.loads(c.load("conan.lock"))["config_requires"] == ["config/1.0"] c.run("lock remove --config-requires=config/1.0") assert json.loads(c.load("conan.lock"))["config_requires"] == [] def test_add_revisions(): """ Is it possible to add revisions explicitly too """ c = TestClient(light=True) c.save({"math/conanfile.py": GenConanfile("math"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("math/[*]"), "game/conanfile.py": GenConanfile("game", "1.0").with_requires("engine/[*]")}) c.run("export math --version=1.0") rev0 = c.exported_recipe_revision() c.save({"math/conanfile.py": GenConanfile("math").with_build_msg("New rev1")}) c.run("export math --version=1.0") rev1 = c.exported_recipe_revision() c.save({"math/conanfile.py": GenConanfile("math").with_build_msg("New rev2")}) c.run("export math --version=1.0") rev2 = c.exported_recipe_revision() c.run("export engine") c.run("graph info game") assert f"math/1.0#{rev2}" in c.out assert f"math/1.0#{rev1}" not in c.out # without revision, it will resolve to latest c.run("lock add --requires=math/1.0 --requires=unrelated/2.0") c.run("graph info game --lockfile=conan.lock --lockfile-out=new.lock --lockfile-partial") assert f"math/1.0#{rev2}" in c.out assert f"math/1.0#{rev1}" not in c.out assert f"math/1.0#{rev0}" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert f"math/1.0#{rev2}" in new_lock assert f"math/1.0#{rev1}" not in new_lock assert f"math/1.0#{rev0}" not in c.out # with revision, it will resolve to that revision c.run(f"lock add --requires=math/1.0#{rev1} --requires=unrelated/2.0") c.run("graph info game --lockfile=conan.lock --lockfile-out=new.lock --lockfile-partial") assert f"math/1.0#{rev1}" in c.out assert f"math/1.0#{rev2}" not in c.out assert f"math/1.0#{rev0}" not in c.out # The resulting lockfile contains the full revision now new_lock = c.load("new.lock") assert f"math/1.0#{rev1}" in new_lock assert f"math/1.0#{rev2}" not in new_lock assert f"math/1.0#{rev0}" not in c.out def test_add_multiple_revisions(): """ What if we add multiple revisions, mix with and without revisions, with and without timestamps and it will not crash """ c = TestClient(light=True) # without revision, it will resolve to latest c.run("lock add --requires=math/1.0#rev1") new_lock = c.load("conan.lock") assert "math/1.0#rev1" in new_lock c.run("lock add --requires=math/1.0#rev2") new_lock = json.loads(c.load("conan.lock")) assert ["math/1.0#rev2", "math/1.0#rev1"] == new_lock["requires"] c.run("lock add --requires=math/1.0#rev0") new_lock = json.loads(c.load("conan.lock")) assert ['math/1.0#rev2', 'math/1.0#rev1', 'math/1.0#rev0'] == new_lock["requires"] c.run("lock add --requires=math/1.0#revx%0.0") new_lock = json.loads(c.load("conan.lock")) assert ['math/1.0#revx%0.0', 'math/1.0#rev2', 'math/1.0#rev1', 'math/1.0#rev0'] == \ new_lock["requires"] c.save({"conanfile.txt": ""}) c.run("install . --lockfile=conan.lock") # Just check that it doesn't crash c.run("install . --lockfile=conan.lock --lockfile-out=new.lock") new_lock = json.loads(c.load("conan.lock")) assert ['math/1.0#revx%0.0', 'math/1.0#rev2', 'math/1.0#rev1', 'math/1.0#rev0'] == \ new_lock["requires"] # add without revision at all, will give us an error, as it doesn't make sense c.run("lock add --requires=math/1.0", assert_error=True) assert "Cannot add math/1.0 to lockfile, already exists" in c.out new_lock = json.loads(c.load("conan.lock")) assert ['math/1.0#revx%0.0', 'math/1.0#rev2', 'math/1.0#rev1', 'math/1.0#rev0'] == \ new_lock["requires"] def test_timestamps_are_updated(): """ When ``conan lock add`` adds a revision with a timestamp, or without it, it will be updated in the lockfile-out to the resolved new timestamp """ c = TestClient(light=True) c.save({"conanfile.txt": "[requires]\nmath/1.0", "math/conanfile.py": GenConanfile("math", "1.0")}) c.run("create math") rev = c.exported_recipe_revision() # Create a new lockfile, wipe the previous c.run(f"lock add --lockfile=\"\" --requires=math/1.0#{rev}%0.123") c.run("install . --lockfile=conan.lock --lockfile-out=conan.lock") assert f" math/1.0#{rev} - Cache" in c.out new_lock = c.load("conan.lock") assert "%0.123" not in new_lock def test_lock_add_error(): # https://github.com/conan-io/conan/issues/14465 c = TestClient(light=True) c.run(f"lock add --requires=math/1.0:pid1", assert_error=True) assert "ERROR: Invalid recipe reference 'math/1.0:pid1' is a package reference" in c.out class TestLockRemove: @pytest.mark.parametrize("args, removed", [ ("--requires=math/*", ["math"]), ("--requires=math/2.0", []), ("--build-requires=cmake/1.0", ["cmake"]), # Not valid ("--build-requires=*", ["cmake", "ninja"]), ("--build-requires=*/*", ["cmake", "ninja"]), # But this is valid ("--python-requires=mytool/*", ["mytool"]), ("--python-requires=*tool/*", ["mytool", "othertool"]), # With version ranges ('--requires="math/[>=1.0 <2]"', ["math"]), ('--requires="math/[>1.0]"', []), ('--requires="*/[>=1.0 <2]"', ["math", "engine"]) ]) def test_lock_remove(self, args, removed): c = TestClient(light=True) lock = textwrap.dedent("""\ { "version": "0.5", "requires": [ "math/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "math/1.0#12345%1702683584.3411012", "engine/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ], "build_requires": [ "cmake/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "ninja/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ], "python_requires": [ "mytool/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "othertool/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ] } """) c.save({"conan.lock": lock}) c.run(f"lock remove {args}") lock = c.load("conan.lock") for remove in removed: assert remove not in lock for pkg in {"math", "engine", "cmake", "ninja", "mytool", "othertool"}.difference(removed): assert pkg in lock @pytest.mark.parametrize("args, removed", [ ("--requires=math/1.0#12345*", ["math/1.0#123456789abcdef"]), ("--requires=math/1.0#*", ["math/1.0#123456789abcdef", "math/1.0#85d927a4a067a531b1a9c7619522c015"]), ]) def test_lock_remove_revisions(self, args, removed): c = TestClient(light=True) lock = textwrap.dedent("""\ { "version": "0.5", "requires": [ "math/1.0#123456789abcdef%1702683584.3411012", "math/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "engine/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ] } """) c.save({"conan.lock": lock}) c.run(f"lock remove {args}") lock = c.load("conan.lock") for remove in removed: assert remove not in lock for pkg in {"math/1.0#123456789abcdef", "math/1.0#85d927a4a067a531b1a9c7619522c015", "engine/1.0#fd2b006646a54397c16a1478ac4111ac"}.difference(removed): assert pkg in lock @pytest.mark.parametrize("args, removed", [ ("--requires=*/*@team", ["pkg/1.0@team"]), ("--requires=*/*@team*", ["pkg/1.0@team", "math/2.0@team/stable"]), ("--requires=*/*@user", ["math/1.0@user", "other/1.0@user"]), ("--requires=*/*@", ["engine/1.0"]), # Remove those without user # with version ranges ("--requires=math/[*]@user", ["math/1.0@user"]), ("--requires=math/[*]@team*", ["math/2.0@team/stable"]), ]) def test_lock_remove_user_channel(self, args, removed): c = TestClient(light=True) lock = textwrap.dedent("""\ { "version": "0.5", "requires": [ "math/1.0@user#123456789abcdef%1702683584.3411012", "math/2.0@team/stable#123456789abcdef%1702683584.3411012", "other/1.0@user#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "pkg/1.0@team#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "engine/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ] } """) c.save({"conan.lock": lock}) c.run(f"lock remove {args}") lock = c.load("conan.lock") for remove in removed: assert remove not in lock rest = {"math/1.0@user", "math/2.0@team/stable", "other/1.0@user", "pkg/1.0@team", "engine/1.0"}.difference(removed) for pkg in rest: assert pkg in lock class TestLockUpdate: @pytest.mark.parametrize("kind, old, new", [ ("requires", "math/1.0", "math/1.1"), ("build-requires", "cmake/1.0", "cmake/1.1"), ("python-requires", "mytool/1.0", "mytool/1.1"), ]) def test_lock_update(self, kind, old, new): c = TestClient(light=True) lock = textwrap.dedent("""\ { "version": "0.5", "requires": [ "math/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "math/1.0#12345%1702683584.3411012", "engine/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ], "build_requires": [ "cmake/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "ninja/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ], "python_requires": [ "mytool/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012", "othertool/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693" ] } """) c.save({"conan.lock": lock}) c.run(f"lock update --{kind}={new}") lock = c.load("conan.lock") assert old not in lock assert new in lock class TestLockUpgrade: @pytest.mark.parametrize("kind, pkg, old, new", [ ("requires", "math", "math/1.0", "math/1.1"), ("build-requires", "cmake", "cmake/1.0", "cmake/1.1"), # TODO there isn't a --build-requires # ("python-requires", "mytool", "mytool/1.0", "mytool/1.1"), # TODO nor a --python-requires ]) def test_lock_upgrade(self, kind, pkg, old, new): c = TestClient(light=True) c.save({f"{pkg}/conanfile.py": GenConanfile(pkg)}) c.run(f"export {pkg} --version=1.0") rev0 = c.exported_recipe_revision() kind_create = "tool-requires" if "build-requires" == kind else kind c.run(f"lock create --{kind_create}={pkg}/[*]") lock = c.load("conan.lock") assert f"{old}#{rev0}" in lock c.run(f"export {pkg} --version=1.1") rev1 = c.exported_recipe_revision() c.run(f"lock upgrade --{kind_create}={pkg}/[*] --update-{kind}={pkg}/[*]") lock = c.load("conan.lock") assert f"{old}#{rev0}" not in lock assert f"{new}#{rev1}" in lock def test_lock_upgrade_path(self): c = TestClient(light=True) c.save({"liba/conanfile.py": GenConanfile("liba"), "libb/conanfile.py": GenConanfile("libb"), "libc/conanfile.py": GenConanfile("libc"), "libd/conanfile.py": GenConanfile("libd")}) c.run(f"export liba --version=1.0") c.run(f"export libb --version=1.0") c.run(f"export libc --version=1.0") c.run(f"export libd --version=1.0") c.save( { f"conanfile.py": GenConanfile() .with_requires(f"liba/[>=1.0 <2]") .with_requires("libb/[<1.2]") .with_tool_requires("libc/[>=1.0]") .with_python_requires("libd/[>=1.0 <1.2]") } ) c.run("lock create .") lock = c.load("conan.lock") assert "liba/1.0" in lock assert "libb/1.0" in lock assert "libc/1.0" in lock assert "libd/1.0" in lock # Check versions are updated accordingly c.run(f"export liba --version=1.9") c.run(f"export libb --version=1.1") c.run(f"export libb --version=1.2") c.run(f"export libc --version=1.1") c.run(f"export libd --version=1.1") c.run("lock upgrade . --update-requires=liba/1.0 --update-requires=libb/[*] --update-build-requires=libc/[*] --update-python-requires=libd/1.0") lock = c.load("conan.lock") assert "liba/1.9" in lock assert "libb/1.1" in lock assert "libc/1.1" in lock assert "libd/1.1" in lock # Check version conanfile version range is respected c.run(f"export libd --version=1.2") c.run("lock upgrade . --update-python-requires=libd/*") lock = c.load("conan.lock") assert "libd/1.1" in lock assert "libd/1.2" not in lock def test_lock_upgrade_new_requirement(self): c = TestClient(light=True) c.save({"liba/conanfile.py": GenConanfile("liba").with_requires("libb/1.0"), "libb/conanfile.py": GenConanfile("libb").with_requires("libc/1.0"), "libc/conanfile.py": GenConanfile("libc"), "libd/conanfile.py": GenConanfile("libd")}) c.run(f"export liba --version=1.0") c.run(f"export libb --version=1.0") c.run(f"export libc --version=1.0") c.run(f"export libd --version=1.0") c.save({f"conanfile.py": GenConanfile().with_requires(f"liba/[>=1.0 <2]")}) c.run("lock create .") c.save({"libb/conanfile.py": GenConanfile("libb").with_requires("libd/1.0")}) c.run(f"export libb --version=2.0") c.run("lock upgrade --requires='libb/[>=2]' --update-requires='libb/*'") lock = c.load("conan.lock") assert "libb/2.0" in lock assert "libd/1.0" in lock assert "libc/1.0" in lock # TODO: libc should be removed from lockfile? It is not required anymore... def test_config_upgrade(self): """ Test that it is possible to lock also config-requires """ c = TestClient(light=True) c.save({"config/conanfile.py": GenConanfile("config").with_package_type("configuration"), "conanfile.py": GenConanfile("pkg", "1.0")}) c.run("create config --version=1.0") c.run("create config --version=2.0") c.run("config install-pkg config/1.0 --lockfile-out=conan.lock") def _check(refs): reqs = json.loads(c.load("conan.lock"))["config_requires"] for a, b in zip(refs, reqs): assert a in b def _check_cache(refs): reqs = json.loads(c.load_home("config_version.json"))["config_version"] for a, b in zip(refs, reqs): assert a in b _check(["config/1.0"]) _check_cache(["config/1.0"]) c.run("lock upgrade-config --requires=config/[*] --update-config-requires=config/*") _check(["config/2.0"]) _check_cache(["config/1.0"]) c.run("config install-pkg config/1.0 --lockfile=conan.lock", assert_error=True) assert "ERROR: Requirement 'config/1.0' not in lockfile 'config_requires'" in c.out c.run("config install-pkg config/2.0 --lockfile=conan.lock") _check(["config/2.0"]) _check_cache(["config/2.0"]) # Force downgrade c.run("config install-pkg config/1.0 --lockfile= --lockfile-out=conan.lock") _check(["config/1.0"]) _check_cache(["config/1.0"]) c.save({"conanconfig.yml": "packages:\n - config/[*]"}) c.run("lock upgrade-config . --update-config-requires=config/1.0") _check(["config/2.0"]) _check_cache(["config/1.0"]) c.run("config install-pkg . --lockfile=conan.lock") _check_cache(["config/2.0"]) ================================================ FILE: test/integration/metadata/__init__.py ================================================ ================================================ FILE: test/integration/metadata/test_metadata_collect.py ================================================ import os import shutil import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save collect = '''\ import os, shutil, fnmatch from conan.errors import ConanException from conan.cli.command import conan_command from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_packages, print_graph_basic # TODO, fix private API from conan.internal.graph.install_graph import InstallGraph @conan_command(group="Metadata") def collect(conan_api, parser, *args): """ command to advanced metadata """ common_graph_args(parser) parser.add_argument("-m", "--metadata", action='append', help='Download the metadata matching the pattern, even if the package is ' 'already in the cache and not downloaded') parser.add_argument("-mr", "--metadata-remote", help='Download the metadata from this remote') parser.add_argument("-of", "--output-folder", help='The root output folder for metadata') parser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided path is a build-require') args = parser.parse_args(*args) validate_common_graph_args(args) # basic paths cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None output_folder = (make_abs_path(args.output_folder, cwd) if args.output_folder else os.path.join(cwd, "metadata")) # Basic collaborators: remotes, lockfile, profiles remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) # Graph computation (without installation of binaries) gapi = conan_api.graph if path: deps_graph = gapi.load_graph_consumer(path, args.name, args.version, args.user, args.channel, profile_host, profile_build, lockfile, remotes, args.update, is_build_require=args.build_require) else: deps_graph = gapi.load_graph_requires(args.requires, args.tool_requires, profile_host, profile_build, lockfile, remotes, args.update) print_graph_basic(deps_graph) deps_graph.report_graph_error() gapi.analyze_binaries(deps_graph, args.build, remotes, update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) # Installation of binaries and consumer generators conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) # Downloading metadata metadata = args.metadata if args.metadata else ["*"] # TODO: This InstallGraph API is EXPERIMENTAL and will change in future versions install_graph = InstallGraph(deps_graph) install_order = install_graph.install_order(flat=True) if args.metadata_remote: out = ConanOutput() out.title(f"Downloading metadata from {args.metadata_remote}") remote = conan_api.remotes.get(args.metadata_remote) for install_reference in install_order: if install_reference.ref.revision is None: # Is an editable/system, do not download continue try: conan_api.download.recipe(install_reference.ref, remote, metadata) except ConanException as e: out.warning(f"Recipe {install_reference.ref} not found in remote: {e}") continue for package in install_reference.packages.values(): try: conan_api.download.package(package.pref, remote, metadata) except ConanException as e: out.warning(f"Package {package.pref} not found in remote: {e}") # Copying and collecting metadata from all packages into local copy def _copy_metadata(src, dst): for root, subfolders, files in os.walk(src): relative_path = os.path.relpath(root, src) for f in files: relative_name = os.path.normpath(os.path.join(relative_path, f)) if any(fnmatch.fnmatch(relative_name, m) for m in metadata): os.makedirs(os.path.dirname(os.path.join(dst, relative_name)), exist_ok=True) shutil.copy2(os.path.join(src, relative_name), os.path.join(dst, relative_name)) for install_reference in install_order: conanfile = install_reference.node.conanfile if conanfile.recipe_metadata_folder is None: continue folder = os.path.join(output_folder, conanfile.ref.name, str(conanfile.ref.version)) if os.path.exists(folder): conanfile.output.warning(f"Folder for {conanfile} already exist, removing it: {folder}") shutil.rmtree(folder) conanfile.output.info(f"Copying recipe metadata from {conanfile.recipe_metadata_folder}") _copy_metadata(conanfile.recipe_metadata_folder, os.path.join(folder, "recipe")) for package in install_reference.packages.values(): pkg_metadata_folder = package.conanfile.package_metadata_folder conanfile.output.info(f"Copying package metadata from {pkg_metadata_folder}") _copy_metadata(pkg_metadata_folder, os.path.join(folder, "package")) ''' def test_custom_command_collect_no_metadata(): c = TestClient(default_server_user=True) command_file_path = os.path.join(c.cache_folder, 'extensions', 'commands', 'metadata', 'cmd_collect.py') save(command_file_path, collect) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1")}) c.run("create dep") c.run("create pkg") c.run("metadata:collect --requires=pkg/0.1 --metadata=* --metadata-remote=default") # It does nothing, but it doesn't crash c.run("upload * -r=default -c") c.run("metadata:collect --requires=pkg/0.1 --metadata=* --metadata-remote=default") # It does nothing, but it doesn't crash c.run("editable add dep") c.run("metadata:collect --requires=pkg/0.1 --metadata=* --metadata-remote=default") # It does nothing, but it doesn't crash conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "{name}" version = "0.1" {requires} def layout(self): self.folders.build = "build" def source(self): save(self, os.path.join(self.recipe_metadata_folder, "logs", "src.log"), f"srclog {{self.name}}!!") def build(self): save(self, "mylogs.txt", f"some logs {{self.name}}!!!") copy(self, "mylogs.txt", src=self.build_folder, dst=os.path.join(self.package_metadata_folder, "logs")) """) def test_custom_command_collect(): c = TestClient(default_server_user=True) command_file_path = os.path.join(c.cache_folder, 'extensions', 'commands', 'metadata', 'cmd_collect.py') save(command_file_path, collect) c.save({"dep/conanfile.py": conanfile.format(name="dep", requires="tool_requires = 'tool/0.1'"), "pkg/conanfile.py": conanfile.format(name="pkg", requires='requires = "dep/0.1"'), "profile": "[platform_tool_requires]\ntool/0.1"}) c.run("create dep -pr=profile") c.run("create pkg -pr=profile") c.run("metadata:collect --requires=pkg/0.1 --metadata=* --metadata-remote=default -pr=profile") assert "srclog dep!!" in c.load("metadata/dep/0.1/recipe/logs/src.log") assert "some logs dep!!!" in c.load("metadata/dep/0.1/package/logs/mylogs.txt") assert "srclog pkg!!" in c.load("metadata/pkg/0.1/recipe/logs/src.log") assert "some logs pkg!!!" in c.load("metadata/pkg/0.1/package/logs/mylogs.txt") shutil.rmtree(os.path.join(c.current_folder, "metadata")) c.run("upload * -r=default -c") c.run("remove * -c") c.run("metadata:collect --requires=pkg/0.1 --metadata=* --metadata-remote=default -pr=profile") assert "srclog dep!!" in c.load("metadata/dep/0.1/recipe/logs/src.log") assert "some logs dep!!!" in c.load("metadata/dep/0.1/package/logs/mylogs.txt") assert "srclog pkg!!" in c.load("metadata/pkg/0.1/recipe/logs/src.log") assert "some logs pkg!!!" in c.load("metadata/pkg/0.1/package/logs/mylogs.txt") shutil.rmtree(os.path.join(c.current_folder, "metadata")) c.run("editable add dep") c.run("source dep") c.run("build dep -pr=profile") c.run("metadata:collect --requires=pkg/0.1 --metadata=* --metadata-remote=default -pr=profile") assert "srclog dep!!" in c.load("metadata/dep/0.1/recipe/logs/src.log") assert "some logs dep!!!" in c.load("metadata/dep/0.1/package/logs/mylogs.txt") assert "srclog pkg!!" in c.load("metadata/pkg/0.1/recipe/logs/src.log") assert "some logs pkg!!!" in c.load("metadata/pkg/0.1/package/logs/mylogs.txt") ================================================ FILE: test/integration/metadata/test_metadata_commands.py ================================================ import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID from conan.internal.util.files import save, load class TestMetadataCommands: @pytest.fixture def create_conan_pkg(self): client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": GenConanfile("pkg", "0.1")}) client.run("create .") pid = client.created_package_id("pkg/0.1") return client, pid @staticmethod def save_metadata_file(client, pkg_ref, filename="somefile.log", content=None): client.run(f"cache path {pkg_ref} --folder=metadata") metadata_path = str(client.stdout).strip() myfile = os.path.join(metadata_path, "logs", filename) save(myfile, f"{content or str(pkg_ref)}!!!!") return metadata_path, myfile def test_upload(self, create_conan_pkg): c, pid = create_conan_pkg # Add some metadata self.save_metadata_file(c, "pkg/0.1", "mylogs.txt") self.save_metadata_file(c, f"pkg/0.1:{pid}", "mybuildlogs.txt") # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out # Add new files to the metadata self.save_metadata_file(c, "pkg/0.1", "mylogs2.txt") self.save_metadata_file(c, f"pkg/0.1:{pid}", "mybuildlogs2.txt") # Upload the metadata, even if the revisions exist in the server # adding the new metadata logs files c.run("upload * -c -r=default --metadata=*") assert "pkg/0.1: Recipe metadata: 2 files" in c.out assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 2 files" in c.out c.run("remove * -c") c.run("install --requires=pkg/0.1") # wont install metadata by default c.run("cache path pkg/0.1 --folder=metadata") metadata_path = str(c.stdout).strip() assert os.listdir(metadata_path) == [] c.run(f"cache path pkg/0.1:{pid} --folder=metadata") metadata_path = str(c.stdout).strip() assert os.listdir(metadata_path) == [] # Forcing the download of the metadata of cache-existing things with the "download" command c.run("download pkg/0.1 -r=default --metadata=*") c.run(f"cache path pkg/0.1 --folder=metadata") metadata_path = str(c.stdout).strip() c.run(f"cache path pkg/0.1:{pid} --folder=metadata") pkg_metadata_path = str(c.stdout).strip() for f in "logs/mylogs.txt", "logs/mylogs2.txt": assert os.path.isfile(os.path.join(metadata_path, f)) for f in "logs/mybuildlogs.txt", "logs/mybuildlogs2.txt": assert os.path.isfile(os.path.join(pkg_metadata_path, f)) def test_update_contents(self): c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") # Add some metadata _, myfile = self.save_metadata_file(c, "pkg/0.1", "mylogs.txt") # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out # Update the metadata save(myfile, "mylogs2!!!!") # Upload the metadata, even if the revisions exist in the server # adding the new metadata logs files c.run("upload * -c -r=default --metadata=*") assert "pkg/0.1: Recipe metadata: 1 files" in c.out c.run("remove * -c") c.run("download pkg/0.1 -r=default --metadata=*") c.run("cache path pkg/0.1 --folder=metadata") metadata_path = str(c.stdout).strip() content = load(os.path.join(metadata_path, "logs", "mylogs.txt")) assert "mylogs2!!!!" in content def test_append_contents(self): # I can add extra files to metadata without necessarily downloading it first c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") pkgid = "da39a3ee5e6b4b0d3255bfef95601890afd80709" # Add some metadata self.save_metadata_file(c, "pkg/0.1", "mylogs.txt", content="mylogs") self.save_metadata_file(c, f"pkg/0.1:{pkgid}", "mybin.txt", content="mybin") # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out c2 = TestClient(servers=c.servers, light=True, inputs=["admin", "password"]) c2.run("install --requires=pkg/0.1") self.save_metadata_file(c2, "pkg/0.1", "mylogs2.txt", content="mylogs2") self.save_metadata_file(c2, f"pkg/0.1:{pkgid}", "mybin2.txt", content="mybin2") c2.run("upload * -c -r=default --metadata=*") assert "pkg/0.1: Recipe metadata: 1 files" in c2.out c.run("remove * -c") c.run("download pkg/0.1 -r=default --metadata=*") c.run("cache path pkg/0.1 --folder=metadata") metadata_path = str(c.stdout).strip() assert "mylogs!!!!" in load(os.path.join(metadata_path, "logs", "mylogs.txt")) assert "mylogs2!!!!" in load(os.path.join(metadata_path, "logs", "mylogs2.txt")) c.run(f"cache path pkg/0.1:{pkgid} --folder=metadata") metadata_path = str(c.stdout).strip() assert "mybin!!!!" in load(os.path.join(metadata_path, "logs", "mybin.txt")) assert "mybin2!!!!" in load(os.path.join(metadata_path, "logs", "mybin2.txt")) def test_overwrite_server_contents(self): # I can overwrite server contents c = TestClient(default_server_user=True, light=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") pkgid = "da39a3ee5e6b4b0d3255bfef95601890afd80709" # Add some metadata self.save_metadata_file(c, "pkg/0.1", "mylogs.txt", content="mylogs") self.save_metadata_file(c, f"pkg/0.1:{pkgid}", "mybin.txt", content="mybin") # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out c2 = TestClient(servers=c.servers, light=True, inputs=["admin", "password"]) c2.run("install --requires=pkg/0.1") self.save_metadata_file(c2, "pkg/0.1", "mylogs.txt", content="mylogs2") self.save_metadata_file(c2, f"pkg/0.1:{pkgid}", "mybin.txt", content="mybin2") c2.run("upload * -c -r=default --metadata=*") assert "pkg/0.1: Recipe metadata: 1 files" in c2.out c.run("remove * -c") c.run("download pkg/0.1 -r=default --metadata=*") c.run("cache path pkg/0.1 --folder=metadata") metadata_path = str(c.stdout).strip() assert "mylogs2!!!!" in load(os.path.join(metadata_path, "logs", "mylogs.txt")) c.run(f"cache path pkg/0.1:{pkgid} --folder=metadata") metadata_path = str(c.stdout).strip() assert "mybin2!!!!" in load(os.path.join(metadata_path, "logs", "mybin.txt")) def test_folder_exist(self, create_conan_pkg): """ so we can cp -R to the metadata folder, having to create the folder in the cache is weird """ c, _ = create_conan_pkg c.run("cache path pkg/0.1 --folder=metadata") metadata_path = str(c.stdout).strip() assert os.path.isdir(metadata_path) c.run(f"cache path pkg/0.1:{NO_SETTINGS_PACKAGE_ID} --folder=metadata") pkg_metadata_path = str(c.stdout).strip() assert os.path.isdir(pkg_metadata_path) def test_direct_download_redownload(self, create_conan_pkg): """ When we directly download things, without "conan install" first, it is also able to fetch the requested metadata Also, re-downloading same thing shouldn't fail """ c, pid = create_conan_pkg # Add some metadata metadata_path, _ = self.save_metadata_file(c, "pkg/0.1", "mylogs.txt") self.save_metadata_file(c, f"pkg/0.1:{pid}", "mybuildlogs.txt") # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out c.run("remove * -c") # Forcing the download of the metadata of cache-existing things with the "download" command c.run("download pkg/0.1 -r=default --metadata=*") assert os.path.isfile(os.path.join(metadata_path, "logs", "mylogs.txt")) c.run(f"cache path pkg/0.1:{pid} --folder=metadata") pkg_metadata_path = str(c.stdout).strip() assert os.path.isfile(os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt")) # Re-download shouldn't fail c.run("download pkg/0.1 -r=default --metadata=*") assert os.path.isfile(os.path.join(metadata_path, "logs", "mylogs.txt")) c.run(f"cache path pkg/0.1:{pid} --folder=metadata") pkg_metadata_path = str(c.stdout).strip() assert os.path.isfile(os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt")) def test_no_download_cached(self, create_conan_pkg): """ as the metadata can change, no checksum, no revision, cannot be cached """ c, pid = create_conan_pkg # Add some metadata _, myrecipefile = self.save_metadata_file(c, "pkg/0.1", "mylogs.txt") _, mypkgfile = self.save_metadata_file(c, f"pkg/0.1:{pid}", "mybuildlogs.txt") # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out c2 = TestClient(servers=c.servers, light=True) tmp_folder = temp_folder() # MOST important part: activate cache c2.save_home({"global.conf": f"core.download:download_cache={tmp_folder}\n"}) # download package and metadata c2.run("download pkg/0.1 -r=default --metadata=*") c2.run("cache path pkg/0.1 --folder=metadata") c2_metadata_path = str(c2.stdout).strip() mylogs = load(os.path.join(c2_metadata_path, "logs", "mylogs.txt")) assert "pkg/0.1!!!!" in mylogs c2.run(f"cache path pkg/0.1:{pid} --folder=metadata") c2_pkg_metadata_path = str(c2.stdout).strip() mybuildlogs = load(os.path.join(c2_pkg_metadata_path, "logs", "mybuildlogs.txt")) assert f"pkg/0.1:{pid}!!!!" in mybuildlogs # Now the other client will update the metadata save(myrecipefile, "mylogs2!!!!") save(mypkgfile, "mybuildlogs2!!!!") c.run("upload * -c -r=default --metadata=*") assert "pkg/0.1: Recipe metadata: 1 files" in c.out assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out # re-download of metadata in c2 c2.run("remove * -c") # to make sure the download cache works c2.run("download pkg/0.1 -r=default --metadata=*") mylogs = load(os.path.join(c2_metadata_path, "logs", "mylogs.txt")) assert "mylogs2!!!!" in mylogs mybuildlogs = load(os.path.join(c2_pkg_metadata_path, "logs", "mybuildlogs.txt")) assert "mybuildlogs2!!!!" in mybuildlogs def test_upload_ignored_metadata(self, create_conan_pkg): """ Upload command should ignore metadata files when passing --metadata="" """ client, pid = create_conan_pkg self.save_metadata_file(client, "pkg/0.1") self.save_metadata_file(client, f"pkg/0.1:{pid}") client.run('upload * --confirm --remote=default --metadata=""') assert "Recipe metadata" not in client.out assert "Package metadata" not in client.out def test_upload_ignored_metadata_with_pattern(self, create_conan_pkg): """ Upload command should fail when passing --metadata="" and a pattern """ client = TestClient(default_server_user=True, light=True) client.save({"conanfile.py": GenConanfile("pkg", "0.1")}) client.run("export .") client.run('upload * --confirm --remote=default --metadata="" --metadata="logs/*"', assert_error=True) assert "ERROR: Empty string and patterns can not be mixed for metadata." in client.out ================================================ FILE: test/integration/metadata/test_metadata_deploy.py ================================================ import os.path import textwrap from collections import OrderedDict import pytest from conan.test.utils.tools import TestClient, TestServer class TestMetadataDeploy: """ prove we can gather metadata too with a deployer""" @pytest.fixture def client(self): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): version = "0.1" def source(self): save(self, os.path.join(self.recipe_metadata_folder, "logs", "src.log"), f"srclog {self.name}!!!") def build(self): save(self, "mylogs.txt", f"some logs {self.name}!!!") copy(self, "mylogs.txt", src=self.build_folder, dst=os.path.join(self.package_metadata_folder, "logs")) """) deploy = textwrap.dedent(""" import os, shutil def deploy(graph, output_folder, **kwargs): conanfile = graph.root.conanfile for r, d in conanfile.dependencies.items(): if not os.listdir(d.package_metadata_folder): continue shutil.copytree(d.package_metadata_folder, os.path.join(output_folder, "pkgs", d.ref.name)) shutil.copytree(d.recipe_metadata_folder, os.path.join(output_folder, "recipes", d.ref.name)) """) servers = OrderedDict([("default", TestServer()), ("remote2", TestServer())]) c = TestClient(servers=servers, inputs=2 * ["admin", "password"], light=True) c.save({"conanfile.py": conanfile, "deploy.py": deploy}) c.run("create . --name=pkg1") c.run("create . --name=pkg2") return c def test_cache(self, client): c = client c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1 --deployer=deploy") assert "some logs pkg1!!!" in c.load("pkgs/pkg1/logs/mylogs.txt") assert "some logs pkg2!!!" in c.load("pkgs/pkg2/logs/mylogs.txt") assert "srclog pkg1!!!" in c.load("recipes/pkg1/logs/src.log") assert "srclog pkg2!!!" in c.load("recipes/pkg2/logs/src.log") def test_remote(self, client): # But the remote story is more complex, metadata is not retrieved by default c = client c.run("upload * -c -r=default") c.run("remove * -c") # First install without metadata c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1") # So this will not deploy metadata c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1 --deployer=deploy -f=json", redirect_stdout="graph.json") assert not os.path.exists(os.path.join(c.current_folder, "pkgs")) # We can obtain the pkg-list for the graph, then "find-remote" and download the metadata c.run("list -g=graph.json -f=json", redirect_stdout="mylist.json") c.run("pkglist find-remote mylist.json -f=json", redirect_stdout="pkg_remotes.json") c.run("download --list=pkg_remotes.json -r=default --metadata=*") # Now we will have the metadata in cache and we can deploy it c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1 --deployer=deploy") assert "some logs pkg1!!!" in c.load("pkgs/pkg1/logs/mylogs.txt") assert "some logs pkg2!!!" in c.load("pkgs/pkg2/logs/mylogs.txt") assert "srclog pkg1!!!" in c.load("recipes/pkg1/logs/src.log") assert "srclog pkg2!!!" in c.load("recipes/pkg2/logs/src.log") def test_multi_remote(self, client): # But the remote story is more complex, metadata is not retrieved by default, # we need to iterate the remotes if data coming from multiple remotes c = client c.run("upload pkg1* -c -r=default") c.run("upload pkg2* -c -r=remote2") c.run("remove * -c") # First install without metadata c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1") # So this will not deploy metadata c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1 --deployer=deploy -f=json", redirect_stdout="graph.json") assert not os.path.exists(os.path.join(c.current_folder, "pkgs")) # We can obtain the pkg-list for the graph, then "find-remote" and download the metadata c.run("list -g=graph.json -f=json", redirect_stdout="mylist.json") c.run("pkglist find-remote mylist.json -f=json", redirect_stdout="pkg_remotes.json") # we need to ITERATE the remotes c.run("download --list=pkg_remotes.json -r=default --metadata=*") c.run("download --list=pkg_remotes.json -r=remote2 --metadata=*") # Now we will have the metadata in cache and we can deploy it c.run("install --requires=pkg1/0.1 --requires=pkg2/0.1 --deployer=deploy") assert "some logs pkg1!!!" in c.load("pkgs/pkg1/logs/mylogs.txt") assert "some logs pkg2!!!" in c.load("pkgs/pkg2/logs/mylogs.txt") assert "srclog pkg1!!!" in c.load("recipes/pkg1/logs/src.log") assert "srclog pkg2!!!" in c.load("recipes/pkg2/logs/src.log") ================================================ FILE: test/integration/metadata/test_metadata_logs.py ================================================ import os import textwrap import pytest from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient from conan.internal.util.files import load, save class TestRecipeMetadataLogs: conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): copy(self, "*.log", src=self.recipe_folder, dst=os.path.join(self.recipe_metadata_folder, "logs")) def layout(self): self.folders.build = "mybuild" self.folders.generators = "mybuild/generators" def source(self): save(self, os.path.join(self.recipe_metadata_folder, "logs", "src.log"), "srclog!!") def build(self): save(self, "mylogs.txt", "some logs!!!") copy(self, "mylogs.txt", src=self.build_folder, dst=os.path.join(self.package_metadata_folder, "logs")) """) def test_metadata_logs(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": self.conanfile, "file.log": "log contents!"}) c.run("create .") # Test local cache looks good ref = RecipeReference.loads("pkg/0.1") ref_layout = c.get_latest_ref_layout(ref) assert os.listdir(ref_layout.metadata()) == ["logs"] assert set(os.listdir(os.path.join(ref_layout.metadata(), "logs"))) == {"file.log", "src.log"} assert load(os.path.join(ref_layout.metadata(), "logs", "file.log")) == "log contents!" assert load(os.path.join(ref_layout.metadata(), "logs", "src.log")) == "srclog!!" pref = c.get_latest_package_reference(ref) pref_layout = c.get_latest_pkg_layout(pref) assert os.listdir(pref_layout.metadata()) == ["logs"] assert os.listdir(os.path.join(pref_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pref_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!" def test_metadata_logs_local(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": self.conanfile, "file.log": "log contents!"}) c.run("source .") assert c.load("metadata/logs/src.log") == "srclog!!" c.run("build .") assert c.load("mybuild/metadata/logs/mylogs.txt") == "some logs!!!" def test_download_pkg_list_from_graph(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": self.conanfile, "file.log": "log contents!"}) c.run("create .") c.run("upload * -r=default -c") c.run("remove * -c") # IMPORTANT: NECESSARY to force the download to gather the full package_list # TODO: Check the case how to download metadata for already installed in cache packages c.run("install --requires=pkg/0.1 --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --format=json", redirect_stdout="pkglist.json") # This list will contain both "Local Cache" and "default" origins, because it was downloaded c.run("download --list=pkglist.json -r=default --metadata=*") ref = RecipeReference.loads("pkg/0.1") pref = c.get_latest_package_reference(ref) pref_layout = c.get_latest_pkg_layout(pref) assert os.listdir(pref_layout.metadata()) == ["logs"] assert os.listdir(os.path.join(pref_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pref_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!" def test_metadata_folder_exist(self): """ make sure the folders exists so recipe don't have to create it for running bulk copies calling self.run(cp -R) """ conanfile = textwrap.dedent(""" import os from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): assert os.path.exists(self.recipe_metadata_folder) def source(self): assert os.path.exists(self.recipe_metadata_folder) def build(self): assert os.path.exists(self.package_metadata_folder) """) c = TestClient(default_server_user=True) c.save({"conanfile.py": conanfile}) c.run("create .") c.run("upload * -r=default -c") c.run("remove * -c") c.run("install --requires=pkg/0.1 --build=*") # If nothing fail, all good, all folder existed, assert passed class TestHooksMetadataLogs: @pytest.fixture() def _client(self): c = TestClient(default_server_user=True) my_hook = textwrap.dedent("""\ import os from conan.tools.files import copy def post_export(conanfile): conanfile.output.info("post_export") copy(conanfile, "*.log", src=conanfile.recipe_folder, dst=os.path.join(conanfile.recipe_metadata_folder, "logs")) def post_source(conanfile): conanfile.output.info("post_source") copy(conanfile, "*", src=os.path.join(conanfile.source_folder, "logs"), dst=os.path.join(conanfile.recipe_metadata_folder, "logs")) def post_build(conanfile): conanfile.output.info("post_build") copy(conanfile, "*", src=os.path.join(conanfile.build_folder, "logs"), dst=os.path.join(conanfile.package_metadata_folder, "logs")) """) hook_path = os.path.join(c.paths.hooks_path, "my_hook", "hook_my_hook.py") save(hook_path, my_hook) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "pkg" version = "0.1" no_copy_source = True def layout(self): self.folders.build = "mybuild" self.folders.generators = "mybuild/generators" def source(self): save(self, "logs/src.log", "srclog!!") def build(self): save(self, "logs/mylogs.txt", "some logs!!!") """) c.save({"conanfile.py": conanfile, "file.log": "log contents!"}) return c def test_metadata_logs_hook(self, _client): c = _client c.run("create .") # Test local cache looks good ref = RecipeReference.loads("pkg/0.1") ref_layout = c.get_latest_ref_layout(ref) assert os.listdir(ref_layout.metadata()) == ["logs"] assert set(os.listdir(os.path.join(ref_layout.metadata(), "logs"))) == {"file.log", "src.log"} assert load(os.path.join(ref_layout.metadata(), "logs", "file.log")) == "log contents!" assert load(os.path.join(ref_layout.metadata(), "logs", "src.log")) == "srclog!!" pref = c.get_latest_package_reference(ref) pref_layout = c.get_latest_pkg_layout(pref) assert os.listdir(pref_layout.metadata()) == ["logs"] assert os.listdir(os.path.join(pref_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pref_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!" def test_metadata_logs_local(self, _client): c = _client c.run("source .") assert c.load("metadata/logs/src.log") == "srclog!!" c.run("build .") assert c.load("mybuild/metadata/logs/mylogs.txt") == "some logs!!!" def test_metadata_export_pkg(): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save, copy class Pkg(ConanFile): name = "pkg" version = "0.1" def build(self): save(self, "mylogs.txt", "some logs!!!") copy(self, "mylogs.txt", src=self.build_folder, dst=os.path.join(self.package_metadata_folder, "logs")) def package(self): copy(self, "*", src=os.path.join(self.build_folder, "metadata"), dst=self.package_metadata_folder) """) c = TestClient() c.save({"conanfile.py": conanfile}) c.run("build .") c.run("export-pkg .") # Test local cache looks good pkg_layout = c.created_layout() assert os.listdir(pkg_layout.metadata()) == ["logs"] assert os.listdir(os.path.join(pkg_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pkg_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!" ================================================ FILE: test/integration/metadata/test_metadata_test_package.py ================================================ import os import shutil import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save class TestMetadataTestPackage: """ It is possible to store the test_package itself in the recipe metadata and recover it later to execute it """ def test_round_trip_with_hook(self): c = TestClient(default_server_user=True) # TODO: Better strategy for storing clean test_package, zipping it, etc my_hook = textwrap.dedent("""\ import os from conan.tools.files import copy def post_export(conanfile): conanfile.output.info("Storing test_package") folder = os.path.join(conanfile.recipe_folder, "test_package") copy(conanfile, "*", src=folder, dst=os.path.join(conanfile.recipe_metadata_folder, "test_package")) """) hook_path = os.path.join(c.paths.hooks_path, "my_hook", "hook_my_hook.py") save(hook_path, my_hook) c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "test_package/conanfile.py": GenConanfile().with_test("pass")}) c.run("create .") assert "Testing the package" in c.out # Now upload and remove everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out c.run("remove * -c") c.run("install --requires=pkg/0.1 -r=default") # Recovery of the test package # TODO: Discuss if we want better UX, in a single step or something like that # Forcing the download of the metadata of cache-existing things with the "download" command c.run("download pkg/0.1 -r=default --metadata=test_package*") c.run("cache path pkg/0.1 --folder=metadata") metadata_path = str(c.stdout).strip() shutil.copytree(metadata_path, os.path.join(c.current_folder, "metadata")) # Execute the test_package c.run("test metadata/test_package pkg/0.1") assert "pkg/0.1 (test package): Running test()" in c.out ================================================ FILE: test/integration/options/__init__.py ================================================ ================================================ FILE: test/integration/options/options_test.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient, GenConanfile class TestOptions: def test_general_scope_options_test_package(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = {"shared": ["1", "2"]} def configure(self): self.output.info("BUILD SHARED: %s" % self.options.shared) """) test = GenConanfile().with_test("pass") client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o *:shared=1") assert "pkg/0.1@user/testing: BUILD SHARED: 1" in client.out client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o shared=2") assert 'legacy: Unscoped option definition is ambiguous' in client.out assert "pkg/0.1@user/testing: BUILD SHARED: 2" in client.out # With test_package client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o *:shared=1") assert "pkg/0.1@user/testing: BUILD SHARED: 1" in client.out client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o pkg*:shared=2") assert "pkg/0.1@user/testing: BUILD SHARED: 2" in client.out client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o shared=1") assert "pkg/0.1@user/testing: BUILD SHARED: 1" in client.out def test_general_scope_options_test_package_notdefined(self): client = TestClient() conanfile = GenConanfile() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o *:shared=True") assert "pkg/0.1@user/testing: Forced build from source" in client.out client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o shared=False", assert_error=True) assert "option 'shared' doesn't exist" in client.out # With test_package client.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_test("pass")}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o *:shared=True") assert "pkg/0.1@user/testing: Forced build from source" in client.out assert "Testing the package: Building" in client.out def test_general_scope_priorities(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = {"shared": ["1", "2", "3"], "other": [4, 5, 6]} def configure(self): self.output.info("BUILD SHARED: %s OTHER: %s" % (self.options.shared, self.options.other)) """) client.save({"conanfile.py": conanfile}) # Consumer has priority client.run("create . --name=pkg --version=0.1 -o *:shared=1 -o shared=2 -o p*:other=4") assert "pkg/0.1: BUILD SHARED: 2 OTHER: 4" in client.out # Consumer has priority over pattern, even if the pattern specifies the package name client.run("create . --name=pkg --version=0.1 -o *:shared=1 -o pkg/*:shared=2 -o shared=3 -o p*:other=4") assert "pkg/0.1: BUILD SHARED: 3 OTHER: 4" in client.out client.run("create . --name=pkg --version=0.1 -o pkg/0.1:shared=2 -o p*:other=4 -o pk*:other=5") assert "pkg/0.1: BUILD SHARED: 2 OTHER: 5" in client.out # With test_package client.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_test("pass")}) # Sorted (longest, alphabetical) patterns, have priority client.run("create . --name=pkg --version=0.1 -o *:shared=1 -o pkg/0.1:shared=2 -o other=4") assert "pkg/0.1: BUILD SHARED: 2 OTHER: 4" in client.out client.run("create . --name=pkg --version=0.1 -o pk*:shared=2 -o p*:shared=1 -o pkg/0.1:other=5") assert "pkg/0.1: BUILD SHARED: 1 OTHER: 5" in client.out client.run("create . --name=pkg --version=0.1 -o pk*:shared=2 -o p*:shared=1 -o pkg/0.1:other=5 -o *g*:other=6") assert "pkg/0.1: BUILD SHARED: 1 OTHER: 6" in client.out def test_parsing(self): client = TestClient() conanfile = ''' from conan import ConanFile class EqualerrorConan(ConanFile): name = "equal" version = "1.0.0" options = {"opt": ["ANY"]} default_options = {"opt": "b=c"} def build(self): self.output.warning("OPTION %s" % self.options.opt) ''' client.save({"conanfile.py": conanfile}) client.run("export . --user=user --channel=testing") conanfile = ''' [requires] equal/1.0.0@user/testing [options] equal/1.0.0@user/testing:opt=a=b ''' client.save({"conanfile.txt": conanfile}, clean_first=True) client.run("install . --build=missing") assert "OPTION a=b" in client.out def test_general_scope_options(self): # https://github.com/conan-io/conan/issues/2538 client = TestClient() conanfile_liba = textwrap.dedent(""" from conan import ConanFile class LibA(ConanFile): options = {"shared": [True, False]} def configure(self): self.output.info("shared=%s" % self.options.shared) """) client.save({"conanfile.py": conanfile_liba}) client.run("create . --name=liba --version=0.1 --user=danimtb --channel=testing -o *:shared=True") assert "liba/0.1@danimtb/testing: shared=True" in client.out conanfile_libb = textwrap.dedent(""" from conan import ConanFile class LibB(ConanFile): options = {"shared": [True, False]} requires = "liba/0.1@danimtb/testing" def configure(self): self.options["*"].shared = self.options.shared self.output.info("shared=%s" % self.options.shared) """) for without_configure_line in [True, False]: if without_configure_line: conanfile = conanfile_libb.replace("self.options[", "#") else: conanfile = conanfile_libb client.save({"conanfile.py": conanfile}) # Test info client.run("graph info . -o *:shared=True") assert "conanfile.py: shared=True" in client.out assert "liba/0.1@danimtb/testing: shared=True" in client.out # Test create client.run("create . --name=libb --version=0.1 --user=danimtb --channel=testing -o *:shared=True") assert "libb/0.1@danimtb/testing: shared=True" in client.out assert "liba/0.1@danimtb/testing: shared=True" in client.out # Test install client.run("install . -o *:shared=True") assert "conanfile.py: shared=True" in client.out assert "liba/0.1@danimtb/testing: shared=True" in client.out def test_define_nested_option_not_freeze(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = {"without_stacktrace": [True, False], "with_stacktrace_backtrace": [True, False]} default_options = {"without_stacktrace": True} def configure(self): if self.options.without_stacktrace: del self.options.with_stacktrace_backtrace else: self.options.with_stacktrace_backtrace = True def build(self): s = self.options.without_stacktrace self.output.info("without_stacktrace: {}".format(s)) if "with_stacktrace_backtrace" in self.options: ss = self.options.get_safe("with_stacktrace_backtrace") self.output.info("with_stacktrace_backtrace: {}".format(ss)) else: self.output.info("with_stacktrace_backtrace success deleted!") """) c.save({"conanfile.py": conanfile}) c.run("create . --name=pkg --version=0.1") assert "pkg/0.1: without_stacktrace: True" in c.out assert "pkg/0.1: with_stacktrace_backtrace success deleted!" in c.out c.run("create . --name=pkg --version=0.1 -o pkg*:without_stacktrace=False") assert "pkg/0.1: without_stacktrace: False" in c.out assert "pkg/0.1: with_stacktrace_backtrace: True" in c.out def test_del_options_configure(self): """ this test was failing because Options was protecting against removal of options with already assigned values. This has been relaxed, to make possible this case """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = { "shared": [True, False], "fPIC": [True, False], } default_options = { "shared": False, "fPIC": True, } def configure(self): if self.options.shared: del self.options.fPIC """) c.save({"conanfile.py": conanfile}) c.run("create . --name=pkg --version=0.1") c.save({"conanfile.py": GenConanfile("consumer", "1.0").with_requirement("pkg/0.1")}, clean_first=True) c.run("install . -o pkg*:shared=True --build=missing") assert "pkg/0.1" in c.out # Real test is the above doesn't crash def test_any(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class EqualerrorConan(ConanFile): name = "equal" version = "1.0.0" options = {"opt": "ANY"} default_options = {"opt": "b=c"} def generate(self): self.output.warning("OPTION %s" % self.options.opt) """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "Error while initializing options. 'b=c' is not a valid 'options.opt' value." in c.out assert "Possible values are ['A', 'N', 'Y']" in c.out class TestOptionsPriorities: # https://github.com/conan-io/conan/issues/11571 @pytest.fixture def _client(self): lib1 = textwrap.dedent(""" from conan import ConanFile class Lib1Conan(ConanFile): name = "lib1" version = "1.0" options = {"foobar": [True, False]} default_options = {"foobar": False} """) lib2 = textwrap.dedent(""" from conan import ConanFile class Lib2Conan(ConanFile): name = "lib2" version = "1.0" options = {"logic_for_foobar": [True, False]} default_options = {"logic_for_foobar": False} def requirements(self): self.requires("lib1/1.0") def configure(self): self.options["lib1/*"].foobar = self.options.logic_for_foobar """) c = TestClient() c.save({"lib1/conanfile.py": lib1, "lib2/conanfile.py": lib2}) c.run("create lib1 -o lib1/*:foobar=True") c.run("create lib1 -o lib1/*:foobar=False") c.run("create lib2 -o lib2/*:logic_for_foobar=True") c.run("create lib2 -o lib2/*:logic_for_foobar=False") return c @staticmethod def _app(lib1, lib2, configure): app = textwrap.dedent(""" from conan import ConanFile class App(ConanFile): def requirements(self): self.requires("{}/1.0") self.requires("{}/1.0") def {}(self): self.options["lib2/*"].logic_for_foobar = True self.options["lib1/*"].foobar = False def generate(self): self.output.info("LIB1 FOOBAR: {{}}".format( self.dependencies["lib1"].options.foobar)) self.output.info("LIB2 LOGIC: {{}}".format( self.dependencies["lib2"].options.logic_for_foobar)) """) return app.format(lib1, lib2, configure) def test_profile_priority(self, _client): c = _client c.save({"app/conanfile.py": self._app("lib1", "lib2", "not_configure")}) # This order works, because lib1 is expanded first, it takes foobar=False c.run("install app -o lib2*:logic_for_foobar=True -o lib1*:foobar=False") assert "conanfile.py: LIB1 FOOBAR: False" in c.out assert "conanfile.py: LIB2 LOGIC: True" in c.out # Now swap order c.save({"app/conanfile.py": self._app("lib2", "lib1", "not_configure")}) c.run("install app -o lib2*:logic_for_foobar=True -o lib1*:foobar=False") assert "conanfile.py: LIB1 FOOBAR: False" in c.out assert "conanfile.py: LIB2 LOGIC: True" in c.out def test_lib1_priority(self, _client): c = _client c.save({"app/conanfile.py": self._app("lib1", "lib2", "not_configure")}) # This order works, because lib1 is expanded first, it takes foobar=False c.run("install app") assert "conanfile.py: LIB1 FOOBAR: False" in c.out assert "conanfile.py: LIB2 LOGIC: False" in c.out c.run("install app -o lib1*:foobar=True") assert "conanfile.py: LIB1 FOOBAR: True" in c.out assert "conanfile.py: LIB2 LOGIC: False" in c.out c.run("install app -o lib2*:logic_for_foobar=True") assert "conanfile.py: LIB1 FOOBAR: False" in c.out assert "conanfile.py: LIB2 LOGIC: True" in c.out def test_lib2_priority(self, _client): c = _client c.save({"app/conanfile.py": self._app("lib2", "lib1", "not_configure")}) # This order works, because lib1 is expanded first, it takes foobar=False c.run("install app") assert "conanfile.py: LIB1 FOOBAR: False" in c.out assert "conanfile.py: LIB2 LOGIC: False" in c.out c.run("install app -o lib1*:foobar=True") assert "conanfile.py: LIB1 FOOBAR: True" in c.out assert "conanfile.py: LIB2 LOGIC: False" in c.out c.run("install app -o lib2*:logic_for_foobar=True") assert "conanfile.py: LIB1 FOOBAR: True" in c.out assert "conanfile.py: LIB2 LOGIC: True" in c.out def test_consumer_configure_priority(self, _client): c = _client c.save({"app/conanfile.py": self._app("lib1", "lib2", "configure")}) c.run("install app") assert "conanfile.py: LIB1 FOOBAR: False" in c.out assert "conanfile.py: LIB2 LOGIC: True" in c.out # Now swap order c.save({"app/conanfile.py": self._app("lib1", "lib2", "configure")}) c.run("install app") assert "conanfile.py: LIB1 FOOBAR: False" in c.out assert "conanfile.py: LIB2 LOGIC: True" in c.out def test_configurable_default_options(): # https://github.com/conan-io/conan/issues/11487 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os" options = {"backend": [1, 2, 3]} def config_options(self): if self.settings.os == "Windows": self.options.backend = 2 else: self.options.backend = 3 def package_info(self): self.output.info("Package with option:{}!".format(self.options.backend)) """) c.save({"conanfile.py": conanfile}) c.run("create . -s os=Windows") assert "pkg/0.1: Package with option:2!" in c.out c.run("create . -s os=Windows -o pkg*:backend=3") assert "pkg/0.1: Package with option:3!" in c.out c.run("create . -s os=Linux") assert "pkg/0.1: Package with option:3!" in c.out c.run("create . -s os=Windows -o pkg*:backend=1") assert "pkg/0.1: Package with option:1!" in c.out consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "consumer" version = "0.1" requires = "pkg/0.1" def configure(self): self.options["pkg*"].backend = 1 """) c.save({"conanfile.py": consumer}) c.run("install . -s os=Windows") assert "pkg/0.1: Package with option:1!" in c.out c.run("create . -s os=Windows") assert "pkg/0.1: Package with option:1!" in c.out # This fails in Conan 1.X c.run("create . -s os=Windows -o pkg*:backend=3") assert "pkg/0.1: Package with option:3!" in c.out class TestMultipleOptionsPatterns: """ https://github.com/conan-io/conan/issues/13240 """ dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): version = "1.0" options = {"shared": [True, False]} default_options = {"shared": False} def package_info(self): self.output.info(f"SHARED: {self.options.shared}!!") """) def test_multiple_options_patterns_cli(self): """ https://github.com/conan-io/conan/issues/13240 """ c = TestClient() consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def requirements(self): self.requires("dep1/1.0") self.requires("dep2/1.0") self.requires("dep3/1.0") self.requires("dep4/1.0") """) c.save({"dep/conanfile.py": self.dep, "pkg/conanfile.py": consumer}) for d in (1, 2, 3, 4): c.run(f"export dep --name dep{d}") # match in order left to right c.run('install pkg -o *:shared=True -o dep1*:shared=False -o dep2*:shared=False -b missing') assert "dep1/1.0: SHARED: False!!" in c.out assert "dep2/1.0: SHARED: False!!" in c.out assert "dep3/1.0: SHARED: True!!" in c.out assert "dep4/1.0: SHARED: True!!" in c.out # All match in order, left to right c.run('install pkg -o dep1*:shared=False -o dep2*:shared=False -o *:shared=True -b missing') assert "dep1/1.0: SHARED: True!!" in c.out assert "dep2/1.0: SHARED: True!!" in c.out assert "dep3/1.0: SHARED: True!!" in c.out assert "dep4/1.0: SHARED: True!!" in c.out def test_multiple_options_patterns(self): c = TestClient() configure_consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def requirements(self): self.requires("dep1/1.0") self.requires("dep2/1.0") self.requires("dep3/1.0") self.requires("dep4/1.0") def configure(self): self.options["*"].shared = True # Without * also works, equivalent to dep1/* self.options["dep1"].shared = False self.options["dep2*"].shared = False """) c.save({"dep/conanfile.py": self.dep, "pkg/conanfile.py": configure_consumer}) for d in (1, 2, 3, 4): c.run(f"export dep --name dep{d}") c.run("install pkg --build=missing") assert "dep1/1.0: SHARED: False!!" in c.out assert "dep2/1.0: SHARED: False!!" in c.out assert "dep3/1.0: SHARED: True!!" in c.out assert "dep4/1.0: SHARED: True!!" in c.out def test_multiple_options_patterns_order(self): c = TestClient() configure_consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def requirements(self): self.requires("dep1/1.0") self.requires("dep2/1.0") self.requires("dep3/1.0") self.requires("dep4/1.0") def configure(self): self.options["dep1*"].shared = False self.options["dep2*"].shared = False self.options["*"].shared = True """) c.save({"dep/conanfile.py": self.dep, "pkg/conanfile.py": configure_consumer}) for d in (1, 2, 3, 4): c.run(f"export dep --name dep{d}") c.run("install pkg --build=missing") assert "dep1/1.0: SHARED: True!!" in c.out assert "dep2/1.0: SHARED: True!!" in c.out assert "dep3/1.0: SHARED: True!!" in c.out assert "dep4/1.0: SHARED: True!!" in c.out def test_pattern_version_range_warn(self): c = TestClient(light=True) profile = textwrap.dedent(""" include(default) [tool_requires] fmt/[*]:cmake/3.31.0 [options] fmt/[*]:shared=True [settings] fmt/[*]:compiler.cppstd=17 """) c.save({ "profile": profile, "conanfile.py": GenConanfile("fmt", "1.0")}) c.run("create -pr=profile") assert "WARN: risk: Settings pattern fmt/[*] contains a version range" in c.out assert "WARN: risk: Options pattern fmt/[*] contains a version range" in c.out assert "WARN: risk: Tool requires pattern fmt/[*] contains a version range" in c.out def test_pattern_version_range_wrong_split(self): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("foo", "1.0")}) c.run("create -o=foo/[>=1]:shared=True", assert_error=True) # We split by the first =, so this version range we can't catch and warn before it breaks assert "option 'foo/[>' doesn't exist" in c.out class TestTransitiveOptionsShared: """ https://github.com/conan-io/conan/issues/13854 """ @pytest.fixture() def client(self): c = TestClient() c.save({"toollib/conanfile.py": GenConanfile("toollib", "0.1").with_shared_option(False), "tool/conanfile.py": GenConanfile("tool", "0.1").with_shared_option(False) .with_requires("toollib/0.1"), "dep2/conanfile.py": GenConanfile("dep2", "0.1").with_shared_option(False) .with_tool_requires("tool/0.1"), "dep1/conanfile.py": GenConanfile("dep1", "0.1").with_shared_option(False) .with_requires("dep2/0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_shared_option(False) .with_requires("dep1/0.1"), "app/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export toollib") c.run("export tool") c.run("export dep2") c.run("export dep1") c.run("export pkg") return c @staticmethod def check(client): # tools and libs in build context do not get propagated the options for dep in ("toollib", "tool"): client.run(f"list {dep}/*:*") assert "shared: False" in client.out # But the whole host context does for dep in ("dep1", "dep2", "pkg"): client.run(f"list {dep}/*:*") assert "shared: True" in client.out def test_transitive_options_shared_cli(self, client): client.run("install app --build=missing -o *:shared=True") self.check(client) def test_transitive_options_shared_profile(self, client): client.save({"profile": "[options]\n*:shared=True"}) client.run("install app --build=missing -pr=profile") self.check(client) def test_transitive_options_conanfile_txt(self, client): client.save({"app/conanfile.txt": "[requires]\npkg/0.1\n[options]\n*:shared=True\n"}) client.run("install app --build=missing") self.check(client) def test_transitive_options_conanfile_py(self, client): client.save({"app/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_default_option("*:shared", True)}) client.run("install app/conanfile.py --build=missing") self.check(client) def test_transitive_options_conanfile_py_create(self, client): conanfile = GenConanfile("app", "0.1").with_requires("pkg/0.1") \ .with_default_option("*:shared", True) client.save({"app/conanfile.py": conanfile}) client.run("create app --build=missing") self.check(client) def test_options_no_user_channel_patterns(): c = TestClient() conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): options = {"myoption": [1, 2, 3]} def configure(self): self.output.info(f"MYOPTION: {self.options.myoption}") """) c.save({"dep/conanfile.py": conanfile, "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep1/0.1", "dep2/0.1@user", "dep3/0.1@user/channel")}) c.run("export dep --name=dep1 --version=0.1") c.run("export dep --name=dep2 --version=0.1 --user=user") c.run("export dep --name=dep3 --version=0.1 --user=user --channel=channel") c.run("graph info pkg -o *:myoption=3 -o *@:myoption=1") assert "dep1/0.1: MYOPTION: 1" in c.out assert "dep2/0.1@user: MYOPTION: 3" in c.out assert "dep3/0.1@user/channel: MYOPTION: 3" in c.out # Recall that order is also important latest matching pattern wins c.run("graph info pkg -o *@:myoption=1 -o *:myoption=1") assert "dep1/0.1: MYOPTION: 1" in c.out assert "dep2/0.1@user: MYOPTION: 1" in c.out assert "dep3/0.1@user/channel: MYOPTION: 1" in c.out # This is a bit weird negation approach, but it works = all packages that have user channel c.run("graph info pkg -o *:myoption=3 -o ~*@:myoption=1") assert "dep1/0.1: MYOPTION: 3" in c.out assert "dep2/0.1@user: MYOPTION: 1" in c.out assert "dep3/0.1@user/channel: MYOPTION: 1" in c.out # Which is identical to '~*@' == '*@*' c.run("graph info pkg -o *:myoption=3 -o *@*:myoption=1") assert "dep1/0.1: MYOPTION: 3" in c.out assert "dep2/0.1@user: MYOPTION: 1" in c.out assert "dep3/0.1@user/channel: MYOPTION: 1" in c.out def test_package_options_negate_patterns(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): version = "0.1" options = {"myoption": [1, 2, 3]} def build(self): self.output.info(f"MYOPTION={self.options.myoption}!!!") """) c.save({"dep/conanfile.py": conanfile, "pkg/conanfile.py": GenConanfile().with_requires("dep1/0.1", "dep2/0.1", "dep3/0.1")}) c.run("export dep --name=dep1") c.run("export dep --name=dep2") c.run("export dep --name=dep3") c.run("install pkg --build=* -o *:myoption=1 -o ~dep1/*:myoption=2") assert "dep1/0.1: MYOPTION=1!!!" in c.out assert "dep2/0.1: MYOPTION=2!!!" in c.out assert "dep3/0.1: MYOPTION=2!!!" in c.out # Order does matter for options with *:myoption patter, evaluates last c.run("install pkg --build=* -o ~dep1/*:myoption=2 -o *:myoption=1") assert "dep1/0.1: MYOPTION=1!!!" in c.out assert "dep2/0.1: MYOPTION=1!!!" in c.out assert "dep3/0.1: MYOPTION=1!!!" in c.out # dep3 comes later, works c.run("install pkg --build=* -o *:myoption=1 -o ~dep1/*:myoption=2 -o dep3/*:myoption=3") assert "dep1/0.1: MYOPTION=1!!!" in c.out assert "dep2/0.1: MYOPTION=2!!!" in c.out assert "dep3/0.1: MYOPTION=3!!!" in c.out # dep3 comes first, then last !dep1 pattern prevails c.run("install pkg --build=* -o *:myoption=1 -o dep3/*:myoption=3 -o ~dep1/*:myoption=2") assert "dep1/0.1: MYOPTION=1!!!" in c.out assert "dep2/0.1: MYOPTION=2!!!" in c.out assert "dep3/0.1: MYOPTION=2!!!" in c.out class TestTransitiveOptionsSharedInvisible: """ https://github.com/conan-io/conan/issues/13854 When a requirement is visible=False """ @pytest.fixture() def client(self): c = TestClient() c.save({"dep2/conanfile.py": GenConanfile("dep2", "0.1").with_shared_option(False), "dep1/conanfile.py": GenConanfile("dep1", "0.1").with_shared_option(False) .with_requirement("dep2/0.1", visible=False), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_shared_option(False) .with_requires("dep1/0.1"), "app/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep2") c.run("export dep1") c.run("export pkg") return c @staticmethod def check(client, value): for dep in ("dep1", "pkg"): client.run(f"list {dep}/*:*") assert f"shared: True" in client.out # dep2 cannot be affected from downstream conanfile consumers options, only from profile client.run(f"list dep2/*:*") assert f"shared: {value}" in client.out def test_transitive_options_shared_cli(self, client): client.run("install app --build=missing -o *:shared=True") self.check(client, True) def test_transitive_options_shared_profile(self, client): client.save({"profile": "[options]\n*:shared=True"}) client.run("install app --build=missing -pr=profile") self.check(client, True) def test_transitive_options_conanfile_txt(self, client): client.save({"app/conanfile.txt": "[requires]\npkg/0.1\n[options]\n*:shared=True\n"}) client.run("install app --build=missing") self.check(client, False) def test_transitive_options_conanfile_py(self, client): client.save({"app/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_default_option("*:shared", True)}) client.run("install app/conanfile.py --build=missing") self.check(client, False) def test_transitive_options_conanfile_py_create(self, client): conanfile = GenConanfile("app", "0.1").with_requires("pkg/0.1") \ .with_default_option("*:shared", True) client.save({"app/conanfile.py": conanfile}) client.run("create app --build=missing") self.check(client, False) class TestImportantOptions: @pytest.mark.parametrize("pkg", ["liba", "libb", "app"]) def test_important_options(self, pkg): c = TestClient() liba = GenConanfile("liba", "0.1").with_option("myoption", [1, 2, 3]) libb = GenConanfile("libb", "0.1").with_requires("liba/0.1") app = GenConanfile().with_requires("libb/0.1") if pkg == "liba": liba.with_default_option("myoption!", 2) elif pkg == "libb": libb.with_default_option("*:myoption!", 2) elif pkg == "app": app.with_default_option("*:myoption!", 2) package_id = textwrap.dedent(""" def package_id(self): self.output.info(f"MYOPTION: {self.info.options.myoption}") """) liba = str(liba) + textwrap.indent(package_id, " ") c.save({"liba/conanfile.py": liba, "libb/conanfile.py": libb, "app/conanfile.py": app}) c.run("export liba") c.run("export libb") c.run("graph info app -o *:myoption=3") assert "liba/0.1: MYOPTION: 2" in c.out # And the profile can always important-override the option c.run("graph info app -o *:myoption!=3") assert "liba/0.1: MYOPTION: 3" in c.out def test_profile_shows_important(self): c = TestClient() c.run("profile show -o *:myoption!=3") assert "*:myoption!=3" in c.out def test_important_options_recipe_priority(self): c = TestClient() liba = GenConanfile("liba", "0.1").with_option("myoption", [1, 2, 3, 4])\ .with_default_option("myoption!", 1) libb = GenConanfile("libb", "0.1").with_requires("liba/0.1")\ .with_default_option("*:myoption!", 2) app = GenConanfile().with_requires("libb/0.1").with_default_option("*:myoption!", 3) package_id = textwrap.dedent(""" def package_id(self): self.output.info(f"MYOPTION: {self.info.options.myoption}") """) liba = str(liba) + textwrap.indent(package_id, " ") c.save({"liba/conanfile.py": liba, "libb/conanfile.py": libb, "app/conanfile.py": app}) c.run("export liba") c.run("export libb") c.run("graph info app") assert "liba/0.1: MYOPTION: 3" in c.out c.run("graph info app -o *:myoption!=4") assert "liba/0.1: MYOPTION: 4" in c.out def test_important_options_recipe_priority_conditional(self): c = TestClient() liba = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): name = "liba" version = "0.1" settings = "os" options = {"myoption": [1, 2, 3]} def config_options(self): if self.settings.os == "Windows": setattr(self.options, "myoption!", 2) def package_id(self): self.output.info(f"MYOPTION: {self.info.options.myoption}") """) c.save({"liba/conanfile.py": liba, "app/conanfile.py": GenConanfile().with_requires("liba/0.1")}) c.run("export liba") c.run("graph info app -s os=Linux -o *:myoption=3") assert "liba/0.1: MYOPTION: 3" in c.out c.run("graph info app -s os=Linux -o *:myoption=2") assert "liba/0.1: MYOPTION: 2" in c.out c.run("graph info app -s os=Linux -o *:myoption=1") assert "liba/0.1: MYOPTION: 1" in c.out c.run("graph info app -s os=Windows -o *:myoption=3") assert "liba/0.1: MYOPTION: 2" in c.out c.run("graph info app -s os=Windows -o *:myoption=2") assert "liba/0.1: MYOPTION: 2" in c.out c.run("graph info app -s os=Windows -o *:myoption=1") assert "liba/0.1: MYOPTION: 2" in c.out def test_wrong_option_syntax_no_trace(self): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile().with_option("myoption", [1, 2, 3])}) tc.run('create . -o="&:myoption"', assert_error=True) assert "ValueError" not in tc.out assert "Error while parsing option" in tc.out class TestConflictOptionsWarnings: @pytest.mark.parametrize("important", [True, False]) def test_options_warnings(self, important): c = TestClient() liba = GenConanfile("liba", "0.1").with_option("myoption", [1, 2, 3], default=1) libb = GenConanfile("libb", "0.1").with_requires("liba/0.1") if important: libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1") \ .with_default_option("liba*:myoption!", 2) else: libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1", options={"myoption": 2}) app = GenConanfile().with_requires("libb/0.1", "libc/0.1") c.save({"liba/conanfile.py": liba, "libb/conanfile.py": libb, "libc/conanfile.py": libc, "app/conanfile.py": app}) c.run("export liba") c.run("export libb") c.run("export libc") c.run("graph info app") expected = textwrap.dedent("""\ Options conflicts liba/0.1:myoption=1 (current value) libc/0.1->myoption=2 It is recommended to define options values in profiles, not in recipes """) assert expected in c.out assert "WARN: risk: There are options conflicts in the dependency graph" in c.out def test_get_safe_none_option_checks(): tc = TestClient(light=True) tc.save({"conanfile.py": GenConanfile("foo", "1.0") .with_package("self.output.info(f'get_safe is None: {self.options.get_safe(\"myoption\") is None}')", "self.output.info(f'get_safe is not None: {self.options.get_safe(\"myoption\") is not None}')", "self.output.info(f'get_safe == None: {self.options.get_safe(\"myoption\") == None}')", "self.output.info(f'get_safe != None: {self.options.get_safe(\"myoption\") != None}')")}) tc.run("create .") assert "get_safe is None: True" in tc.out assert "get_safe is not None: False" in tc.out assert "get_safe == None: True" in tc.out assert "get_safe != None: False" in tc.out def test_option_apply_version_range(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("dep", "0.1").with_shared_option(False)}) c.run("create -o shared=True") c.run("install --requires=dep/0.1 -o shared=True") # This worked without problem c.run("install --requires=dep/[*] -o shared=True") assert "WARN: risk" not in c.out # This failed because of dep/[*] not matching pattern, now it works assert "Install finished successfully" in c.out ================================================ FILE: test/integration/options/test_configure_options.py ================================================ import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestConfigureOptions: """ Test config_options(), configure() and package_id() methods can manage shared, fPIC and header_only options automatically. """ @pytest.mark.parametrize("settings_os, shared, fpic, header_only, result", [ ["Linux", False, False, False, [False, False, False]], ["Windows", False, False, False, [False, None, False]], ["Windows", True, False, False, [True, None, False]], ["Windows", False, False, True, [None, None, True]], ["Linux", False, False, True, [None, None, True]], ["Linux", True, True, False, [True, None, False]], ["Linux", True, False, False, [True, None, False]], ["Linux", True, True, True, [None, None, True]], ["Linux", True, True, True, [None, None, True]], ["Linux", False, True, False, [False, True, False]], ["Linux", False, True, False, [False, True, False]], ]) def test_methods_not_defined(self, settings_os, shared, fpic, header_only, result): """ Test that options are managed automatically when methods config_options and configure are not defined and implements = ["auto_shared_fpic", "auto_header_only"]. Check that header only package gets its unique package ID. """ client = TestClient() conanfile = textwrap.dedent(f"""\ from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" options = {{"shared": [True, False], "fPIC": [True, False], "header_only": [True, False]}} default_options = {{"shared": {shared}, "fPIC": {fpic}, "header_only": {header_only}}} implements = ["auto_shared_fpic", "auto_header_only"] def build(self): shared = self.options.get_safe("shared") fpic = self.options.get_safe("fPIC") header_only = self.options.get_safe("header_only") self.output.info(f"shared: {{shared}}, fPIC: {{fpic}}, header only: {{header_only}}") """) client.save({"conanfile.py": conanfile}) client.run(f"create . --name=pkg --version=0.1 -s os={settings_os}") result = f"shared: {result[0]}, fPIC: {result[1]}, header only: {result[2]}" assert result in client.out if header_only: assert "Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created" in client.out @pytest.mark.parametrize("settings_os, shared, fpic, header_only, result", [ ["Linux", False, False, False, [False, False, False]], ["Linux", False, False, True, [False, False, True]], ["Linux", False, True, False, [False, True, False]], ["Linux", False, True, True, [False, True, True]], ["Linux", True, False, False, [True, False, False]], ["Linux", True, False, True, [True, False, True]], ["Linux", True, True, False, [True, True, False]], ["Linux", True, True, True, [True, True, True]], ["Windows", False, False, False, [False, False, False]], ["Windows", False, False, True, [False, False, True]], ["Windows", False, True, False, [False, True, False]], ["Windows", False, True, True, [False, True, True]], ["Windows", True, False, False, [True, False, False]], ["Windows", True, False, True, [True, False, True]], ["Windows", True, True, False, [True, True, False]], ["Windows", True, True, True, [True, True, True]], ]) def test_optout(self, settings_os, shared, fpic, header_only, result): """ Test that options are not managed automatically when methods are defined even if implements = ["auto_shared_fpic", "auto_header_only"] Check that header only package gets its unique package ID. """ client = TestClient() conanfile = textwrap.dedent(f"""\ from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" options = {{"shared": [True, False], "fPIC": [True, False], "header_only": [True, False]}} default_options = {{"shared": {shared}, "fPIC": {fpic}, "header_only": {header_only}}} implements = ["auto_shared_fpic", "auto_header_only"] def config_options(self): pass def configure(self): pass def build(self): shared = self.options.get_safe("shared") fpic = self.options.get_safe("fPIC") header_only = self.options.get_safe("header_only") self.output.info(f"shared: {{shared}}, fPIC: {{fpic}}, header only: {{header_only}}") """) client.save({"conanfile.py": conanfile}) client.run(f"create . --name=pkg --version=0.1 -s os={settings_os}") result = f"shared: {result[0]}, fPIC: {result[1]}, header only: {result[2]}" assert result in client.out if header_only: assert "Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created" in client.out def test_header_package_type_pid(self): """ Test that we get the pid for header only when package type is set to header-library """ client = TestClient() conanfile = textwrap.dedent(f"""\ from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" package_type = "header-library" implements = ["auto_shared_fpic", "auto_header_only"] """) client.save({"conanfile.py": conanfile}) client.run(f"create . --name=pkg --version=0.1") assert "Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created" in client.out def test_config_options_override_behaviour(): tc = TestClient(light=True) tc.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" options = {"foo": [1, 2, 3]} default_options = {"foo": 1} def config_options(self): self.options.foo = 2 def build(self): self.output.info(f"Foo: {self.options.foo}") """)}) tc.run("create .") assert "Foo: 2" in tc.out tc.run("create . -o=&:foo=3") assert "Foo: 3" in tc.out def test_configure_transitive_option(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("zlib", "0.1").with_shared_option(False)}) c.run("create . -o *:shared=True") boost = textwrap.dedent(""" from conan import ConanFile class BoostConan(ConanFile): name = "boostdbg" version = "1.0" options = {"shared": [True, False]} default_options ={"shared": False} requires = "zlib/0.1" def configure(self): self.options["zlib/*"].shared = self.options.shared """) c.save({"conanfile.py": boost}, clean_first=True) c.run("create . -o boostdbg/*:shared=True") # It doesn't fail due to missing binary, as it assigned dependency shared=True pkg_folder = c.created_layout().package() conaninfo = c.load(os.path.join(pkg_folder, "conaninfo.txt")) assert "shared=True" in conaninfo ================================================ FILE: test/integration/options/test_options_build_requires.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_build_requires_options_different(): # copied from https://github.com/conan-io/conan/pull/9839 # This is a test that crashed in 1.X, because of conflicting options client = TestClient() conanfile_openssl_1_1_1 = GenConanfile("openssl", "1.1.1") conanfile_openssl_3_0_0 = GenConanfile("openssl", "3.0.0") \ .with_option("no_fips", [True, False]) \ .with_default_option("no_fips", True) conanfile_cmake = GenConanfile("cmake", "0.1") \ .with_requires("openssl/1.1.1") conanfile_consumer = GenConanfile("consumer", "0.1") \ .with_build_requires("cmake/0.1") \ .with_requires("openssl/3.0.0") client.save({"openssl_1_1_1.py": conanfile_openssl_1_1_1, "openssl_3_0_0.py": conanfile_openssl_3_0_0, "conanfile_cmake.py": conanfile_cmake, "conanfile.py": conanfile_consumer}) client.run("create openssl_1_1_1.py") client.run("create openssl_3_0_0.py") client.run("create conanfile_cmake.py") client.run("install conanfile.py") # This test used to crash, not crashing means ok assert "openssl/1.1.1" in client.out assert "openssl/3.0.0: Already installed!" in client.out assert "cmake/0.1: Already installed!" in client.out def test_different_options_values_profile(): """ consumer -> protobuf (library) \\--(build)-> protobuf (protoc) protobuf by default is a static library (shared=False) The profile or CLI args can select for each one (library and protoc) the "shared" value """ c = TestClient() protobuf = textwrap.dedent(""" from conan import ConanFile class Proto(ConanFile): options = {"shared": [True, False]} default_options = {"shared": False} def package_info(self): self.output.info("MYOPTION: {}-{}".format(self.context, self.options.shared)) """) c.save({"protobuf/conanfile.py": protobuf, "consumer/conanfile.py": GenConanfile().with_requires("protobuf/1.0") .with_build_requires("protobuf/1.0")}) c.run("create protobuf --name=protobuf --version=1.0") c.run("create protobuf --name=protobuf --version=1.0 -o protobuf/*:shared=True") c.run("install consumer") assert "protobuf/1.0: MYOPTION: host-False" in c.out assert "protobuf/1.0: MYOPTION: build-False" in c.out # specifying it in the profile works c.run("install consumer -o protobuf/*:shared=True") assert "protobuf/1.0: MYOPTION: host-True" in c.out assert "protobuf/1.0: MYOPTION: build-False" in c.out c.run("install consumer -o protobuf/*:shared=True -o:b protobuf/*:shared=False") assert "protobuf/1.0: MYOPTION: host-True" in c.out assert "protobuf/1.0: MYOPTION: build-False" in c.out c.run("install consumer -o protobuf/*:shared=False -o:b protobuf/*:shared=True") assert "protobuf/1.0: MYOPTION: host-False" in c.out assert "protobuf/1.0: MYOPTION: build-True" in c.out c.run("install consumer -o:b protobuf/*:shared=True") assert "protobuf/1.0: MYOPTION: host-False" in c.out assert "protobuf/1.0: MYOPTION: build-True" in c.out @pytest.mark.parametrize("scope", ["protobuf/*:", ""]) def test_different_options_values_recipe(scope): """ consumer -> protobuf (library) \\--(build)-> protobuf (protoc) protobuf by default is a static library (shared=False) The "consumer" conanfile.py can use ``self.requires(...,options=)`` to define protobuf:shared """ c = TestClient() protobuf = textwrap.dedent(""" from conan import ConanFile class Proto(ConanFile): options = {"shared": [True, False]} default_options = {"shared": False} def package_info(self): self.output.info("MYOPTION: {}-{}".format(self.context, self.options.shared)) """) consumer_recipe = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): def requirements(self): self.requires("protobuf/1.0", options={{"{scope}shared": {host}}}) def build_requirements(self): self.build_requires("protobuf/1.0", options={{"{scope}shared": {build}}}) """) c.save({"conanfile.py": protobuf}) c.run("create . --name=protobuf --version=1.0") c.run("create . --name=protobuf --version=1.0 -o protobuf/*:shared=True") for host, build in ((True, True), (True, False), (False, True), (False, False)): c.save({"conanfile.py": consumer_recipe.format(host=host, build=build, scope=scope)}) c.run("install .") assert f"protobuf/1.0: MYOPTION: host-{host}" in c.out assert f"protobuf/1.0: MYOPTION: build-{build}" in c.out def test_different_options_values_recipe_attributes(): """ consumer -> protobuf (library) \\--(build)-> protobuf (protoc) protobuf by default is a static library (shared=False) The "consumer" conanfile.py can use ``default_options`` to define protobuf:shared """ c = TestClient() protobuf = textwrap.dedent(""" from conan import ConanFile class Proto(ConanFile): options = {"shared": [True, False]} default_options = {"shared": False} def package_info(self): self.output.info("MYOPTION: {}-{}".format(self.context, self.options.shared)) """) c.save({"conanfile.py": protobuf}) c.run("create . --name=protobuf --version=1.0") c.run("create . --name=protobuf --version=1.0 -o protobuf/*:shared=True") consumer_recipe = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): default_options = {{"protobuf/*:shared": {host}}} default_build_options = {{"protobuf/*:shared": {build}}} def requirements(self): self.requires("protobuf/1.0") def build_requirements(self): self.build_requires("protobuf/1.0") """) for host, build in ((True, True), (True, False), (False, True), (False, False)): c.save({"conanfile.py": consumer_recipe.format(host=host, build=build)}) c.run("install .") assert f"protobuf/1.0: MYOPTION: host-{host}" in c.out assert f"protobuf/1.0: MYOPTION: build-{build}" in c.out def test_different_options_values_recipe_priority(): """ consumer ---> mypkg ---> protobuf (library) \\--(build)-> protobuf (protoc) protobuf by default is a static library (shared=1) "consumer" defines a protobuf:shared=3 value, that must be respected for HOST context But build context, it is assigned by "mypkg", and build-require is private """ c = TestClient() protobuf = textwrap.dedent(""" from conan import ConanFile class Proto(ConanFile): options = {"shared": [1, 2, 3]} default_options = {"shared": 1} def package_id(self): self.output.info("MYOPTION: {}-{}".format(self.context, self.info.options.shared)) """) my_pkg = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): def requirements(self): self.requires("protobuf/1.0", options={"shared": 2}) def build_requirements(self): self.build_requires("protobuf/1.0", options={"shared": 2}) """) c.save({"protobuf/conanfile.py": protobuf, "mypkg/conanfile.py": my_pkg, "consumer/conanfile.py": GenConanfile().with_requires("mypkg/1.0") .with_default_option("protobuf/*:shared", 3)}) c.run("create protobuf --name=protobuf --version=1.0 -o protobuf/*:shared=2") c.run("create protobuf --name=protobuf --version=1.0 -o protobuf/*:shared=3") c.run("create mypkg --name=mypkg --version=1.0") c.run("install consumer") assert f"protobuf/1.0: MYOPTION: host-3" in c.out assert f"protobuf/1.0: MYOPTION: build-2" in c.out ================================================ FILE: test/integration/options/test_package_config_test.py ================================================ import pytest from conan.test.utils.tools import TestClient test_conanfile = """from conan import ConanFile class Test(ConanFile): options = {"shared": [True, False]} default_options = {"shared": False} def requirements(self): self.requires(self.tested_reference_str) self.output.info("shared (requirements): %s" % (self.options.shared)) def configure(self): self.output.info("shared (configure): %s" % (self.options.shared)) def build(self): self.output.info("shared (build): %s" % (self.options.shared)) def test(self): self.output.info("shared (test): %s" % (self.options.shared)) """ dep = """from conan import ConanFile class PkgConan(ConanFile): name = "dep" version = "0.1" options = {"shared": [True, False]} default_options = {"shared": False} def configure(self): self.output.info("shared (configure): %s" % str(self.options.shared)) def requirements(self): self.output.info("shared (requirements): %s" % str(self.options.shared)) def build(self): self.output.info("shared (build): %s" % str(self.options.shared)) """ conanfile = """from conan import ConanFile class PkgConan(ConanFile): name = "pkg" version = "0.1" options = {"shared": [True, False]} default_options = {"shared": False} def configure(self): self.output.info("shared (configure): %s" % str(self.options.shared)) def requirements(self): self.requires("dep/0.1") self.output.info("shared (requirements): %s" % str(self.options.shared)) def build(self): self.output.info("shared (build): %s" % str(self.options.shared)) """ class TestPackageOptionsCreate: def test_test_package(self): """ non scoped options will be put in the package scope -o shared=True <=> -o pkg:shared=True """ c = TestClient() c.save({"dep/conanfile.py": dep, "conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("export dep") c.run("create . -o shared=True --build=missing") assert "dep/0.1: shared (configure): False" in c.out assert "dep/0.1: shared (requirements): False" in c.out assert "dep/0.1: shared (build): False" in c.out assert "pkg/0.1: shared (configure): True" in c.out assert "pkg/0.1: shared (requirements): True" in c.out assert "pkg/0.1: shared (build): True" in c.out assert "pkg/0.1 (test package): shared (configure): False" in c.out assert "pkg/0.1 (test package): shared (requirements): False" in c.out assert "pkg/0.1 (test package): shared (build): False" in c.out assert "pkg/0.1 (test package): shared (test): False" in c.out def test_test_package_all_shared(self): """ all shared should affect both the package and the test_package """ c = TestClient() c.save({"dep/conanfile.py": dep, "conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("export dep") c.run("create . -o *:shared=True --build=missing") assert "dep/0.1: shared (configure): True" in c.out assert "dep/0.1: shared (requirements): True" in c.out assert "dep/0.1: shared (build): True" in c.out assert "pkg/0.1: shared (configure): True" in c.out assert "pkg/0.1: shared (requirements): True" in c.out assert "pkg/0.1: shared (build): True" in c.out assert "pkg/0.1 (test package): shared (configure): True" in c.out assert "pkg/0.1 (test package): shared (requirements): True" in c.out assert "pkg/0.1 (test package): shared (build): True" in c.out assert "pkg/0.1 (test package): shared (test): True" in c.out def test_test_package_consumers(self): c = TestClient() c.save({"dep/conanfile.py": dep, "conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("export dep") c.run("create . -o &:shared=True --build=missing") assert "dep/0.1: shared (configure): False" in c.out assert "dep/0.1: shared (requirements): False" in c.out assert "dep/0.1: shared (build): False" in c.out assert "pkg/0.1: shared (configure): True" in c.out assert "pkg/0.1: shared (requirements): True" in c.out assert "pkg/0.1: shared (build): True" in c.out assert "pkg/0.1 (test package): shared (configure): True" in c.out assert "pkg/0.1 (test package): shared (requirements): True" in c.out assert "pkg/0.1 (test package): shared (build): True" in c.out assert "pkg/0.1 (test package): shared (test): True" in c.out c.run("install --requires=dep/0.1 -o &:shared=True -b=missing") assert "dep/0.1: shared (configure): True" in c.out assert "dep/0.1: shared (requirements): True" in c.out assert "dep/0.1: shared (build): True" in c.out def test_test_package_non_consumers(self): c = TestClient() c.save({"dep/conanfile.py": dep, "conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("export dep") c.run("create . -o !&:shared=True --build=missing") assert "dep/0.1: shared (configure): True" in c.out assert "dep/0.1: shared (requirements): True" in c.out assert "dep/0.1: shared (build): True" in c.out assert "pkg/0.1: shared (configure): False" in c.out assert "pkg/0.1: shared (requirements): False" in c.out assert "pkg/0.1: shared (build): False" in c.out assert "pkg/0.1 (test package): shared (configure): False" in c.out assert "pkg/0.1 (test package): shared (requirements): False" in c.out assert "pkg/0.1 (test package): shared (build): False" in c.out assert "pkg/0.1 (test package): shared (test): False" in c.out def test_test_package_only(self): c = TestClient() c.save({"dep/conanfile.py": dep, "conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("export dep") c.run("create . -o &:shared=True -o shared=False --build=missing") assert "dep/0.1: shared (configure): False" in c.out assert "dep/0.1: shared (requirements): False" in c.out assert "dep/0.1: shared (build): False" in c.out assert "pkg/0.1: shared (configure): False" in c.out assert "pkg/0.1: shared (requirements): False" in c.out assert "pkg/0.1: shared (build): False" in c.out assert "pkg/0.1 (test package): shared (configure): True" in c.out assert "pkg/0.1 (test package): shared (requirements): True" in c.out assert "pkg/0.1 (test package): shared (build): True" in c.out assert "pkg/0.1 (test package): shared (test): True" in c.out class TestPackageOptionsInstall: @pytest.mark.parametrize("pattern", ["", "*:", "&:"]) def test_test_package(self, pattern): c = TestClient() c.save({"dep/conanfile.py": dep, "conanfile.py": conanfile}) c.run("export dep") c.run(f"build . -o {pattern}shared=True --build=missing") dep_shared = "False" if "*" not in pattern else "True" assert f"dep/0.1: shared (configure): {dep_shared}" in c.out assert f"dep/0.1: shared (requirements): {dep_shared}" in c.out assert f"dep/0.1: shared (build): {dep_shared}" in c.out assert "conanfile.py (pkg/0.1): shared (configure): True" in c.out assert "conanfile.py (pkg/0.1): shared (requirements): True" in c.out assert "conanfile.py (pkg/0.1): shared (build): True" in c.out ================================================ FILE: test/integration/package_id/__init__.py ================================================ ================================================ FILE: test/integration/package_id/build_id_test.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient conanfile = """import os from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.1" settings = "os", "build_type" build_policy = "missing" def build_id(self): if self.settings.os == "Windows": self.info_build.settings.build_type = "Any" def build(self): self.output.info("Building my code!") def package(self): self.output.info("Packaging %s!" % self.settings.build_type) """ class TestBuildIdTest: def test_create(self): # Ensure that build_id() works when multiple create calls are made client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . -s os=Windows -s build_type=Release") assert "pkg/0.1: Calling build()" in client.out assert "Building my code!" in client.out assert "Packaging Release!" in client.out # Debug must not build client.run("create . -s os=Windows -s build_type=Debug") assert "pkg/0.1: Calling build()" not in client.out assert "Building my code!" not in client.out assert "Packaging Debug!" in client.out client.run("create . -s os=Linux -s build_type=Release") assert "pkg/0.1: Calling build()" in client.out assert "Building my code!" in client.out assert "Packaging Release!" in client.out client.run("create . -s os=Linux -s build_type=Debug") assert "Building my code!" in client.out assert "pkg/0.1: Calling build()" in client.out assert "Packaging Debug!" in client.out def test_basic_install(self): client = TestClient() client.save({"conanfile.py": conanfile}) client.run("export . ") # Windows Debug client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Debug') assert "Building my code!" in client.out assert "Packaging Debug!" in client.out # Package Windows Release, it will reuse the previous build client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Release') assert "Building my code!" not in client.out assert "Packaging Release!" in client.out # Now Linux Debug client.run('install --requires=pkg/0.1 -s os=Linux -s build_type=Debug') assert "Building my code!" in client.out assert "Packaging Debug!" in client.out # Linux Release must build again, as it is not affected by build_id() client.run('install --requires=pkg/0.1 -s os=Linux -s build_type=Release') assert "Building my code!" in client.out assert "Packaging Release!" in client.out # But if the packages are removed, and we change the order, keeps working client.run("remove *:* -c") client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Release') assert "Building my code!" in client.out assert "Packaging Release!" in client.out client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Debug') assert "Building my code!" not in client.out assert "Packaging Debug!" in client.out def test_clean_build(self): client = TestClient() client.save({"conanfile.py": conanfile}) client.run("export . ") client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Debug') # Package Windows Release, it will reuse the previous build client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Release') assert "Building my code!" not in client.out assert "Packaging Release!" in client.out client.run("cache clean") # packages are still there client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Debug') assert "Building my code!" not in client.out assert "Packaging Release!" not in client.out client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Release') assert "Building my code!" not in client.out assert "Packaging Release!" not in client.out # Lets force the first rebuild, different order Linux first client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Release --build=pkg*') assert "Building my code!" in client.out assert "Packaging Release!" in client.out client.run('install --requires=pkg/0.1 -s os=Windows -s build_type=Debug --build=pkg*') assert "Building my code!" not in client.out assert "Packaging Debug!" in client.out def test_failed_build(self): # Repeated failed builds keep failing fail_conanfile = textwrap.dedent("""\ from conan import ConanFile class MyTest(ConanFile): settings = "build_type" def build(self): raise Exception("Failed build!!") """) client = TestClient() # NORMAL case, every create fails client.save({"conanfile.py": fail_conanfile}) client.run("create . --name=pkg --version=0.1 ", assert_error=True) assert "ERROR: pkg/0.1: Error in build() method, line 5" in client.out client.run("create . --name=pkg --version=0.1 ", assert_error=True) assert "ERROR: pkg/0.1: Error in build() method, line 5" in client.out # now test with build_id client.save({"conanfile.py": fail_conanfile + " def build_id(self): self.info_build.settings.build_type = 'any'"}) client.run("create . --name=pkg --version=0.1 ", assert_error=True) assert "ERROR: pkg/0.1: Error in build() method, line 5" in client.out client.run("create . --name=pkg --version=0.1 ", assert_error=True) assert "ERROR: pkg/0.1: Error in build() method, line 5" in client.out def test_any_os_arch(self): conanfile_os = textwrap.dedent(""" import os from conan import ConanFile class MyTest(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "build_type", "compiler" def build_id(self): self.info_build.settings.build_type = "AnyValue" self.info_build.settings.os = "AnyValue" self.info_build.settings.arch = "AnyValue" self.info_build.settings.compiler = "AnyValue" def build(self): self.output.info("Building my code!") def package(self): self.output.info("Packaging %s-%s!" % (self.settings.os, self.settings.arch)) """) client = TestClient() client.save({"conanfile.py": conanfile_os}) client.run("create . -s os=Windows -s arch=x86_64 -s build_type=Release") assert "pkg/0.1: Calling build()" in client.out assert "Building my code!" in client.out assert "Packaging Windows-x86_64!" in client.out # Others must not build client.run("create . -s os=Linux -s arch=x86 -s build_type=Debug") assert "pkg/0.1: Calling build()" not in client.out assert "Building my code!" not in client.out assert "Packaging Linux-x86!" in client.out def test_remove_require(): c = TestClient() remove = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "consumer" version = "1.0" requires = "dep/1.0" def build_id(self): self.info_build.requires.remove("dep") """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "consumer/conanfile.py": remove}) c.run("create dep") c.run("create consumer") def test_build_id_error(): """ https://github.com/conan-io/conan/issues/14537 This was failing because the ``DB get_matching_build_id()`` function was returning all prefs for a given ref, and the order could be by alphabetic package_id, but only the first one really contains the build folder with the artifacts. It was fixed by defining the DB "build_id" only for the first reference containing that build_id, not all """ c = TestClient() myconan = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Version class BuildIdTestConan(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False]} default_options = {"shared": False} def build_id(self): self.info_build.settings.build_type = "Any" self.info_build.options.shared = "Any" def package_id(self): compilerVer = Version(str(self.info.settings.compiler.version)) if self.info.settings.compiler == "gcc": if compilerVer >= "7": self.info.settings.compiler.version = "7+" """) host_profile = textwrap.dedent(""" [settings] arch = x86_64 build_type = Release compiler = gcc compiler.cppstd = gnu17 compiler.libcxx = libstdc++11 compiler.version = 8 os = Linux """) c.save({"conanfile.py": myconan, "myprofile": host_profile}) c.run("create . " "-pr:h myprofile -pr:b myprofile " "-s build_type=Debug -o pkg/*:shared=True") c.assert_listed_binary({"pkg/0.1": ("538f60f3919ea9b8ea9e7c63c5948abe44913bec", "Build")}) assert "pkg/0.1: build_id() computed 7d169d0d018d239cff27eb081e3f6575554e05c5" in c.out c.run("create . " "-pr:h myprofile -pr:b myprofile " "-s build_type=Debug -o pkg/*:shared=False") c.assert_listed_binary({"pkg/0.1": ("ba41c80b0373ef66e2ca95ed56961153082fbcd9", "Build")}) assert "pkg/0.1: build_id() computed 7d169d0d018d239cff27eb081e3f6575554e05c5" in c.out assert "pkg/0.1: Won't be built, using previous build folder as defined in build_id()" in c.out c.run("create . " "-pr:h myprofile -pr:b myprofile " "-s build_type=Release -o pkg/*:shared=True") c.assert_listed_binary({"pkg/0.1": ("2eb252449df37d245568e32e5a41d0540db3c6e2", "Build")}) assert "pkg/0.1: build_id() computed 7d169d0d018d239cff27eb081e3f6575554e05c5" in c.out assert "pkg/0.1: Won't be built, using previous build folder as defined in build_id()" in c.out c.run("create . " "-pr:h myprofile -pr:b myprofile " "-s build_type=Release -o pkg/*:shared=False") c.assert_listed_binary({"pkg/0.1": ("978c5442906f96846ccb201129ba1559071ce4ab", "Build")}) assert "pkg/0.1: build_id() computed 7d169d0d018d239cff27eb081e3f6575554e05c5" in c.out assert "pkg/0.1: Won't be built, using previous build folder as defined in build_id()" in c.out ================================================ FILE: test/integration/package_id/compatible_test.py ================================================ import json import textwrap import pytest from conan.test.utils.env import environment_update from conan.test.utils.tools import TestClient, GenConanfile, TestServer class TestCompatibleIDsTest: def test_compatible_setting_no_binary(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler" def compatibility(self): if self.settings.compiler == "gcc" and self.settings.compiler.version == "4.9": return [{"settings": [("compiler.version", v)]} for v in ("4.8", "4.7", "4.6")] def package_info(self): self.output.info("PackageInfo!: Gcc version: %s!" % self.settings.compiler.version) """) profile = textwrap.dedent(""" [settings] os = Linux compiler=gcc compiler.version=4.9 compiler.libcxx=libstdc++ """) client.save({"conanfile.py": conanfile, "myprofile": profile}) # Create package with gcc 4.8 client.run("export . --name=pkg --version=0.1 --user=user --channel=stable") assert ("pkg/0.1@user/stable: Exported: " "pkg/0.1@user/stable#d165eb4bcdd1c894a97d2a212956f5fe") in client.out client.run("export . --name=lib --version=0.1 --user=user --channel=stable") # package can be used with a profile gcc 4.9 falling back to 4.8 binary client.save({"conanfile.py": GenConanfile().with_requires("pkg/0.1@user/stable", "lib/0.1@user/stable")}) # No fallback client.run("install . -pr=myprofile --build=missing -u=lib") assert "pkg/0.1@user/stable: PackageInfo!: Gcc version: 4.9!" in client.out client.assert_listed_binary({"pkg/0.1@user/stable": ("1ded27c9546219fbd04d4440e05b2298f8230047", "Build")}) assert ("lib/0.1@user/stable: Compatible configurations not found in cache, " "checking servers") not in client.out assert ("pkg/0.1@user/stable: Compatible configurations not found in cache, " "checking servers") in client.out def test_compatible_setting_no_user_channel(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler" def compatibility(self): if self.settings.compiler == "gcc" and self.settings.compiler.version == "4.9": return [{"settings": [("compiler.version", v)]} for v in ("4.8", "4.7", "4.6")] """) profile = textwrap.dedent(""" [settings] os = Linux compiler=gcc compiler.version=4.9 compiler.libcxx=libstdc++ """) client.save({"conanfile.py": conanfile, "myprofile": profile}) # No user/channel client.run("create . --name=pkg --version=0.1 -pr=myprofile -s compiler.version=4.8") package_id = client.created_package_id("pkg/0.1") client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1")}) client.run("install . -pr=myprofile") client.assert_listed_binary({"pkg/0.1": (package_id, "Cache")}) assert "pkg/0.1: Already installed!" in client.out def test_compatible_option(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = {"optimized": [1, 2, 3]} default_options = {"optimized": 1} def compatibility(self): return [{"options": [("optimized", v)]} for v in range(int(self.options.optimized), 0, -1)] def package_info(self): self.output.info("PackageInfo!: Option optimized %s!" % self.options.optimized) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=stable") package_id = client.created_package_id("pkg/0.1@user/stable") assert f"pkg/0.1@user/stable: Package '{package_id}' created" in client.out client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1@user/stable")}) client.run("install . -o pkg/*:optimized=2 -vv") # Information messages missing_id = "0a8157f8083f5ece34828d27fb2bf5373ba26366" assert "pkg/0.1@user/stable: PackageInfo!: Option optimized 1!" in client.out assert (f"pkg/0.1@user/stable: Compatible package ID {missing_id} " f"equal to the default package ID") in client.out assert f"pkg/0.1@user/stable: Main binary package '{missing_id}' missing" in client.out assert f"Found compatible package '{package_id}'" in client.out # checking the resulting dependencies client.assert_listed_binary({"pkg/0.1@user/stable": (package_id, "Cache")}) assert "pkg/0.1@user/stable: Already installed!" in client.out client.run("install . -o pkg/*:optimized=3") client.assert_listed_binary({"pkg/0.1@user/stable": (package_id, "Cache")}) assert "pkg/0.1@user/stable: Already installed!" in client.out def test_package_id_consumers(self): # If we fallback to a different binary upstream and we are using a "package_revision_mode" # the current package should have a different binary package ID too. client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler" def compatibility(self): return [{"settings": [("compiler.version", "4.8")]}] def package_info(self): self.output.info("PackageInfo!: Gcc version: %s!" % self.settings.compiler.version) """) profile = textwrap.dedent(""" [settings] os = Linux compiler=gcc compiler.version=4.9 compiler.libcxx=libstdc++ """) client.save_home({"global.conf": "core.package_id:default_unknown_mode=recipe_revision_mode"}) client.save({"conanfile.py": conanfile, "myprofile": profile}) # Create package with gcc 4.8 client.run("create . --name=pkg --version=0.1 --user=user --channel=stable " "-pr=myprofile -s compiler.version=4.8") package_id = client.created_package_id("pkg/0.1@user/stable") assert f"pkg/0.1@user/stable: Package '{package_id}' created" in client.out # package can be used with a profile gcc 4.9 falling back to 4.8 binary client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1@user/stable")}) client.run("create . --name=consumer --version=0.1 --user=user --channel=stable -pr=myprofile") assert "pkg/0.1@user/stable: PackageInfo!: Gcc version: 4.8!" in client.out client.assert_listed_binary({"pkg/0.1@user/stable": (package_id, "Cache")}) assert "pkg/0.1@user/stable: Already installed!" in client.out consumer_id = "96465a24a53766aaac28e270d196db295e2fd22a" client.assert_listed_binary({"consumer/0.1@user/stable": (consumer_id, "Build")}) assert f"consumer/0.1@user/stable: Package '{consumer_id}' created" in client.out # Create package with gcc 4.9 client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=stable -pr=myprofile") package_id = "1ded27c9546219fbd04d4440e05b2298f8230047" assert f"pkg/0.1@user/stable: Package '{package_id}' created" in client.out # Consume it client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1@user/stable")}) client.run("create . --name=consumer --version=0.1 --user=user --channel=stable -pr=myprofile") assert "pkg/0.1@user/stable: PackageInfo!: Gcc version: 4.9!" in client.out client.assert_listed_binary({"pkg/0.1@user/stable": (f"{package_id}", "Cache")}) assert "pkg/0.1@user/stable: Already installed!" in client.out consumer_id = "41bc915fa380e9a046aacbc21256fcb46ad3179d" client.assert_listed_binary({"consumer/0.1@user/stable": (consumer_id, "Build")}) assert f"consumer/0.1@user/stable: Package '{consumer_id}' created" in client.out def test_build_missing(self): # https://github.com/conan-io/conan/issues/6133 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): settings = "os" def compatibility(self): if self.settings.os == "Windows": return [{"settings": [("os", "Linux")]}] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -s os=Linux") package_id = client.created_package_id("pkg/0.1@user/testing") client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1@user/testing")}) client.run("install . -s os=Windows --build=missing") client.assert_listed_binary({"pkg/0.1@user/testing": (package_id, "Cache")}) assert "pkg/0.1@user/testing: Already installed!" in client.out def test_compatible_package_python_requires(self): # https://github.com/conan-io/conan/issues/6609 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=tool --version=0.1") conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): settings = "os" python_requires = "tool/0.1" def compatibility(self): if self.settings.os == "Windows": return [{"settings": [("os", "Linux")]}] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -s os=Linux") package_id = client.created_package_id("pkg/0.1@user/testing") client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1@user/testing")}) client.run("install . -s os=Windows") client.assert_listed_binary({"pkg/0.1@user/testing": (package_id, "Cache")}) assert "pkg/0.1@user/testing: Already installed!" in client.out def test_compatible_lockfile(self): # https://github.com/conan-io/conan/issues/9002 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def compatibility(self): if self.settings.os == "Windows": return [{"settings": [("os", "Linux")]}] def package_info(self): self.output.info("PackageInfo!: OS: %s!" % self.settings.os) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 -s os=Linux") assert "pkg/0.1: PackageInfo!: OS: Linux!" in client.out assert "pkg/0.1: Package '9a4eb3c8701508aa9458b1a73d0633783ecc2270' built" in client.out client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1")}) client.run("lock create . -s os=Windows --lockfile-out=deps.lock") client.run("install . -s os=Windows --lockfile=deps.lock") assert "pkg/0.1: PackageInfo!: OS: Linux!" in client.out assert "9a4eb3c8701508aa9458b1a73d0633783ecc2270" in client.out assert "pkg/0.1: Already installed!" in client.out def test_compatible_diamond(self): # https://github.com/conan-io/conan/issues/9880 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): {} settings = "build_type" def compatibility(self): if self.settings.build_type == "Debug": return [{{"settings": [("build_type", "Release")]}}] """) private = """def requirements(self): self.requires("pkga/0.1", visible=False) """ client.save({"pkga/conanfile.py": conanfile.format(""), "pkgb/conanfile.py": conanfile.format(private), "pkgc/conanfile.py": conanfile.format('requires = "pkga/0.1"'), "pkgd/conanfile.py": conanfile.format('requires = "pkgb/0.1", "pkgc/0.1"') }) client.run("create pkga --name=pkga --version=0.1 -s build_type=Release") client.run("create pkgb --name=pkgb --version=0.1 -s build_type=Release") client.run("create pkgc --name=pkgc --version=0.1 -s build_type=Release") client.run("install pkgd -s build_type=Debug") client.assert_listed_binary({"pkga/0.1": ("efa83b160a55b033c4ea706ddb980cd708e3ba1b", "Cache")}) class TestNewCompatibility: def test_compatible_setting(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler" def compatibility(self): if self.settings.compiler == "gcc" and self.settings.compiler.version == "4.9": return [{"settings": [("compiler.version", v)]} for v in ("4.8", "4.7", "4.6")] def package_info(self): self.output.info("PackageInfo!: Gcc version: %s!" % self.settings.compiler.version) """) profile = textwrap.dedent(""" [settings] os = Linux compiler=gcc compiler.version=4.9 compiler.libcxx=libstdc++ """) c.save({"conanfile.py": conanfile, "myprofile": profile}) # Create package with gcc 4.8 c.run("create . -pr=myprofile -s compiler.version=4.8") package_id = "c0c95d81351786c6c1103566a27fb1c1f78629ac" assert f"pkg/0.1: Package '{package_id}' created" in c.out # package can be used with a profile gcc 4.9 falling back to 4.8 binary c.save({"conanfile.py": GenConanfile().with_require("pkg/0.1")}) c.run("install . -pr=myprofile") assert "pkg/0.1: PackageInfo!: Gcc version: 4.8!" in c.out c.assert_listed_binary({"pkg/0.1": (f"{package_id}", "Cache")}) assert "pkg/0.1: Already installed!" in c.out def test_compatibility_remove_package_id(self): # https://github.com/conan-io/conan/issues/13727 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PdfiumConan(ConanFile): name = "pdfium" version = "2020.9" settings = "os", "compiler", "arch", "build_type" build_policy = "never" def compatibility(self): result = [] if self.info.settings.build_type == "Debug": result.append({"settings": [("build_type", "Release")]}) return result def package_id(self): del self.info.settings.compiler.runtime del self.info.settings.compiler.runtime_type """) profile = textwrap.dedent(""" [settings] os = Windows compiler=msvc compiler.version=192 compiler.runtime=dynamic build_type=Release arch=x86_64 """) c.save({"conanfile.py": conanfile, "myprofile": profile}) c.run("create . -pr=myprofile", assert_error=True) assert "ERROR: This package cannot be created, 'build_policy=never', " \ "it can only be 'export-pkg'" in c.out c.run("export-pkg . -pr=myprofile") c.run("list pdfium/2020.9:*") c.run("install --requires=pdfium/2020.9 -pr=myprofile -s build_type=Debug") assert "Found compatible package" in c.out def test_compatibility_erase_package_id(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PdfiumConan(ConanFile): name = "diligent-core" version = "1.0" settings = "compiler" options = {"foo": ["no"]} def package_id(self): self.info.settings.compiler.runtime = "foobar" self.info.options.foo = "yes" """) profile = textwrap.dedent(""" [settings] os = Windows compiler=msvc compiler.version=192 compiler.runtime=dynamic build_type=Release arch=x86_64 """) c.save({"conanfile.py": conanfile, "myprofile": profile}) c.run("create . -pr:a=myprofile -s compiler.cppstd=20") c.run("install --requires=diligent-core/1.0 -pr:a=myprofile -s compiler.cppstd=17") assert "ERROR: Invalid setting 'foobar' is not a valid 'settings.compiler.runtime' value." not in c.out def test_compatibility_msvc_and_cppstd(self): """msvc 194 would not find compatible packages built with same version but different cppstd due to an issue in the msvc fallback compatibility rule.""" tc = TestClient() profile = textwrap.dedent(""" [settings] compiler=msvc compiler.version=194 compiler.runtime=dynamic """) tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_setting("compiler"), "profile": profile}) tc.run("create dep -pr=profile -s compiler.cppstd=20") tc.run("install --requires=dep/1.0 -pr=profile -s compiler.cppstd=17") tc.assert_listed_binary({"dep/1.0": ("b6d26a6bc439b25b434113982791edf9cab4d004", "Cache")}) tc.run("remove * -c") tc.run("create dep -pr=profile -s compiler.version=193 -s compiler.cppstd=20") tc.run("install --requires=dep/1.0 -pr=profile -s compiler.cppstd=17") assert "compiler.cppstd=20, compiler.version=193" in tc.out tc.assert_listed_binary({"dep/1.0": ("535899bb58c3ca7d80a380313d31f4729e735d1c", "Cache")}) class TestCompatibleBuild: def test_build_compatible(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler" def validate(self): check_min_cppstd(self, 14) """) c.save({"conanfile.py": conanfile}) settings = "-s os=Windows -s compiler=gcc -s compiler.version=11 " \ "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=11" c.run(f"create . {settings}", assert_error=True) c.assert_listed_binary({"pkg/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid")}) assert "pkg/0.1: Invalid: Current cppstd (11)" in c.out c.run(f"create . {settings} --build=compatible:&") # the one for cppstd=14 is built!! c.assert_listed_binary({"pkg/0.1": ("389803bed06200476fcee1af2023d4e9bfa24ff9", "Build")}) c.run("list *:*") assert "compiler.cppstd: 14" in c.out c.run(f"create . {settings} --build=& --build=compatible:&") # the one for cppstd=14 is built!! c.assert_listed_binary({"pkg/0.1": ("389803bed06200476fcee1af2023d4e9bfa24ff9", "Cache")}) def test_build_compatible_cant_build(self): # requires c++17 to build, can be consumed with c++14 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler" def validate(self): check_min_cppstd(self, 14) def validate_build(self): check_min_cppstd(self, 17) """) c.save({"conanfile.py": conanfile}) settings = "-s os=Windows -s compiler=gcc -s compiler.version=11 " \ "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=11" c.run(f"create . {settings}", assert_error=True) c.assert_listed_binary({"pkg/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid")}) assert "pkg/0.1: Invalid: Current cppstd (11)" in c.out c.run(f"create . {settings} --build=missing", assert_error=True) c.assert_listed_binary({"pkg/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid")}) assert "pkg/0.1: Invalid: Current cppstd (11)" in c.out c.run(f"create . {settings} --build=compatible:&") # the one for cppstd=17 is built!! c.assert_listed_binary({"pkg/0.1": ("58fb8ac6c2dc3e3f837253ce1a6ea59011525866", "Build")}) c.run("list *:*") assert "compiler.cppstd: 17" in c.out def test_build_compatible_cant_build2(self): # requires c++17 to build, can be consumed with c++11 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler" def validate(self): check_min_cppstd(self, 11) def validate_build(self): check_min_cppstd(self, 17) """) c.save({"conanfile.py": conanfile}) settings = "-s os=Windows -s compiler=gcc -s compiler.version=11 " \ "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=11" c.run(f"create . {settings}", assert_error=True) c.assert_listed_binary({"pkg/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid")}) assert "pkg/0.1: Cannot build for this configuration: Current cppstd (11)" in c.out c.run(f"create . {settings} --build=missing", assert_error=True) # the one for cppstd=17 is built!! c.assert_listed_binary({"pkg/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid")}) assert "pkg/0.1: Cannot build for this configuration: Current cppstd (11)" in c.out c.run(f"create . {settings} --build=compatible:&") # the one for cppstd=17 is built!! c.assert_listed_binary({"pkg/0.1": ("58fb8ac6c2dc3e3f837253ce1a6ea59011525866", "Build")}) c.run("list *:*") assert "compiler.cppstd: 17" in c.out def test_build_compatible_cant_build_only(self): # requires c++17 to build, but don't specify consumption c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler" def validate_build(self): check_min_cppstd(self, 17) """) c.save({"conanfile.py": conanfile}) settings = "-s os=Windows -s compiler=gcc -s compiler.version=11 " \ "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=11" c.run(f"create . {settings}", assert_error=True) c.assert_listed_binary({"pkg/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid")}) assert "pkg/0.1: Cannot build for this configuration: Current cppstd (11)" in c.out c.run(f"create . {settings} --build=missing", assert_error=True) # the one for cppstd=17 is built!! c.assert_listed_binary({"pkg/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid")}) assert "pkg/0.1: Cannot build for this configuration: Current cppstd (11)" in c.out c.run(f"create . {settings} --build=compatible:&") # the one for cppstd=17 is built!! c.assert_listed_binary({"pkg/0.1": ("58fb8ac6c2dc3e3f837253ce1a6ea59011525866", "Build")}) c.run("list *:*") assert "compiler.cppstd: 17" in c.out def test_multi_level_build_compatible(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "{name}" version = "0.1" settings = "os", "compiler" {requires} def validate(self): check_min_cppstd(self, {cppstd}) """) c.save({"liba/conanfile.py": conanfile.format(name="liba", cppstd=14, requires=""), "libb/conanfile.py": conanfile.format(name="libb", cppstd=17, requires='requires="liba/0.1"')}) c.run("export liba") c.run("export libb") settings = "-s os=Windows -s compiler=gcc -s compiler.version=11 " \ "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=11" c.run(f"install --requires=libb/0.1 {settings}", assert_error=True) c.assert_listed_binary({"liba/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Invalid"), "libb/0.1": ("144910d65b27bcbf7d544201f5578555bbd0376e", "Invalid")}) assert "liba/0.1: Invalid: Current cppstd (11)" in c.out assert "libb/0.1: Invalid: Current cppstd (11)" in c.out c.run(f"install --requires=libb/0.1 {settings} --build=compatible") # the one for cppstd=14 is built!! c.assert_listed_binary({"liba/0.1": ("389803bed06200476fcee1af2023d4e9bfa24ff9", "Build"), "libb/0.1": ("8f29f49be3ba2b6cbc9fa1e05432ce928b96ae5d", "Build")}) c.run("list liba:*") assert "compiler.cppstd: 14" in c.out c.run("list libb:*") assert "compiler.cppstd: 17" in c.out def test_multi_level_build_compatible_build_order(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "{name}" version = "0.1" settings = "os", "compiler" {requires} def validate_build(self): check_min_cppstd(self, {cppstd}) """) c.save({"liba/conanfile.py": conanfile.format(name="liba", cppstd=14, requires=""), "libb/conanfile.py": conanfile.format(name="libb", cppstd=17, requires='requires="liba/0.1"')}) c.run("export liba") c.run("export libb") settings = "-s os=Windows -s compiler=gcc -s compiler.version=11 " \ "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=11" c.run(f"graph build-order --requires=libb/0.1 {settings} --format=json", assert_error=True, redirect_stdout="build_order.json") c.assert_listed_binary({"liba/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Missing"), "libb/0.1": ("144910d65b27bcbf7d544201f5578555bbd0376e", "Missing")}) c.run(f"graph build-order --requires=libb/0.1 {settings} --build=compatible " "--order-by=configuration --format=json", redirect_stdout="build_order.json") bo = json.loads(c.load("build_order.json")) liba = bo["order"][0][0] assert liba["ref"] == "liba/0.1#c1459d256a9c2d3c49d149fd7c43310c" assert liba["info"]["compatibility_delta"] == {"settings": [["compiler.cppstd", "14"]]} assert liba["build_args"] == "--requires=liba/0.1 --build=compatible:liba/0.1" # Lets make sure the build works too c.run(f"install {settings} {liba['build_args']}") libb = bo["order"][1][0] assert libb["ref"] == "libb/0.1#62bb167aaa5306d1ac757bb817797f9e" assert libb["info"]["compatibility_delta"] == {"settings": [["compiler.cppstd", "17"]]} assert libb["build_args"] == "--requires=libb/0.1 --build=compatible:libb/0.1" # Lets make sure the build works too c.run(f"install {settings} {libb['build_args']}") # Now lets make sure that build-order-merge works too c.run(f"graph build-order --requires=libb/0.1 {settings} -s compiler.version=12 " "--build=compatible --order-by=configuration --format=json", redirect_stdout="build_order2.json") c.run(f"graph build-order-merge --file=build_order.json --file=build_order2.json -f=json", redirect_stdout="build_order_merged.json") bo = json.loads(c.load("build_order_merged.json")) for pkg_index in (0, 1): liba = bo["order"][0][pkg_index] assert liba["ref"] == "liba/0.1#c1459d256a9c2d3c49d149fd7c43310c" assert liba["info"]["compatibility_delta"] == {"settings": [["compiler.cppstd", "14"]]} assert liba["build_args"] == "--requires=liba/0.1 --build=compatible:liba/0.1" # By recipe also works c.run("remove *:* -c") c.run(f"graph build-order --requires=libb/0.1 {settings} --build=compatible " "--order-by=recipe --format=json", redirect_stdout="build_order.json") bo = json.loads(c.load("build_order.json")) liba = bo["order"][0][0] assert liba["ref"] == "liba/0.1#c1459d256a9c2d3c49d149fd7c43310c" pkga = liba["packages"][0][0] assert pkga["info"]["compatibility_delta"] == {"settings": [["compiler.cppstd", "14"]]} assert pkga["build_args"] == "--requires=liba/0.1 --build=compatible:liba/0.1" libb = bo["order"][1][0] assert libb["ref"] == "libb/0.1#62bb167aaa5306d1ac757bb817797f9e" pkgb = libb["packages"][0][0] assert pkgb["info"]["compatibility_delta"] == {"settings": [["compiler.cppstd", "17"]]} assert pkgb["build_args"] == "--requires=libb/0.1 --build=compatible:libb/0.1" # Now lets make sure that build-order-merge works too c.run(f"graph build-order --requires=libb/0.1 {settings} -s compiler.version=12 " "--order-by=recipe --build=compatible --format=json", redirect_stdout="build_order2.json") c.run(f"graph build-order-merge --file=build_order.json --file=build_order2.json -f=json", redirect_stdout="build_order_merged.json") bo = json.loads(c.load("build_order_merged.json")) liba = bo["order"][0][0] assert liba["ref"] == "liba/0.1#c1459d256a9c2d3c49d149fd7c43310c" for pkg_index in (0, 1): pkga = liba["packages"][0][pkg_index] assert pkga["info"]["compatibility_delta"] == {"settings": [["compiler.cppstd", "14"]]} assert pkga["build_args"] == "--requires=liba/0.1 --build=compatible:liba/0.1" @pytest.mark.parametrize("validate, validate_build, expected", [("pass", "pass", 14), ("check_min_cppstd(self, 17)", "pass", 17), ("pass", "check_min_cppstd(self, 20)", 20), ("check_min_cppstd(self, 17)", "check_min_cppstd(self, 20)", 20)]) def test_compatible_build_test_package(self, validate, validate_build, expected): """ This test shows that the --build=compatible does not trigger the build of a dependency twice when there is a "test_package" """ tc = TestClient() conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "compiler" def validate(self): {validate} def validate_build(self): {validate_build} """) tc.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_test("pass")}) tc.run("create . -s os=Linux -s compiler=gcc -s compiler.version=13 " "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=14 --build=compatible:&") assert f"compiler.cppstd={expected}" in tc.out # Without checking if the compatibles are already in the cache, it will build twice, # once for the package and once for the test_package assert tc.out.count("pkg/0.1: Building your package") == 1 tc.run("list *:*") assert f"compiler.cppstd: {expected}" in tc.out @pytest.mark.parametrize("validate, validate_build, expected", [("check_min_cppstd(self, 17)", "pass", 17), ("pass", "check_min_cppstd(self, 20)", 20), ("check_min_cppstd(self, 17)", "check_min_cppstd(self, 20)", 20)]) def test_compatible_consumer_rebuild(self, validate, validate_build, expected): """ This test shows that the --build=compatible does not trigger the build of a dependency when it already exists in the cache """ tc = TestClient() conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "compiler" def validate(self): {validate} def validate_build(self): {validate_build} """) tc.save({"pkg/conanfile.py": conanfile, "app/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) settings = ("-s os=Linux -s compiler=gcc -s compiler.version=13 " "-s compiler.libcxx=libstdc++11") tc.run(f"create pkg {settings} -s compiler.cppstd=14 --build=compatible:&") tc.run("list *:*") assert f"compiler.cppstd: {expected}" in tc.out if validate == "pass": tc.run(f"install app {settings} -s compiler.cppstd=14") assert ("pkg/0.1: Found compatible package '151b937d845d306265254e74d7af81d35b2099fc': " "compiler.cppstd=20") in tc.out else: tc.run(f"install app {settings} -s compiler.cppstd=14", assert_error=True) assert "pkg/0.1: Invalid: Current cppstd (14) is lower" in tc.out tc.run(f"install app {settings} -s compiler.cppstd=14 --build=compatible") assert "pkg/0.1: Found compatible package" in tc.out # Without checking if the compatibles are already in the cache, it will build twice, # once for the package and once for the test_package assert tc.out.count("pkg/0.1: Building your package") == 0 def test_compatibility_new_setting_forwards_compat(): """ This test tries to reflect the following scenario: - User adds a new setting (libc.version in this case) - This setting is forward compatible How is it solved with compatibility.py? Like this: """ tc = TestClient() tc.save_home({"settings_user.yml": "libc_version: [1, 2, 3]"}) tc.save({"conanfile.py": GenConanfile("dep", "1.0").with_settings("libc_version", "compiler")}) # The extra cppstd and compiler versions are for later demonstrations of combinations of settings # The cppstd=17 and compiler.version=193 are used thought until the last 2 install calls tc.run("create . -s=libc_version=2 -s=compiler.cppstd=17") dep_package_id = tc.created_package_id("dep/1.0") tc.run("install --requires=dep/1.0 -s=libc_version=3 -s=compiler.cppstd=17", assert_error=True) # We can't compile, because the dep is not compatible assert "Missing prebuilt package for 'dep/1.0'" in tc.out # Let's create a compatibility extensions libc_compat = textwrap.dedent(""" from conan.tools.scm import Version def libc_compat(conanfile): # Do we have the setting? libc_version = conanfile.settings.get_safe("libc_version") if libc_version is None: return [] available_libc_versions = conanfile.settings.libc_version.possible_values() ret = [] for possible_libc_version in available_libc_versions: if Version(possible_libc_version) < Version(libc_version): ret.append({"libc_version": possible_libc_version}) return ret """) compat = tc.load_home("extensions/plugins/compatibility/compatibility.py") compat = "from libc_compat import libc_compat\n" + compat compat = compat.replace("# Append more factors for your custom compatibility rules here", "factors.append(libc_compat(conanfile))") tc.save_home({"extensions/plugins/compatibility/libc_compat.py": libc_compat, "extensions/plugins/compatibility/compatibility.py": compat}) # Now we try again, this time app will find the compatible dep with libc_version 2 tc.run("install --requires=dep/1.0 -s=libc_version=3 -s=compiler.cppstd=17") assert f"dep/1.0: Found compatible package '{dep_package_id}'" in tc.out # And now we try to create the app with libc_version 1, which is still not compatible tc.run("install --requires=dep/1.0 -s=libc_version=1 -s=compiler.cppstd=17", assert_error=True) assert "Missing prebuilt package for 'dep/1.0'" in tc.out # Now we try again, this time app will find the compatible dep with libc_version 2 # And see how we're also compatible over a different cppstd tc.run("install --requires=dep/1.0 -s=libc_version=3 -s=compiler.cppstd=14") assert f"dep/1.0: Found compatible package '{dep_package_id}': compiler.cppstd=17, " \ f"libc_version=2" in tc.out class TestListOnlyCompatibilityOptimization: @pytest.fixture() def client(self): tc = TestClient(default_server_user=True, light=True) compiler_settings = textwrap.dedent(""" compiler: foo: version: [1] cppstd: [7, 11, 14, 17, 20, 23]""") tc.run("version") tc.save_home({"settings_user.yml": compiler_settings}) compat = tc.load_home("extensions/plugins/compatibility/compatibility.py") compat = compat.replace("cppstd_possible_values = supported_cppstd(conanfile)", "cppstd_possible_values = [7, 11, 14, 17, 20, 23]") tc.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) return tc @pytest.mark.parametrize("update", [True, False]) def test_remote_compatible_package(self, client, update): tc = client update_arg = "-u" if update else "" compiler_args = "-s compiler=foo -s compiler.version=1" tc.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("compiler")}) tc.run(f"create . {compiler_args} -s=compiler.cppstd=14") std14_id = tc.created_layout().reference.package_id tc.run(f"create . {compiler_args} -s=compiler.cppstd=17") std17_id = tc.created_layout().reference.package_id tc.run(f"create . {compiler_args} -s=compiler.cppstd=20") std20_id = tc.created_layout().reference.package_id tc.run(f"upload pkg/0.1:{std20_id} -r=default -c") tc.run(f"upload pkg/0.1:{std14_id} -r=default -c") tc.run(f"upload pkg/0.1:{std17_id} -r=default -c") tc.run("remove * -c") tc.run(f"install --requires=pkg/0.1 {compiler_args} -s=compiler.cppstd=11 {update_arg} " "-cc core.graph:compatibility_mode=optimized") assert f"Found compatible package '{std14_id}'" in tc.out tc.run("remove * -c") tc.run(f"remove pkg/0.1:{std17_id} -r=default -c") tc.run(f"install --requires=pkg/0.1 {compiler_args} -s=compiler.cppstd=17 {update_arg} " "-cc core.graph:compatibility_mode=optimized") if not update: assert "Found 2 compatible configurations in remotes" in tc.out assert f"Found compatible package '{std14_id}'" in tc.out tc.run(f"create . {compiler_args} -s=compiler.cppstd=11") std11_layout = tc.created_layout() std11_id = tc.created_layout().reference.package_id tc.run(f"upload pkg/0.1:{std11_id} -r=default -c") tc.run("remove * -c") tc.run(f"install --requires=pkg/0.1 {compiler_args} -s=compiler.cppstd=17 -vvv {update_arg} " "-cc core.graph:compatibility_mode=optimized") assert f"Found compatible package '{std11_id}'" in tc.out # An HTTP request is made to the server to search for compatible packages if not update: assert "Found 3 compatible configurations in remotes" in tc.out assert f"{std11_layout.reference.ref.revision}/search?list_only=True" in tc.out def test_remote_compatible_package_update_cache(self, client): tc = client compiler_args = "-s compiler=foo -s compiler.version=1" tc.save({"conanfile.py": GenConanfile("pkg", "0.1") .with_settings("compiler") .with_import("import os, time") .with_import("from conan.tools.files import save") .with_package("save(self, os.path.join(self.package_folder, 'data.txt'), str(time.time()))") }) tc.run(f"create . {compiler_args} -s=compiler.cppstd=17") std17_old_ref = tc.created_layout().reference std17_id = std17_old_ref.package_id tc.run(f"create . {compiler_args} -s=compiler.cppstd=17") std17_new_ref = tc.created_layout().reference assert std17_old_ref.revision != std17_new_ref.revision tc.run(f"upload pkg/0.1:{std17_id}#latest -r=default -c") tc.run(f"remove pkg/0.1:{std17_id}#latest -c") tc.run(f"install --requires=pkg/0.1 {compiler_args} -s=compiler.cppstd=11 -r=default -u " "-cc core.graph:compatibility_mode=optimized") assert "Current package revision is older than the remote one " assert f"Found compatible package '{std17_id}'" in tc.out assert std17_old_ref.revision not in tc.out assert std17_new_ref.revision in tc.out tc.run("remove * -c") tc.run("remove * -r=default -c") tc.run(f"create . {compiler_args} -s=compiler.cppstd=20") std20_old_ref = tc.created_layout().reference std20_id = std20_old_ref.package_id tc.run(f"create . {compiler_args} -s=compiler.cppstd=20") std20_new_ref = tc.created_layout().reference assert std20_old_ref.revision != std20_new_ref.revision tc.run(f"upload pkg/0.1:{std20_id}#* -r=default -c") tc.run(f"remove pkg/0.1:{std20_id} -c") tc.run(f"install --requires=pkg/0.1 {compiler_args} -s=compiler.cppstd=11 -r=default -u " "-cc core.graph:compatibility_mode=optimized") assert f"Found compatible package '{std20_id}'" in tc.out assert std20_old_ref.revision not in tc.out assert std20_new_ref.revision in tc.out @pytest.mark.parametrize("enable", [True, False]) @pytest.mark.parametrize("from_remote", [True, False]) def test_message_if_not_enabled(self, enable, from_remote): tc = TestClient(default_server_user=True) tc.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("compiler")}) tc.run("create . -s=compiler.cppstd=17") if from_remote: tc.run("upload * -r=default -c") tc.run("remove * -c") arg = "-cc core.graph:compatibility_mode=optimized" if enable else "" tc.run(f"install --requires=pkg/0.1 -s=compiler.cppstd=14 {arg}") if not enable and from_remote: assert "A new experimental approach for binary compatibility detection is available" in tc.out else: assert "A new experimental approach for binary compatibility detection is available" not in tc.out @pytest.mark.parametrize("from_remote", [True, False]) @pytest.mark.parametrize("update", [True, False]) def test_compatibility_different_settings_per_context(self, from_remote, update): tc = TestClient(default_server_user=True) tc.save({"protobuf/conanfile.py": GenConanfile("protobuf", "1.0") .with_settings("compiler"), "conanfile.py": GenConanfile("consumer", "1.0") .with_require("protobuf/1.0") .with_tool_requires("protobuf/1.0") }) tc.run("create protobuf -s=compiler.cppstd=14") if from_remote: tc.run("upload * -r=default -c") tc.run("remove * -c") update_arg = "--update" if update else "" tc.run( f"install . -s=compiler.cppstd=14 -s:b=compiler.cppstd=17 --build=missing {update_arg} " "-cc core.graph:compatibility_mode=optimized") @pytest.mark.parametrize("update", [True, False]) def test_compatibility_different_settings_per_context_prevs(self, update): tc = TestClient(default_server_user=True) proto = GenConanfile("protobuf", "1.0").with_settings("compiler") proto.with_package_file("file.txt", env_var="MY_VAR") consumer = GenConanfile().with_requires("protobuf/1.0").with_tool_requires("protobuf/1.0") tc.save({"protobuf/conanfile.py": proto, "conanfile.py": consumer}) settings = "-s:a compiler=gcc -s:a compiler.version=9 -s:a compiler.libcxx=libstdc++" with environment_update({"MY_VAR": "value"}): tc.run(f"create protobuf {settings} -s=compiler.cppstd=14") tc.run("upload * -r=default -c") tc2 = TestClient(servers=tc.servers) tc2.save({"conanfile.py": consumer}) update_arg = "--update" if update else "" tc2.run(f"install . {settings} -s=compiler.cppstd=14 -s:b=compiler.cppstd=17 {update_arg} " "-cc core.graph:compatibility_mode=optimized") tc2.assert_listed_binary({"protobuf/1.0": ("36d978cbb4dc35906d0fd438732d5e17cd1e388d", "Download (default)")}) with environment_update({"MY_VAR": "value2"}): tc.run(f"create protobuf {settings} -s=compiler.cppstd=14") tc.run("upload * -r=default -c") tc2.run(f"install . {settings} -s=compiler.cppstd=14 -s:b=compiler.cppstd=17 {update_arg} " "-cc core.graph:compatibility_mode=optimized") origin = "Cache" if not update else "Update (default)" tc2.assert_listed_binary({"protobuf/1.0": ("36d978cbb4dc35906d0fd438732d5e17cd1e388d", origin)}) def test_multi_remote(self): # https://github.com/conan-io/conan/issues/19342 c = TestClient(servers={"r1": TestServer(), "r2": TestServer()}, inputs=["admin", "password"] * 2) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("compiler")}) c.run(f"create . -s=compiler.cppstd=14") c.run(f"upload * -r=r1 -c") c.run(f"upload * -r=r2 -c") c.run("remove * -c") c.run(f"install --requires=pkg/0.1 -s=compiler.cppstd=17 " "-cc core.graph:compatibility_mode=optimized") # It doesn't crash assert "pkg/0.1: Found 1 compatible configurations in remotes" in c.out def test_compatibility_remove_cppstd(): """ This test tries to reflect the following scenario: - User recently added compiler.cppstd to their settings - But up until now, no package was built with that setting - At the user's own risk, we can tell Conan to accept packages built without that setting """ tc = TestClient() profile = textwrap.dedent(""" [settings] compiler=gcc compiler.version=11 compiler.libcxx=libstdc++11 """) tc.save({"conanfile.py": GenConanfile("dep", "1.0").with_settings("compiler"), "profile": profile}) tc.run("create . -pr=profile") dep_package_id = tc.created_package_id("dep/1.0") tc.run("install --requires=dep/1.0 -pr=profile -s=compiler.cppstd=17", assert_error=True) # We can't compile, because the dep is not compatible, it's looking for a package with cppstd assert "Missing prebuilt package for 'dep/1.0'" in tc.out # Let's create a compatibility extensions no_cppstd_compat = textwrap.dedent(""" from conan.tools.scm import Version def no_cppstd_compat(conanfile): # Do we have the setting? cppstd_version = conanfile.settings.get_safe("compiler.cppstd") if cppstd_version is None: return [] return [{"compiler.cppstd": None}] """) compat = tc.load_home("extensions/plugins/compatibility/compatibility.py") compat = "from no_cppstd_compat import no_cppstd_compat\n" + compat compat = compat.replace("# Append more factors for your custom compatibility rules here", "factors.append(no_cppstd_compat(conanfile))") tc.save_home({"extensions/plugins/compatibility/no_cppstd_compat.py": no_cppstd_compat, "extensions/plugins/compatibility/compatibility.py": compat}) # Now we try again, this time app will find the compatible dep without cppstd tc.run("install --requires=dep/1.0 -pr=profile -s=compiler.cppstd=17") assert f"dep/1.0: Found compatible package '{dep_package_id}'" in tc.out def test_compatible_setting(): c = TestClient() profile = textwrap.dedent(""" [settings] os = Linux compiler=gcc compiler.version=11 compiler.libcxx=libstdc++ """) pkg = GenConanfile(version="0.1").with_settings("os", "compiler").with_tool_requires("tool/0.1") c.save({"tool/conanfile.py": GenConanfile("tool", "0.1").with_settings("os", "compiler"), "pkg/conanfile.py": pkg, "profile": profile}) c.run("export tool") c.run("export pkg --name=pkga") c.run("export pkg --name=pkgb") c.run("export pkg --name=pkgc") c.run("install --requires=pkga/0.1 --requires=pkgb/0.1 --requires=pkgc/0.1 " "-pr:a=profile -s:a compiler.cppstd=17 --build=pkg*", assert_error=True) assert str(c.out).count("tool/0.1: Main binary package " "'e297d0212cdeb8744c601b0e5ea294e62a74582f' missing") == 1 ================================================ FILE: test/integration/package_id/package_id_and_confs_test.py ================================================ import textwrap import pytest from conan.test.utils.tools import GenConanfile, TestClient, NO_SETTINGS_PACKAGE_ID PKG_ID_NO_CONF = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" PKG_ID_1 = "89d32f25195a77f4ae2e77414b870781853bdbc1" PKG_ID_2 = "7f9ed92704709f56ecc7b133322479caf3ffd7ad" PKG_ID_3 = "a9f917f3bad3b48b5bceae70214764274ccd6337" PKG_ID_USER_1 = "571b5dae13b37a78c1993842739bd475879092ea" PKG_ID_USER_2 = "54a394d26f9c35add86f20ac02cacc3c7e18f02c" PKG_ID_USER_3 = "54a394d26f9c35add86f20ac02cacc3c7e18f02c" @pytest.mark.parametrize("package_id_confs, package_id", [ ('[]', PKG_ID_NO_CONF), ('["user.fake:no_existing_conf"]', PKG_ID_NO_CONF), ('["tools.build:cxxflags", "tools.build:cflags"]', PKG_ID_1), ('["tools.build:defines"]', PKG_ID_2), ('["tools.build:cxxflags", "tools.build:sharedlinkflags"]', PKG_ID_3), ('["user.foo:value"]', PKG_ID_USER_1), ('["user.foo:value", "user.bar:value"]', PKG_ID_USER_2), ('["user.*"]', PKG_ID_USER_3), ]) def test_package_id_including_confs(package_id_confs, package_id): client = TestClient() profile = textwrap.dedent(f""" include(default) [conf] tools.info.package_id:confs={package_id_confs} tools.build:cxxflags=["--flag1", "--flag2"] tools.build:cflags+=["--flag3", "--flag4"] tools.build:sharedlinkflags=+["--flag5", "--flag6"] tools.build:exelinkflags=["--flag7", "--flag8"] tools.build:defines=["D1", "D2"] user.foo:value=1 user.bar:value=2 """) client.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("os"), "profile": profile}) client.run('create . -s os=Windows -pr profile') client.assert_listed_binary({"pkg/0.1": (package_id, "Build")}) PKG_ID_4 = "9b334fc314f2f2ce26e5280901eabcdd7b3f55a6" PKG_ID_5 = "5510413d2e6186662cb473fb16ce0a18a3f9e98f" @pytest.mark.parametrize("cxx_flags, package_id", [ ('[]', PKG_ID_NO_CONF), ('["--flag1", "--flag2"]', PKG_ID_4), ('["--flag3", "--flag4"]', PKG_ID_5), ]) def test_same_package_id_configurations_but_changing_values(cxx_flags, package_id): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" """) profile = textwrap.dedent(f""" include(default) [conf] tools.info.package_id:confs=["tools.build:cxxflags"] tools.build:cxxflags={cxx_flags} tools.build:cflags+=["--flag3", "--flag4"] tools.build:sharedlinkflags=+["--flag5", "--flag6"] tools.build:exelinkflags=["--flag7", "--flag8"] tools.build:defines=["D1", "D2"] """) client.save({"conanfile.py": conanfile, "profile": profile}) client.run('create . --name=pkg --version=0.1 -s os=Windows -pr profile') client.assert_listed_binary({"pkg/0.1": (package_id, "Build")}) def test_package_id_confs_header_only(): """ The tools.info.package_id:confs cannot affect header-only libraries and any other library that does ``self.info.clear()`` in ``package_id()`` method """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): package_type = "header-library" implements = ["auto_header_only"] """) profile = textwrap.dedent(f""" include(default) [conf] tools.info.package_id:confs=["tools.build:cxxflags"] """) client.save({"conanfile.py": conanfile, "profile": profile}) client.run('create . --name=pkg --version=0.1 -pr profile -c tools.build:cxxflags=["--flag1"]') client.assert_listed_binary({"pkg/0.1": (NO_SETTINGS_PACKAGE_ID, "Build")}) client.run("list *:*") assert "tools.build:cxxflags" not in client.out client.run('create . --name=pkg --version=0.1 -pr profile -c tools.build:cxxflags=["--flag2"]') client.assert_listed_binary({"pkg/0.1": (NO_SETTINGS_PACKAGE_ID, "Build")}) client.run("list *:*") assert "tools.build:cxxflags" not in client.out def test_conf_pkg_id_user_pattern_not_defined(): tc = TestClient(light=True) tc.save({"lib/conanfile.py": GenConanfile("lib", "1.0")}) tc.save_home({"global.conf": "tools.info.package_id:confs=['user.*']"}) # This used to break the build because `user.*` conf was not valid tc.run("create lib") assert "lib/1.0: Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created" in tc.out @pytest.mark.parametrize("conf,pkgconf", [ ("user.foo:value=1\nuser.bar:value=2", "['user.foo:value', 'user.bar:value']"), ("user.foo:value=1\nuser.bar:value=2", "['user.bar:value', 'user.foo:value']"), ("user.bar:value=2\nuser.foo:value=1", "['user.foo:value', 'user.bar:value']"), ("user.bar:value=2\nuser.foo:value=1", "['user.bar:value', 'user.foo:value']"), ]) def test_package_id_order(conf, pkgconf): """Ensure the order of the definitions in the conf file does not affect the package_id""" tc = TestClient(light=True) tc.save({"profile": f"[conf]\ntools.info.package_id:confs={pkgconf}\n" + conf, "conanfile.py": GenConanfile("pkg", "1.0")}) tc.run("create . -pr=profile") # They should all have the same pkg id - note that this did not happen before 2.0.17 tc.assert_listed_binary({"pkg/1.0": ("43227c40b8725e89d30a9f97c0652629933a3685", "Build")}) ================================================ FILE: test/integration/package_id/package_id_modes_test.py ================================================ import textwrap from conan.test.utils.tools import GenConanfile, TestClient def test_basic_default_modes_unknown(): c = TestClient() c.save({"matrix/conanfile.py": GenConanfile("matrix"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("matrix/[*]")}) c.run("create matrix --version=1.0") c.run("create engine") package_id = c.created_package_id("engine/1.0") # Using a patch version doesn't kick a engine rebuild c.run("create matrix --version=1.0.1") c.run("create engine --build=missing") c.assert_listed_require({"matrix/1.0.1": "Cache"}) c.assert_listed_binary({"engine/1.0": (package_id, "Cache")}) # same with minor version will not need rebuild c.run("create matrix --version=1.1.0") c.run("create engine --build=missing") c.assert_listed_require({"matrix/1.1.0": "Cache"}) c.assert_listed_binary({"engine/1.0": (package_id, "Cache")}) # Major will require re-build # TODO: Reconsider this default c.run("create matrix --version=2.0.0") c.run("create engine --build=missing") c.assert_listed_require({"matrix/2.0.0": "Cache"}) c.assert_listed_binary({"engine/1.0": ("805fafebc9f7769a90dafb8c008578c6aa7f5d86", "Build")}) def test_basic_default_modes_application(): """ if the consumer package is a declared "package_type = "application"" recipe_revision_mode will be used """ c = TestClient() c.save({"matrix/conanfile.py": GenConanfile("matrix"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("matrix/[*]") .with_package_type("application")}) c.run("create matrix --version=1.0") c.run("create engine") package_id = c.created_package_id("engine/1.0") # Using a patch version requires a rebuild c.run("create matrix --version=1.0.1") c.run("create engine --build=missing") c.assert_listed_require({"matrix/1.0.1": "Cache"}) new_package_id = "efe870a1b1b4fe60e55aa6e2d17436665404370f" assert new_package_id != package_id c.assert_listed_binary({"engine/1.0": (new_package_id, "Build")}) class TestDepDefinedMode: def test_dep_defined(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" package_type = "static-library" package_id_embed_mode = "major_mode" package_id_non_embed_mode = "major_mode" """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/[*]") .with_shared_option(False)}) c.run("create dep --version=0.1") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("66a9e6a31e63f77952fd72744d0d5da07970f42e", "Build")}) c.run("create pkg -o pkg/*:shared=True") c.assert_listed_binary({"pkg/0.1": ("5a5828e18eef6a86813b01d4f5a83ea7d87d1139", "Build")}) # using dep 0.2, still same, because dependency chose "major_mode" c.run("create dep --version=0.2") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("66a9e6a31e63f77952fd72744d0d5da07970f42e", "Build")}) c.run("create pkg -o pkg/*:shared=True") c.assert_listed_binary({"pkg/0.1": ("5a5828e18eef6a86813b01d4f5a83ea7d87d1139", "Build")}) def test_dep_tool_require_defined(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" package_type = "application" build_mode = "minor_mode" """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_tool_requires("dep/[*]")}) c.run("create dep --version=0.1") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("fcf70699eb821f51cd4f3e228341ac4f405ad220", "Build")}) # using dep 0.2, still same, because dependency chose "minor" c.run("create dep --version=0.1.1") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("fcf70699eb821f51cd4f3e228341ac4f405ad220", "Build")}) # using dep 0.2, still same, because dependency chose "minor" c.run("create dep --version=0.2") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("56934f87c11792e356423e081c7cd490f3c1fbe0", "Build")}) def test_dep_python_require_defined(self): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" package_type = "python-require" package_id_python_mode = "major_mode" """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("dep/[*]")}) c.run("create dep --version=0.1") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("331c17383dcdf37f79bc2b86fa55ac56afdc6fec", "Build")}) # using dep 0.2, still same, because dependency chose "major" c.run("create dep --version=0.1.1") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("331c17383dcdf37f79bc2b86fa55ac56afdc6fec", "Build")}) # using dep 0.2, still same, because dependency chose "major" c.run("create dep --version=0.2") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("331c17383dcdf37f79bc2b86fa55ac56afdc6fec", "Build")}) c.run("list *:*") assert "dep/0.Y.Z" in c.out # using dep 0.2, new package_id, because dependency chose "major" c.run("create dep --version=1.0") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": ("9b015e30b768df0217ffa2c270f60227c998e609", "Build")}) c.run("list *:*") assert "dep/1.Y.Z" in c.out ================================================ FILE: test/integration/package_id/package_id_requires_modes_test.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient, GenConanfile class TestPackageIDRequirementsModes: @pytest.mark.parametrize("mode, accepted_version, rejected_version, pattern", [("unrelated_mode", "2.0", "", ""), ("patch_mode", "1.0.0.1", "1.0.1", "1.0.1"), ("minor_mode", "1.0.1", "1.2", "1.2.Z"), ("major_mode", "1.5", "2.0", "2.Y.Z"), ("semver_mode", "1.5", "2.0", "2.Y.Z"), ("full_mode", "1.0", "1.0.0.1", "1.0.0.1")]) def test(self, mode, accepted_version, rejected_version, pattern): c = TestClient() package_id_text = f'self.info.requires["dep"].{mode}()' c.save({"dep/conanfile.py": GenConanfile("dep"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/[*]") .with_package_id(package_id_text)}) c.run("create dep --version=1.0") pkgid = c.created_package_id("dep/1.0") c.run("create pkg") c.run(f"create dep --version={accepted_version}") c.run("install --requires=pkg/0.1") # binary existing c.assert_listed_binary({f"dep/{accepted_version}": (pkgid, "Cache")}) if rejected_version: c.run(f"create dep --version={rejected_version}") c.run("install --requires=pkg/0.1", assert_error=True) # binary missing assert "ERROR: Missing prebuilt package for 'pkg/0.1'" in c.out assert f"dep/{pattern}" in c.out @pytest.mark.parametrize("mode, pkg_id", [("unrelated_mode", "da39a3ee5e6b4b0d3255bfef95601890afd80709"), ("semver_mode", "13b9e753af3958dd1b2d4b3f935b04b8fb6b6760"), ("patch_mode", "38d7a3ec6a09165ab3e5306f81c539a2e0a784bd"), ("minor_mode", "a5e7ad26ccf4a5049090976846da1c6ed165cced"), ("major_mode", "6ac597ffb99c3747ed78699f206dc1041537a8df"), # This is equal to semver_mode for 0.X.Y.Z.. ("full_version_mode", "13b9e753af3958dd1b2d4b3f935b04b8fb6b6760"), ("full_recipe_mode", "13b9e753af3958dd1b2d4b3f935b04b8fb6b6760"), ("full_package_mode", "19906d8a245d9f466d7d7f697c666222a9854a1a"), ("revision_mode", "ae5b9eeb74880aeb1cfa3db7f84c007a05ce3a76"), ("full_mode", "d1b2a9538cd69363b4bae7e66c9f900b8f4c58bb")]) def test_modes(mode, pkg_id): c = TestClient(light=True) package_id_text = f'self.info.requires.{mode}()' c.save({"dep/conanfile.py": GenConanfile("dep", "0.1.1.1").with_settings("os"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/[*]") .with_package_id(package_id_text)}) c.run("create dep -s os=Linux") c.run("create pkg -s os=Linux") pkgid = c.created_package_id("pkg/0.1") assert pkgid == pkg_id class TestPackageIDError: def test_transitive_multi_mode_package_id(self): # https://github.com/conan-io/conan/issues/6942 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=dep1 --version=1.0 --user=user --channel=testing") client.save({"conanfile.py": GenConanfile().with_require("dep1/1.0@user/testing")}) client.run("export . --name=dep2 --version=1.0 --user=user --channel=testing") pkg_revision_mode = "self.info.requires.recipe_revision_mode()" client.save({"conanfile.py": GenConanfile().with_require("dep1/1.0@user/testing") .with_package_id(pkg_revision_mode)}) client.run("export . --name=dep3 --version=1.0 --user=user --channel=testing") client.save({"conanfile.py": GenConanfile().with_require("dep2/1.0@user/testing") .with_require("dep3/1.0@user/testing")}) client.run('create . --name=consumer --version=1.0 --user=user --channel=testing --build=*') assert "consumer/1.0@user/testing: Created" in client.out def test_transitive_multi_mode2_package_id(self): # https://github.com/conan-io/conan/issues/6942 client = TestClient() # This is mandatory, otherwise it doesn't work client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=dep1 --version=1.0 --user=user --channel=testing") pkg_revision_mode = "self.info.requires.full_version_mode()" package_id_print = "self.output.info('PkgNames: %s' % sorted(self.info.requires.pkg_names))" client.save({"conanfile.py": GenConanfile().with_require("dep1/1.0@user/testing") .with_package_id(pkg_revision_mode) .with_package_id(package_id_print)}) client.run("export . --name=dep2 --version=1.0 --user=user --channel=testing") consumer = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): requires = "dep2/1.0@user/testing" def package_id(self): self.output.info("PKGNAMES: %s" % sorted(self.info.requires.pkg_names)) """) client.save({"conanfile.py": consumer}) client.run('create . --name=consumer --version=1.0 --user=user --channel=testing --build=*') assert "dep2/1.0@user/testing: PkgNames: ['dep1']" in client.out assert "consumer/1.0@user/testing: PKGNAMES: ['dep1', 'dep2']" in client.out assert "consumer/1.0@user/testing: Created" in client.out def test_transitive_multi_mode_build_requires(self): # https://github.com/conan-io/conan/issues/6942 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=dep1 --version=1.0 --user=user --channel=testing") client.run("create . --name=tool --version=1.0 --user=user --channel=testing") pkg_revision_mode = "self.info.requires.full_version_mode()" package_id_print = "self.output.info('PkgNames: %s' % sorted(self.info.requires.pkg_names))" client.save({"conanfile.py": GenConanfile().with_require("dep1/1.0@user/testing") .with_tool_requires("tool/1.0@user/testing") .with_package_id(pkg_revision_mode) .with_package_id(package_id_print)}) client.run("export . --name=dep2 --version=1.0 --user=user --channel=testing") consumer = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): requires = "dep2/1.0@user/testing" build_requires = "tool/1.0@user/testing" def package_id(self): self.output.info("PKGNAMES: %s" % sorted(self.info.requires.pkg_names)) """) client.save({"conanfile.py": consumer}) client.run('create . --name=consumer --version=1.0 --user=user --channel=testing --build=*') assert "dep2/1.0@user/testing: PkgNames: ['dep1']" in client.out assert "consumer/1.0@user/testing: PKGNAMES: ['dep1', 'dep2']" in client.out assert "consumer/1.0@user/testing: Created" in client.out class TestRequirementPackageId: """ defining requires(..., package_id_mode) """ @pytest.mark.parametrize("mode, pattern", [("patch_mode", "1.2.3"), ("minor_mode", "1.2.Z"), ("major_mode", "1.Y.Z")]) def test(self, mode, pattern): c = TestClient(light=True) pkg = GenConanfile("pkg", "0.1").with_requirement("dep/1.2.3", package_id_mode=mode) c.save({"dep/conanfile.py": GenConanfile("dep", "1.2.3"), "pkg/conanfile.py": pkg}) c.run("create dep") c.run("create pkg") c.run("list pkg:*") assert f"dep/{pattern}" in c.out def test_wrong_mode(self): c = TestClient(light=True) pkg = GenConanfile("pkg", "0.1").with_requirement("dep/1.2.3", package_id_mode="nothing") c.save({"dep/conanfile.py": GenConanfile("dep", "1.2.3"), "pkg/conanfile.py": pkg}) c.run("create dep") c.run("create pkg", assert_error=True) assert "ERROR: require dep/1.2.3 package_id_mode='nothing' is not a known package_id_mode" \ in c.out @pytest.mark.parametrize("mode, pattern", [("patch_mode", "1.2.3"), ("minor_mode", "1.2.Z"), ("major_mode", "1.Y.Z")]) def test_half_diamond(self, mode, pattern): # pkg -> libb -> liba # \----(mode)----/ c = TestClient(light=True) pkg = GenConanfile("pkg", "0.1").with_requirement("liba/1.2.3", package_id_mode=mode)\ .with_requirement("libb/1.2.3") c.save({"liba/conanfile.py": GenConanfile("liba", "1.2.3"), "libb/conanfile.py": GenConanfile("libb", "1.2.3").with_requires("liba/1.2.3"), "pkg/conanfile.py": pkg}) c.run("create liba") c.run("create libb") c.run("create pkg") c.run("list pkg:*") assert f"liba/{pattern}" in c.out # reverse order also works the same pkg = GenConanfile("pkg", "0.1").with_requirement("libb/1.2.3")\ .with_requirement("liba/1.2.3", package_id_mode=mode) c.save({"pkg/conanfile.py": pkg}) c.run("create pkg") c.run("list pkg:*") assert f"liba/{pattern}" in c.out @pytest.mark.parametrize("mode, pattern", [("patch_mode", "1.2.3"), ("minor_mode", "1.2.Z"), ("major_mode", "1.Y.Z")]) def test_half_diamond_conflict(self, mode, pattern): # pkg -> libb --(mode)-> liba # \----(mode)-----------/ # The libb->liba mode is not propagated down to pkg, so it doesn't really conflict c = TestClient(light=True) pkg = GenConanfile("pkg", "0.1").with_requirement("liba/1.2.3", package_id_mode=mode) \ .with_requirement("libb/1.2.3") libb = GenConanfile("libb", "1.2.3").with_requirement("liba/1.2.3", package_id_mode="patch_mode") c.save({"liba/conanfile.py": GenConanfile("liba", "1.2.3"), "libb/conanfile.py": libb, "pkg/conanfile.py": pkg}) c.run("create liba") c.run("create libb") c.run("create pkg") c.run("list pkg:*") assert f"liba/{pattern}" in c.out # reverse order also works the same pkg = GenConanfile("pkg", "0.1").with_requirement("libb/1.2.3") \ .with_requirement("liba/1.2.3", package_id_mode=mode) c.save({"pkg/conanfile.py": pkg}) c.run("create pkg") c.run("list pkg:*") assert f"liba/{pattern}" in c.out ================================================ FILE: test/integration/package_id/package_id_test.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient from conan.internal.util.files import save def test_double_package_id_call(): # https://github.com/conan-io/conan/issues/3085 conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): settings = "os", "arch" def package_id(self): self.output.info("Calling package_id()") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") out = str(client.out) assert 1 == out.count("pkg/0.1@user/testing: Calling package_id()") def test_remove_option_setting(): # https://github.com/conan-io/conan/issues/2826 conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): settings = "os" options = {"opt": [True, False]} default_options = {"opt": False} def package_id(self): self.output.info("OPTION OPT=%s" % self.info.options.opt) del self.info.settings.os del self.info.options.opt """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -s os=Windows") assert "pkg/0.1@user/testing: OPTION OPT=False" in client.out assert "pkg/0.1@user/testing: Package '%s' created" % NO_SETTINGS_PACKAGE_ID in client.out client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -s os=Linux -o pkg/*:opt=True") assert "pkg/0.1@user/testing: OPTION OPT=True" in client.out assert "pkg/0.1@user/testing: Package '%s' created" % NO_SETTINGS_PACKAGE_ID in client.out def test_value_parse(): # https://github.com/conan-io/conan/issues/2816 conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): name = "test" version = "0.1" settings = "os", "arch", "build_type" def package_id(self): self.info.settings.arch = "kk=kk" self.info.settings.os = "yy=yy" """) client = TestClient(default_server_user=True) client.save({"conanfile.py": conanfile}) client.run("create . ") client.run("list *:*") assert "arch: kk=kk" in client.out client.run("upload * -r default -c") client.run("list *:* -r=default") assert "arch: kk=kk" in client.out client.run("remove * -c") client.run("install --requires=test/0.1") client.run("list *:*") assert "arch: kk=kk" in client.out def test_option_in(): # https://github.com/conan-io/conan/issues/7299 conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): options = {"fpic": [True, False]} default_options = {"fpic": True} def package_id(self): if "fpic" in self.info.options: self.output.info("fpic is an option!!!") if "fpic" in self.info.options: # Not documented self.output.info("fpic is an info.option!!!") if "other" not in self.info.options: self.output.info("other is not an option!!!") if "other" not in self.info.options: # Not documented self.output.info("other is not an info.option!!!") try: self.options.whatever except Exception as e: self.output.error("OPTIONS: %s" % e) try: self.info.options.whatever except Exception as e: self.output.error("INFO: %s" % e) """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "fpic is an option!!!" in client.out assert "fpic is an info.option!!!" in client.out assert "other is not an option!!!" in client.out assert "other is not an info.option!!!" in client.out assert "OPTIONS: 'self.options' access in 'package_id()' method is forbidden" in client.out assert "ERROR: INFO: option 'whatever' doesn't exist" in client.out def test_build_type_remove_windows(): # https://github.com/conan-io/conan/issues/7603 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def package_id(self): if self.info.settings.os == "Windows" and self.info.settings.compiler == "msvc": del self.info.settings.build_type del self.info.settings.compiler.runtime del self.info.settings.compiler.runtime_type """) client.save({"conanfile.py": conanfile}) client.run('create . --name=pkg --version=0.1 -s os=Windows -s compiler=msvc -s arch=x86_64 ' '-s compiler.version=190 -s build_type=Release -s compiler.runtime=dynamic') package_id = "6a98270da6641cc6668b83daf547d67451910cf0" client.assert_listed_binary({"pkg/0.1": (package_id, "Build")}) client.run('install --requires=pkg/0.1@ -s os=Windows -s compiler=msvc -s arch=x86_64 ' '-s compiler.version=190 -s build_type=Debug -s compiler.runtime=dynamic') client.assert_listed_binary({"pkg/0.1": (package_id, "Cache")}) def test_package_id_requires_info(): """ if we dont restrict ``package_id()`` to use only ``self.info`` it will do nothing and fail if we ``del self.settings.arch`` instead of ``del self.info.settings.arch`` https://github.com/conan-io/conan/issues/12693 """ conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): settings = "os", "arch" def package_id(self): if self.info.settings.os == "Windows": del self.info.settings.arch """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 -s os=Windows -s arch=armv8") client.assert_listed_binary({"pkg/0.1": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Build")}) client.run("create . --name=pkg --version=0.1 -s os=Windows -s arch=x86_64") client.assert_listed_binary({"pkg/0.1": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Build")}) def test_package_id_validate_settings(): """ ``self.info`` has no validation, as it allows to be mutated """ conanfile = textwrap.dedent(""" from conan import ConanFile class TestConan(ConanFile): settings = "os", "arch" def package_id(self): if self.info.settings.os == "DONT_EXIST": del self.info.settings.arch """) c = TestClient() c.save({"conanfile.py": conanfile}) c.run("create . --name=pkg --version=0.1") # It used to fail, class TestBuildRequiresHeaderOnly: def test_header_only(self): c = TestClient(light=True) save(c.paths.global_conf_path, "core.package_id:default_build_mode=minor_mode") pkg = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" tool_requires = "tool/[*]" def package_id(self): self.info.clear() """) c.save({"tool/conanfile.py": GenConanfile("tool"), "pkg/conanfile.py": pkg}) c.run("create tool --version=1.0") c.run("create pkg") pkgid = c.created_package_id("pkg/0.1") c.run("create tool --version=1.2") c.run("install --requires=pkg/0.1") c.assert_listed_binary({"pkg/0.1": (pkgid, "Cache")}) def test_header_only_implements(self): c = TestClient(light=True) save(c.paths.global_conf_path, "core.package_id:default_build_mode=minor_mode") pkg = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" tool_requires = "tool/[*]" package_type = "header-library" implements = ["auto_header_only"] """) c.save({"tool/conanfile.py": GenConanfile("tool"), "pkg/conanfile.py": pkg}) c.run("create tool --version=1.0") c.run("create pkg") pkgid = c.created_package_id("pkg/0.1") c.run("create tool --version=1.2") c.run("install --requires=pkg/0.1") c.assert_listed_binary({"pkg/0.1": (pkgid, "Cache")}) def test_explicit_implements(): c = TestClient() pkg = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os" options = {"header_only": [True, False]} def package_id(self): if self.package_type == "header-library": self.info.clear() """) c.save({"conanfile.py": pkg}) c.run("create . -s os=Windows -o *:header_only=True") pkgid = c.created_package_id("pkg/0.1") c.run("create . -s os=Linux -o *:header_only=True") pkgid2 = c.created_package_id("pkg/0.1") assert pkgid == pkgid2 c.run("create . -s os=Windows -o *:header_only=False") pkgid3 = c.created_package_id("pkg/0.1") assert pkgid3 != pkgid c.run("create . -s os=Linux -o *:header_only=False") pkgid4 = c.created_package_id("pkg/0.1") assert pkgid4 != pkgid assert pkgid3 != pkgid4 ================================================ FILE: test/integration/package_id/python_requires_package_id_test.py ================================================ import textwrap import pytest from conan.test.utils.tools import TestClient, GenConanfile PKG_ID_1 = "47b42eaf657374a3d040394f03961b66c53bda5e" PKG_ID_2 = "8b7006bf91e5b52cc1ac24a7a4d9c326ee954bb2" class TestPythonRequiresPackageID: @pytest.fixture(autouse=True) def set_up(self): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=tool --version=1.1.1") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): python_requires ="tool/[*]" """) client2 = TestClient(cache_folder=client.cache_folder) client2.save({"conanfile.py": conanfile}) self.client = client self.client2 = client2 def test_default(self): self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.1.1" in self.client2.out pkg_id = "170e82ef3a6bf0bbcda5033467ab9d7805b11d0b" self.client2.assert_listed_binary({"pkg/0.1": (pkg_id, "Build")}) self.client.run("export . --name=tool --version=1.1.2") self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.1.2" in self.client2.out self.client2.assert_listed_binary({"pkg/0.1": (pkg_id, "Build")}) # With a minor change, it fires a rebuild self.client.run("export . --name=tool --version=1.2.0") self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.2.0" in self.client2.out self.client2.assert_listed_binary({"pkg/0.1": ("5eb1e7ea93fdd67fe3c3b166d240844648ba2b7a", "Build")}) def test_change_mode_conf(self): # change the policy in conan.conf self.client2.save_home({"global.conf": "core.package_id:default_python_mode=patch_mode"}) self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.1.1" in self.client2.out self.client2.assert_listed_binary({"pkg/0.1": (PKG_ID_1, "Build")}) # with a patch change, new ID self.client.run("export . --name=tool --version=1.1.2") self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.1.2" in self.client2.out self.client2.assert_listed_binary({"pkg/0.1": (PKG_ID_2, "Build")}) def test_unrelated_conf(self): # change the policy in conan.conf self.client2.save_home({"global.conf": "core.package_id:default_python_mode=unrelated_mode"}) self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.1.1" in self.client2.out pkg_id = "da39a3ee5e6b4b0d3255bfef95601890afd80709" self.client2.assert_listed_binary({"pkg/0.1": (pkg_id, "Build")}) # with any change the package id doesn't change self.client.run("export . --name=tool --version=1.1.2") self.client2.run("create . --name=pkg --version=0.1 --build missing") assert "tool/1.1.2" in self.client2.out self.client2.assert_listed_binary({"pkg/0.1": (pkg_id, "Cache")}) def test_change_mode_package_id(self): # change the policy in package_id conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): python_requires ="tool/[*]" def package_id(self): self.info.python_requires.patch_mode() """) self.client2.save({"conanfile.py": conanfile}) self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.1.1" in self.client2.out self.client2.assert_listed_binary({"pkg/0.1": (PKG_ID_1, "Build")}) # with a patch change, new ID self.client.run("export . --name=tool --version=1.1.2") self.client2.run("create . --name=pkg --version=0.1") assert "tool/1.1.2" in self.client2.out self.client2.assert_listed_binary({"pkg/0.1": (PKG_ID_2, "Build")}) def test_python_requires_for_build_requires(): client = TestClient() client.save_home({"global.conf": "core.package_id:default_python_mode=full_version_mode"}) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=tool --version=1.1.1") client2 = TestClient(cache_folder=client.cache_folder) client2.save({"conanfile.py": GenConanfile().with_python_requires("tool/[>=0.0]"), "myprofile": "[tool_requires]\ntool/[>=0.0]\n"}) client2.run("create . --name=pkg --version=0.1 -pr=myprofile") assert "tool/1.1.1" in client2.out assert f"pkg/0.1: Package '{PKG_ID_1}' created" in client2.out client.run("create . --name=tool --version=1.1.2") client2.run("install --requires=pkg/0.1@ -pr=myprofile", assert_error=True) assert f"ERROR: Missing binary: pkg/0.1:{PKG_ID_2}" in client2.out assert "tool/1.1.2" in client2.out assert "tool/1.1.1" not in client2.out client2.run("create . --name=pkg --version=0.1 -pr=myprofile") # assert "pkg/0.1: Applying build-requirement: tool/1.1.2", client2.out) assert f"pkg/0.1: Package '{PKG_ID_2}' created" in client2.out class TestPythonRequiresHeaderOnly: def test_header_only(self): c = TestClient(light=True) pkg = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" python_requires = "tool/[*]" def package_id(self): self.info.clear() """) c.save({"tool/conanfile.py": GenConanfile("tool"), "pkg/conanfile.py": pkg}) c.run("create tool --version=1.0") c.run("create pkg") pkgid = c.created_package_id("pkg/0.1") c.run("create tool --version=1.2") c.run("install --requires=pkg/0.1") c.assert_listed_binary({"pkg/0.1": (pkgid, "Cache")}) def test_header_only_implements(self): c = TestClient(light=True) pkg = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" python_requires = "tool/[*]" package_type = "header-library" implements = ["auto_header_only"] """) c.save({"tool/conanfile.py": GenConanfile("tool"), "pkg/conanfile.py": pkg}) c.run("create tool --version=1.0") c.run("create pkg") pkgid = c.created_package_id("pkg/0.1") c.run("create tool --version=1.2") c.run("install --requires=pkg/0.1") c.assert_listed_binary({"pkg/0.1": (pkgid, "Cache")}) @pytest.mark.parametrize("mode, pkg_id", [("unrelated_mode", "da39a3ee5e6b4b0d3255bfef95601890afd80709"), ("semver_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), ("patch_mode", "b9ca872dfd5b48f5f1f69d66f0950fc35469d0cd"), ("minor_mode", "c53bd9e48dd09ceeaa1bb425830490d8b243e39c"), ("major_mode", "331c17383dcdf37f79bc2b86fa55ac56afdc6fec"), ("full_version_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), ("full_recipe_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), # Doesn't make much sense, but was doable, not worth removing it ("full_package_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), ("revision_mode", "0071dd0296afa0db533e21f924273485c87f0d32"), ("full_mode", "0071dd0296afa0db533e21f924273485c87f0d32")]) def test_modes(mode, pkg_id): c = TestClient(light=True) c.save_home({"global.conf": f"core.package_id:default_python_mode={mode}"}) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1.1.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("dep/[*]")}) c.run("create dep") c.run("create pkg") pkgid = c.created_package_id("pkg/0.1") assert pkgid == pkg_id @pytest.mark.parametrize("mode, pkg_id", [("unrelated_mode", "da39a3ee5e6b4b0d3255bfef95601890afd80709"), ("semver_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), ("patch_mode", "b9ca872dfd5b48f5f1f69d66f0950fc35469d0cd"), ("minor_mode", "c53bd9e48dd09ceeaa1bb425830490d8b243e39c"), ("major_mode", "331c17383dcdf37f79bc2b86fa55ac56afdc6fec"), ("full_version_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), ("full_recipe_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), ("revision_mode", "0071dd0296afa0db533e21f924273485c87f0d32"), ("full_mode", "0071dd0296afa0db533e21f924273485c87f0d32")]) def test_modes_recipe(mode, pkg_id): c = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" python_requires ="dep/[*]" def package_id(self): self.info.python_requires.{mode}() """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1.1.1"), "pkg/conanfile.py": conanfile}) c.run("create dep") c.run("create pkg") c.assert_listed_binary({"pkg/0.1": (pkg_id, "Build")}) ================================================ FILE: test/integration/package_id/test_cache_compatibles.py ================================================ import os import re import textwrap import pytest from conan.internal.cache.home_paths import HomePaths from conan.test.utils.tools import TestClient, GenConanfile class TestCacheCompatibles: @pytest.fixture() def client(self): client = TestClient() debug_compat = textwrap.dedent("""\ def debug_compat(conanfile): result = [] if conanfile.settings.build_type == "Debug": result.append({"settings": [("build_type", "Release")]}) return result """) compatibles = textwrap.dedent("""\ from debug_compat import debug_compat def compatibility(conanfile): if conanfile.name != "dep": return return debug_compat(conanfile) """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compatibles, "extensions/plugins/compatibility/debug_compat.py": debug_compat}) return client def test_compatible_build_type(self, client): client.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_setting("build_type"), "consumer/conanfile.py": GenConanfile().with_requires("dep/0.1")}) client.run("create dep -s build_type=Release") package_id = client.created_package_id("dep/0.1") client.run("install consumer -s build_type=Debug") assert "dep/0.1: Main binary package '9e186f6d94c008b544af1569d1a6368d8339efc5' missing"\ in client.out assert f"Found compatible package '{package_id}'" in client.out def test_compatible_recipe_reference(self, client): """ check that the recipe name can be used to filter """ client.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_setting("build_type"), "consumer/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) client.run("create pkg -s build_type=Release") # The compatibility doesn't fire for package "pkg" client.run("install consumer -s build_type=Debug", assert_error=True) assert "ERROR: Missing binary" in client.out def test_cppstd(): client = TestClient() compatibles = textwrap.dedent("""\ def compatibility(conanfile): cppstd = conanfile.settings.get_safe("compiler.cppstd") if not cppstd: return result = [] for cppstd in ["11", "14", "17", "20"]: result.append({"settings": [("compiler.cppstd", cppstd)]}) if conanfile.settings.build_type == "Debug": for cppstd in ["11", "14", "17", "20"]: result.append({"settings": [("compiler.cppstd", cppstd), ("build_type", "Release")]}) return result """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compatibles}) conanfile = GenConanfile("dep", "0.1").with_settings("compiler", "build_type") client.save({"dep/conanfile.py": conanfile, "consumer/conanfile.py": GenConanfile().with_requires("dep/0.1")}) base_settings = "-s compiler=gcc -s compiler.version=7 -s compiler.libcxx=libstdc++11" client.run(f"create dep {base_settings} -s build_type=Release -s compiler.cppstd=14") package_id = client.created_package_id("dep/0.1") client.run(f"install consumer {base_settings} -s compiler.cppstd=17") assert "dep/0.1: Checking 3 compatible configurations" in client.out assert "dep/0.1: Main binary package 'ec174bec4a5ee2d44d3e33d9f4fdacd9b65a6772' missing" \ in client.out assert f"Found compatible package '{package_id}'" in client.out client.run(f"install consumer {base_settings} -s build_type=Debug -s compiler.cppstd=17") assert "dep/0.1: Main binary package '94758b7bbcb365aaf355913b35431c0da6ed6da5' missing" \ in client.out assert f"Found compatible package '{package_id}'" in client.out def test_cppstd_validated(): """ this test proves that 1 only configuration, the latest one, is tested and compatible, because the ``valiate()`` method is rejecting all cppstd<20 """ client = TestClient() compatibles = textwrap.dedent("""\ def compatibility(conanfile): return [{"settings": [("compiler.cppstd", v)]} for v in ("11", "14", "17", "20")] """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compatibles}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "dep" version = "0.1" settings = "compiler" def validate(self): check_min_cppstd(self, "20") """) client.save({"dep/conanfile.py": conanfile, "consumer/conanfile.py": GenConanfile().with_requires("dep/0.1")}) base_settings = "-s compiler=gcc -s compiler.version=8 -s compiler.libcxx=libstdc++11" client.run(f"create dep {base_settings} -s compiler.cppstd=20") client.run(f"install consumer {base_settings} -s compiler.cppstd=17", assert_error=True) assert "dep/0.1: Invalid: Current cppstd (17) is lower than the required C++ standard (20)." \ in client.out def test_cppstd_server(): """ this test proves the order, first from cache """ c = TestClient(default_server_user=True) compatibles = textwrap.dedent("""\ def compatibility(conanfile): return [{"settings": [("compiler.cppstd", v)]} for v in ("11", "14", "17", "20")] """) c.save_home({"extensions/plugins/compatibility/compatibility.py": compatibles}) conanfile = GenConanfile("dep", "0.1").with_settings("compiler") c.save({"dep/conanfile.py": conanfile, "consumer/conanfile.py": GenConanfile().with_requires("dep/0.1")}) base_settings = "-s compiler=gcc -s compiler.version=8 -s compiler.libcxx=libstdc++11" c.run(f"create dep {base_settings} -s compiler.cppstd=20") c.run("upload * -r=default -c") c.run("remove * -c") c.run(f"install consumer {base_settings} -s compiler.cppstd=17") assert "dep/0.1: Checking 3 compatible configurations" in c.out assert "dep/0.1: Compatible configurations not found in cache, checking servers" in c.out assert "dep/0.1: Main binary package '6179018ccb6b15e6443829bf3640e25f2718b931' missing" \ in c.out assert "Found compatible package '326c500588d969f55133fdda29506ef61ef03eee': " \ "compiler.cppstd=20" in c.out c.assert_listed_binary({"dep/0.1": ("326c500588d969f55133fdda29506ef61ef03eee", "Download (default)")}) # second time, not download, already in cache c.run(f"install consumer {base_settings} -s compiler.cppstd=17") assert "dep/0.1: Checking 3 compatible configurations" in c.out assert "dep/0.1: Compatible configurations not found in cache, checking servers" not in c.out assert "dep/0.1: Main binary package '6179018ccb6b15e6443829bf3640e25f2718b931' missing" in c.out assert "Found compatible package '326c500588d969f55133fdda29506ef61ef03eee': " \ "compiler.cppstd=20" in c.out c.assert_listed_binary({"dep/0.1": ("326c500588d969f55133fdda29506ef61ef03eee", "Cache")}) # update checks in servers c2 = TestClient(servers=c.servers, inputs=["admin", "password"]) c2.save({"dep/conanfile.py": conanfile}) c2.run(f"create dep {base_settings} -s compiler.cppstd=14") c2.run("upload * -r=default -c") c.run(f"install consumer {base_settings} -s compiler.cppstd=17 --update") assert "dep/0.1: Checking 3 compatible configurations" in c.out assert "dep/0.1: Compatible configurations not found in cache, checking servers" not in c.out assert "dep/0.1: Main binary package '6179018ccb6b15e6443829bf3640e25f2718b931' missing" in c.out assert "Found compatible package 'ce92fac7c26ace631e30875ddbb3a58a190eb601': " \ "compiler.cppstd=14" in c.out c.assert_listed_binary({"dep/0.1": ("ce92fac7c26ace631e30875ddbb3a58a190eb601", "Download (default)")}) # Now what happens if we have it in cache already c.run(f"install consumer {base_settings} -s compiler.cppstd=17 --update") assert "dep/0.1: Checking 3 compatible configurations" in c.out assert "dep/0.1: Main binary package '6179018ccb6b15e6443829bf3640e25f2718b931' missing" in c.out assert "Found compatible package 'ce92fac7c26ace631e30875ddbb3a58a190eb601': " \ "compiler.cppstd=14" in c.out c.assert_listed_binary({"dep/0.1": ("ce92fac7c26ace631e30875ddbb3a58a190eb601", "Cache")}) class TestDefaultCompat: def test_default_cppstd_compatibility(self): c = TestClient() c.save_home({"profiles/default": ""}) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "mylib" version = "1.0" package_type = "library" options = {"shared": [True, False]} default_options = {"shared": False} settings = "os", "arch", "compiler", "build_type" """) c.save({"conanfile.py": conanfile, "profile_build": "[settings]\nos=Windows\narch=x86_64"}) os_ = "Windows" build_type = "Release" arch = "x86_64" compiler = "msvc" version = "191" cppstd = "17" runtime = "dynamic" c.run(f"create . -s os={os_} -s arch={arch} -s build_type={build_type} " f"-s compiler={compiler} " f"-s compiler.version={version} -s compiler.cppstd={cppstd} " f"-s compiler.runtime={runtime} -pr:b=profile_build") package_id1 = c.created_package_id("mylib/1.0") # Try to install with cppstd 14, it will find cppstd 17 as compatible c.run(f"install --requires=mylib/1.0@ -s os={os_} -s arch={arch} -s build_type={build_type} " f"-s compiler={compiler} " f"-s compiler.version={version} -s compiler.cppstd=14 " f"-s compiler.runtime={runtime} -pr:b=profile_build") assert "mylib/1.0: Main binary package 'e340edd75790e7156c595edebd3d98b10a2e091e' missing."\ f"Using compatible package '{package_id1}'" def test_fail_with_options_deleted(self): """ This test used to fail with "ConanException: option 'with_fmt_alias' doesn't exist", because it was removed by the package_id() """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "mylib" version = "1.0" options = {"with_fmt_alias": [True, False]} default_options = {"with_fmt_alias": False} settings = "os", "arch", "compiler", "build_type" def package_id(self): del self.info.options.with_fmt_alias def package_info(self): self.output.warning("WITH_FMT_ALIAS={}".format(self.options.with_fmt_alias)) """) c.save({"conanfile.py": conanfile}) c.run("create . -s compiler.cppstd=14") c.run("create . --build=missing -s compiler.cppstd=17") assert "mylib/1.0: Main binary package" in c.out assert "Found compatible package" in c.out assert "Possible options are ['shared', 'header_only']" not in c.out assert "mylib/1.0: WARN: WITH_FMT_ALIAS=False" in c.out def test_header_only_build_missing(self): """ this test failed with self.settings.compiler setting didn't exist """ conanfile = textwrap.dedent(""" from conan import ConanFile class SumConan(ConanFile): name = "sum" version = "0.1" settings = "os", "arch", "compiler", "build_type" def build(self): self.output.warning("My compiler is '{}'".format(self.settings.compiler)) def package_id(self): self.info.clear() """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . -s compiler.cppstd=17 --build missing") client.assert_listed_binary({"sum/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) # Just check that it works and doesn't fail assert "Installing packages" in client.out client.run("create . -s compiler.cppstd=14 --build missing") # Now it will not build, as package exist client.assert_listed_binary({"sum/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}) assert "Installing packages" in client.out def test_check_min_cppstd(self): """ test that the check_min_cppstd works fine wiht compatibility, as it is based on ``conanfile.info.settings`` not ``conanfile.settings`` """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd, valid_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" def validate(self): check_min_cppstd(self, "17", False) self.output.info("valid standard!!") def package_info(self): self.output.info("CPPSTD: {}".format(self.settings.compiler.cppstd)) """) c = TestClient() c.save({"conanfile.py": conanfile}) settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++11" c.run(f"create . {settings} -s compiler.cppstd=17") assert "pkg/0.1: valid standard!!" in c.out assert "pkg/0.1: CPPSTD: 17" in c.out c.run(f"install {settings} --requires=pkg/0.1 -s compiler.cppstd=14", assert_error=True) assert "pkg/0.1: Invalid: Current cppstd (14) is lower than the required C++ standard (17)."\ in c.out c.run(f"install {settings} --requires=pkg/0.1 -s compiler.cppstd=20") assert "valid standard!!" in c.out assert "pkg/0.1: CPPSTD: 17" in c.out def test_check_min_cstd(self): """ test that the check_min_cstd works fine with compatibility """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" languages = "C" def validate(self): check_min_cstd(self, "17", False) self.output.info("valid standard!!") def package_info(self): self.output.info("CSTD: {}".format(self.settings.compiler.cstd)) """) c = TestClient() c.save({"conanfile.py": conanfile}) settings = "-s compiler=gcc -s compiler.version=14 -s compiler.libcxx=libstdc++11" c.run(f"create . {settings} -s compiler.cstd=17") assert "pkg/0.1: valid standard!!" in c.out assert "pkg/0.1: CSTD: 17" in c.out c.run(f"install {settings} --requires=pkg/0.1 -s compiler.cstd=11", assert_error=True) assert "pkg/0.1: Invalid: Current cstd (11) is lower than the required C standard (17)."\ in c.out c.run(f"install {settings} --requires=pkg/0.1 -s compiler.cstd=23") assert "valid standard!!" in c.out assert "pkg/0.1: CSTD: 17" in c.out def test_check_min_cppstd_interface(self): """ test that says that compatible binaries are ok, as long as the user defined cppstd>=14. The syntax is a bit forced, maybe we want to improve ``check_min_cppstd`` capabilities to be able to raise ConanInvalidConfiguration too """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration from conan.tools.build import check_min_cppstd, valid_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" def validate(self): if int(str(self.info.settings.compiler.cppstd).replace("gnu", "")) <= 14: raise ConanInvalidConfiguration("incompatible cppstd!") check_min_cppstd(self, "17", False) # based on self.info self.output.info("valid standard!!") def package_info(self): self.output.info("CPPSTD: {}".format(self.settings.compiler.cppstd)) """) c = TestClient() c.save({"conanfile.py": conanfile}) settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++11" c.run(f"create . {settings} -s compiler.cppstd=17") assert "pkg/0.1: valid standard!!" in c.out assert "pkg/0.1: CPPSTD: 17" in c.out c.run(f"install {settings} --requires=pkg/0.1 -s compiler.cppstd=14", assert_error=True) assert "valid standard!!" not in c.out assert "pkg/0.1: Invalid: incompatible cppstd!" in c.out c.run(f"install {settings} --requires=pkg/0.1 -s compiler.cppstd=20") assert "valid standard!!" in c.out assert "pkg/0.1: CPPSTD: 17" in c.out def test_can_create_multiple(self): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("os", "arch", "compiler", "build_type")}) settings = "-s os=Linux -s arch=x86_64 -s compiler=gcc -s compiler.version=9 "\ "-s compiler.libcxx=libstdc++11" c.run(f"create . {settings} -s compiler.cppstd=11") c.assert_listed_binary({"pkg/0.1": ("0d5f0b9d89187b4e62abb10ae409997e152db9de", "Build")}) c.run(f"create . {settings} -s compiler.cppstd=14") c.assert_listed_binary({"pkg/0.1": ("145f423d315bee340546093be5b333ef5238668e", "Build")}) c.run(f"create . {settings} -s compiler.cppstd=17") c.assert_listed_binary({"pkg/0.1": ("00fcbc3b6ab76a68f15e7e750e8081d57a6f5812", "Build")}) def test_unnecessary_builds(self): # https://github.com/conan-io/conan/issues/15657 c = TestClient() c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "dep/conanfile.py": GenConanfile("dep", "0.1").with_tool_requires("tool/0.1"), "app/conanfile.py": GenConanfile("app", "0.1").with_requires("dep/0.1") .with_tool_requires("tool/0.1")}) c.run("create tool") c.run("create dep ") c.run("create app ") c.run("remove tool:* -c") c.run("install --requires=dep/0.1 --build=missing") assert re.search(r"Skipped binaries(\s*)tool/0.1", c.out) c.run("graph info --requires=app/0.1 --build=missing") assert re.search(r"Skipped binaries(\s*)tool/0.1", c.out) c.run("install --requires=app/0.1 --build=missing") assert re.search(r"Skipped binaries(\s*)tool/0.1", c.out) def test_msvc_194_fallback(self): c = TestClient() c.save_home({"profiles/default": ""}) c.save({"conanfile.py": GenConanfile("mylib", "1.0").with_settings("os", "arch", "compiler", "build_type"), "profile_build": "[settings]\nos=Windows\narch=x86_64"}) c.run("create . -s os=Windows -s arch=x86_64 -s build_type=Release " "-s compiler=msvc " "-s compiler.version=193 -s compiler.cppstd=17 " "-s compiler.runtime=dynamic -pr:b=profile_build") package_id1 = c.created_package_id("mylib/1.0") # Try to install with cppstd 14, it will find cppstd 17 as compatible c.run("install --requires=mylib/1.0@ -s os=Windows -s arch=x86_64 -s build_type=Release " "-s compiler=msvc " "-s compiler.version=194 -s compiler.cppstd=14 " "-s compiler.runtime=dynamic -pr:b=profile_build") assert "mylib/1.0: Main binary package 'e340edd75790e7156c595edebd3d98b10a2e091e' missing."\ f"Using compatible package '{package_id1}'" c.run("install --requires=mylib/1.0@ -s os=Windows -s arch=x86_64 -s build_type=Release " "-s compiler=msvc " "-s compiler.version=194 -s compiler.cppstd=17 " "-s compiler.runtime=dynamic -pr:b=profile_build") assert "mylib/1.0: Main binary package 'e340edd75790e7156c595edebd3d98b10a2e091e' missing." \ f"Using compatible package '{package_id1}'" class TestErrorsCompatibility: """ when the plugin fails, we want a clear message and a helpful trace """ def test_error_compatibility(self): c = TestClient() debug_compat = textwrap.dedent("""\ def debug_compat(conanfile): other(conanfile) def other(conanfile): conanfile.settings.os """) compatibles = textwrap.dedent("""\ from debug_compat import debug_compat def compatibility(conanfile): return debug_compat(conanfile) """) c.save_home({"extensions/plugins/compatibility/compatibility.py": compatibles, "extensions/plugins/compatibility/debug_compat.py": debug_compat}) conanfile = GenConanfile("dep", "0.1") c.save({"dep/conanfile.py": conanfile, "consumer/conanfile.py": GenConanfile().with_requires("dep/0.1")}) c.run(f"export dep") c.run(f"install consumer", assert_error=True) assert "Error while processing 'compatibility.py' plugin for 'dep/0.1', line 3" in c.out assert "while calling 'debug_compat', line 2" in c.out assert "while calling 'other', line 5" in c.out def test_remove_plugin_file(self): c = TestClient() c.run("version") # to trigger the creation os.remove(os.path.join(HomePaths(c.cache_folder).compatibility_plugin_path, "compatibility.py")) c.save({"conanfile.txt": ""}) c.run("install .", assert_error=True) assert "ERROR: The 'compatibility.py' plugin file doesn't exist" in c.out def test_compatible_prev_from_cache(): tc = TestClient() tc.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("compiler")}) settings = ("-s os=Linux -s arch=x86_64 -s compiler=gcc -s compiler.version=8 " "-s compiler.libcxx=libstdc++11") tc.run(f"create . {settings} -s=compiler.cppstd=17") tc.run(f"install --requires=pkg/0.1 {settings} -s=compiler.cppstd=14 -u") tc.assert_listed_binary({"pkg/0.1": ("6179018ccb6b15e6443829bf3640e25f2718b931", "Cache")}) ================================================ FILE: test/integration/package_id/test_config_package_id.py ================================================ import json import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save @pytest.mark.parametrize("config_version, mode, result", [ ("myconfig/1.2.3#rev1", "minor_mode", "myconfig/1.2.Z"), ("myconfig/1.2.3#rev1", "patch_mode", "myconfig/1.2.3"), ("myconfig/1.2.3#rev1", "full_mode", "myconfig/1.2.3#rev1"), ("myconfig/1.2.3#rev1", "revision_mode", "myconfig/1.2.3#rev1"), ("myconfig/1.2.3", "minor_mode", "myconfig/1.2.Z")]) def test_config_package_id(config_version, mode, result): c = TestClient() config_version = json.dumps({"config_version": [config_version]}) save(c.paths.config_version_path, config_version) save(c.paths.global_conf_path, f"core.package_id:config_mode={mode}") c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") assert f"config_version: {result}" in c.out c.run("list pkg/0.1:* --format=json") info = json.loads(c.stdout) rrev = info["Local Cache"]["pkg/0.1"]["revisions"]["485dad6cb11e2fa99d9afbe44a57a164"] package_id = {"myconfig/1.2.Z": "c78b4d8224154390356fe04fe598d67aec930199", "myconfig/1.2.3": "60005f5b11bef3ddd686b13f5c6bf576a9b882b8", "myconfig/1.2.3#rev1:pid1": "3e4270809028e4566b55d3958e94ae3d6f0c92a7", "myconfig/1.2.3#rev1": "aae875ae226416f177bf386a3e4ad6aaffce09e7"} package_id = package_id.get(result) pkg = rrev["packages"][package_id] assert pkg["info"] == {"config_version": [result]} def test_error_config_package_id(): c = TestClient() c.save_home({"global.conf": "core.package_id:config_mode=minor_mode"}) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .", assert_error=True) assert "ERROR: core.package_id:config_mode defined, " \ "but error while loading 'config_version.json'" in c.out @pytest.mark.parametrize("config_version, mode, result", [ ("myconfig/1.2.3#rev1", "minor_mode", "myconfig/1.2.Z"), ("myconfig/1.2.3#rev1", "patch_mode", "myconfig/1.2.3"), ]) def test_config_package_id_clear(config_version, mode, result): c = TestClient(light=True) config_version = json.dumps({"config_version": [config_version]}) save(c.paths.config_version_path, config_version) save(c.paths.global_conf_path, f"core.package_id:config_mode={mode}") c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_package_id("self.info.clear()")}) c.run("create .") assert f"config_version: {result}" not in c.out def test_recipe_revision_mode(): clienta = TestClient() clienta.save_home({"global.conf": "core.package_id:default_unknown_mode=recipe_revision_mode"}) clienta.save({"conanfile.py": GenConanfile()}) clienta.run("create . --name=liba --version=0.1 --user=user --channel=testing") clientb = TestClient(cache_folder=clienta.cache_folder) clientb.save({"conanfile.py": GenConanfile("libb", "0.1").with_require("liba/0.1@user/testing")}) clientb.run("create . --user=user --channel=testing") clientc = TestClient(cache_folder=clienta.cache_folder) clientc.save({"conanfile.py": GenConanfile("libc", "0.1").with_require("libb/0.1@user/testing")}) clientc.run("install . --user=user --channel=testing") # Do a minor change to the recipe, it will change the recipe revision clienta.save({"conanfile.py": str(GenConanfile()) + "# comment"}) clienta.run("create . --name=liba --version=0.1 --user=user --channel=testing") clientc.run("install . --user=user --channel=testing", assert_error=True) assert "ERROR: Missing prebuilt package for 'libb/0.1@user/testing'" in clientc.out # Building b with the new recipe revision of liba works clientc.run("install . --user=user --channel=testing --build=libb*") # Now change only the package revision of liba clienta.run("create . --name=liba --version=0.1 --user=user --channel=testing") clientc.run("install . --user=user --channel=testing") def test_config_package_id_mode(): # chicken and egg problem when the configuration package itself is affected by the mode c = TestClient(light=True) config_version = json.dumps({"config_version": ["myconfig/0.1"]}) save(c.paths.config_version_path, config_version) c.save_home({"global.conf": "core.package_id:config_mode=minor_mode"}) c.save({"conanfile.py": GenConanfile("myconfig", "0.1").with_package_type("configuration")}) c.run("create . ") c.run("list *:*") # The binary is independent of the configuration version assert "config_version" not in c.out ================================================ FILE: test/integration/package_id/test_default_package_id.py ================================================ import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("typedep, typeconsumer, different_id", [("header-library", "application", False), ("header-library", "shared-library", False), ("header-library", "static-library", False), ("static-library", "application", True), ("static-library", "shared-library", True), ("static-library", "header-library", False), ("static-library", "static-library", False), ("shared-library", "header-library", False), ("shared-library", "static-library", False), ("shared-library", "shared-library", False), ("shared-library", "application", False) ]) def test_default_package_id_options(typedep, typeconsumer, different_id): """ test that some consumer package ids are changed when the dependency change one of its options """ c = TestClient(light=True) dep = GenConanfile("dep", "0.1").with_option("myopt", [True, False]) \ .with_package_type(typedep).with_class_attribute('implements = ["auto_shared_fpic", "auto_header_only"]') consumer = GenConanfile("consumer", "0.1").with_requires("dep/0.1")\ .with_package_type(typeconsumer).with_class_attribute('implements = ["auto_shared_fpic", "auto_header_only"]') c.save({"dep/conanfile.py": dep, "consumer/conanfile.py": consumer}) c.run("create dep -o dep/*:myopt=True") pid1 = c.created_package_id("dep/0.1") c.run("create dep -o dep/*:myopt=False") pid2 = c.created_package_id("dep/0.1") if typedep != "header-library": assert pid1 != pid2 c.run("create consumer -o dep/*:myopt=True") pid1 = c.created_package_id("consumer/0.1") c.run("create consumer -o dep/*:myopt=False") pid2 = c.created_package_id("consumer/0.1") if different_id: assert pid1 != pid2 else: assert pid1 == pid2 @pytest.mark.parametrize("typedep, versiondep, typeconsumer, different_id", [("static-library", "1.1", "header-library", False), ("static-library", "1.0.1", "static-library", False), ("static-library", "1.1", "static-library", True), ("shared-library", "1.1", "header-library", False), ("shared-library", "1.0.1", "static-library", False), ("shared-library", "1.1", "static-library", True), ("shared-library", "1.0.1", "shared-library", False), ("shared-library", "1.1", "shared-library", True), ("shared-library", "1.0.1", "application", False), ("shared-library", "1.1", "application", True), ("application", "2.1", "application", False), ]) def test_default_package_id_versions(typedep, versiondep, typeconsumer, different_id): """ test that some consumer package ids are changed when the dependency changes its version """ c = TestClient(light=True) dep = GenConanfile("dep").with_package_type(typedep) consumer = GenConanfile("consumer", "0.1").with_requires("dep/[>0.0]") \ .with_package_type(typeconsumer) c.save({"dep/conanfile.py": dep, "consumer/conanfile.py": consumer}) c.run("create dep --version=1.0") c.run("create consumer") pid1 = c.created_package_id("consumer/0.1") c.run(f"create dep --version={versiondep}") c.run("create consumer") pid2 = c.created_package_id("consumer/0.1") if different_id: assert pid1 != pid2 else: assert pid1 == pid2 ================================================ FILE: test/integration/package_id/test_package_id_test_requires.py ================================================ import pytest from conan.test.utils.tools import GenConanfile, TestClient @pytest.mark.parametrize("build_mode", [None, "patch_mode"]) def test_package_id_not_affected_test_requires(build_mode): """ By default, test_requires do not affect the package_id """ c = TestClient() if build_mode is not None: c.save_home({"global.conf": f"core.package_id:default_build_mode={build_mode}"}) c.save({"gtest/conanfile.py": GenConanfile("gtest", "1.0"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_test_requires("gtest/1.0")}) c.run("create gtest") c.run("create engine") c.run("list engine:*") assert "engine/1.0" in c.out assert "gtest" not in c.out def test_package_id_not_affected_test_requires_transitive(): """ By default, transitive deps of test_requires do not affect the package_id """ c = TestClient() c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "gtest/conanfile.py": GenConanfile("gtest", "1.0").with_requires("zlib/1.0"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_test_requires("gtest/1.0")}) c.run("create zlib") c.run("create gtest") c.run("create engine") c.run("list engine:*") assert "engine/1.0" in c.out assert "gtest" not in c.out assert "zlib" not in c.out ================================================ FILE: test/integration/package_id/test_valid_package_id_values.py ================================================ import textwrap from conan.test.utils.tools import TestClient class TestValidPackageIdValue: def test_valid(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): options = {"shared": [False, True]} """) c.save({"conanfile.py": conanfile}) c.run("create . --name=pkg --version=0.1", assert_error=True) assert "pkg/0.1: Invalid: 'options.shared' value not defined" in c.out ================================================ FILE: test/integration/package_id/test_validate.py ================================================ import json import os import platform import re import textwrap import pytest from conan.cli.exit_codes import ERROR_INVALID_CONFIGURATION, ERROR_GENERAL from conan.internal.graph.graph import BINARY_INVALID from conan.test.assets.genconanfile import GenConanfile from conan.internal.util.files import save, load from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID class TestValidate: def test_validate_create(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): settings = "os" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Windows not supported") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 -s os=Linux") assert "pkg/0.1: Package '9a4eb3c8701508aa9458b1a73d0633783ecc2270' created" in client.out error = client.run("create . --name=pkg --version=0.1 -s os=Windows", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg/0.1: Invalid: Windows not supported" in client.out client.run("graph info --require pkg/0.1 -s os=Windows") assert "binary: Invalid" in client.out assert "info_invalid: Windows not supported" in client.out client.run("graph info --require pkg/0.1 -s os=Windows --format json") myjson = json.loads(client.stdout) assert myjson["graph"]["nodes"]["1"]["binary"] == BINARY_INVALID assert myjson["graph"]["nodes"]["1"]["info_invalid"] == "Windows not supported" in client.out def test_validate_header_only(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration from conan.tools.build import check_min_cppstd class Pkg(ConanFile): settings = "os", "compiler" options = {"shared": [True, False], "header_only": [True, False],} default_options = {"shared": False, "header_only": True} def package_id(self): if self.info.options.header_only: self.info.clear() def validate(self): if self.info.options.get_safe("header_only") == "False": if self.info.settings.get_safe("compiler.version") == "12": raise ConanInvalidConfiguration("This package cannot exist in gcc 12") check_min_cppstd(self, 11) # These configurations are impossible if self.info.settings.os != "Windows" and self.info.options.shared: raise ConanInvalidConfiguration("shared is only supported under windows") # HOW CAN WE VALIDATE CPPSTD > 11 WHEN HEADER ONLY? """) client.save({"conanfile.py": conanfile}) client.run("create . --name pkg --version=0.1 -s os=Linux -s compiler=gcc " "-s compiler.version=11 -s compiler.libcxx=libstdc++11") assert re.search(r"Package '(.*)' created", str(client.out)) client.run("create . --name pkg --version=0.1 -o header_only=False -s os=Linux " "-s compiler=gcc -s compiler.version=12 -s compiler.libcxx=libstdc++11", assert_error=True) assert "Invalid: This package cannot exist in gcc 12" in client.out client.run("create . --name pkg --version=0.1 -o header_only=False -s os=Macos " "-s compiler=gcc -s compiler.version=11 -s compiler.libcxx=libstdc++11 " "-s compiler.cppstd=98", assert_error=True) assert "Invalid: Current cppstd (98) is lower than the required C++ " \ "standard (11)" in client.out client.run("create . --name pkg --version=0.1 -o header_only=False -o shared=True " "-s os=Macos -s compiler=gcc " "-s compiler.version=11 -s compiler.libcxx=libstdc++11 -s compiler.cppstd=11", assert_error=True) assert "Invalid: shared is only supported under windows" in client.out def test_validate_compatible(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): settings = "os" def validate_build(self): if self.settings.os == "Windows": raise ConanInvalidConfiguration("Windows not supported") def compatibility(self): if self.settings.os == "Windows": return [{"settings": [("os", "Linux")]}] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 -s os=Linux") package_id = "9a4eb3c8701508aa9458b1a73d0633783ecc2270" missing_id = "ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715" assert f"pkg/0.1: Package '{package_id}' created" in client.out # This is the main difference, building from source for the specified conf, fails client.run("create . --name=pkg --version=0.1 -s os=Windows", assert_error=True) assert "pkg/0.1: Cannot build for this configuration: Windows not supported" in client.out client.assert_listed_binary({"pkg/0.1": (missing_id, "Invalid")}) client.run("install --requires=pkg/0.1@ -s os=Windows --build=pkg*", assert_error=True) assert "pkg/0.1: Cannot build for this configuration: Windows not supported" in client.out assert "Windows not supported" in client.out client.run("install --requires=pkg/0.1@ -s os=Windows") assert f"pkg/0.1: Main binary package '{missing_id}' missing" in client.out assert f"Found compatible package '{package_id}'" in client.out client.assert_listed_binary({"pkg/0.1": (package_id, "Cache")}) # --build=missing means "use existing binary if possible", and compatibles are valid binaries client.run("install --requires=pkg/0.1@ -s os=Windows --build=missing") assert f"pkg/0.1: Main binary package '{missing_id}' missing" in client.out assert f"Found compatible package '{package_id}'" in client.out client.assert_listed_binary({"pkg/0.1": (package_id, "Cache")}) client.run("graph info --requires=pkg/0.1@ -s os=Windows") assert f"pkg/0.1: Main binary package '{missing_id}' missing" in client.out assert f"Found compatible package '{package_id}'" in client.out assert f"package_id: {package_id}" in client.out client.run("graph info --requires=pkg/0.1@ -s os=Windows --build=pkg*") assert "binary: Invalid" in client.out def test_validate_remove_package_id_create(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): settings = "os" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Windows not supported") def package_id(self): del self.info.settings.os """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 -s os=Linux") assert "pkg/0.1: Package '{}' created".format(NO_SETTINGS_PACKAGE_ID) in client.out client.run("create . --name=pkg --version=0.1 -s os=Windows", assert_error=True) assert "pkg/0.1: Invalid: Windows not supported" in client.out client.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Invalid")}) client.run("graph info --requires=pkg/0.1@ -s os=Windows") assert "package_id: {}".format(NO_SETTINGS_PACKAGE_ID) in client.out def test_validate_compatible_also_invalid(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): settings = "os", "build_type" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Windows not supported") def compatibility(self): if self.settings.build_type == "Debug" and self.settings.os != "Windows": return [{"settings": [("build_type", "Release")]}] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 -s os=Linux -s build_type=Release") package_id = "c26ded3c7aa4408e7271e458d65421000e000711" client.assert_listed_binary({"pkg/0.1": (package_id, "Build")}) # compatible_packges fallback works client.run("install --requires=pkg/0.1@ -s os=Linux -s build_type=Debug") client.assert_listed_binary({"pkg/0.1": (package_id, "Cache")}) error = client.run("create . --name=pkg --version=0.1 -s os=Windows -s build_type=Release", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg/0.1: Invalid: Windows not supported" in client.out client.run("graph info --requires=pkg/0.1@ -s os=Windows") assert "binary: Invalid" in client.out def test_validate_compatible_also_invalid_fail(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): settings = "os", "build_type" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Windows not supported") def compatibility(self): if self.settings.build_type == "Debug": return [{"settings": [("build_type", "Release")]}] """) client.save({"conanfile.py": conanfile}) package_id = "c26ded3c7aa4408e7271e458d65421000e000711" client.run("create . --name=pkg --version=0.1 -s os=Linux -s build_type=Release") assert f"pkg/0.1: Package '{package_id}' created" in client.out # compatible_packges fallback works client.run("install --requires=pkg/0.1@ -s os=Linux -s build_type=Debug") client.assert_listed_binary({"pkg/0.1": (package_id, "Cache")}) # Windows invalid configuration error = client.run("create . --name=pkg --version=0.1 -s os=Windows -s build_type=Release", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg/0.1: Invalid: Windows not supported" in client.out error = client.run("install --requires=pkg/0.1@ -s os=Windows -s build_type=Release", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg/0.1: Invalid: Windows not supported" in client.out # Windows missing binary: INVALID error = client.run("install --requires=pkg/0.1@ -s os=Windows -s build_type=Debug", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg/0.1: Invalid: Windows not supported" in client.out error = client.run("create . --name=pkg --version=0.1 -s os=Windows -s build_type=Debug", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg/0.1: Invalid: Windows not supported" in client.out # info client.run("graph info --requires=pkg/0.1@ -s os=Windows") assert "binary: Invalid" in client.out client.run("graph info --requires=pkg/0.1@ -s os=Windows -s build_type=Debug") assert "binary: Invalid" in client.out def test_validate_options(self): # The dependency option doesn't affect pkg package_id, so it could find a valid binary # in the cache. So ConanInvalidConfiguration will solve this issue. client = TestClient() client.save({"conanfile.py": GenConanfile().with_option("myoption", [1, 2, 3]) .with_default_option("myoption", 1)}) client.run("create . --name=dep --version=0.1") client.run("create . --name=dep --version=0.1 -o dep/*:myoption=2") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): requires = "dep/0.1" def validate(self): if self.dependencies["dep"].options.myoption == 2: raise ConanInvalidConfiguration("Option 2 of 'dep' not supported") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg1 --version=0.1 -o dep/*:myoption=1") client.save({"conanfile.py": GenConanfile().with_requires("dep/0.1") .with_default_option("dep/*:myoption", 2)}) client.run("create . --name=pkg2 --version=0.1") client.save({"conanfile.py": GenConanfile().with_requires("pkg2/0.1", "pkg1/0.1")}) error = client.run("install .", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg1/0.1: Invalid: Option 2 of 'dep' not supported" in client.out def test_validate_requires(self): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=dep --version=0.1") client.run("create . --name=dep --version=0.2") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): requires = "dep/0.1" def validate(self): if self.dependencies["dep"].ref.version > "0.1": raise ConanInvalidConfiguration("dep> 0.1 is not supported") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg1 --version=0.1") client.save({"conanfile.py": GenConanfile() .with_requirement("pkg1/0.1") .with_requirement("dep/0.2", override=True)}) error = client.run("install .", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg1/0.1: Invalid: dep> 0.1 is not supported" in client.out client.save({"conanfile.py": GenConanfile() .with_requirement("pkg1/0.1") .with_requirement("dep/0.2", force=True)}) error = client.run("install .", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION assert "pkg1/0.1: Invalid: dep> 0.1 is not supported" in client.out def test_validate_package_id_mode(self): client = TestClient() client.save_home({"global.conf": "core.package_id:default_unknown_mode=full_package_mode"}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): settings = "os" def validate(self): if self.info.settings.os == "Windows": raise ConanInvalidConfiguration("Windows not supported") """) client.save({"conanfile.py": conanfile}) client.run("export . --name=dep --version=0.1") client.save({"conanfile.py": GenConanfile().with_requires("dep/0.1")}) error = client.run("create . --name=pkg --version=0.1 -s os=Windows", assert_error=True) assert error == ERROR_INVALID_CONFIGURATION client.assert_listed_binary({"dep/0.1": ("ebec3dc6d7f6b907b3ada0c3d3cdc83613a2b715", "Invalid")}) client.assert_listed_binary({"pkg/0.1": ("19ad5731bb09f24646c81060bd7730d6cb5b6108", "Build")}) assert "ERROR: There are invalid packages:" in client.out assert "dep/0.1: Invalid: Windows not supported" in client.out def test_validate_export_pkg(self): # https://github.com/conan-io/conan/issues/9797 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class TestConan(ConanFile): def validate(self): raise ConanInvalidConfiguration("never ever") """) c.save({"conanfile.py": conanfile}) c.run("export-pkg . --name=test --version=1.0", assert_error=True) assert "ERROR: conanfile.py (test/1.0): Invalid ID: Invalid: never ever" in c.out def test_validate_build_export_pkg(self): # https://github.com/conan-io/conan/issues/9797 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class TestConan(ConanFile): def validate_build(self): raise ConanInvalidConfiguration("never ever") """) c.save({"conanfile.py": conanfile}) c.run("export-pkg . --name=test --version=1.0", assert_error=True) assert "conanfile.py (test/1.0): Cannot build for this configuration: never ever" in c.out def test_validate_install(self): # https://github.com/conan-io/conan/issues/10602 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class TestConan(ConanFile): def validate(self): raise ConanInvalidConfiguration("never ever") """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "ERROR: conanfile.py: Invalid ID: Invalid: never ever" in c.out class TestValidateCppstd: """ aims to be a very close to real use case of cppstd management and validation in recipes """ def test_build_17_consume_14(self): client = TestClient() # simplify it a bit compat = textwrap.dedent("""\ def compatibility(conanfile): return [{"settings": [("compiler.cppstd", v)]} for v in ("11", "14", "17", "20")] """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "compiler" def validate_build(self): # Explicit logic instead of using check_min_cppstd that hides details if int(str(self.settings.compiler.cppstd)) < 17: raise ConanInvalidConfiguration("I need at least cppstd=17 to build") def validate(self): if int(str(self.settings.compiler.cppstd)) < 14: raise ConanInvalidConfiguration("I need at least cppstd=14 to be used") """) client.save({"conanfile.py": conanfile}) settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++" client.run(f"create . {settings} -s compiler.cppstd=17") client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Build")}) # create with cppstd=14 fails, not enough client.run(f"create . {settings} -s compiler.cppstd=14", assert_error=True) client.assert_listed_binary({"pkg/0.1": ("36d978cbb4dc35906d0fd438732d5e17cd1e388d", "Invalid")}) assert "pkg/0.1: Cannot build for this configuration: I need at least cppstd=17 to build" \ in client.out # Install with cppstd=14 can fallback to the previous one client.run(f"install --requires=pkg/0.1 {settings} -s compiler.cppstd=14") # 2 valid binaries, 17 and 20 assert "pkg/0.1: Checking 2 compatible configurations" in client.out client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Cache")}) # install with not enough cppstd should fail client.run(f"install --requires=pkg/0.1@ {settings} -s compiler.cppstd=11", assert_error=True) # not even trying to fallback to compatibles assert "pkg/0.1: Checking" not in client.out client.assert_listed_binary({"pkg/0.1": ("8415595b7485d90fc413c2f47298aa5fb05a5468", "Invalid")}) assert "I need at least cppstd=14 to be used" in client.out def test_header_only_14(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import check_min_cppstd class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "compiler" def package_id(self): self.info.clear() def validate(self): check_min_cppstd(self, 14) """) client.save({"conanfile.py": conanfile}) settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++" client.run(f"create . {settings} -s compiler.cppstd=17") client.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) client.run(f"create . {settings} -s compiler.cppstd=14") client.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) client.run(f"create . {settings} -s compiler.cppstd=11", assert_error=True) client.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Invalid")}) assert "Current cppstd (11) is lower than the required C++ standard (14)" in client.out # Install with cppstd=14 can fallback to the previous one client.run(f"install --requires=pkg/0.1 {settings} -s compiler.cppstd=14") client.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}) # install with not enough cppstd should fail client.run(f"install --requires=pkg/0.1@ {settings} -s compiler.cppstd=11", assert_error=True) # not even trying to fallback to compatibles assert "pkg/0.1: Checking" not in client.out client.assert_listed_binary({"pkg/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Invalid")}) assert "Current cppstd (11) is lower than the required C++ standard (14)" in client.out def test_build_17_consume_14_transitive(self): """ what happens if we have: app->engine(shared-lib)->pkg(static-lib) if pkg is only buildable with cppstd>=17 and needs cppstd>=14 to be consumed, but as it is static it becomes an implementation detail of engine, that doesn't have any constraint or validate() at all """ client = TestClient() # simplify it a bit compat = textwrap.dedent("""\ def compatibility(conanfile): return [{"settings": [("compiler.cppstd", v)]} for v in ("11", "14", "17", "20")] """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "compiler" package_type = "static-library" def validate_build(self): # Explicit logic instead of using check_min_cppstd that hides details if int(str(self.settings.compiler.cppstd)) < 17: raise ConanInvalidConfiguration("I need at least cppstd=17 to build") def validate(self): if int(str(self.settings.compiler.cppstd)) < 14: raise ConanInvalidConfiguration("I need at least cppstd=14 to be used") """) engine = GenConanfile("engine", "0.1").with_package_type("shared-library") \ .with_requires("pkg/0.1") app = GenConanfile("app", "0.1").with_package_type("application") \ .with_requires("engine/0.1") client.save({"pkg/conanfile.py": conanfile, "engine/conanfile.py": engine, "app/conanfile.py": app}) settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++" client.run(f"create pkg {settings} -s compiler.cppstd=17") client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Build")}) client.run(f"create engine {settings} -s compiler.cppstd=17") client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Cache")}) client.run(f"install app {settings} -s compiler.cppstd=17 -v") client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Skip")}) client.run(f"install app {settings} -s compiler.cppstd=14 -v") client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Skip")}) # No binary for engine exist for cppstd=11 client.run(f"install app {settings} -s compiler.cppstd=11", assert_error=True) client.assert_listed_binary({"engine/0.1": ("dc24e2caf6e1fa3e8bb047ca0f5fa053c71df6db", "Missing")}) client.run(f"install app {settings} -s compiler.cppstd=11 --build=missing", assert_error=True) assert 'pkg/0.1: Invalid: I need at least cppstd=14 to be used' in client.out def test_build_17_consume_14_transitive_erasure(self): """ The same as the above test: app->engine(shared-lib)->pkg(static-lib) but in this test, the engine shared-lib does "package_id()" erasure of "pkg" dependency, being able to reuse it then even when cppstd==11 """ client = TestClient() # simplify it a bit compat = textwrap.dedent("""\ def compatibility(conanfile): return [{"settings": [("compiler.cppstd", v)]} for v in ("11", "14", "17", "20")] """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "compiler" package_type = "static-library" def validate_build(self): # Explicit logic instead of using check_min_cppstd that hides details if int(str(self.settings.compiler.cppstd)) < 17: raise ConanInvalidConfiguration("I need at least cppstd=17 to build") def validate(self): if int(str(self.settings.compiler.cppstd)) < 14: raise ConanInvalidConfiguration("I need at least cppstd=14 to be used") """) engine = textwrap.dedent(""" from conan import ConanFile from conan.errors import ConanInvalidConfiguration class Pkg(ConanFile): name = "engine" version = "0.1" settings = "compiler" package_type = "shared-library" requires = "pkg/0.1" def package_id(self): del self.info.settings.compiler.cppstd self.info.requires["pkg"].full_version_mode() """) app = GenConanfile("app", "0.1").with_package_type("application") \ .with_requires("engine/0.1") client.save({"pkg/conanfile.py": conanfile, "engine/conanfile.py": engine, "app/conanfile.py": app}) settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++" client.run(f"create pkg {settings} -s compiler.cppstd=17") client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Build")}) client.run(f"create engine {settings} -s compiler.cppstd=17") client.assert_listed_binary({"engine/0.1": ("493976208e9989b554704f94f9e7b8e5ba39e5ab", "Build")}) client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Cache")}) client.run(f"install app {settings} -s compiler.cppstd=17 -v") client.assert_listed_binary({"engine/0.1": ("493976208e9989b554704f94f9e7b8e5ba39e5ab", "Cache")}) client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Skip")}) client.run(f"install app {settings} -s compiler.cppstd=14 -v") client.assert_listed_binary({"engine/0.1": ("493976208e9989b554704f94f9e7b8e5ba39e5ab", "Cache")}) client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Skip")}) # No binary for engine exist for cppstd=11 client.run(f"install app {settings} -s compiler.cppstd=11 -v") client.assert_listed_binary({"pkg/0.1": ("8415595b7485d90fc413c2f47298aa5fb05a5468", "Skip")}) client.assert_listed_binary({"engine/0.1": ("493976208e9989b554704f94f9e7b8e5ba39e5ab", "Cache")}) client.run(f"install app {settings} -s compiler.cppstd=11 --build=engine*", assert_error=True) assert 'pkg/0.1: Invalid: I need at least cppstd=14 to be used' in client.out @pytest.mark.parametrize("use_attribute", [True, False]) def test_exact_cppstd(self, use_attribute): """ Using the default cppstd_compat sometimes is not desired, and a recipe can explicitly opt-out this default cppstd_compat behavior, if it knows its binaries won't be binary compatible among them for different cppstd values """ client = TestClient() if use_attribute: conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "compiler" extension_properties = {"compatibility_cppstd": False} """) else: conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "compiler" def compatibility(self): self.extension_properties = {"compatibility_cppstd": False} """) client.save({"conanfile.py": conanfile}) settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++" client.run(f"create . --name=pkg --version=0.1 {settings} -s compiler.cppstd=17") client.assert_listed_binary({"pkg/0.1": ("91faf062eb94767a31ff62a46767d3d5b41d1eff", "Build")}) # Install with cppstd=14 can NOT fallback to the previous one client.run(f"install --requires=pkg/0.1 {settings} -s compiler.cppstd=14", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkg/0.1'" in client.out assert "compiler.cppstd=14" in client.out assert "compiler.cppstd=17" not in client.out client.run(f"install --requires=pkg/0.1 {settings} -s compiler.cppstd=14 --build=missing") client.assert_listed_binary({"pkg/0.1": ("36d978cbb4dc35906d0fd438732d5e17cd1e388d", "Build")}) assert "compiler.cppstd=14" in client.out assert "compiler.cppstd=17" not in client.out @pytest.mark.skipif(platform.system() == "Windows", reason="Needs at least 3 cppstd values available, msvc 191 does not") def test_extension_properties_cppstd_compat_non_transitiveness(self): """ The cppstd_compat is not transitive, so if a recipe has cppstd_compat=False, its dependencies will still be checked for compatibility """ tc = TestClient() tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_setting("compiler"), "lib/conanfile.py": GenConanfile("lib", "1.0").with_setting("compiler") .with_class_attribute('extension_properties = {"compatibility_cppstd": False}') .with_requirement("dep/1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_setting("compiler") .with_requirement("lib/1.0")}) tc.run("create dep -s=compiler.cppstd=20") tc.run("create lib -s=compiler.cppstd=17") tc.run("create app -s=compiler.cppstd=14", assert_error=True) tc.run("install --requires=app/1.0 -s=compiler.cppstd=11", assert_error=True) assert "dep/1.0: Found compatible package" in tc.out assert "ERROR: Missing binary: app/1.0" in tc.out def test_extension_properties_make_transitive(self): """ The cppstd_compat is not transitive, so if a recipe has cppstd_compat=False, its dependencies will still be checked for compatibility """ tc = TestClient() tc.save({"lib/conanfile.py": GenConanfile("lib", "1.0").with_setting("compiler"), "dep/conanfile.py": GenConanfile("dep", "1.0").with_setting("compiler") .with_class_attribute('extension_properties = {"compatibility_cppstd": False}') .with_requirement("lib/1.0"), "conanfile.py": GenConanfile("app", "1.0").with_setting("compiler") .with_requirement("dep/1.0")}) tc.run("create lib -s=compiler.cppstd=17") tc.run("create dep -s=compiler.cppstd=14") tc.run("create . -s=compiler.cppstd=14") compat_path = os.path.join(tc.cache_folder, "extensions/plugins/compatibility/compatibility.py") compat_contents = load(compat_path) transitive_expansions = textwrap.indent(textwrap.dedent(""" extension_properties = getattr(conanfile, "extension_properties", {}).copy() for dep in conanfile.dependencies.values(): if not dep.extension_properties.get("compatibility_cppstd", True): extension_properties["compatibility_cppstd"] = False """), " ") existing = 'extension_properties = getattr(conanfile, "extension_properties", {})' compat_contents = compat_contents.replace(existing, transitive_expansions) save(compat_path, compat_contents) tc.run("install --requires=app/1.0 -s=compiler.cppstd=17", assert_error=True) assert "Missing prebuilt package for 'app/1.0', 'dep/1.0'" in tc.out class TestCompatibleSettingsTarget: """ aims to be a very close to real use case of tool being used across different settings_target """ def test_settings_target_in_compatibility_method_within_recipe(self): client = TestClient() """ test setting_target in recipe's compatibility method """ tool_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def compatibility(self): if self.settings_target.arch == "armv7": return [{"settings_target": [("arch", "armv6")]}] def package_id(self): self.info.settings_target = self.settings_target for field in self.info.settings_target.fields: if field != "arch": self.info.settings_target.rm_safe(field) """) client.save({"conanfile.py": tool_conanfile}) client.run("create . --name=tool --version=0.1 -s os=Linux -s:h arch=armv6 --build-require") package_id = client.created_package_id("tool/0.1") assert f"tool/0.1: Package '{package_id}' created" in client.out app_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def requirements(self): self.tool_requires("tool/0.1") """) client.save({"conanfile.py": app_conanfile}) client.run("create . --name=app --version=0.1 -s os=Linux -s:h arch=armv7") assert f"Found compatible package '{package_id}'" in client.out def test_settings_target_in_compatibility_in_global_compatibility_py(self): client = TestClient() """ test setting_target in global compatibility method """ compat = textwrap.dedent("""\ def compatibility(self): if self.settings_target.arch == "armv7": return [{"settings_target": [("arch", "armv6")]}] """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) tool_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def package_id(self): self.info.settings_target = self.settings_target for field in self.info.settings_target.fields: if field != "arch": self.info.settings_target.rm_safe(field) """) client.save({"conanfile.py": tool_conanfile}) client.run("create . --name=tool --version=0.1 -s os=Linux -s:h arch=armv6 --build-require") package_id = client.created_package_id("tool/0.1") assert f"tool/0.1: Package '{package_id}' created" in client.out app_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def requirements(self): self.tool_requires("tool/0.1") """) client.save({"conanfile.py": app_conanfile}) client.run("create . --name=app --version=0.1 -s os=Linux -s:h arch=armv7") assert f"Found compatible package '{package_id}'" in client.out def test_no_settings_target_in_recipe_but_in_compatibility_method(self): client = TestClient() """ test settings_target in compatibility method when recipe is not a build-require this should not crash. When building down-stream package it should end up with ERROR_GENERAL instead of crash """ tool_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def compatibility(self): if self.settings_target.arch == "armv7": return [{"settings_target": [("arch", "armv6")]}] """) client.save({"conanfile.py": tool_conanfile}) client.run("create . --name=tool --version=0.1 -s os=Linux -s:h arch=armv6") package_id = client.created_package_id("tool/0.1") assert f"tool/0.1: Package '{package_id}' created" in client.out app_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def requirements(self): self.tool_requires("tool/0.1") """) client.save({"conanfile.py": app_conanfile}) error = client.run("create . --name=app --version=0.1 -s os=Linux -s:h arch=armv7", assert_error=True) assert error == ERROR_GENERAL assert "ERROR: Missing prebuilt package for 'tool/0.1'" in client.out def test_no_settings_target_in_recipe_but_in_global_compatibility(self): client = TestClient() """ test settings_target in global compatibility method when recipe is not a build-require this should not crash. When building down-stream package it should end up with ERROR_GENERAL instead of crash """ compat = textwrap.dedent("""\ def compatibility(self): if self.settings_target.arch == "armv7": return [{"settings_target": [("arch", "armv6")]}] """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) tool_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" """) client.save({"conanfile.py": tool_conanfile}) client.run("create . --name=tool --version=0.1 -s os=Linux -s:h arch=armv6") package_id = client.created_package_id("tool/0.1") assert f"tool/0.1: Package '{package_id}' created" in client.out app_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def requirements(self): self.tool_requires("tool/0.1") """) client.save({"conanfile.py": app_conanfile}) error = client.run("create . --name=app --version=0.1 -s os=Linux -s:h arch=armv7", assert_error=True) assert error == ERROR_GENERAL assert "ERROR: Missing prebuilt package for 'tool/0.1'" in client.out def test_three_packages_with_and_without_settings_target(self): client = TestClient() """ test 3 packages, tool_a and tool_b have a mutual downstream package (app), and when build app it should find tool_a (a compatible version of it), and find tool_b, and conan create should be successful. """ compat = textwrap.dedent("""\ def compatibility(self): if self.settings_target.arch == "armv7": return [{"settings_target": [("arch", "armv6")]}] """) client.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) tool_a_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def package_id(self): self.info.settings_target = self.settings_target for field in self.info.settings_target.fields: if field != "arch": self.info.settings_target.rm_safe(field) """) client.save({"conanfile.py": tool_a_conanfile}) client.run("create . --name=tool_a --version=0.1 " "-s os=Linux -s:h arch=armv6 -s:b arch=x86_64 --build-require") package_id_tool_a = client.created_package_id("tool_a/0.1") assert f"tool_a/0.1: Package '{package_id_tool_a}' created" in client.out tool_b_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" """) client.save({"conanfile.py": tool_b_conanfile}) client.run("create . --name=tool_b --version=0.1 -s os=Linux " "-s arch=x86_64 -s:b arch=x86_64") package_id_tool_b = client.created_package_id("tool_b/0.1") assert f"tool_b/0.1: Package '{package_id_tool_b}' created" in client.out app_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def requirements(self): self.tool_requires("tool_a/0.1") self.tool_requires("tool_b/0.1") """) client.save({"conanfile.py": app_conanfile}) client.run("create . --name=app --version=0.1 -s os=Linux -s:h arch=armv7 -s:b arch=x86_64") assert f"Found compatible package '{package_id_tool_a}'" in client.out assert package_id_tool_b in client.out def test_settings_target_in_compatibility_method_within_recipe_package_info(self): # https://github.com/conan-io/conan/issues/14823 client = TestClient() tool_conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "arch" def compatibility(self): return [{'settings': [('arch', 'armv6')]}] def package_info(self): # This used to crash self.settings_target.get_safe('compiler.link_time_optimization') """) client.save({"conanfile.py": tool_conanfile}) client.run("create . --name=tool --version=0.1 -s os=Linux -s:b arch=armv6 --build-require") package_id = client.created_package_id("tool/0.1") assert f"tool/0.1: Package '{package_id}' created" in client.out client.run("install --tool-requires=tool/0.1 -s os=Linux -s:b arch=armv7") # This used to crash, not anymore assert f"Found compatible package '{package_id}'" in client.out ================================================ FILE: test/integration/package_id/transitive_header_only_test.py ================================================ from conan.test.utils.tools import TestClient, GenConanfile class TestTransitiveIds: def test_transitive_library(self): # https://github.com/conan-io/conan/issues/6450 client = TestClient(light=True) client.save_home({"global.conf": "core.package_id:default_unknown_mode=full_version_mode"}) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=liba --version=1.0") client.run("create . --name=liba --version=1.1") client.save({"conanfile.py": GenConanfile().with_require("liba/1.0")}) client.run("create . --name=libb --version=1.0") client.save({"conanfile.py": GenConanfile().with_require("libb/1.0")}) client.run("create . --name=libc --version=1.0") client.save({"conanfile.py": GenConanfile().with_require("libc/1.0") .with_require("liba/1.0")}) client.run("create . --name=libd --version=1.0") # The consumer forces to depend on liba/2, instead of liba/1 client.save({"conanfile.py": GenConanfile().with_require("libc/1.0") .with_requirement("liba/1.1", force=True)}) client.run("create . --name=libd --version=1.0", assert_error=True) # both B and C require a new binary client.assert_listed_binary( {"liba/1.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libb/1.0": ("efdeb15efa0e3e6ce0c9ef9bd82c56e4b2188c8a", "Missing"), "libc/1.0": ("fea281081e89fcc36242a68b0dbb16276a84f624", "Missing"), "libd/1.0": ("13a3ed814d78c8b91f303db7d4cdd7c4e614323c", "Build") }) def test_transitive_major_mode(self): # https://github.com/conan-io/conan/issues/6450 # Test LibE->LibD->LibC->LibB->LibA # LibC declares that it only depends on major version changes of its upstream # So LibC package ID doesn't change, even if LibA changes # But LibD package ID changes, even if its direct dependency LibC doesn't client = TestClient(light=True) client.save_home({"global.conf": "core.package_id:default_unknown_mode=full_version_mode"}) # LibA client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=liba --version=1.0") client.run("create . --name=liba --version=1.1") # libB -> LibA client.save({"conanfile.py": GenConanfile().with_require("liba/1.0")}) client.run("create . --name=libb --version=1.0") # libC -> libB major_mode = "self.info.requires.major_mode()" client.save({"conanfile.py": GenConanfile().with_require("libb/1.0") .with_package_id(major_mode)}) client.run("create . --name=libc --version=1.0") # Check the LibC ref with RREV keeps the same client.assert_listed_binary({"libc/1.0": ("2aa3bb6726dfc6c825a91f3b465b769e770ee902", "Build")}) # LibD -> LibC client.save({"conanfile.py": GenConanfile().with_require("libc/1.0")}) client.run("create . --name=libd --version=1.0") # LibE -> LibD, LibA/2.0 client.save({"conanfile.py": GenConanfile().with_require("libd/1.0") .with_requirement("liba/1.1", force=True)}) client.run("create . --name=libe --version=1.0", assert_error=True) # Check the LibC ref with RREV keeps the same, it is in cache, not missing # But LibD package ID changes and is missing, because it depends transitively on LibA client.assert_listed_binary( {"liba/1.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libb/1.0": ("efdeb15efa0e3e6ce0c9ef9bd82c56e4b2188c8a", "Missing"), "libc/1.0": ("2aa3bb6726dfc6c825a91f3b465b769e770ee902", "Cache"), "libd/1.0": ("13a3ed814d78c8b91f303db7d4cdd7c4e614323c", "Missing"), "libe/1.0": ("f93a37dd8bc53991d7485235c6b3e448942c52ff", "Build") }) def test_transitive_unrelated(self): # https://github.com/conan-io/conan/issues/6450 client = TestClient(light=True) # LibA client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=liba --version=1.0") client.run("create . --name=liba --version=2.0") # libB -> LibA client.save({"conanfile.py": GenConanfile().with_require("liba/1.0")}) client.run("create . --name=libb --version=1.0") # libC -> libB unrelated = "self.info.requires['libb'].unrelated_mode()" client.save({"conanfile.py": GenConanfile().with_require("libb/1.0") .with_package_id(unrelated)}) client.run("create . --name=libc --version=1.0") # LibD -> LibC client.save({"conanfile.py": GenConanfile().with_require("libc/1.0")}) client.run("create . --name=libd --version=1.0") # LibE -> LibD, LibA/2.0 client.save({"conanfile.py": GenConanfile().with_require("libd/1.0") .with_requirement("liba/2.0", force=True)}) client.run("create . --name=libe --version=1.0", assert_error=True) client.assert_listed_binary({"liba/2.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libb/1.0": ("9c5273fc489264e39c2c16494d8a4178f239e6e2", "Missing"), "libc/1.0": ("9c5273fc489264e39c2c16494d8a4178f239e6e2", "Missing"), "libd/1.0": ("060865748907651202e4caa396a4c11a11f91e28", "Missing")}) def test_transitive_second_level_header_only(self): # https://github.com/conan-io/conan/issues/6450 client = TestClient(light=True) # LibA client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=liba --version=1.0") client.run("create . --name=liba --version=2.0") # libB -> LibA client.save({"conanfile.py": GenConanfile().with_require("liba/1.0")}) client.run("create . --name=libb --version=1.0") # libC -> libB unrelated = "self.info.clear()" client.save({"conanfile.py": GenConanfile().with_require("libb/1.0") .with_package_id(unrelated)}) client.run("create . --name=libc --version=1.0") # LibD -> LibC client.save({"conanfile.py": GenConanfile().with_require("libc/1.0")}) client.run("create . --name=libd --version=1.0") client.assert_listed_binary({"libc/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache")}) # LibE -> LibD, LibA/2.0 client.save({"conanfile.py": GenConanfile().with_require("libd/1.0") .with_requirement("liba/2.0", force=True)}) client.run("create . --name=libe --version=1.0", assert_error=True) # LibD is NOT missing! client.assert_listed_binary( {"liba/2.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libb/1.0": ("9c5273fc489264e39c2c16494d8a4178f239e6e2", "Missing"), "libc/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libd/1.0": ("060865748907651202e4caa396a4c11a11f91e28", "Missing")}) def test_transitive_header_only(self): # https://github.com/conan-io/conan/issues/6450 client = TestClient(light=True) client.save_home({"global.conf": "core.package_id:default_unknown_mode=full_version_mode"}) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=liba --version=1.0") client.run("create . --name=liba --version=2.0") client.save({"conanfile.py": GenConanfile().with_require("liba/1.0") .with_package_id("self.info.clear()")}) client.run("create . --name=libb --version=1.0") client.save({"conanfile.py": GenConanfile().with_require("libb/1.0")}) client.run("create . --name=libc --version=1.0") client.save({"conanfile.py": GenConanfile().with_require("libc/1.0") .with_require("liba/1.0")}) client.run("create . --name=libd --version=1.0") client.save({"conanfile.py": GenConanfile().with_require("libc/1.0") .with_requirement("liba/2.0", force=True)}) # USE THE NEW FIXED PACKAGE_ID client.run("create . --name=libd --version=1.0", assert_error=True) client.assert_listed_binary({"liba/2.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libb/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Cache"), "libc/1.0": ("2d7000cf800f37e01880897faf74a366a21bebdc", "Missing"), }) ================================================ FILE: test/integration/package_id/transitive_options_affect_id_test.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient class TestTransitiveOptionsAffectPackageID: def test_basic(self): client = TestClient() conanfile = GenConanfile("pkg", "0.1").with_option("noaffect", [1, 2])\ .with_option("affect", [True, False])\ .with_package_type("static-library") client.save({"conanfile.py": conanfile}) client.run("create . -o pkg*:noaffect=1 -o pkg*:affect=False") client.assert_listed_binary({"pkg": ("7ff068ae587920b4f40b0dd81e891808c419f78c", "Build")}) client.run("create . -o pkg*:noaffect=2 -o pkg*:affect=False") client.assert_listed_binary({"pkg": ("66de80286b3d7ae1d5c05a9b795d6a6e622a778e", "Build")}) client.run("create . -o pkg*:noaffect=1 -o pkg*:affect=True") client.assert_listed_binary({"pkg": ("8e0cdf1d5b1c1c557b6f4b4ec01a4383b8f50dc3", "Build")}) client.run("create . -o pkg*:noaffect=2 -o pkg*:affect=True") client.assert_listed_binary({"pkg": ("4a18825b7c66155f03b27a1963e42ab3a836fd28", "Build")}) app = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "app" version = "0.1" package_type = "application" requires = "pkg/0.1" def package_id(self): if self.dependencies["pkg"].package_type == "static-library": self.output.info("EMBED MODE!!") self.info.requires["pkg"].package_id = None self.info.options["pkg/*"].affect = self.dependencies["pkg"].options.affect """) client.save({"conanfile.py": app}) client.run("create . -o pkg*:noaffect=1 -o pkg*:affect=False") assert "EMBED MODE!!" in client.out common_pkg_id = "2060adfe3cc9040e5ee7e54c783d752f9ca1ba43" # Should be the same! client.assert_listed_binary({"app": (common_pkg_id, "Build")}) client.run("create . -o pkg*:noaffect=2 -o pkg*:affect=False") assert "EMBED MODE!!" in client.out client.assert_listed_binary({"app": (common_pkg_id, "Build")}) client.run("create . -o pkg*:noaffect=1 -o pkg*:affect=True") assert "EMBED MODE!!" in client.out different_pkg_id = "acfad205713f0d2fce61e7927d059635c0abbfff" # Should be the same! client.assert_listed_binary({"app": (different_pkg_id, "Build")}) client.run("create . -o pkg*:noaffect=2 -o pkg*:affect=True") assert "EMBED MODE!!" in client.out client.assert_listed_binary({"app": (different_pkg_id, "Build")}) def test_transitive_shared(self): # https://github.com/conan-io/conan/issues/18900 c = TestClient() lib1 = GenConanfile("lib1", "0.1").with_shared_option(True) lib2 = (GenConanfile("lib2", "0.1").with_shared_option(True) .with_requirement("lib1/0.1", transitive_libs=True)) lib3 = (GenConanfile("lib3", "0.1").with_shared_option(True) .with_requirement("lib2/0.1")) lib4 = (GenConanfile("lib4", "0.1").with_shared_option(True) .with_requirement("lib3/0.1")) c.save({"lib1/conanfile.py": lib1, "lib2/conanfile.py": lib2, "lib3/conanfile.py": lib3, "lib4/conanfile.py": lib4}) c.run("create lib1") c.run("create lib2") c.run("create lib3") c.run("create lib4") c.run("install --requires=lib4/0.1 -o lib1/*:shared=False --build=missing") c.assert_listed_binary({"lib1/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Build"), "lib2/0.1": ("76844632e497abea8503d65ffd8324460dc70745", "Build"), "lib3/0.1": ("7b10301e532fc0269d6ac70470aee5780f0836cd", "Build"), "lib4/0.1": ("635372f179ad582c713637e361a7cd7ac7cd1d09", "Cache"), }) lib3 = (GenConanfile("lib3", "0.1").with_shared_option(True) .with_requirement("lib2/0.1", transitive_libs=True)) c.save({"lib3/conanfile.py": lib3}) c.run("remove * -c") c.run("create lib1") c.run("create lib2") c.run("create lib3") c.run("create lib4") c.run("install --requires=lib4/0.1 -o lib1/*:shared=False --build=missing") c.assert_listed_binary({"lib1/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Build"), "lib2/0.1": ("76844632e497abea8503d65ffd8324460dc70745", "Build"), "lib3/0.1": ("7b10301e532fc0269d6ac70470aee5780f0836cd", "Build"), "lib4/0.1": ("dd8f5355b399fd7d96c883ddd39b992ae968cb14", "Build"), }) class TestPackageIDABIOptions: """ These tests use the ``shared=True/False`` options for a proxy of the real issue reported in https://github.com/conan-io/conan/issues/19108 The issue appears in MSVC when headers define define FOO_EXPORT __declspec(dllimport) Then, a static library will be a different artifact if it links against a static library or a shared library. This isn't a very common issue because of 2 reasons: - The most general linkage method are either all static or all shared, it is not that frequent to have static libraries linking shared libraries - Many third parties and open source libraries declare and use the __declspec(dllexport), which is necessary for correctly exporting the symbols in MSVC. But the dllimport is not that necessary and mostly a linking optimization, so not that frequent """ def test_package_id_abi_options(self): c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" options = {"shared": [True, False]} default_options = {"shared": False} package_id_abi_options = ["shared"] """) app = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "app" version = "0.1" package_type = "static-library" requires = "pkg/0.1" """) c.save({"pkg/conanfile.py": conanfile, "app/conanfile.py": app}) c.run("create pkg") c.run("create pkg -o *:shared=True") c.run("create app") c.assert_listed_binary({"app": ("e822341e143eb3bba372e24b7cd908c8f91dc24e", "Build")}) c.run("list app/0.1:e822341e143eb3bba372e24b7cd908c8f91dc24e") assert "pkg/*:shared: False" in c.out c.run("create app -o *:shared=True") c.assert_listed_binary({"app": ("8c15f2b19bd994dcd5b44780eda3f03bde74c217", "Build")}) c.run("list app/0.1:8c15f2b19bd994dcd5b44780eda3f03bde74c217") assert "pkg/*:shared: True" in c.out def test_package_id_abi_options_conditional(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" options = {"shared": [True, False]} default_options = {"shared": False} settings = "os" def configure(self): if self.settings.os == "Windows": self.package_id_abi_options = ["shared"] """) app = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "app" version = "0.1" package_type = "static-library" requires = "pkg/0.1" """) c.save({"pkg/conanfile.py": conanfile, "app/conanfile.py": app}) c.run("create pkg -s os=Linux") c.run("create pkg -s os=Linux -o *:shared=True") c.run("create pkg -s os=Windows") c.run("create pkg -s os=Windows -o *:shared=True") c.run("create app -s os=Linux") c.assert_listed_binary({"app": ("e250b55435052b5e55b151d0b03900c73d262473", "Build")}) c.run("list app/0.1:e250b55435052b5e55b151d0b03900c73d262473") assert "pkg/*:shared: False" not in c.out c.run("create app -o *:shared=True -s os=Linux --build=missing:&") c.assert_listed_binary({"app": ("e250b55435052b5e55b151d0b03900c73d262473", "Cache")}) c.run("create app -s os=Windows") c.assert_listed_binary({"app": ("e822341e143eb3bba372e24b7cd908c8f91dc24e", "Build")}) c.run("list app/0.1:e822341e143eb3bba372e24b7cd908c8f91dc24e") assert "pkg/*:shared: False" in c.out c.run("create app -o *:shared=True -s os=Windows --build=missing:&") c.assert_listed_binary({"app": ("8c15f2b19bd994dcd5b44780eda3f03bde74c217", "Build")}) c.run("list app/0.1:8c15f2b19bd994dcd5b44780eda3f03bde74c217") assert "pkg/*:shared: True" in c.out def test_package_id_abi_options_transitive(self): c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" options = {"shared": [True, False]} default_options = {"shared": False} package_id_abi_options = ["shared"] """) middle = (GenConanfile("middle", "0.1").with_requires("pkg/0.1") .with_package_type("shared-library")) app = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "app" version = "0.1" package_type = "application" requires = "middle/0.1" """) c.save({"pkg/conanfile.py": conanfile, "middle/conanfile.py": middle, "app/conanfile.py": app}) c.run("create pkg") c.run("create pkg -o *:shared=True") c.run("create middle") c.run("create middle -o *:shared=True") c.run("create app") c.assert_listed_binary({"app": ("6da48adc0fa03ddc8b74de14b3fd5513a3688a52", "Build")}) # binary not affected, because headers not propagated! c.run("list app/0.1:6da48adc0fa03ddc8b74de14b3fd5513a3688a52") assert "pkg/*:shared: False" not in c.out c.run("create app -o *:shared=True") # Still affected by "middle" full package-id, that is static c.assert_listed_binary({"app": ("6da48adc0fa03ddc8b74de14b3fd5513a3688a52", "Build")}) c.run("list app/0.1:6da48adc0fa03ddc8b74de14b3fd5513a3688a52") assert "pkg/*:shared: False" not in c.out # But if the header is propagated middle = (GenConanfile("middle", "0.1").with_requirement("pkg/0.1", transitive_headers=True) .with_package_type("shared-library")) c.save({"middle/conanfile.py": middle}) c.run("create middle") c.run("create middle -o *:shared=True") c.run("create app") c.assert_listed_binary({"app": ("ae20c28d303d1c561f95683add6638d6155c2bd9", "Build")}) c.run("list app/0.1:ae20c28d303d1c561f95683add6638d6155c2bd9") assert "pkg/*:shared: False" in c.out c.run("create app -o *:shared=True") # Still affected by "middle" full package-id, that is static c.assert_listed_binary({"app": ("321ad086fc1bbb3c2cf7f3e4d8d69c6d2096196d", "Build")}) c.run("list app/0.1:321ad086fc1bbb3c2cf7f3e4d8d69c6d2096196d") assert "pkg/*:shared: True" in c.out ================================================ FILE: test/integration/py_requires/__init__.py ================================================ ================================================ FILE: test/integration/py_requires/python_requires_test.py ================================================ import os import textwrap import time from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient, GenConanfile class TestPyRequiresExtend: @staticmethod def _define_base(client): conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): def source(self): self.output.info("My cool source!") def build(self): self.output.info("My cool build!") def package(self): self.output.info("My cool package!") def package_info(self): self.output.info("My cool package_info!") """) client.save({"conanfile.py": conanfile}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") def test_reuse(self): client = TestClient(light=True, default_server_user=True) self._define_base(client) reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") package_id = client.created_package_id("pkg/0.1@user/testing") assert "pkg/0.1@user/testing: My cool source!" in client.out assert "pkg/0.1@user/testing: My cool build!" in client.out assert "pkg/0.1@user/testing: My cool package!" in client.out assert "pkg/0.1@user/testing: My cool package_info!" in client.out client.run("upload * --confirm -r default") client.run("remove * -c") client.run("install --requires=pkg/0.1@user/testing") assert "pkg/0.1@user/testing: My cool package_info!" in client.out client.run("remove * -c") client.run("download pkg/0.1@user/testing#latest:* -r default") assert f"pkg/0.1@user/testing: Package installed {package_id}" in client.out # But it's broken with a single download client.run("install --requires=pkg/0.1@user/testing -nr", assert_error=True) assert "Cannot resolve python_requires 'base/1.1@user/testing'" in client.out # If pyrequires are expected, then first graph info -f=json and then get recipes from pkglist client.run("remove * -c") client.run("graph info --requires=pkg/0.1@user/testing -f=json", redirect_stdout="graph_info.json") # We can even remove from the cache now (The pyrequires is already downloaded in above step) client.run("remove * -c") client.run("list --graph=graph_info.json --graph-recipes=* -f=json", redirect_stdout="pkglist.json") client.run("download --list=pkglist.json -r default") assert "Downloading recipe 'base/1.1@user/testing" in client.out client.run("install --requires=pkg/0.1@user/testing -nr") assert "pkg/0.1@user/testing: My cool package_info!" in client.out def test_reuse_super(self): client = TestClient(light=True, default_server_user=True) self._define_base(client) reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" def source(self): super().source() self.output.info("MY OWN SOURCE") """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("source . --name=pkg --version=0.1") assert "conanfile.py (pkg/0.1): My cool source!" in client.out assert "conanfile.py (pkg/0.1): MY OWN SOURCE" in client.out def test_reuse_dot(self): client = TestClient(light=True, default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): def build(self): self.output.info("My cool build!") """) client.save({"conanfile.py": conanfile}) client.run("export . --name=my.base --version=1.1 --user=user --channel=testing") reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "my.base/1.1@user/testing" python_requires_extend = "my.base.MyConanfileBase" """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: My cool build!" in client.out def test_with_alias(self): client = TestClient(light=True) self._define_base(client) client.alias("base/latest@user/testing", "base/1.1@user/testing") reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): name = "pkg" version = "1.0" python_requires = "base/latest@user/testing" """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("install .", assert_error=True) assert "python-requires 'alias' are not supported in Conan 2.0" in client.out client.run("create .", assert_error=True) assert "python-requires 'alias' are not supported in Conan 2.0" in client.out def test_reuse_version_ranges(self): client = TestClient(light=True) self._define_base(client) reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/[>1.0 <1.2]@user/testing" python_requires_extend = "base.MyConanfileBase" """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") client.assert_listed_require({"base/1.1@user/testing": "Cache"}, python=True) assert "pkg/0.1@user/testing: My cool source!" in client.out assert "pkg/0.1@user/testing: My cool build!" in client.out assert "pkg/0.1@user/testing: My cool package!" in client.out assert "pkg/0.1@user/testing: My cool package_info!" in client.out def test_multiple_reuse(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class SourceBuild(ConanFile): def source(self): self.output.info("My cool source!") def build(self): self.output.info("My cool build!") """) client.save({"conanfile.py": conanfile}) client.run("export . --name=sourcebuild --version=1.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class PackageInfo(ConanFile): def package(self): self.output.info("My cool package!") def package_info(self): self.output.info("My cool package_info!") """) client.save({"conanfile.py": conanfile}) client.run("export . --name=packageinfo --version=1.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "sourcebuild/1.0@user/channel", "packageinfo/1.0@user/channel" python_requires_extend = "sourcebuild.SourceBuild", "packageinfo.PackageInfo" """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: My cool source!" in client.out assert "pkg/0.1@user/testing: My cool build!" in client.out assert "pkg/0.1@user/testing: My cool package!" in client.out assert "pkg/0.1@user/testing: My cool package_info!" in client.out @staticmethod def test_transitive_access(): client = TestClient(light=True) client.save({"conanfile.py": GenConanfile()}) client.run("export . --name=base --version=1.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class Helper(ConanFile): python_requires = "base/1.0@user/channel" """) client.save({"conanfile.py": conanfile}) client.run("export . --name=helper --version=1.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): python_requires = "helper/1.0@user/channel" def build(self): self.python_requires["base"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=channel") assert "pkg/0.1@user/channel: Created package" in client.out conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): python_requires = "helper/1.0@user/channel" python_requires_extend = "base.HelloConan" """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=channel") assert "pkg/0.1@user/channel: Created package" in client.out def test_multiple_requires_error(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile myvar = 123 def myfunct(): return 123 class Pkg(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg1 --version=1.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile myvar = 234 def myfunct(): return 234 class Pkg(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=pkg2 --version=1.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "pkg1/1.0@user/channel", "pkg2/1.0@user/channel" def build(self): self.output.info("PKG1 N: %s" % self.python_requires["pkg1"].conanfile.name)\ .info("PKG1 V: %s" % self.python_requires["pkg1"].conanfile.version)\ .info("PKG1 U: %s" % self.python_requires["pkg1"].conanfile.user)\ .info("PKG1 C: %s" % self.python_requires["pkg1"].conanfile.channel)\ .info("PKG1 : %s" % self.python_requires["pkg1"].module.myvar)\ .info("PKG2 : %s" % self.python_requires["pkg2"].module.myvar)\ .info("PKG1F : %s" % self.python_requires["pkg1"].module.myfunct())\ .info("PKG2F : %s" % self.python_requires["pkg2"].module.myfunct()) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=consumer --version=0.1 --user=user --channel=testing") assert "consumer/0.1@user/testing: PKG1 N: pkg1" in client.out assert "consumer/0.1@user/testing: PKG1 V: 1.0" in client.out assert "consumer/0.1@user/testing: PKG1 U: user" in client.out assert "consumer/0.1@user/testing: PKG1 C: channel" in client.out assert "consumer/0.1@user/testing: PKG1 : 123" in client.out assert "consumer/0.1@user/testing: PKG2 : 234" in client.out assert "consumer/0.1@user/testing: PKG1F : 123" in client.out assert "consumer/0.1@user/testing: PKG2F : 234" in client.out def test_local_import(self): client = TestClient(light=True, default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile import mydata class MyConanfileBase(ConanFile): exports = "*.py" def source(self): self.output.info(mydata.src) def build(self): self.output.info(mydata.build) def package(self): self.output.info(mydata.pkg) def package_info(self): self.output.info(mydata.info) """) mydata = textwrap.dedent(""" src = "My cool source!" build = "My cool build!" pkg = "My cool package!" info = "My cool package_info!" """) client.save({"conanfile.py": conanfile, "mydata.py": mydata}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") package_id = client.created_package_id("pkg/0.1@user/testing") assert "pkg/0.1@user/testing: My cool source!" in client.out assert "pkg/0.1@user/testing: My cool build!" in client.out assert "pkg/0.1@user/testing: My cool package!" in client.out assert "pkg/0.1@user/testing: My cool package_info!" in client.out client.run("upload * --confirm -r default") client.run("remove * -c") client.run("install --requires=pkg/0.1@user/testing") assert "pkg/0.1@user/testing: My cool package_info!" in client.out client.run("remove * -c") client.run("download pkg/0.1@user/testing#*:* -r default") assert f"pkg/0.1@user/testing: Package installed {package_id}" in client.out def test_reuse_class_members(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): license = "MyLicense" author = "author@company.com" exports = "*.txt" exports_sources = "*.h" generators = "CMakeToolchain" """) client.save({"conanfile.py": conanfile, "header.h": "some content"}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") reuse = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load import os class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" def build(self): self.output.info("Exports sources! %s" % self.exports_sources) self.output.info("HEADER CONTENT!: %s" % load(self, "header.h")) self.output.info("License! %s" % self.license) self.output.info("Author! %s" % self.author) assert os.path.exists("conan_toolchain.cmake") """) client.save({"conanfile.py": reuse, "header.h": "pkg new header contents", "other.txt": "text"}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: Exports sources! *.h" in client.out assert "pkg/0.1@user/testing: Copied 1 '.txt' file: other.txt" in client.out assert "pkg/0.1@user/testing: Copied 1 '.h' file: header.h" in client.out assert "pkg/0.1@user/testing: License! MyLicense" in client.out assert "pkg/0.1@user/testing: Author! author@company.com" in client.out assert "pkg/0.1@user/testing: HEADER CONTENT!: pkg new header contents" in client.out ref = RecipeReference.loads("pkg/0.1@user/testing") assert os.path.exists(os.path.join(client.get_latest_ref_layout(ref).export(), "other.txt")) def test_reuse_system_requirements(self): # https://github.com/conan-io/conan/issues/7718 client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): def system_requirements(self): self.output.info("My system_requirements %s being called!" % self.name) """) client.save({"conanfile.py": conanfile}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: My system_requirements pkg being called!" in client.out def test_reuse_requirements(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): def requirements(self): self.output.info("My requirements %s being called!" % self.name) self.requires("foo/1.0") """) client.save({"conanfile.py": conanfile}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") client.save({"conanfile.py": GenConanfile("foo", "1.0")}) client.run("create .") reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: My requirements pkg being called!" in client.out client.assert_listed_require({"foo/1.0#f5288356d9cc303f25cb05bccbad8fbb": "Cache"}) def test_overwrite_class_members(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): license = "MyLicense" author = "author@company.com" settings = "os", # tuple! """) client.save({"conanfile.py": conanfile}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): license = "MIT" author = "frodo" settings = "arch", # tuple! python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" def init(self): base = self.python_requires["base"].module.MyConanfileBase self.settings = base.settings + self.settings self.license = base.license def build(self): self.output.info("License! %s" % self.license) self.output.info("Author! %s" % self.author) self.output.info("os: %s arch: %s" % (self.settings.get_safe("os"), self.settings.arch)) """) client.save({"conanfile.py": reuse}) client.run("create . --name=pkg --version=0.1 -s os=Windows -s arch=armv7") assert "pkg/0.1: License! MyLicense" in client.out assert "pkg/0.1: Author! frodo" in client.out assert "pkg/0.1: os: Windows arch: armv7" in client.out def test_basic_option_inherit(self): client = TestClient() base = textwrap.dedent(""" from conan import ConanFile class MyBase: settings = "os", "build_type", "arch" options = {"base_option": [1, 2, 3]} default_options = {"base_option": 2} class BaseConanFile(ConanFile): pass """) client.save({"conanfile.py": base}) client.run("export . --name=base --version=1.0") derived = textwrap.dedent(""" from conan import ConanFile class DerivedConan(ConanFile): settings = "os", "build_type", "arch" python_requires = "base/1.0" python_requires_extend = 'base.MyBase' def generate(self): self.output.info(f"MYOPTION: {self.options.base_option}!!!!") """) client.save({"conanfile.py": derived}) client.run("install .") assert "conanfile.py: MYOPTION: 2!!!!" in client.out def test_failure_init_method(self): client = TestClient() base = textwrap.dedent(""" from conan import ConanFile class MyBase(object): settings = "os", "build_type", "arch" options = {"base_option": [True, False]} default_options = {"base_option": False} class BaseConanFile(ConanFile): pass """) client.save({"conanfile.py": base}) client.run("export . --name=base --version=1.0") derived = textwrap.dedent(""" from conan import ConanFile class DerivedConan(ConanFile): settings = "os", "build_type", "arch" python_requires = "base/1.0" python_requires_extend = 'base.MyBase' options = {"derived_option": [True, False]} default_options = {"derived_option": False} def init(self): base = self.python_requires['base'].module.MyBase self.options.update(base.options, base.default_options) """) client.save({"conanfile.py": derived}) client.run("create . --name=pkg --version=0.1 -o base_option=True -o derived_option=True") assert "pkg/0.1: Created package" in client.out client.run("create . --name=pkg --version=0.1 -o whatever=True", assert_error=True) assert "Possible options are ['derived_option', 'base_option']" in client.out def test_transitive_imports_conflicts(self): # https://github.com/conan-io/conan/issues/3874 client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile import myhelper class SourceBuild(ConanFile): exports = "*.py" """) helper = textwrap.dedent(""" def myhelp(output): output.info("MyHelperOutput!") """) client.save({"conanfile.py": conanfile, "myhelper.py": helper}) client.run("export . --name=base1 --version=1.0 --user=user --channel=channel") client.save({"myhelper.py": helper.replace("MyHelperOutput!", "MyOtherHelperOutput!")}) client.run("export . --name=base2 --version=1.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "base2/1.0@user/channel", "base1/1.0@user/channel" def build(self): self.python_requires["base1"].module.myhelper.myhelp(self.output) self.python_requires["base2"].module.myhelper.myhelp(self.output) """) # This should work, even if there is a local "myhelper.py" file, which could be # accidentaly imported (and it was, it was a bug) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: MyHelperOutput!" in client.out assert "pkg/0.1@user/testing: MyOtherHelperOutput!" in client.out # Now, the same, but with "clean_first=True", should keep working client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: MyHelperOutput!" in client.out assert "pkg/0.1@user/testing: MyOtherHelperOutput!" in client.out def test_update(self): client = TestClient(light=True, default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile somevar = 42 class MyConanfileBase(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") client.run("upload * --confirm -r default") client2 = TestClient(light=True, servers=client.servers, inputs=["user", "password"]) reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" def configure(self): self.output.info("PYTHON REQUIRE VAR %s" % self.python_requires["base"].module.somevar) """) client2.save({"conanfile.py": reuse}) client2.run("install .") assert "conanfile.py: PYTHON REQUIRE VAR 42" in client2.out client.save({"conanfile.py": conanfile.replace("42", "143")}) time.sleep(1) # guarantee time offset client.run("export . --name=base --version=1.1 --user=user --channel=testing") client.run("upload * --confirm -r default") client2.run("install . --update") assert "conanfile.py: PYTHON REQUIRE VAR 143" in client2.out def test_update_ranges(self): # Same as the above, but using a version range, and no --update # https://github.com/conan-io/conan/issues/4650#issuecomment-497464305 client = TestClient(light=True, default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile somevar = 42 class MyConanfileBase(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=base --version=1.1 --user=user --channel=testing") client.run("upload * --confirm -r default") client2 = TestClient(light=True, servers=client.servers, inputs=["user", "password"]) reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/[>1.0]@user/testing" python_requires_extend = "base.MyConanfileBase" def configure(self): self.output.info("PYTHON REQUIRE VAR %s" % self.python_requires["base"].module.somevar) """) client2.save({"conanfile.py": reuse}) client2.run("install .") assert "conanfile.py: PYTHON REQUIRE VAR 42" in client2.out client.save({"conanfile.py": conanfile.replace("42", "143")}) # Make sure to bump the version! client.run("export . --name=base --version=1.2 --user=user --channel=testing") client.run("upload * --confirm -r default") client2.run("install . --update") assert "conanfile.py: PYTHON REQUIRE VAR 143" in client2.out def test_duplicate_pyreq(self): t = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class PyReq(ConanFile): pass """) t.save({"conanfile.py": conanfile}) t.run("export . --name=pyreq --version=1.0 --user=user --channel=channel") t.run("export . --name=pyreq --version=2.0 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class Lib(ConanFile): python_requires = "pyreq/1.0@user/channel", "pyreq/2.0@user/channel" """) t.save({"conanfile.py": conanfile}) t.run("create . --name=name --version=version --user=user --channel=channel", assert_error=True) assert "ERROR: Error loading conanfile" in t.out assert "The python_require 'pyreq' already exists" in t.out def test_local_build(self): client = TestClient(light=True) client.save({"conanfile.py": "var=42\n" + str(GenConanfile())}) client.run("export . --name=tool --version=0.1 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "tool/0.1@user/channel" def source(self): self.output.info("Pkg1 source: %s" % self.python_requires["tool"].module.var) def build(self): self.output.info("Pkg1 build: %s" % self.python_requires["tool"].module.var) def package(self): self.output.info("Pkg1 package: %s" % self.python_requires["tool"].module.var) """) client.save({"conanfile.py": conanfile}) client.run("source .") assert "conanfile.py: Pkg1 source: 42" in client.out client.run("install .") client.run("build .") assert "conanfile.py: Pkg1 build: 42" in client.out client.run("export-pkg . --name=pkg1 --version=0.1 --user=user --channel=testing") def test_reuse_name_version(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load import os class Source(object): def set_name(self): self.name = load(self, "name.txt") def set_version(self): self.version = load(self, "version.txt") class MyConanfileBase(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=tool --version=0.1 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "tool/0.1@user/channel" python_requires_extend = "tool.Source" def source(self): self.output.info("Pkg1 source: %s:%s" % (self.name, self.version)) def build(self): self.output.info("Pkg1 build: %s:%s" % (self.name, self.version)) def package(self): self.output.info("Pkg1 package: %s:%s" % (self.name, self.version)) """) client.save({"conanfile.py": conanfile, "name.txt": "mypkg", "version.txt": "myversion"}) client.run("export .") assert "mypkg/myversion: Exported" in client.out client.run("create .") assert "mypkg/myversion: Pkg1 source: mypkg:myversion" in client.out assert "mypkg/myversion: Pkg1 build: mypkg:myversion" in client.out assert "mypkg/myversion: Pkg1 package: mypkg:myversion" in client.out def test_reuse_name_version_override(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load import os class Source(object): def set_name(self): self.name = load(self, "name.txt") def set_version(self): self.version = load(self, "version.txt") class MyConanfileBase(ConanFile): pass """) client.save({"conanfile.py": conanfile}) client.run("export . --name=tool --version=0.1 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): python_requires = "tool/0.1@user/channel" python_requires_extend = "tool.Source" def set_version(self): super().set_version() self.version = self.version + "2" def source(self): self.output.info("Pkg1 source: %s:%s" % (self.name, self.version)) def build(self): self.output.info("Pkg1 build: %s:%s" % (self.name, self.version)) def package(self): self.output.info("Pkg1 package: %s:%s" % (self.name, self.version)) """) client.save({"conanfile.py": conanfile, "name.txt": "mypkg", "version.txt": "myversion"}) client.run("export .") assert "mypkg/myversion2: Exported" in client.out client.run("create .") assert "mypkg/myversion2: Pkg1 source: mypkg:myversion2" in client.out assert "mypkg/myversion2: Pkg1 build: mypkg:myversion2" in client.out assert "mypkg/myversion2: Pkg1 package: mypkg:myversion2" in client.out def test_reuse_export_sources(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): exports = "*" """) client.save({"conanfile.py": conanfile, "file.h": "myheader", "folder/other.h": "otherheader"}) client.run("export . --name=tool --version=0.1 --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load import os class MyConanfileBase(ConanFile): python_requires = "tool/0.1@user/channel" def source(self): sources = self.python_requires["tool"].path file_h = os.path.join(sources, "file.h") other_h = os.path.join(sources, "folder/other.h") self.output.info("Source: tool header: %s" % load(self, file_h)) self.output.info("Source: tool other: %s" % load(self, other_h)) def build(self): sources = self.python_requires["tool"].path file_h = os.path.join(sources, "file.h") other_h = os.path.join(sources, "folder/other.h") self.output.info("Build: tool header: %s" % load(self, file_h)) self.output.info("Build: tool other: %s" % load(self, other_h)) def package(self): sources = self.python_requires["tool"].path file_h = os.path.join(sources, "file.h") other_h = os.path.join(sources, "folder/other.h") self.output.info("Package: tool header: %s" % load(self, file_h)) self.output.info("Package: tool other: %s" % load(self, other_h)) """) client.save({"conanfile.py": conanfile, "name.txt": "MyPkg", "version.txt": "MyVersion"}) client.run("export . --name=pkg --version=1.0 --user=user --channel=channel") assert "pkg/1.0@user/channel: Exported" in client.out client.run("create . --name=pkg --version=1.0 --user=user --channel=channel") assert "pkg/1.0@user/channel: Source: tool header: myheader" in client.out assert "pkg/1.0@user/channel: Source: tool other: otherheader" in client.out assert "pkg/1.0@user/channel: Build: tool header: myheader" in client.out assert "pkg/1.0@user/channel: Build: tool other: otherheader" in client.out assert "pkg/1.0@user/channel: Package: tool header: myheader" in client.out assert "pkg/1.0@user/channel: Package: tool other: otherheader" in client.out # The local flow client.run("install .") client.run("source .") assert "conanfile.py: Source: tool header: myheader" in client.out assert "conanfile.py: Source: tool other: otherheader" in client.out client.run("build .") assert "conanfile.py: Build: tool header: myheader" in client.out assert "conanfile.py: Build: tool other: otherheader" in client.out def test_reuse_editable_exports(self): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase(ConanFile): exports = "*" """) client.save({"conanfile.py": conanfile, "file.h": "myheader", "folder/other.h": "otherheader"}) client.run("editable add . --name=tool --version=0.1") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load import os class MyConanfileBase(ConanFile): python_requires = "tool/0.1" def source(self): sources = self.python_requires["tool"].path file_h = os.path.join(sources, "file.h") other_h = os.path.join(sources, "folder/other.h") self.output.info("Source: tool header: %s" % load(self, file_h)) self.output.info("Source: tool other: %s" % load(self, other_h)) def build(self): sources = self.python_requires["tool"].path file_h = os.path.join(sources, "file.h") other_h = os.path.join(sources, "folder/other.h") self.output.info("Build: tool header: %s" % load(self, file_h)) self.output.info("Build: tool other: %s" % load(self, other_h)) """) client2 = TestClient(light=True, cache_folder=client.cache_folder) client2.save({"conanfile.py": conanfile, "name.txt": "MyPkg", "version.txt": "MyVersion"}) # The local flow client2.run("install .") client2.run("source .") assert "conanfile.py: Source: tool header: myheader" in client2.out assert "conanfile.py: Source: tool other: otherheader" in client2.out client2.run("build .") assert "conanfile.py: Build: tool header: myheader" in client2.out assert "conanfile.py: Build: tool other: otherheader" in client2.out def test_build_id(self): client = TestClient(light=True, default_server_user=True) self._define_base(client) reuse = textwrap.dedent(""" from conan import ConanFile class PkgTest(ConanFile): python_requires = "base/1.1@user/testing" python_requires_extend = "base.MyConanfileBase" def build_id(self): pass """) client.save({"conanfile.py": reuse}, clean_first=True) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: My cool source!" in client.out assert "pkg/0.1@user/testing: My cool build!" in client.out assert "pkg/0.1@user/testing: My cool package!" in client.out assert "pkg/0.1@user/testing: My cool package_info!" in client.out def test_options_errors(self): c = TestClient(light=True) base = textwrap.dedent(""" from conan import ConanFile class BaseConan: options = {"base": [True, False]} default_options = {"base": True} class PyReq(ConanFile): name = "base" version = "1.0.0" package_type = "python-require" """) derived = textwrap.dedent(""" import conan class DerivedConan(conan.ConanFile): name = "derived" python_requires = "base/1.0.0" python_requires_extend = "base.BaseConan" options = {"derived": [True, False]} default_options = {"derived": False} def init(self): base = self.python_requires["base"].module.BaseConan self.options.update(base.options, base.default_options) def generate(self): self.output.info(f"OptionBASE: {self.options.base}") self.output.info(f"OptionDERIVED: {self.options.derived}") """) c.save({"base/conanfile.py": base, "derived/conanfile.py": derived}) c.run("create base") c.run("install derived") assert "OptionBASE: True" in c.out assert "OptionDERIVED: False" in c.out def test_options_default_update(self): c = TestClient(light=True) base = textwrap.dedent(""" from conan import ConanFile class BaseConan: options = {"base": [True, False]} default_options = {"base": True} class PyReq(ConanFile): name = "base" version = "1.0.0" package_type = "python-require" """) derived = textwrap.dedent(""" import conan class DerivedConan(conan.ConanFile): name = "derived" python_requires = "base/1.0.0" python_requires_extend = "base.BaseConan" options = {"derived": [True, False]} default_options = {"derived": False} def init(self): base = self.python_requires["base"].module.BaseConan self.options.update(base.options, base.default_options) self.options.base = False def generate(self): self.output.info(f"OptionBASE: {self.options.base}") self.output.info(f"OptionDERIVED: {self.options.derived}") """) c.save({"base/conanfile.py": base, "derived/conanfile.py": derived}) c.run("create base") c.run("install derived") assert "OptionBASE: False" in c.out assert "OptionDERIVED: False" in c.out c.run("install derived -o=&:base=True") assert "OptionBASE: True" in c.out assert "OptionDERIVED: False" in c.out def test_transitive_python_requires(): # https://github.com/conan-io/conan/issues/8546 client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile myvar = 123 def myfunct(): return 234 class SharedFunction(ConanFile): name = "shared-function" version = "1.0" """) client.save({"conanfile.py": conanfile}) client.run("export . --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class BaseClass(ConanFile): name = "base-class" version = "1.0" python_requires = "shared-function/1.0@user/channel" def build(self): pyreqs = self.python_requires v = pyreqs["shared-function"].module.myvar # v will be 123 f = pyreqs["shared-function"].module.myfunct() # f will be 234 self.output.info("%s, %s" % (v, f)) """) client.save({"conanfile.py": conanfile}) client.run("export . --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): name = "consumer" version = "1.0" python_requires = "base-class/1.0@user/channel" python_requires_extend = "base-class.BaseClass" """) client.save({"conanfile.py": conanfile}) client.run("create . --user=user --channel=channel") assert "consumer/1.0@user/channel: Calling build()\nconsumer/1.0@user/channel: 123, 234" in \ client.out def test_transitive_diamond_python_requires(): client = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile myvar = 123 def myfunct(): return 234 class SharedFunction(ConanFile): name = "shared-function" version = "1.0" """) client.save({"conanfile.py": conanfile}) client.run("export . --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile myvar = 222 def myfunct(): return 2222 class SharedFunction2(ConanFile): name = "shared-function2" version = "1.0" """) client.save({"conanfile.py": conanfile}) client.run("export . --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class BaseClass(ConanFile): name = "base-class" version = "1.0" python_requires = "shared-function/1.0@user/channel", "shared-function2/1.0@user/channel" def build(self): pyreqs = self.python_requires v = pyreqs["shared-function"].module.myvar # v will be 123 f = pyreqs["shared-function2"].module.myfunct() # f will be 2222 self.output.info("%s, %s" % (v, f)) """) client.save({"conanfile.py": conanfile}) client.run("export . --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class BaseClass2(ConanFile): name = "base-class2" version = "1.0" python_requires = "shared-function/1.0@user/channel", "shared-function2/1.0@user/channel" def package(self): pyreqs = self.python_requires v = pyreqs["shared-function2"].module.myvar # v will be 222 f = pyreqs["shared-function"].module.myfunct() # f will be 234 self.output.info("%s, %s" % (v, f)) """) client.save({"conanfile.py": conanfile}) client.run("export . --user=user --channel=channel") conanfile = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): name = "consumer" version = "1.0" python_requires = "base-class/1.0@user/channel", "base-class2/1.0@user/channel" python_requires_extend = "base-class.BaseClass", "base-class2.BaseClass2" """) client.save({"conanfile.py": conanfile}) client.run("create . --user=user --channel=channel") assert "consumer/1.0@user/channel: Calling build()\nconsumer/1.0@user/channel: 123, 2222" in \ client.out assert "consumer/1.0@user/channel: Calling package()\nconsumer/1.0@user/channel: 222, 234" in \ client.out class TestConflictPyRequires: # https://github.com/conan-io/conan/issues/15016 def test_diamond_conflict_fixed(self): c = TestClient(light=True) c.save({"tool/conanfile.py": GenConanfile("tool"), "sub1/conanfile.py": GenConanfile("sub1", "1.0").with_python_requires("tool/1.0"), "sub2/conanfile.py": GenConanfile("sub2", "1.0").with_python_requires("tool/1.1"), "app/conanfile.py": GenConanfile().with_python_requires("sub1/1.0", "sub2/1.0")}) c.run("export tool --version=1.0") c.run("export tool --version=1.1") c.run("export sub1") c.run("export sub2") c.run("install app", assert_error=True) assert "Conflict in py_requires tool/1.0 - tool/1.1" in c.out def test_diamond_conflict_ranges(self): c = TestClient(light=True) c.save({"tool/conanfile.py": GenConanfile("tool"), "sub1/conanfile.py": GenConanfile("sub1", "1.0").with_python_requires("tool/[*]"), "sub2/conanfile.py": GenConanfile("sub2", "1.0").with_python_requires("tool/1.0"), "app/conanfile.py": GenConanfile().with_python_requires("sub1/1.0", "sub2/1.0")}) c.run("export tool --version=1.0") c.run("export tool --version=1.1") c.run("export sub1") c.run("export sub2") c.run("install app", assert_error=True) assert "Conflict in py_requires tool/1.1 - tool/1.0" in c.out def test_multiple_reuse(): """ test how to enable the multiple code reuse for custom user generators # https://github.com/conan-io/conan/issues/11589 """ c = TestClient(light=True) common = textwrap.dedent(""" from conan import ConanFile def mycommon(): return 42 class Common(ConanFile): name = "common" version = "0.1" """) tool = textwrap.dedent(""" from conan import ConanFile class MyGenerator: common = None def __init__(self, conanfile): self.conanfile = conanfile def generate(self): self.conanfile.output.info("VALUE TOOL: {}!!!".format(MyGenerator.common.mycommon())) class Tool(ConanFile): name = "tool" version = "0.1" python_requires = "common/0.1" def init(self): MyGenerator.common = self.python_requires["common"].module """) consumer = textwrap.dedent(""" from conan import ConanFile class Consumer(ConanFile): python_requires = "tool/0.1", "common/0.1" def generate(self): mycommon = self.python_requires["common"].module.mycommon self.output.info("VALUE COMMON: {}!!!".format(mycommon())) mygenerator = self.python_requires["tool"].module.MyGenerator(self) mygenerator.generate() """) c.save({"common/conanfile.py": common, "tool/conanfile.py": tool, "consumer/conanfile.py": consumer}) c.run("export common") c.run("export tool") c.run("install consumer") assert "VALUE COMMON: 42!!!" in c.out assert "VALUE TOOL: 42!!!" in c.out class TestTestPackagePythonRequire: def test_test_package_python_requires(self): """ test how to test_package a python_require """ c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("mytool", "0.1")}) c.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile def mycommon(): return 42 class Common(ConanFile): name = "common" version = "0.1" package_type = "python-require" """) test = textwrap.dedent(""" from conan import ConanFile class Tool(ConanFile): python_requires = "tested_reference_str" def test(self): self.output.info("{}!!!".format(self.python_requires["common"].module.mycommon())) """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test}) c.run("create .") assert "WARN: deprecated: test_package/conanfile.py" not in c.out assert "common/0.1 (test package): 42!!!" in c.out c.run("test test_package common/0.1") assert "WARN: deprecated: test_package/conanfile.py" not in c.out assert "common/0.1 (test package): 42!!!" in c.out # https://github.com/conan-io/conan/issues/18224 c.save({"myprofile": "[tool_requires]\nmytool/0.1"}) c.run("create . -pr=myprofile") assert "common/0.1 (test package): 42!!!" in c.out def test_test_package_python_requires_configs(self): """ test how to test_package a python_require with various configurations """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile def mycommon(build_type): return str(build_type).upper() + "OK" class Common(ConanFile): name = "common" version = "0.1" package_type = "python-require" """) test = textwrap.dedent(""" from conan import ConanFile class Tool(ConanFile): settings = "build_type" def test(self): result = self.python_requires["common"].module.mycommon(self.settings.build_type) self.output.info("{}!!!".format(result)) """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test}) c.run("create . ") assert "common/0.1 (test package): RELEASEOK!!!" in c.out assert "WARN: deprecated: test_package/conanfile.py should declare 'python_requires" in c.out c.run("create . -s build_type=Debug") assert "common/0.1 (test package): DEBUGOK!!!" in c.out class TestResolveRemote: def test_resolve_remote_export(self): """ a "conan export" command should work even when a python_requires is in the server """ c = TestClient(light=True, default_server_user=True) c.save({"common/conanfile.py": GenConanfile("tool", "0.1"), "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("tool/0.1")}) c.run("export common") c.run("upload * -r=default -c") c.run("remove * -c") c.run("create pkg") assert "tool/0.1: Downloaded recipe" in c.out c.run("remove * -c") c.run("export pkg") assert "tool/0.1: Downloaded recipe" in c.out c.run("remove * -c") c.run("export pkg -r=default") assert "tool/0.1: Downloaded recipe" in c.out c.run("remove * -c") c.run("create pkg -r=default") assert "tool/0.1: Downloaded recipe" in c.out def test_missing_python_require_error(self): """ make sure the error is clear enough for users UX """ c = TestClient(light=True) c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("tool/0.1")}) c.run("create pkg", assert_error=True) assert "Cannot resolve python_requires 'tool/0.1'" in c.out class TestTransitiveExtend: # https://github.com/conan-io/conan/issues/10511 # https://github.com/conan-io/conan/issues/10565 def test_transitive_extend(self): client = TestClient(light=True) company = textwrap.dedent(""" from conan import ConanFile class CompanyConanFile(ConanFile): name = "company" version = "1.0" def msg1(self): return "company" def msg2(self): return "company" """) project = textwrap.dedent(""" from conan import ConanFile class ProjectBaseConanFile(ConanFile): name = "project" version = "1.0" python_requires = "company/1.0" python_requires_extend = "company.CompanyConanFile" def msg1(self): return "project" """) consumer = textwrap.dedent(""" from conan import ConanFile class Base(ConanFile): name = "consumer" version = "1.0" python_requires = "project/1.0" python_requires_extend = "project.ProjectBaseConanFile" def generate(self): self.output.info("Msg1:{}!!!".format(self.msg1())) self.output.info("Msg2:{}!!!".format(self.msg2())) """) client.save({"company/conanfile.py": company, "project/conanfile.py": project, "consumer/conanfile.py": consumer}) client.run("export company") client.run("export project") client.run("install consumer") assert "conanfile.py (consumer/1.0): Msg1:project!!!" in client.out assert "conanfile.py (consumer/1.0): Msg2:company!!!" in client.out def test_transitive_extend2(self): client = TestClient(light=True) company = textwrap.dedent(""" from conan import ConanFile class CompanyConanFile(ConanFile): name = "company" version = "1.0" class CompanyBase: def msg1(self): return "company" def msg2(self): return "company" """) project = textwrap.dedent(""" from conan import ConanFile class ProjectBase: def msg1(self): return "project" class ProjectBaseConanFile(ConanFile): name = "project" version = "1.0" python_requires = "company/1.0" def init(self): pkg_name, base_class_name = "company", "CompanyBase" base_class = getattr(self.python_requires[pkg_name].module, base_class_name) global ProjectBase ProjectBase = type('ProjectBase', (ProjectBase, base_class, object), {}) """) consumer = textwrap.dedent(""" from conan import ConanFile class Base(ConanFile): name = "consumer" version = "1.0" python_requires = "project/1.0" python_requires_extend = "project.ProjectBase" def generate(self): self.output.info("Msg1:{}!!!".format(self.msg1())) self.output.info("Msg2:{}!!!".format(self.msg2())) """) client.save({"company/conanfile.py": company, "project/conanfile.py": project, "consumer/conanfile.py": consumer}) client.run("export company") client.run("export project") client.run("install consumer") assert "conanfile.py (consumer/1.0): Msg1:project!!!" in client.out assert "conanfile.py (consumer/1.0): Msg2:company!!!" in client.out def test_multi_top_missing_from_remote(): """ https://github.com/conan-io/conan/issues/13656 """ tc = TestClient(light=True, default_server_user=True) tc.save({"conanfile.py": GenConanfile("base", "1.1")}) tc.run("create . --user=user --channel=testing") tc.save({"conanfile.py": GenConanfile("dep", "1.0") .with_python_requires("base/1.1@user/testing")}) tc.run("create . --user=user --channel=testing -r default") tc.run("upload * -c -r default") tc.run("remove * -c") tc.save({"conanfile.py": GenConanfile("pkg", "1.0") .with_python_requires("dep/1.0@user/testing")}) # This used to crash, with errors about not defining remotes tc.run("create . --name=pkg --version=1.0 -r default") # Ensure we found them in the remote assert "dep/1.0@user/testing: Not found in local cache, looking in remotes..." in tc.out assert "dep/1.0@user/testing: Downloaded recipe revision" in tc.out assert "base/1.1@user/testing: Not found in local cache, looking in remotes..." in tc.out assert "base/1.1@user/testing: Downloaded recipe revision" in tc.out def test_transitive_range_not_found_in_cache(): """ https://github.com/conan-io/conan/issues/13761 """ c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pr", "1.0")}) c.run("create .") c.save({"conanfile.py": GenConanfile("dep", "1.0").with_python_requires("pr/[>0]")}) c.run("create .") c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_requires("dep/1.0")}) c.run("create . ") c.assert_listed_require({"pr/1.0": "Cache"}, python=True) assert "pr/[>0]: pr/1.0" in c.out def test_export_pkg(): c = TestClient(light=True) c.save({"conanfile.py": GenConanfile("pytool", "0.1").with_package_type("python-require")}) c.run("export-pkg .", assert_error=True) assert "export-pkg can only be used for binaries, not for 'python-require'" in c.out # Make sure nothing is exported c.run("list *") assert "WARN: There are no matching recipe references" in c.out assert "pytool/0.1" not in c.out def test_py_requires_override_method(): tc = TestClient(light=True) pyreq = textwrap.dedent(""" from conan import ConanFile class MyConanfileBase: def get_toolchain(self): return "mybasetoolchain" def generate(self): self.output.info("MyConanfileBase generate with value %s" % self.get_toolchain()) class MyConanfile(ConanFile): name = "myconanfile" version = "1.0" package_type = "python-require" """) conanfile = textwrap.dedent(""" from conan import ConanFile class MyConanfile(ConanFile): name = "app" version = "1.0" python_requires = "myconanfile/1.0" python_requires_extend = "myconanfile.MyConanfileBase" def get_toolchain(self): return "mycustomtoolchain" """) tc.save({"myconanfile/conanfile.py": pyreq, "conanfile.py": conanfile}) tc.run("create myconanfile") tc.run("create .") assert "MyConanfileBase generate with value mycustomtoolchain" in tc.out ================================================ FILE: test/integration/remote/__init__.py ================================================ ================================================ FILE: test/integration/remote/auth_bearer_test.py ================================================ from bottle import request from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer class AuthorizationHeaderSpy: """ Generic plugin to handle Authorization header. Must be extended and implement some abstract methods in subclasses""" name = 'authorizationheaderspy' api = 2 def __init__(self): self.auths = [] def apply(self, callback, context): # noqa auth = request.headers.get("Authorization") name = callback.__name__ self.auths.append((name, auth)) return callback class TestAuthorizeBearer: def test_basic(self): auth = AuthorizationHeaderSpy() server = TestServer(plugins=[auth]) client = TestClient(servers={"default": server}, inputs=["admin", "password"]) client.save({"conanfile.py": GenConanfile("hello", "0.1")}) client.run("export . --user=lasote --channel=stable") errors = client.run("upload hello/0.1@lasote/stable -r default --only-recipe") assert not errors expected_calls = [('get_recipe_revisions_references', None), ('check_credentials', None), ('authenticate', 'Basic'), ('upload_recipe_file', 'Bearer')] assert len(expected_calls) == len(auth.auths) for i, (method, auth_type) in enumerate(expected_calls): real_call = auth.auths[i] assert method == real_call[0] if auth_type: assert auth_type in real_call[1] ================================================ FILE: test/integration/remote/auth_test.py ================================================ import copy import os import textwrap import pytest from requests.models import Response from conan.api.model import RecipeReference from conan.internal.api.remotes.localdb import LocalDB from conan.internal.util.files import save from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.env import environment_update from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.test.utils.tools import TestRequester from conan.test.utils.tools import TestServer class TestAuthorize: @pytest.fixture(autouse=True) def setup(self): self.servers = {} self.ref = RecipeReference.loads("openssl/2.0.1@lasote/testing") # Create a default remote. R/W is not authorized for ref, # just for pepe, nacho and owner self.test_server = TestServer([(str(self.ref), "pepe,nacho@gmail.com")], # read permissions [(str(self.ref), "pepe,nacho@gmail.com")], # write permissions users={"lasote": "mypass", "pepe": "pepepass", "nacho@gmail.com": "nachopass"}) # exported creds self.servers["default"] = self.test_server def test_retries(self): """Bad login 2 times""" self.conan = TestClient(servers=self.servers, inputs=["bad", "1", "bad2", "2", "nacho@gmail.com", "nachopass"]) self.conan.save({"conanfile.py": GenConanfile("openssl", "2.0.1")}) self.conan.run("export . --user=lasote --channel=testing") errors = self.conan.run("upload %s -r default --only-recipe -v=quiet" % str(self.ref)) # Check that return was ok assert not errors # Check that upload was granted rev = self.test_server.server_store.get_last_revision(self.ref).revision ref = copy.copy(self.ref) ref.revision = rev assert os.path.exists(self.test_server.server_store.export(ref)) assert "Please enter a password for user 'bad'" in self.conan.out assert "Please enter a password for user 'bad2'" in self.conan.out assert "Please enter a password for user 'nacho@gmail.com'" in self.conan.out def test_auth_with_env(self): def _upload_with_credentials(credentials, assert_error=False): cli = TestClient(servers=self.servers) cli.save({"conanfile.py": GenConanfile("openssl", "2.0.1")}) cli.run("export . --user=lasote --channel=testing") with environment_update(credentials): cli.run("upload %s -r default --only-recipe" % str(self.ref), assert_error=assert_error) return cli # Try with remote name in credentials client = _upload_with_credentials({"CONAN_PASSWORD_DEFAULT": "pepepass", "CONAN_LOGIN_USERNAME_DEFAULT": "pepe"}) assert "Got username 'pepe' from environment" in client.out assert "Got password '******' from environment" in client.out # Try with generic password and login client = _upload_with_credentials({"CONAN_PASSWORD": "pepepass", "CONAN_LOGIN_USERNAME_DEFAULT": "pepe"}) assert "Got username 'pepe' from environment" in client.out assert "Got password '******' from environment" in client.out # Try with generic password and generic login client = _upload_with_credentials({"CONAN_PASSWORD": "pepepass", "CONAN_LOGIN_USERNAME": "pepe"}) assert "Got username 'pepe' from environment" in client.out assert "Got password '******' from environment" in client.out # Bad pass raise client = _upload_with_credentials({"CONAN_PASSWORD": "bad", "CONAN_LOGIN_USERNAME": "pepe"}, assert_error=True) assert "Authentication error in remote 'default'" in client.out def test_max_retries(self): """Bad login 3 times""" client = TestClient(servers=self.servers, inputs=["baduser", "badpass", "baduser", "badpass2", "baduser3", "badpass3"]) client.save({"conanfile.py": GenConanfile("openssl", "2.0.1")}) client.run("export . --user=lasote --channel=testing") errors = client.run("upload %s -r default --only-recipe" % str(self.ref), assert_error=True) # Check that return was not ok assert errors # Check that upload was not granted rev = self.servers["default"].server_store.get_last_revision(self.ref) assert rev is None # Check that login failed all times assert "Too many failed login attempts, bye!" in client.out def test_no_client_username_checks(self): """Checks whether client username checks are disabled.""" # Try with a load of names that contain special characters client = TestClient(servers=self.servers, inputs=["some_random.special!characters", "badpass", "nacho@gmail.com", "nachopass"]) client.save({"conanfile.py": GenConanfile("openssl", "2.0.1")}) client.run("export . --user=lasote --channel=testing") client.run("upload %s -r default --only-recipe" % str(self.ref)) # Check that upload was granted rev = self.test_server.server_store.get_last_revision(self.ref).revision ref = copy.copy(self.ref) ref.revision = rev assert os.path.exists(self.test_server.server_store.export(ref)) assert ("Please enter a password for user 'some_random.special!characters' " "on remote 'default'") in client.out def test_authorize_disabled_remote(self): tc = TestClient(servers=self.servers) # Sanity check, this should not fail tc.run("remote login default pepe -p pepepass") tc.run("remote logout default") # This used to fail when the authentication was not possible for disabled remotes tc.run("remote disable default") tc.run("remote login default pepe -p pepepass") assert ("Changed user of remote 'default' " "from 'None' (anonymous) to 'pepe' (authenticated)") in tc.out class TestAuthenticationTest: def test_unauthorized_during_capabilities(self): class RequesterMock(TestRequester): @staticmethod def get(url, **kwargs): resp_basic_auth = Response() resp_basic_auth.status_code = 200 if "authenticate" in url: assert kwargs["auth"].password == "PASSWORD!" resp_basic_auth._content = b"TOKEN" resp_basic_auth.headers = {"Content-Type": "text/plain"} elif "ping" in url: resp_basic_auth.headers = {"Content-Type": "application/json", "X-Conan-Server-Capabilities": "revisions"} bearer = getattr(kwargs["auth"], "bearer", None) password = getattr(kwargs["auth"], "password", None) assert bearer == "Bearer TOKEN" or password else: assert "search" in url assert kwargs["auth"].bearer == "Bearer TOKEN" resp_basic_auth._content = b'{"results": []}' resp_basic_auth.headers = {"Content-Type": "application/json"} return resp_basic_auth client = TestClient(requester_class=RequesterMock, default_server_user=True) client.run("remote login default user -p PASSWORD!") assert "Changed user of remote 'default' from 'None' (anonymous) to 'user'" in client.out client.run("search pkg -r=default") assert "ERROR: Recipe 'pkg' not found" in client.out @pytest.mark.xfail(reason="This test is randomly failing") def test_token_expired(): server_folder = temp_folder() server_conf = textwrap.dedent(""" [server] jwt_expire_minutes: 0.02 authorize_timeout: 0 disk_authorize_timeout: 0 disk_storage_path: ./data updown_secret: 12345 jwt_secret: mysecretmysecretmysecretmysecret port: 12345 [read_permissions] */*@*/*: * [write_permissions] */*@*/*: admin """) save(os.path.join(server_folder, ".conan_server", "server.conf"), server_conf) server = TestServer(base_path=server_folder, users={"admin": "password", "other": "pass"}) c = TestClient(servers={"default": server}, inputs=["admin", "password", "other", "pass"]) c.save({"conanfile.py": GenConanfile()}) c.run("create . --name=pkg --version=0.1 --user=user --channel=stable") c.run("upload * -r=default -c") localdb = LocalDB(c.cache_folder) user, token, _ = localdb.get_login(server.fake_url) assert user == "admin" assert token is not None import time time.sleep(3) c.users = {} c.run("remove * -c") c.run("install --requires=pkg/0.1@user/stable") assert "Remote 'default' needs authentication, obtaining credentials" in c.out user, token, _ = localdb.get_login(server.fake_url) assert user == "other" assert token is not None def test_auth_username_space(): server = TestServer(users={"super admin": "password"}) c = TestClient(servers={"default": server}, inputs=["super admin", "password"]) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("export .") c.run("upload * -r=default -c") # it doesn't crash, it accepts user with space ================================================ FILE: test/integration/remote/broken_download_test.py ================================================ import json import os import pytest from requests import Response from requests.exceptions import ConnectionError from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestRequester, TestServer from conan.internal.util.files import save class TestBrokenDownload: @pytest.fixture() def setup(self): server = TestServer() client = TestClient(servers={"default": server}, inputs=["admin", "password"]) client.save({"conanfile.py": GenConanfile("hello", "0.1")}) client.run("create .") pref = client.created_package_reference("hello/0.1") client.run("upload * -r default -c") client.run("remove * -c") return client, pref def test_corrupt_export_tgz(self, setup): client, pref = setup server = client.servers["default"] path = server.test_server.server_store.export(pref.ref) tgz = os.path.join(path, "conan_export.tgz") save(tgz, "contents") # dummy content to break it, so the download decompress will fail client.run("install --requires=hello/0.1", assert_error=True) assert "Error while extracting downloaded file" in client.out assert not os.path.exists(client.get_latest_ref_layout(pref.ref).export()) def test_remove_conaninfo(self, setup): """ if the conaninfo is removed, it is considered at least a broken package by the client """ client, pref = setup server = client.servers["default"] path = server.test_server.server_store.package(pref) conaninfo = os.path.join(path, "conaninfo.txt") os.unlink(conaninfo) client.run("install --requires=hello/0.1", assert_error=True) assert "ERROR: Corrupted hello/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709" \ " in 'default' remote: no conaninfo.txt" in client.out def test_remove_conanfile(self, setup): """ if the conanfile is removed, it is considered at least a broken package by the client """ client, pref = setup server = client.servers["default"] path = server.test_server.server_store.export(pref.ref) conanfile = os.path.join(path, "conanfile.py") os.unlink(conanfile) client.run("install --requires=hello/0.1", assert_error=True) assert "Corrupted hello/0.1 in 'default' remote: no conanfile.py" in client.out def test_remove_pkg_conanmanifest(self, setup): """ if the manifest is missing the server side can say there is no package """ client, pref = setup server = client.servers["default"] path = server.test_server.server_store.package(pref) manifest = os.path.join(path, "conanmanifest.txt") os.unlink(manifest) client.run("install --requires=hello/0.1", assert_error=True) assert "ERROR: Binary package not found: 'hello/0.1" in client.out def test_remove_recipe_conanmanifest(self, setup): """ if the manifest is missing the server side can say there is no recipe """ client, pref = setup server = client.servers["default"] path = server.test_server.server_store.export(pref.ref) manifest = os.path.join(path, "conanmanifest.txt") os.unlink(manifest) client.run("install --requires=hello/0.1", assert_error=True) assert "Recipe not found: 'hello/0.1" in client.out def test_client_retries(): server = TestServer() servers = {"default": server} client = TestClient(servers=servers, inputs=["admin", "password"]) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0 --user=lasote --channel=stable") client.run("upload lib/1.0@lasote/stable -c -r default") class DownloadFilesBrokenRequester(TestRequester): def __init__(self, times_to_fail=1, *args, **kwargs): self.times_to_fail = times_to_fail super(DownloadFilesBrokenRequester, self).__init__(*args, **kwargs) def get(self, url, **kwargs): # conaninfo is skipped sometimes from the output, use manifest if "conanmanifest.txt" in url and self.times_to_fail > 0: self.times_to_fail = self.times_to_fail - 1 raise ConnectionError("Fake connection error exception") else: return super(DownloadFilesBrokenRequester, self).get(url, **kwargs) def DownloadFilesBrokenRequesterTimesOne(*args, **kwargs): # noqa return DownloadFilesBrokenRequester(1, *args, **kwargs) client = TestClient(servers=servers, inputs=["admin", "password"], requester_class=DownloadFilesBrokenRequesterTimesOne) client.run("install --requires=lib/1.0@lasote/stable") assert "WARN: network: Error downloading file" in client.out assert 'Fake connection error exception' in client.out assert 1 == str(client.out).count("Waiting 0 seconds to retry...") client = TestClient(servers=servers, inputs=["admin", "password"], requester_class=DownloadFilesBrokenRequesterTimesOne) client.save_home({"global.conf": "core.download:retry_wait=1"}) client.run("install --requires=lib/1.0@lasote/stable") assert 1 == str(client.out).count("Waiting 1 seconds to retry...") def DownloadFilesBrokenRequesterTimesTen(*args, **kwargs): # noqa return DownloadFilesBrokenRequester(10, *args, **kwargs) client = TestClient(servers=servers, inputs=["admin", "password"], requester_class=DownloadFilesBrokenRequesterTimesTen) client.save_home({"global.conf": "core.download:retry_wait=0\n" "core.download:retry=11"}) client.run("install --requires=lib/1.0@lasote/stable") assert 10 == str(client.out).count("Waiting 0 seconds to retry...") def test_forbidden_blocked_conanmanifest(): """ this is what happens when a server blocks downloading a specific file """ server = TestServer() servers = {"default": server} client = TestClient(servers=servers, inputs=["admin", "password"]) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0") client.run("upload lib/1.0* -c -r default") class DownloadForbidden(TestRequester): def get(self, url, **kwargs): if "conanmanifest.txt" in url: r = Response() r._content = "Forbidden because of security!!!" r.status_code = 403 return r else: return super(DownloadForbidden, self).get(url, **kwargs) client = TestClient(servers=servers, inputs=["admin", "password"], requester_class=DownloadForbidden) client.run("download lib/1.0 -r=default", assert_error=True) assert "Forbidden because of security!!!" in client.out client.run("list *") assert "lib/1.0" not in client.out client.run("install --requires=lib/1.0", assert_error=True) assert "Forbidden because of security!!!" in client.out client.run("list *") assert "lib/1.0" not in client.out def test_forbidden_blocked_package_conanmanifest(): """ this is what happens when a server blocks downloading a specific file """ server = TestServer() servers = {"default": server} client = TestClient(servers=servers, inputs=["admin", "password"]) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0") client.run("upload lib/1.0* -c -r default") class DownloadForbidden(TestRequester): def get(self, url, **kwargs): if "packages/" in url and "conanmanifest.txt" in url: r = Response() r._content = "Forbidden because of security!!!" r.status_code = 403 return r else: return super(DownloadForbidden, self).get(url, **kwargs) client = TestClient(servers=servers, inputs=["admin", "password"], requester_class=DownloadForbidden) client.run("download lib/1.0 -r=default", assert_error=True) def check_cache(): assert "Forbidden because of security!!!" in client.out client.run("list *:* --format=json") listjson = json.loads(client.stdout) revisions = listjson["Local Cache"]["lib/1.0"]["revisions"] packages = revisions["4d670581ccb765839f2239cc8dff8fbd"]["packages"] assert packages == {} # No binaries" check_cache() client.run("install --requires=lib/1.0", assert_error=True) assert "Forbidden because of security!!!" in client.out check_cache() ================================================ FILE: test/integration/remote/download_retries_test.py ================================================ from conan.internal.paths import CONANFILE from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer, TestRequester class TestDownloadRetries: def test_recipe_download_retry(self): test_server = TestServer() client = TestClient(servers={"default": test_server}, inputs=["admin", "password"]) client.save({CONANFILE: GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=stable") client.run("upload '*' -c -r default") class Response: def __init__(self, ok, status_code): self.ok = ok self.status_code = status_code class BuggyRequester(TestRequester): def get(self, *args, **kwargs): if "files/conanfile.py" not in args[0]: return super(BuggyRequester, self).get(*args, **kwargs) else: return Response(False, 200) # The buggy requester will cause a failure only downloading files, not in regular requests client = TestClient(servers={"default": test_server}, inputs=["admin", "password"], requester_class=BuggyRequester) client.run("install --requires=pkg/0.1@lasote/stable", assert_error=True) assert str(client.out).count("Waiting 0 seconds to retry...") == 2 assert str(client.out).count("Error 200 downloading") == 3 ================================================ FILE: test/integration/remote/download_test.py ================================================ from requests import Response from conan.test.utils.tools import TestClient, TestServer, TestRequester myconan1 = """ from conan import ConanFile import platform class HelloConan(ConanFile): name = "hello" version = "1.2.1" """ class TestDownload: def test_returns_on_failures(self): test_server = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")]) servers = {"default": test_server} class BuggyRequester(TestRequester): def get(self, *args, **kwargs): resp = Response() resp.status_code = 404 resp._content = b'' return resp client2 = TestClient(servers=servers, requester_class=BuggyRequester) client2.run("remote add remotename url") client2.run("install --requires=foo/bar@ -r remotename", assert_error=True) assert "Package 'foo/bar' not resolved: Unable to find 'foo/bar' in remotes" in client2.out class BuggyRequester2(BuggyRequester): def get(self, *args, **kwargs): resp = Response() resp.status_code = 500 resp._content = b'This server is under maintenance' return resp client2 = TestClient(servers=servers, requester_class=BuggyRequester2) client2.run("remote add remotename url") client2.run("install --requires=foo/bar@ -r remotename", assert_error=True) assert "ERROR: Package 'foo/bar' not resolved" in client2.out assert "This server is under maintenance" in client2.out assert "not found" not in client2.out ================================================ FILE: test/integration/remote/multi_remote_checks_test.py ================================================ from collections import OrderedDict from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer class TestRemoteChecks: def test_binary_defines_remote(self): servers = OrderedDict([("server1", TestServer()), ("server2", TestServer()), ("server3", TestServer())]) client = TestClient(servers=servers, inputs=3*["admin", "password"]) conanfile = """from conan import ConanFile class Pkg(ConanFile): pass""" client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("upload pkg* -r=server1 --confirm") client.run("upload pkg* -r=server2 --confirm") # It takes the default remote client.run("remove * -c") # Exported recipe gets binary from default remote client.run("export . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("install --requires=pkg/0.1@lasote/testing") client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server1)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ "%s from remote 'server1'" % NO_SETTINGS_PACKAGE_ID in client.out # Explicit remote also defines the remote client.run("remove * -c") client.run("export . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("install --requires=pkg/0.1@lasote/testing -r=server2") client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server2)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out # Ordered search of binary works client.run("remove * -c") client.run("remove * -c -r=server1") client.run("export . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("install --requires=pkg/0.1@lasote/testing") client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server2)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out # Download recipe and binary from the remote2 by iterating client.run("remove * -c") client.run("remove * -c -r=server1") client.run("install --requires=pkg/0.1@lasote/testing") client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server2)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out def test_binaries_from_different_remotes(self): servers = OrderedDict() servers["server1"] = TestServer() servers["server2"] = TestServer() client = TestClient(servers=servers, inputs=2*["admin", "password"]) conanfile = """from conan import ConanFile class Pkg(ConanFile): options = {"opt": [1, 2, 3]} """ client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing -o pkg/*:opt=1") client.run("upload pkg* -r=server1 --confirm") client.run("remove *:* -c") client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing -o pkg/*:opt=2") package_id2 = client.created_package_id("pkg/0.1@lasote/testing") client.run("upload pkg* -r=server2 --confirm") client.run("remove *:* -c") # recipe is cached, takes binary from server2 client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=2 -r=server2") client.assert_listed_binary({"pkg/0.1@lasote/testing": (package_id2, "Download (server2)")}) assert f"pkg/0.1@lasote/testing: Retrieving package {package_id2} " \ "from remote 'server2'" in client.out # Nothing to update client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=2 -r=server2 -u") client.assert_listed_binary({"pkg/0.1@lasote/testing": (package_id2, "Cache")}) # Build missing client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=3 -r=server2", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkg/0.1@lasote/testing'" in client.out client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=3", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkg/0.1@lasote/testing'" in client.out def test_version_range_multi_remote(): """ captures the behavior of version-range resolution with multiple remotes: - normally the first occurrence stops: if a valid is found in the cache, it returns, if a valid is found in the first server, it is return - Using --update forces to really update to the latest version available, anywhere """ servers = OrderedDict([("server1", TestServer()), ("server2", TestServer()), ("server3", TestServer())]) client = TestClient(servers=servers, inputs=3*["admin", "password"]) client.save({"conanfile.py": GenConanfile()}) for i in (1, 2, 3): client.run(f"create . --name=pkg1 --version=1.{i}") client.run(f"upload pkg1/1.{i} -r=server{i}") client.run(f"create . --name=pkg2 --version=1.{i}") client.run(f"upload pkg2/1.{i} -r=server{4-i}") client.run("remove * -c") client.run("install --requires=pkg1/[*] --requires=pkg2/[*]") # First found assert "pkg1/1.1#4d670581ccb765839f2239cc8dff8fbd - Downloaded (server1)" in client.out assert "pkg2/1.3#4d670581ccb765839f2239cc8dff8fbd - Downloaded (server1)" in client.out assert "pkg1/[*]: pkg1/1.1" in client.out assert "pkg2/[*]: pkg2/1.3" in client.out client.run("remove * -c") client.run("install --requires=pkg1/[*] --requires=pkg2/[*] --update") # with --update, it guarantees the greatest version is found among remotes assert "pkg1/1.3#4d670581ccb765839f2239cc8dff8fbd - Downloaded (server3)" in client.out assert "pkg2/1.3#4d670581ccb765839f2239cc8dff8fbd - Downloaded (server1)" in client.out assert "pkg1/[*]: pkg1/1.3" in client.out assert "pkg2/[*]: pkg2/1.3" in client.out ================================================ FILE: test/integration/remote/multi_remote_test.py ================================================ import pytest from collections import OrderedDict from time import sleep from conan.api.model import RecipeReference from conan.internal.paths import CONANFILE from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer class TestExportsSourcesMissing: def test_exports_sources_missing(self): client = TestClient(default_server_user=True) client.save({"conanfile.py": GenConanfile().with_exports_sources("*"), "source.txt": "somesource"}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") client.run("upload pkg/0.1@user/testing -r default") # Failure because remote is removed servers = OrderedDict(client.servers) servers["new_server"] = TestServer(users={"user": "password"}) client2 = TestClient(servers=servers, inputs=["user", "password"]) client2.run("install --requires=pkg/0.1@user/testing") client2.run("remote remove default") client2.run("upload pkg/0.1@user/testing -r=new_server", assert_error=True) assert "The 'pkg/0.1@user/testing' package has 'exports_sources' but sources " \ "not found in local cache." in client2.out assert "Probably it was installed from a remote that is no longer available." \ in client2.out # Failure because remote removed the package client2 = TestClient(servers=servers, inputs=2*["admin", "password"]) client2.run("install --requires=pkg/0.1@user/testing") client2.run("remove * -r=default -c") client2.run("upload pkg/0.1@user/testing -r=new_server", assert_error=True) assert "pkg/0.1@user/testing Error while compressing: The 'pkg/0.1@user/testing' " \ in client2.out assert "The 'pkg/0.1@user/testing' package has 'exports_sources' but sources " \ in client2.out assert "Probably it was installed from a remote that is no longer available." \ in client2.out class TestMultiRemotes: @pytest.fixture(autouse=True) def setup(self): self.servers = OrderedDict() self.servers["default"] = TestServer() self.servers["local"] = TestServer() @staticmethod def _create(client, number, version, modifier=""): files = {CONANFILE: str(GenConanfile(number, version)) + modifier} client.save(files, clean_first=True) client.run("export . --user=lasote --channel=stable") def test_conan_install_build_flag(self): """ Checks conan install --update works with different remotes """ client_a = TestClient(servers=self.servers, inputs=2*["admin", "password"]) client_b = TestClient(servers=self.servers, inputs=2*["admin", "password"]) # Upload hello0 to local and default from client_a self._create(client_a, "hello0", "0.0") client_a.run("upload hello0/0.0@lasote/stable -r local --only-recipe") client_a.run("upload hello0/0.0@lasote/stable -r default --only-recipe") sleep(1) # For timestamp and updates checks # Download hello0 from local with client_b client_b.run("install --requires=hello0/0.0@lasote/stable -r local --build missing") # Update hello0 with client_a and reupload self._create(client_a, "hello0", "0.0", modifier="\n") client_a.run("upload hello0/0.0@lasote/stable -r local --only-recipe") assert "Uploading recipe 'hello0/0.0@lasote/stable" in client_a.out # Execute info method in client_b, should advise that there is an update client_b.run("graph info --requires=hello0/0.0@lasote/stable --check-updates") assert "recipe: Update available" in client_b.out assert "binary: Cache" in client_b.out # Now try to update the package with install -u client_b.run("install --requires=hello0/0.0@lasote/stable -u --build='*'") assert "hello0/0.0@lasote/stable#64fd8ae21db9eff69c6c681b0e2fc178 - Updated" \ in client_b.out # Upload a new version from client A, but only to the default server (not the ref-listed) # Upload hello0 to local and default from client_a sleep(1) # For timestamp and updates checks self._create(client_a, "hello0", "0.0", modifier="\n\n") client_a.run("upload hello0/0.0@lasote/stable#latest -r default --only-recipe") # Now client_b checks for updates without -r parameter # TODO: cache2.0 conan info not yet implemented with new cache client_b.run("graph info --requires=hello0/0.0@lasote/stable --check-updates") assert "recipe: Update available" in client_b.out # assert "Recipe: Cache" in client_b.out # But if we connect to default, should tell us that there is an update IN DEFAULT! # TODO: cache2.0 conan info not yet implemented with new cache client_b.run("graph info --requires=hello0/0.0@lasote/stable -r default --check-updates") # assert "Remote: local" in client_b.out assert "recipe: Update available" in client_b.out # Well, now try to update the package with -r default -u client_b.run("install --requires=hello0/0.0@lasote/stable -r default -u --build='*'") assert "hello0/0.0@lasote/stable: Forced build from source" \ in str(client_b.out) # TODO: cache2.0 conan info not yet implemented with new cache client_b.run("graph info --requires=hello0/0.0@lasote/stable -u") assert "recipe: Cache" in client_b.out assert "binary: Cache" in client_b.out def test_conan_install_update(self): """ Checks conan install --update works only with the remote associated """ client = TestClient(servers=self.servers, inputs=2*["admin", "password"]) self._create(client, "hello0", "0.0") default_remote_rev = client.exported_recipe_revision() client.run("install --requires=hello0/0.0@lasote/stable --build missing") client.run("upload hello0/0.0@lasote/stable -r default") sleep(1) # For timestamp and updates checks self._create(client, "hello0", "0.0", modifier=" ") local_remote_rev = client.exported_recipe_revision() client.run("install --requires=hello0/0.0@lasote/stable --build missing") client.run("upload hello0/0.0@lasote/stable#latest -r local") client.run("remove '*' -c") client.run("install --requires=hello0/0.0@lasote/stable") # If we don't set a remote we find between all remotes and get the first match assert f"hello0/0.0@lasote/stable#{default_remote_rev} - Downloaded" in client.out client.run("install --requires=hello0/0.0@lasote/stable --update") assert f"hello0/0.0@lasote/stable#{local_remote_rev} - Updated" in client.out client.run("install --requires=hello0/0.0@lasote/stable --update -r default") assert f"hello0/0.0@lasote/stable#{local_remote_rev} - Newer" \ in client.out sleep(1) # For timestamp and updates checks # Check that it really updates in case of newer package uploaded to the associated remote client_b = TestClient(servers=self.servers, inputs=3*["admin", "password"]) self._create(client_b, "hello0", "0.0", modifier=" ") new_local_remote_rev = client_b.exported_recipe_revision() client_b.run("install --requires=hello0/0.0@lasote/stable --build missing") client_b.run("upload hello0/0.0@lasote/stable -r local") client.run("install --requires=hello0/0.0@lasote/stable --update") assert f"hello0/0.0@lasote/stable#{new_local_remote_rev} - Updated" in client.out class TestMultiRemote: @pytest.fixture(autouse=True) def setup(self): self.servers = OrderedDict() self.users = {} for i in range(3): test_server = TestServer() self.servers["remote%d" % i] = test_server self.users["remote%d" % i] = [("admin", "password")] self.client = TestClient(servers=self.servers, inputs=3*["admin", "password"]) def test_fail_when_not_notfound(self): """ If a remote fails with a 404 it has to keep looking in the next remote, but if it fails by any other reason it has to stop """ servers = OrderedDict() servers["s0"] = TestServer() servers["s1"] = TestServer() servers["s2"] = TestServer() client = TestClient(servers=servers) client.save({"conanfile.py": GenConanfile("mylib", "0.1")}) client.run("create . --user=lasote --channel=testing") client.run("remote login s1 admin -p password") client.run("upload mylib* -r s1 -c") servers["s1"].fake_url = "http://asdlhaljksdhlajkshdljakhsd.com" # Do not exist client2 = TestClient(servers=servers) client2.run("install --requires=mylib/0.1@conan/testing --build=missing", assert_error=True) assert "mylib/0.1@conan/testing: Checking remote: s0" in client2.out assert "mylib/0.1@conan/testing: Checking remote: s1" in client2.out assert "Unable to connect to remote s1=http://asdlhaljksdhlajkshdljakhsd.com" \ in client2.out # s2 is not even tried assert "mylib/0.1@conan/testing: Trying with 's2'..." not in client2.out def test_install_from_remotes(self): for i in range(3): ref = RecipeReference.loads("hello%d/0.1@lasote/stable" % i) self.client.save({"conanfile.py": GenConanfile("hello%d" % i, "0.1")}) self.client.run("export . --user=lasote --channel=stable") self.client.run("upload %s -r=remote%d" % (str(ref), i)) # Now install it in other machine from remote 0 client2 = TestClient(servers=self.servers) refs = ["hello0/0.1@lasote/stable", "hello1/0.1@lasote/stable", "hello2/0.1@lasote/stable"] client2.save({"conanfile.py": GenConanfile("helloX", "0.1").with_requires(*refs)}) client2.run("install . --build=missing") client2.assert_listed_require({"hello0/0.1@lasote/stable": "Downloaded (remote0)", "hello1/0.1@lasote/stable": "Downloaded (remote1)", "hello2/0.1@lasote/stable": "Downloaded (remote2)", }) ================================================ FILE: test/integration/remote/requester_test.py ================================================ from requests import Response from conan.test.utils.tools import TestClient, TestRequester conanfile = """ from conan import ConanFile from conan.tools.files import download class HelloConan(ConanFile): def source(self): download(self, "http://foo", "bar.txt") """ class MyRequester(TestRequester): def get(self, _, **kwargs): print("TIMEOUT: {}".format(kwargs.get("timeout", "NOT SPECIFIED"))) resp = Response() resp.status_code = 200 resp._content = b'' return resp class TestRequester: def test_requester_timeout(self): client = TestClient(requester_class=MyRequester) client.save_home({"global.conf": "core.net.http:timeout=4.3"}) client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") assert "TIMEOUT: 4.3" in client.out def test_requester_timeout_tuple(self): client = TestClient(requester_class=MyRequester) client.save_home({"global.conf": "core.net.http:timeout=(2, 3.4)"}) client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") assert "TIMEOUT: (2, 3.4)" in client.out def test_request_infinite_timeout(self): # Test that not having timeout works client = TestClient(requester_class=MyRequester) client.save_home({"global.conf": "core.net.http:timeout=-1"}) client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") assert "TIMEOUT: NOT SPECIFIED" in client.out def test_unset_request_timeout_use_default_one(self): client = TestClient(requester_class=MyRequester) client.save_home({"global.conf": "core.net.http:timeout=!"}) client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") assert "TIMEOUT: (30, 60)" in client.out ================================================ FILE: test/integration/remote/rest_api_test.py ================================================ import os import pytest from conan.api.model import Remote from conan.internal.model.conf import ConfDefinition from conan.internal.model.manifest import FileTreeManifest from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.paths import CONANFILE, CONANINFO, CONAN_MANIFEST from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.env import environment_update from conan.test.utils.server_launcher import TestServerLauncher from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import get_free_port from conan.internal.rest.conan_requester import ConanRequester from conan.internal.rest.rest_client import RestApiClient from conan.internal.util.files import md5, save class TestRestApi: """Open a real server (sockets) to test rest_api function.""" @pytest.fixture(scope="class", autouse=True) def setup_class(self): with environment_update({"CONAN_SERVER_PORT": str(get_free_port())}): read_perms = [("*/*@*/*", "private_user")] write_perms = [("*/*@*/*", "private_user")] self.server = TestServerLauncher(server_capabilities=['ImCool', 'TooCool'], read_permissions=read_perms, write_permissions=write_perms) self.server.start() config = ConfDefinition() requester = ConanRequester(config) remote = Remote("myremote", f"http://127.0.0.1:{self.server.port}", True, True) self.api = RestApiClient(remote, None, requester, config) self.api._token = self.api.authenticate(user="private_user", password="private_pass") # Necessary for setup_class approach TestRestApi.api = self.api TestRestApi.server = self.server yield self.server.stop() self.server.clean() def test_get_conan(self): # Upload a conans ref = RecipeReference.loads("conan1/1.0.0@private_user/testing#myreciperev") self._upload_recipe(ref) # Get the conans tmp_dir = temp_folder() self.api.get_recipe(ref, tmp_dir, metadata=None, only_metadata=False) assert CONANFILE in os.listdir(tmp_dir) assert CONAN_MANIFEST in os.listdir(tmp_dir) def test_get_package(self): # Upload a conans ref = RecipeReference.loads("conan3/1.0.0@private_user/testing#myreciperev") self._upload_recipe(ref) # Upload an package pref = PkgReference(ref, "1F23223EFDA2", "mypackagerev") self._upload_package(pref) # Get the package tmp_dir = temp_folder() self.api.get_package(pref, tmp_dir, metadata=None, only_metadata=False) # The hello.cpp file is not downloaded! assert "hello.cpp" not in os.listdir(tmp_dir) def test_upload_huge_conan(self): ref = RecipeReference.loads("conanhuge/1.0.0@private_user/testing#myreciperev") self._upload_recipe(ref, {"file9.cpp": ""}) tmp = temp_folder() files = self.api.get_recipe(ref, tmp, metadata=None, only_metadata=False) assert files is not None assert not os.path.exists(os.path.join(tmp, "file9.cpp")) def test_search(self): # Upload a conan1 conan_name1 = "HelloOnly/0.10@private_user/testing#myreciperev" ref1 = RecipeReference.loads(conan_name1) self._upload_recipe(ref1) # Upload a package conan_info = """[settings] arch=x86_64 compiler=gcc os=Linux [options] 386=False [requires] Hello Bye/2.9 Say/2.1@user/testing Chat/2.1@user/testing:SHA_ABC """ pref = PkgReference(ref1, "1F23223EFDA", "mypackagerev") self._upload_package(pref, {CONANINFO: conan_info}) # Upload a conan2 conan_name2 = "helloonlyToo/2.1@private_user/stable#myreciperev" ref2 = RecipeReference.loads(conan_name2) self._upload_recipe(ref2) # Get the info about this ConanFileReference info = self.api.search_packages(ref1) assert conan_info == info["1F23223EFDA"]["content"] # Search packages results = self.api.search("HelloOnly*", ignorecase=False) results = [RecipeReference(r.name, r.version, r.user, r.channel, revision=None) for r in results] ref1.revision = None assert results == [ref1] def test_remove(self): # Upload a conans ref = RecipeReference.loads("MyFirstConan/1.0.0@private_user/testing#myreciperev") self._upload_recipe(ref) ref.revision = "myreciperev" path1 = self.server.server_store.base_folder(ref) assert os.path.exists(path1) # Remove conans and packages self.api.remove_recipe(ref) assert not os.path.exists(path1) def test_remove_packages(self): ref = RecipeReference.loads("MySecondConan/2.0.0@private_user/testing#myreciperev") self._upload_recipe(ref) folders = {} for sha in ["1", "2", "3", "4", "5"]: # Upload an package pref = PkgReference(ref, sha, "mypackagerev") self._upload_package(pref, {CONANINFO: ""}) folder = self.server.server_store.package(pref) assert os.path.exists(folder) folders[sha] = folder data = self.api.search_packages(ref) assert len(data) == 5 self.api.remove_packages([PkgReference(ref, "1")]) assert os.path.exists(self.server.server_store.base_folder(ref)) assert not os.path.exists(folders["1"]) assert os.path.exists(folders["2"]) assert os.path.exists(folders["3"]) assert os.path.exists(folders["4"]) assert os.path.exists(folders["5"]) self.api.remove_packages([PkgReference(ref, "2"), PkgReference(ref, "3")]) assert os.path.exists(self.server.server_store.base_folder(ref)) assert not os.path.exists(folders["1"]) assert not os.path.exists(folders["2"]) assert not os.path.exists(folders["3"]) assert os.path.exists(folders["4"]) assert os.path.exists(folders["5"]) self.api.remove_all_packages(ref) assert os.path.exists(self.server.server_store.base_folder(ref)) for sha in ["1", "2", "3", "4", "5"]: assert not os.path.exists(folders[sha]) def _upload_package(self, package_reference, base_files=None): files = {"conanfile.py": GenConanfile("3").with_requires("1", "12").with_exports("*"), "hello.cpp": "hello", "conanmanifest.txt": "", "conan_package.tgz": ""} if base_files: files.update(base_files) tmp_dir = temp_folder() abs_paths = {} for filename, content in files.items(): abs_path = os.path.join(tmp_dir, filename) save(abs_path, str(content)) abs_paths[filename] = abs_path self.api.upload_package(package_reference, abs_paths) def _upload_recipe(self, ref, base_files=None): files = {"conanfile.py": GenConanfile("3").with_requires("1", "12")} if base_files: files.update(base_files) content = """ from conan import ConanFile class MyConan(ConanFile): name = "%s" version = "%s" settings = arch, compiler, os """ % (ref.name, ref.version) files[CONANFILE] = content files_md5s = {filename: md5(content) for filename, content in files.items()} conan_digest = FileTreeManifest(123123123, files_md5s) tmp_dir = temp_folder() abs_paths = {} for filename, content in files.items(): abs_path = os.path.join(tmp_dir, filename) save(abs_path, content) abs_paths[filename] = abs_path abs_paths[CONAN_MANIFEST] = os.path.join(tmp_dir, CONAN_MANIFEST) conan_digest.save(tmp_dir) self.api.upload_recipe(ref, abs_paths) ================================================ FILE: test/integration/remote/retry_test.py ================================================ import os import pytest from collections import namedtuple, Counter from requests.exceptions import HTTPError from conan.internal.rest.file_uploader import FileUploader from conan.internal.errors import AuthenticationException, ForbiddenException from conan.test.utils.mocks import RedirectedTestOutput from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import redirect_output from conan.internal.util.files import save class _ResponseMock: def __init__(self, status_code, content): self.status_code = status_code self.content = content def raise_for_status(self): """Raises stored :class:`HTTPError`, if one occurred.""" http_error_msg = '' if 500 <= self.status_code < 600: http_error_msg = u'%s Server Error: %s' % (self.status_code, self.content) if http_error_msg: raise HTTPError(http_error_msg, response=self) class _RequesterMock: def __init__(self, status_code, content): self.response = _ResponseMock(status_code, content) self.retry = 0 self.retry_wait = 0 def put(self, *args, **kwargs): return self.response class _ConfigMock: def get(self, conf_name, default=None, check_type=None): # noqa return 0 class TestRetryDownload: @pytest.fixture(autouse=True) def setup(self): self.filename = os.path.join(temp_folder(), "anyfile") save(self.filename, "anything") def test_error_401(self): output = RedirectedTestOutput() with redirect_output(output): uploader = FileUploader(requester=_RequesterMock(401, "content"), verify=False, config=_ConfigMock()) with pytest.raises(AuthenticationException, match="content"): uploader.upload(url="fake", abs_path=self.filename, retry=2) output_lines = output.getvalue().splitlines() counter = Counter(output_lines) assert counter["ERROR: content"] == 0 assert counter["Waiting 0 seconds to retry..."] == 0 def test_error_403_forbidden(self): output = RedirectedTestOutput() with redirect_output(output): uploader = FileUploader(requester=_RequesterMock(403, "content"), verify=False, config=_ConfigMock()) with pytest.raises(ForbiddenException, match="content"): auth = namedtuple("auth", "bearer") uploader.upload(url="fake", abs_path=self.filename, retry=2, auth=auth("token")) output_lines = output.getvalue().splitlines() counter = Counter(output_lines) assert counter["ERROR: content"] == 0 assert counter["Waiting 0 seconds to retry..."] == 0 def test_error_403_authentication(self): output = RedirectedTestOutput() with redirect_output(output): uploader = FileUploader(requester=_RequesterMock(403, "content"), verify=False, config=_ConfigMock()) with pytest.raises(AuthenticationException, match="content"): auth = namedtuple("auth", "bearer") uploader.upload(url="fake", abs_path=self.filename, retry=2, auth=auth(None)) output_lines = output.getvalue().splitlines() counter = Counter(output_lines) assert counter["ERROR: content"] == 0 assert counter["Waiting 0 seconds to retry..."] == 0 def test_error_requests(self): class _RequesterExcp: def put(self, *args, **kwargs): raise Exception("any exception") output = RedirectedTestOutput() with redirect_output(output): uploader = FileUploader(requester=_RequesterExcp(), verify=False, config=_ConfigMock()) with pytest.raises(Exception, match="any exception"): uploader.upload(url="fake", abs_path=self.filename, retry=2) output_lines = output.getvalue().splitlines() counter = Counter(output_lines) assert counter["WARN: network: any exception"] == 2 assert counter["Waiting 0 seconds to retry..."] == 2 def test_error_500(self): output = RedirectedTestOutput() with redirect_output(output): uploader = FileUploader(requester=_RequesterMock(500, "content"), verify=False, config=_ConfigMock()) with pytest.raises(Exception, match="500 Server Error: content"): uploader.upload(url="fake", abs_path=self.filename, retry=2) output_lines = output.getvalue().splitlines() counter = Counter(output_lines) assert counter["WARN: network: 500 Server Error: content"] == 2 assert counter["Waiting 0 seconds to retry..."] == 2 ================================================ FILE: test/integration/remote/selected_remotes_test.py ================================================ from collections import OrderedDict import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer class TestSelectedRemotesInstall: @pytest.fixture(autouse=True) def _setup(self): servers = OrderedDict() for index in range(3): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) self.client = TestClient(servers=servers, inputs=3 * ["user", "password"]) def test_selected_remotes(self): self.client.save({"conanfile.py": GenConanfile("liba", "1.0").with_build_msg("OLDREV")}) self.client.run("create .") self.client.run("upload liba/1.0 -r server0 -c") self.client.run("remove * -c") self.client.save({"conanfile.py": GenConanfile("liba", "1.0").with_build_msg("NEWER_REV")}) self.client.run("create .") self.client.run("upload liba/1.0 -r server1 -c") self.client.run("remove * -c") self.client.run("install --requires=liba/1.0 -r server0 -r server1 --build='*'") # we install the revision from the server with more preference assert "OLDREV" in self.client.out self.client.run("remove * -c") self.client.run("install --requires=liba/1.0 -r server1 -r server0 --build='*'") # changing the order of the remotes in the arguments does change the result # we install the revision from the server with more preference assert "NEWER_REV" in self.client.out # select two remotes, just one has liba, will install the rev from that one self.client.run("remove * -c") self.client.run("install --requires=liba/1.0 -r server2 -r server1 --build='*'") assert "NEWER_REV" in self.client.out self.client.save({"consumer.py": GenConanfile().with_require("liba/1.0")}) self.client.run("remove * -c") self.client.run("create . --build='*' -r server0 -r server1 -r server2") assert "NEWER_REV" in self.client.out def test_upload_raise_multiple_remotes(self): self.client.run("upload liba -r server0 -r server1 -c", assert_error=True) assert "conan upload: error: -r can only be specified once" in self.client.out def test_remove_raise_multiple_remotes(self): self.client.run("remove liba -r server0 -r server1 -c", assert_error=True) assert "conan remove: error: -r can only be specified once" in self.client.out ================================================ FILE: test/integration/remote/server_error_test.py ================================================ from requests import Response from conan.internal import REVISIONS from conan.test.utils.tools import TestClient, TestServer, TestRequester from collections import namedtuple class TestError200NoJson: def test_error_no_json(self): class RequesterMock(TestRequester): def get(self, *args, **kwargs): # @UnusedVariable # Response must be binary, it is decoded in RestClientCommon headers = {"X-Conan-Server-Capabilities": REVISIONS} return namedtuple("Response", "status_code headers content ok")(200, headers, b'<>', True) # https://github.com/conan-io/conan/issues/3432 client = TestClient(servers={"default": TestServer()}, requester_class=RequesterMock, inputs=["admin", "password"]) client.run("install --requires=pkg/ref@user/testing", assert_error=True) assert "Response from remote is not json, but 'None'" in client.out def test_error_broken_json(self): class RequesterMock(TestRequester): def get(self, *args, **kwargs): # @UnusedVariable # Response must be binary, it is decoded in RestClientCommon headers = {"Content-Type": "application/json", "X-Conan-Server-Capabilities": REVISIONS} return namedtuple("Response", "status_code headers content ok")(200, headers, b'<>', True) # https://github.com/conan-io/conan/issues/3432 client = TestClient(servers={"default": TestServer()}, requester_class=RequesterMock, inputs=["admin", "password"]) client.run("install --requires=pkg/ref@user/testing", assert_error=True) assert "Remote responded with broken json: <>" in client.out def test_error_json(self): class RequesterMock(TestRequester): def get(self, *args, **kwargs): # @UnusedVariable # Response must be binary, it is decoded in RestClientCommon headers = {"Content-Type": "application/json", "X-Conan-Server-Capabilities": REVISIONS} return namedtuple("Response", "status_code headers content ok")(200, headers, b'[1, 2, 3]', True) # https://github.com/conan-io/conan/issues/3432 client = TestClient(servers={"default": TestServer()}, requester_class=RequesterMock, inputs=["admin", "password"]) client.run("install --requires=pkg/ref@user/testing", assert_error=True) assert "Unexpected server response [1, 2, 3]" in client.out def test_unrecongized_exception(): class BuggyRequester(TestRequester): def get(self, *args, **kwargs): resp = Response() resp.status_code = 444 resp._content = 'some 444 error message' return resp c = TestClient(default_server_user=True, requester_class=BuggyRequester) c.run("install --requires=zlib/1.2 -r=default", assert_error=True) assert ("ERROR: Package 'zlib/1.2' not resolved: Server exception 444:" " some 444 error message") in c.out ================================================ FILE: test/integration/remote/test_conaninfo_parsing.py ================================================ # coding=utf-8 import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.artifactory_ready def test_conaninfo_special_chars(): t = TestClient(default_server_user=True) conanfile = textwrap.dedent(""" # coding=utf-8 from conan import ConanFile class Recipe(ConanFile): name = "weird_info" version = "1.0" options = {"ññ¨¨&是": ['是"是', '][{}"是是是']} default_options = {"ññ¨¨&是": '][{}"是是是'} """) t.save({"conanfile.py": conanfile}) t.run("create . ") t.run('list weird_info/1.0:*') assert 'ññ¨¨&是: ][{}"是是是' in t.out t.run("upload * -c -r default") # TODO: I have struggled with this, it was not accepting "latest", revision needs explicit one t.run('list weird_info/1.0#8c9e59246220eef8ca3bd4ac4f39ceb3:* -r default') assert 'ññ¨¨&是: ][{}"是是是' in t.out ================================================ FILE: test/integration/remote/test_local_recipes_index.py ================================================ import json import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import mkdir, save, save_files @pytest.fixture(scope="module") def c3i_folder(): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "1.2.8": folder: all "1.2.11": folder: all """) zlib = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class Zlib(ConanFile): name = "zlib" exports_sources = "*" def build(self): self.output.info(f"CONANDATA: {self.conan_data}") self.output.info(f"BUILDING: {load(self, 'file.h')}") """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": zlib, "zlib/all/conandata.yml": "", "zlib/all/file.h": "//myheader"}) mkdir(os.path.join(recipes_folder, "openssl", "1.X")) mkdir(os.path.join(recipes_folder, "openssl", "2.X")) save(os.path.join(recipes_folder, "openssl", "config.yml"), textwrap.dedent(""" versions: "1.0": folder: "1.X" "1.1": folder: "1.X" "2.0": folder: "2.X" """)) save(os.path.join(recipes_folder, "openssl", "1.X", "conanfile.py"), str(GenConanfile().with_require("zlib/1.2.8"))) save(os.path.join(recipes_folder, "openssl", "2.X", "conanfile.py"), str(GenConanfile().with_require("zlib/1.2.11"))) mkdir(os.path.join(recipes_folder, "libcurl", "all")) save(os.path.join(recipes_folder, "libcurl", "config.yml"), textwrap.dedent(""" versions: "1.0": folder: "all" """)) save(os.path.join(recipes_folder, "libcurl", "all", "conanfile.py"), str(GenConanfile().with_require("openssl/2.0"))) save(os.path.join(recipes_folder, ".DS_Store", "foo"), "") return folder class TestSearchList: def test_basic_search(self, c3i_folder): client = TestClient(light=True) client.run(f"remote add local '{c3i_folder}' --type=local-recipes-index") # Keep --type test assert "WARN" not in client.out # Make sure it does not complain about url client.run("search *") assert textwrap.dedent("""\ local libcurl libcurl/1.0 openssl openssl/1.0 openssl/1.1 openssl/2.0 zlib zlib/1.2.8 zlib/1.2.11 """) in client.out def test_list_refs(self, c3i_folder): client = TestClient(light=True) client.run(f"remote add local '{c3i_folder}'") client.run("list *#* -r=local --format=json") listjson = json.loads(client.stdout) revs = listjson["local"]["libcurl/1.0"]["revisions"] assert len(revs) == 1 and "e468388f0e4e098d5b62ad68979aebd5" in revs revs = listjson["local"]["openssl/1.0"]["revisions"] assert len(revs) == 1 and "b35ffb31b6d5a9d8af39f5de3cf4fd63" in revs revs = listjson["local"]["openssl/1.1"]["revisions"] assert len(revs) == 1 and "b35ffb31b6d5a9d8af39f5de3cf4fd63" in revs revs = listjson["local"]["openssl/2.0"]["revisions"] assert len(revs) == 1 and "e50e871efca149f160fa6354c8534449" in revs revs = listjson["local"]["zlib/1.2.8"]["revisions"] assert len(revs) == 1 and "6f5c31bb1219e9393743d1fbf2ee1b52" in revs revs = listjson["local"]["zlib/1.2.11"]["revisions"] assert len(revs) == 1 and "6f5c31bb1219e9393743d1fbf2ee1b52" in revs def test_list_revisions_notfound(self, c3i_folder): client = TestClient(light=True) client.run(f"remote add local '{c3i_folder}'") client.run("list potato/1.0#* -r=local") # More like remotes than cache assert "ERROR: Recipe not found: 'potato/1.0'" def test_list_rrevs(self, c3i_folder): client = TestClient(light=True) client.run(f"remote add local '{c3i_folder}'") client.run("list libcurl/1.0#* -r=local --format=json") listjson = json.loads(client.stdout) revs = listjson["local"]["libcurl/1.0"]["revisions"] assert len(revs) == 1 and "e468388f0e4e098d5b62ad68979aebd5" in revs def test_list_binaries(self, c3i_folder): client = TestClient(light=True) client.run(f"remote add local '{c3i_folder}'") client.run("list libcurl/1.0:* -r=local --format=json") listjson = json.loads(client.stdout) rev = listjson["local"]["libcurl/1.0"]["revisions"]["e468388f0e4e098d5b62ad68979aebd5"] assert rev["packages"] == {} class TestInstall: def test_install(self, c3i_folder): c = TestClient(light=True) c.run(f"remote add local '{c3i_folder}'") c.run("install --requires=libcurl/1.0 --build missing") assert "zlib/1.2.11: CONANDATA: {}" in c.out assert "zlib/1.2.11: BUILDING: //myheader" in c.out bins = {"libcurl/1.0": ("aa69c1e1e39a18fe70001688213dbb7ada95f890", "Build"), "openssl/2.0": ("594ed0eb2e9dfcc60607438924c35871514e6c2a", "Build"), "zlib/1.2.11": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")} c.assert_listed_binary(bins) # Already installed in the cache c.run("install --requires=libcurl/1.0") assert "zlib/1.2.11: Already installed!" in c.out assert "openssl/2.0: Already installed!" in c.out assert "libcurl/1.0: Already installed!" in c.out # Update doesn't fail, but doesn't update revision time c.run("install --requires libcurl/1.0 --update") bins = {"libcurl/1.0": "Cache (Updated date) (local)", "openssl/2.0": "Cache (Updated date) (local)", "zlib/1.2.11": "Cache (Updated date) (local)"} c.assert_listed_require(bins) assert "zlib/1.2.11: Already installed!" in c.out assert "openssl/2.0: Already installed!" in c.out assert "libcurl/1.0: Already installed!" in c.out # Doing local changes creates a new revision # New recipe revision for the zlib library save(os.path.join(c3i_folder, "recipes", "zlib", "all", "conanfile.py"), str(GenConanfile()) + "\n") c.run("install --requires=libcurl/1.0 --build missing --update") # it is updated assert "zlib/1.2.11#dd82451a95902c89bb66a2b980c72de5 - Updated (local)" in c.out def test_install_with_exported_files(self): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") boost_config = textwrap.dedent(""" versions: "1.0": folder: all """) boost = textwrap.dedent(""" import os from conan.tools.files import load from conan import ConanFile class Boost(ConanFile): name = "boost" version = "1.0" exports = "*" def source(self): myfile = os.path.join(self.recipe_folder, "dependencies", "myfile.json") self.output.info(load(self, myfile)) """) deps_json = '{"potato": 42}' save_files(recipes_folder, {"boost/config.yml": boost_config, "boost/all/conanfile.py": boost, "boost/all/dependencies/myfile.json": deps_json}) c = TestClient(light=True) c.run(f"remote add local '{folder}'") c.run("install --requires=boost/[*] --build missing") assert 'boost/1.0: {"potato": 42}' in c.out def test_trim_conandata_yaml(self): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") config = textwrap.dedent(""" versions: "1.0": folder: all """) conandata = textwrap.dedent("""\ sources: "1.0": url: sha256: "ff0ba4c292013dbc27530b3a81e1f9a813cd39de01ca5e0f8bf355702efa593e" patches: "1.0": - patch_file: "patches/1.3/0001-fix-cmake.patch" """) save_files(recipes_folder, {"pkg/config.yml": config, "pkg/all/conanfile.py": str(GenConanfile("pkg")), "pkg/all/conandata.yml": conandata}) c = TestClient(light=True) c.run(f"remote add local '{folder}'") c.run("install --requires=pkg/1.0 --build missing -vvv") assert "pkg/1.0#86b609916bbdfe63c579f034ad0edfe7" in c.out # User modifies conandata.yml to add new version new_conandata = textwrap.dedent("""\ sources: "1.0": url: sha256: "ff0ba4c292013dbc27530b3a81e1f9a813cd39de01ca5e0f8bf355702efa593e" "1.1": url: patches: "1.0": - patch_file: "patches/1.3/0001-fix-cmake.patch" """) save_files(recipes_folder, {"pkg/all/conandata.yml": new_conandata}) c.run("install --requires=pkg/1.0 --build missing --update -vvv") assert "pkg/1.0#86b609916bbdfe63c579f034ad0edfe7" in c.out def test_export_patches(self): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "0.1": folder: all """) zlib = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import export_conandata_patches, apply_conandata_patches class Zlib(ConanFile): name = "zlib" exports_sources = "*.cpp" def export_sources(self): export_conandata_patches(self) def source(self): apply_conandata_patches(self) """) conandata_yml = textwrap.dedent("""\ versions: "0.1": patches: "0.1": - patch_file: "patches/patch1" """) patch = textwrap.dedent("""\ --- a/main.cpp +++ b/main.cpp @@ -0,0 +1 @@ +hello """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": zlib, "zlib/all/conandata.yml": conandata_yml, "zlib/all/patches/patch1": patch, "zlib/all/main.cpp": "\n"}) client = TestClient(light=True) client.run(f"remote add local '{folder}'") client.run("install --requires=zlib/0.1 --build=missing -vv") assert "zlib/0.1: Copied 1 file: patch1" in client.out assert "zlib/0.1: Apply patch (file): patches/patch1" in client.out def test_export_user_channel(self): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "0.1": folder: all """) zlib = GenConanfile("zlib").with_class_attribute("user='myuser'")\ .with_class_attribute("channel='mychannel'") conandata_yml = textwrap.dedent("""\ versions: "0.1": """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": str(zlib), "zlib/all/conandata.yml": conandata_yml}) client = TestClient() client.run(f"remote add local '{folder}'") client.run("install --requires=zlib/0.1@myuser/mychannel --build=missing") assert "zlib/0.1@myuser/mychannel:" in client.out client.run("list * -r=local") assert "zlib/0.1@myuser/mychannel" in client.out class TestRestrictedOperations: def test_upload(self): folder = temp_folder() c3i_folder = os.path.join(folder, "recipes") mkdir(c3i_folder) c = TestClient(light=True) c.run(f"remote add local '{c3i_folder}'") c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run("upload pkg/0.1 -r=local", assert_error=True) assert "ERROR: Remote local-recipes-index 'local' doesn't support upload" in c.out class TestErrorsUx: def test_errors(self): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "1.2.11": folder: all """) zlib = textwrap.dedent(""" class Zlib(ConanFile): name = "zlib" """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": zlib}) c = TestClient(light=True) c.run(f"remote add local '{folder}'") c.run("install --requires=zlib/[*] --build missing", assert_error=True) assert "NameError: name 'ConanFile' is not defined" in c.out def test_require_revision(self): # https://github.com/conan-io/conan/issues/17814 folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "1.2.11": folder: all """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": str(GenConanfile("zlib"))}) c = TestClient(light=True) c.run(f"remote add local '{folder}'") c.run("install --requires=zlib/1.2.11#rev1", assert_error=True) assert "A specific revision 'zlib/1.2.11#rev1' was requested" in c.out def test_no_user_channel(self): # https://github.com/conan-io/conan/issues/18142 folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "0.1": folder: all """) zlib = GenConanfile("zlib") conandata_yml = textwrap.dedent("""\ versions: "0.1": """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": str(zlib), "zlib/all/conandata.yml": conandata_yml}) c = TestClient() c.run(f"remote add local '{folder}'") c.run("install --requires=zlib/0.1@myuser/mychannel", assert_error=True) assert "ERROR: Package 'zlib/0.1@myuser/mychannel' not resolved" in c.out c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("zlib", "0.1")}) c.run("create . --user=myuser --channel=mychannel") c.run("upload * -r=default -c") c.run(f"remote add local '{folder}' --index=0") c.run("install --requires=zlib/0.1@myuser/mychannel") c.assert_listed_require({"zlib/0.1@myuser/mychannel": "Cache"}) # Force resolving in remotes c.run("remove * -c") c.run("install --requires=zlib/0.1@myuser/mychannel") c.assert_listed_require({"zlib/0.1@myuser/mychannel": "Downloaded (default)"}) def test_errors_missing_folder(self): folder = temp_folder() repo = os.path.join(folder, "repo") mkdir(repo) c = TestClient(light=True) c.run(f"remote add local '{repo}'") # shutil.rmtree(repo) c.run("install --requires=zlib/[*] --build missing", assert_error=True) assert "Cannot connect to 'local-recipes-index' repository, missing 'recipes'" in c.out class TestPythonRequires: @pytest.fixture(scope="class") def c3i_pyrequires_folder(self): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") config = textwrap.dedent(""" versions: "1.0": folder: all """) pkg = str(GenConanfile("pkg").with_python_requires("pyreq/1.0")) save_files(recipes_folder, {"pkg/config.yml": config, "pkg/all/conanfile.py": pkg, "pyreq/config.yml": config, "pyreq/all/conanfile.py": str(GenConanfile("pyreq", "1.0"))}) return folder def test_install(self, c3i_pyrequires_folder): c = TestClient(light=True) c.run(f"remote add local '{c3i_pyrequires_folder}'") c.run("list * -r=local") assert "pyreq/1.0" in c.out assert "pkg/1.0" in c.out c.run("install --requires=pkg/1.0 --build missing -vvv") assert "pyreq/1.0#a0d63ca853edefa33582a24a1bb3c75f - Downloaded (local)" in c.out assert "pkg/1.0: Created package" in c.out class TestUserChannel: @pytest.fixture(scope="class") def c3i_user_channel_folder(self): folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") config = textwrap.dedent(""" versions: "1.0": folder: all "2.0": folder: other """) pkg = str(GenConanfile("pkg").with_class_attribute("user='myuser'") .with_class_attribute("channel='mychannel'")) save_files(recipes_folder, {"pkg/config.yml": config, "pkg/all/conanfile.py": pkg, "pkg/other/conanfile.py": str(GenConanfile("pkg"))}) return folder def test_user_channel_requirement(self, c3i_user_channel_folder): tc = TestClient(light=True) tc.run(f"remote add local '{c3i_user_channel_folder}'") tc.run("graph info --requires=pkg/[*]@myuser/mychannel") assert "Version range '*' from requirement 'pkg/[*]@myuser/mychannel'" not in tc.out assert "pkg/[*]@myuser/mychannel: pkg/1.0@myuser/mychannel" in tc.out assert "pkg/1.0@myuser/mychannel#0b23a5938afb0457079e41aac8991595 - Downloaded (local)" in tc.out @pytest.mark.parametrize("user_channel", ["@foo/bar", "@foo"]) def test_user_channel_requirement_no_match(self, c3i_user_channel_folder, user_channel): tc = TestClient(light=True) tc.run(f"remote add local '{c3i_user_channel_folder}'") tc.run(f"graph info --requires=pkg/[*]{user_channel}", assert_error=True) assert f"Version range '*' from requirement 'pkg/[*]{user_channel}' required by 'None' could not be resolved." in tc.out def test_user_channel_requirement_only_at(self, c3i_user_channel_folder): tc = TestClient(light=True) tc.run(f"remote add local '{c3i_user_channel_folder}'") tc.run(f"graph info --requires=pkg/[*]@") assert f"pkg/[*]: pkg/2.0" in tc.out tc.run(f"graph info --requires=pkg/[<2]@", assert_error=True) assert f" Package 'pkg/[<2]' not resolved" in tc.out tc.run(f"graph info --requires=pkg/[<2]", assert_error=True) assert f" Package 'pkg/[<2]' not resolved" in tc.out class TestResetRemote: def test_resetting_remote_error(self): # https://github.com/conan-io/conan/issues/18371 folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "1.2.11": folder: all """) zlib = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class Zlib(ConanFile): name = "zlib" exports_sources = "*" """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": zlib, "zlib/all/conandata.yml": "", "zlib/all/file.h": "//myheader"}) client = TestClient(light=True) client.run(f"remote add local '{folder}'") client.run("graph info --requires=zlib/[*]") # This second --force destroys the previous remote database client.run(f"remote add local '{folder}' --force") client.run("install --requires=zlib/[*] --build=missing") # It doesn't fail or crash anymore def test_changing_revisions(self): # https://github.com/conan-io/conan/issues/18371 folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "1.2.11": folder: all """) zlib = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import load class Zlib(ConanFile): name = "zlib" exports_sources = "*" def build(self): self.output.info(f"BUILDING: {load(self, 'file.h')}") """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": zlib, "zlib/all/conandata.yml": "", "zlib/all/file.h": "//myheader"}) c = TestClient(light=True) c.run(f"remote add local '{folder}'") c.run("graph info --requires=zlib/[*]") rev1 = "bd69839cb4c933336fceb32302aaf91f" # Modify zlib code save_files(recipes_folder, {"zlib/all/file.h": "//myheader 222"}) c.run("graph info --requires=zlib/[*] --update") rev2 = "c912566276abca17d2fb5fb6fc957852" c.run(f"remote add local '{folder}' --force") c.run(f"install --requires=zlib/1.2.11#{rev2} --build=missing") # works c.run(f"install --requires=zlib/1.2.11#{rev1} --build=missing", assert_error=True) assert ("WARN: A specific revision 'zlib/1.2.11#bd69839cb4c933336fceb32302aaf91f' was " "requested, but it doesn't match the current available revision in source") in c.out assert ("ERROR: The 'zlib/1.2.11' package has 'exports_sources' but sources " "not found in local cache") in c.out def test_reverting_to_older_revision(self): # https://github.com/conan-io/conan/issues/19313 folder = temp_folder() recipes_folder = os.path.join(folder, "recipes") zlib_config = textwrap.dedent(""" versions: "1.2.11": folder: all """) zlib = textwrap.dedent(""" from conan import ConanFile class Zlib(ConanFile): name = "zlib" exports_sources = "*" """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": zlib, "zlib/all/conandata.yml": "", "zlib/all/file.h": "//myheader"}) c = TestClient(light=True) c.run(f"remote add local '{folder}'") c.run("install --requires=zlib/[*] --build=missing") rev1 = "169da4321a56b77e8538821613a81f1d" c.assert_listed_require({f"zlib/1.2.11#{rev1}": "Downloaded (local)"}) # Modify zlib code save_files(recipes_folder, {"zlib/all/file.h": "//myheader 222"}) c.run("install --requires=zlib/[*] --build=missing --update") rev2 = "8e9fa314a3fd51ab4c930f7e4972f3e7" c.assert_listed_require({f"zlib/1.2.11#{rev2}": "Updated (local)"}) # Modify zlib code again to a new one save_files(recipes_folder, {"zlib/all/file.h": "//myheader 333"}) c.run("install --requires=zlib/[*] --update --build=missing") rev3 = "dd54aebe11b96b3661d8360c68619a72" c.assert_listed_require({f"zlib/1.2.11#{rev3}": "Updated (local)"}) # Revert to previous one save_files(recipes_folder, {"zlib/all/file.h": "//myheader"}) c.run("install --requires=zlib/[*] --update --build=missing") # This crashed https://github.com/conan-io/conan/issues/19313 c.assert_listed_require({f"zlib/1.2.11#{rev1}": "Updated (local)"}) ================================================ FILE: test/integration/remote/test_offline.py ================================================ import re from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestRequester def test_offline(): c = TestClient(servers={"default": None}, requester_class=None) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run("install --requires=pkg/0.1") # doesn't fail, install without issues assert "Install finished successfully" in c.out def test_offline_uploaded(): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg")}) c.run("create . --version=0.1") c.run("create . --version=0.2") c.run("upload * -r=default -c") c.run("remove * -c") c.run("install --requires=pkg/0.1") assert "Install finished successfully" in c.out c.servers = {"default": None} c.run("install --requires=pkg/0.1") assert "Install finished successfully" in c.out # Lets make sure the server is broken c.run("install --requires=pkg/0.2", assert_error=True) assert "ERROR: Package 'pkg/0.2' not resolved" in c.out def test_offline_build_requires(): """ When there are tool-requires that are not installed locally, Conan will try to check its existence in the remote, because that package might be needed later. Even if some packages can be marked later as "skip" and not fail, that cannot be known a priori, so if the package is not in the cache, it will be checked in servers https://github.com/conan-io/conan/issues/15339 Approaches to avoid the WARNING: - conan remote disable - conan install ... -nr (--no-remotes) - prepopulate the cache with all tool-requires with `-c:a tools.graph:skip_binaries=False` """ c = TestClient(default_server_user=True) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "lib/conanfile.py": GenConanfile("pkg", "0.1").with_tool_requires("tool/0.1")}) c.run("create tool") c.run("create lib") c.run("upload * -r=default -c") c.run("remove * -c") c.run("install --requires=pkg/0.1") assert "Install finished successfully" in c.out class MyHttpRequester(TestRequester): def get(self, _, **kwargs): from requests.exceptions import ConnectionError raise ConnectionError("ALL BAD") c.requester_class = MyHttpRequester # this will fail c.run("install --requires=pkg/0.1", assert_error=True) assert "ERROR: Failed checking for binary 'tool/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709'" \ in c.out # Explicitly telling that no-remotes, works c.run("install --requires=pkg/0.1 -nr") assert "tool/0.1: WARN" not in c.out assert "Install finished successfully" in c.out # graph info also breaks c.run("graph info --requires=pkg/0.1", assert_error=True) assert "ERROR: Failed checking for binary 'tool/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709'" \ in c.out c.run("graph info --requires=pkg/0.1 -nr") assert "tool/0.1: WARN" not in c.out assert re.search(r"Skipped binaries(\s*)tool/0.1", c.out) ================================================ FILE: test/integration/remote/test_remote_file_credentials.py ================================================ import json import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer from conan.internal.util.files import save @pytest.fixture() def client(): test_server = TestServer() c = TestClient(servers={"default": test_server}) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run("upload * -r=default -c", assert_error=True) return c def test_remote_file_credentials(client): c = client content = {"credentials": [{"remote": "default", "user": "admin", "password": "password"}]} save(os.path.join(c.cache_folder, "credentials.json"), json.dumps(content)) c.run("upload * -r=default -c") # it works without problems! assert "Uploading recipe" in c.out def test_remote_file_credentials_remote_login(client): c = client content = {"credentials": [{"remote": "default", "user": "admin", "password": "password"}]} save(os.path.join(c.cache_folder, "credentials.json"), json.dumps(content)) c.run("remote login default") assert "Changed user of remote 'default' from 'None' (anonymous) " \ "to 'admin' (authenticated)" in c.out def test_remote_file_credentials_error(client): c = client content = {"credentials": [{"remote": "default", "user": "admin", "password": "wrong"}]} save(os.path.join(c.cache_folder, "credentials.json"), json.dumps(content)) c.run("upload * -r=default -c", assert_error=True) assert "ERROR: Wrong user or password" in c.out def test_remote_file_credentials_bad_file(client): c = client save(os.path.join(c.cache_folder, "credentials.json"), "") c.run("upload * -r=default -c", assert_error=True) assert "ERROR: Error loading 'credentials.json'" in c.out content = {"credentials": [{"remote": "default"}]} save(os.path.join(c.cache_folder, "credentials.json"), json.dumps(content)) c.run("upload * -r=default -c", assert_error=True) assert "ERROR: Error loading 'credentials.json'" in c.out ================================================ FILE: test/integration/remote/test_remote_recipes_only.py ================================================ import json import os import pytest from conan.api.model import PkgReference from conan.internal.util.files import save from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient PKG_ID = "da39a3ee5e6b4b0d3255bfef95601890afd80709" RREV = "a9ec2e5fbb166568d4670a9cd1ef4b26" @pytest.fixture(scope="module") def client(): tc = TestClient(light=True, default_server_user=True) tc.save({"conanfile.py": GenConanfile("pkg")}) tc.run("create . --version=1.0") assert RREV == tc.exported_recipe_revision() assert PKG_ID == tc.created_package_id("pkg/1.0") tc.run("upload * -r=default -c") tc.run("remove * -c") tc.run("remote update default --recipes-only") return tc def test_graph_binary_analyze(client): client.run("install --requires=pkg/1.0 -f=json", assert_error=True) client.assert_listed_binary({"pkg/1.0": (PKG_ID, "Missing")}) client.run("graph explain --requires=pkg/1.0") assert "ERROR: No package binaries exist" in client.out def test_list_packages(client): client.run("list pkg/1.0:* -r=default -f=json", redirect_stdout="list.json") result = json.loads(client.load("list.json")) assert len(result["default"]["pkg/1.0"]["revisions"][RREV]["packages"]) == 0 client.run(f"list pkg/1.0:{PKG_ID} -r=default -f=json", redirect_stdout="list.json") result = json.loads(client.load("list.json")) assert "error" in result["default"] # not found error def test_download_package(client): client.run("download pkg/1.0:* -r=default -f=json", redirect_stdout="download.json") result = json.loads(client.load("download.json")) assert len(result["Local Cache"]["pkg/1.0"]["revisions"][RREV]["packages"]) == 0 def test_remove_package(client): client.run(f"remove pkg/1.0:{PKG_ID} -r=default -c -f=json", redirect_stdout="remove.json") result = json.loads(client.load("remove.json")) assert result["default"] == {} client.run(f"remove pkg/1.0:* -r=default -c -f=json", redirect_stdout="remove.json") result = json.loads(client.load("remove.json")) assert result["default"] == {} client.run(f"remove pkg/1.0 -r=default -c -f=json", redirect_stdout="remove.json") result = json.loads(client.load("remove.json")) assert "packages" not in result["default"]["pkg/1.0"]["revisions"][RREV] @pytest.mark.parametrize("pattern", ["*", "pkg/2.0", "pkg/2.0:*", f"pkg/2.0:{PKG_ID}"]) def test_upload_still_works(client, pattern): client.run("create . --version=2.0") layout = client.created_layout() save(os.path.join(layout.metadata(), "metadata.log"), "New hello") client.run(f"upload {pattern} -r=default -c") # Now check that the binary was in fact uploaded client.run("remote update default --recipes-only=False") client.run("remove * -c") client.run("list pkg/2.0:* -r=default -f=json", redirect_stdout="list.json") result = json.loads(client.load("list.json")) assert len(result["default"]["pkg/2.0"]["revisions"][RREV]["packages"]) == 1 assert PKG_ID in result["default"]["pkg/2.0"]["revisions"][RREV]["packages"] client.run(f"download {pattern} -r=default -f=json --metadata=*", redirect_stdout="download.json") pkg2_layout = client.get_latest_pkg_layout(PkgReference().loads(f"pkg/2.0#{RREV}:{PKG_ID}")) assert os.path.exists(os.path.join(pkg2_layout.metadata(), "metadata.log")) ================================================ FILE: test/integration/remote/test_request_headers.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestRequester class RequesterClass(TestRequester): def get(self, url, headers=None, **kwargs): print(f"URL: {url}-HEADERS: {headers}") return super(RequesterClass, self).get(url, headers=headers, **kwargs) def test_request_info_headers(): c = TestClient(requester_class=RequesterClass, default_server_user=True) conanfile = GenConanfile("pkg", "0.1").with_settings('os', 'arch', 'compiler') \ .with_shared_option(False) c.save({'conanfile.py': conanfile}) c.run("export .") c.run("install --requires=pkg/0.1 -s arch=x86_64", assert_error=True) assert "'Conan-PkgID-Options': 'shared=False'" in c.out assert "'Conan-PkgID-Settings': 'arch=x86_64;" in c.out ================================================ FILE: test/integration/sbom/__init__.py ================================================ ================================================ FILE: test/integration/sbom/test_cyclonedx.py ================================================ import textwrap import json import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save import os # Using the sbom tool with "conan create" sbom_hook_post_package = """ import json import os from conan.errors import ConanException from conan.api.output import ConanOutput from conan.tools.sbom import {cyclone_version} def post_package(conanfile): sbom_{cyclone_version} = {cyclone_version}(conanfile, add_build={add_build}, add_tests={add_tests}) metadata_folder = conanfile.package_metadata_folder file_name = "sbom.cdx.json" with open(os.path.join(metadata_folder, file_name), 'w') as f: json.dump(sbom_{cyclone_version}, f, indent=4) ConanOutput().success(f"CYCLONEDX CREATED - {{conanfile.package_metadata_folder}}") """ # Using the sbom tool with "conan install" sbom_hook_post_generate = """ import json import os from conan.errors import ConanException from conan.api.output import ConanOutput from conan.tools.sbom import {cyclone_version} def post_generate(conanfile): sbom_{cyclone_version} = {cyclone_version}(conanfile, name={name}) generators_folder = conanfile.generators_folder file_name = "sbom.cdx.json" os.mkdir(os.path.join(generators_folder, "sbom")) with open(os.path.join(generators_folder, "sbom", file_name), 'w') as f: json.dump(sbom_{cyclone_version}, f, indent=4) ConanOutput().success(f"CYCLONEDX CREATED - {{conanfile.generators_folder}}") """ @pytest.mark.parametrize("cyclone_version", ["cyclonedx_1_4", "cyclonedx_1_6"]) class TestCyclonedx: @pytest.fixture() def hook_setup_post_package(self, cyclone_version): tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_package.format(cyclone_version=cyclone_version, add_build=True, add_tests=True)) return tc @pytest.fixture() def hook_setup_post_package_no_tool_requires(self, cyclone_version): tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_package.format(cyclone_version=cyclone_version, add_build=False, add_tests=True)) return tc @pytest.fixture() def hook_setup_post_package_no_test(self, cyclone_version): tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_package.format(cyclone_version=cyclone_version, add_build=True, add_tests=False)) return tc def test_sbom_generation_skipped_dependencies(self, hook_setup_post_package): tc = hook_setup_post_package tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0") .with_package_type("application") .with_requires("dep/1.0"), "conanfile.py": GenConanfile("foo", "1.0").with_tool_requires("app/1.0")}) tc.run("create dep") tc.run("create app") tc.run("create .") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = tc.load(cyclone_path) # A skipped dependency also shows up in the sbom assert "pkg:conan/dep@1.0?rref=6a99f55e933fb6feeb96df134c33af44" in content @pytest.mark.parametrize("lic, n", [('"simple"', 1), ('"multi1", "multi2"', 2), ('("tuple1", "tuple2")', 2)]) def test_multi_license(self, hook_setup_post_package, lic, n): tc = hook_setup_post_package conanfile = textwrap.dedent(f""" from conan import ConanFile class HelloConan(ConanFile): name = 'foo' version = '1.0' license = {lic} """) tc.save({"conanfile.py": conanfile}) tc.run("create .") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = json.loads(tc.load(cyclone_path)) assert len(content["components"][0]["licenses"]) == n @pytest.mark.parametrize("lic, keys", [('"Mit"', ["id"]), ('"custom_license name"', ["name"]), ('("mIT", "custom")', ["id", "name"])]) def test_license_spdx_valid(self, hook_setup_post_package, lic, keys): tc = hook_setup_post_package conanfile = textwrap.dedent(f""" from conan import ConanFile class HelloConan(ConanFile): name = 'foo' version = '1.0' license = {lic} """) tc.save({"conanfile.py": conanfile}) tc.run("create .") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = json.loads(tc.load(cyclone_path)) for i, l in enumerate(content["components"][0]["licenses"]): assert next(iter(l["license"])) == keys[i] def test_sbom_generation_no_tool_requires(self, hook_setup_post_package_no_tool_requires): tc = hook_setup_post_package_no_tool_requires tc.save({"app/conanfile.py": GenConanfile("app", "1.0") .with_package_type("application"), "conanfile.py": GenConanfile("foo", "1.0").with_tool_requires("app/1.0")}) tc.run("create app") tc.run("create .") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = tc.load(cyclone_path) assert "pkg:conan/app" not in content def test_sbom_generation_transitive_test_requires(self, hook_setup_post_package_no_test): tc = hook_setup_post_package_no_test tc.save({"test_re/conanfile.py": GenConanfile("test_re", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0") .with_package_type("application") .with_test_requires("test_re/1.0"), "conanfile.py": GenConanfile("foo", "1.0").with_tool_requires("app/1.0")}) tc.run("create test_re") tc.run("create app") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = tc.load(cyclone_path) assert "pkg:conan/test_re@1.0" not in content tc.run("create .") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = tc.load(cyclone_path) assert "pkg:conan/test_re@1.0" not in content def test_sbom_generation_dependency_test_require(self, hook_setup_post_package_no_test): tc = hook_setup_post_package_no_test tc.save({"special/conanfile.py": GenConanfile("special", "1.0"), "foo/conanfile.py": GenConanfile("foo", "1.0") .with_test_requires("special/1.0"), "conanfile.py": GenConanfile("bar", "1.0").with_tool_requires( "foo/1.0").with_require("special/1.0")}) tc.run("create special") tc.run("create foo") tc.run("create .") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = tc.load(cyclone_path) assert "pkg:conan/special@1.0" in content @pytest.fixture() def hook_setup_post_generate(self, cyclone_version): tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_generate.format(cyclone_version=cyclone_version, name=None)) return tc def test_sbom_generation_install_requires(self, hook_setup_post_generate): tc = hook_setup_post_generate tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": GenConanfile("foo", "1.0").with_requires("dep/1.0")}) tc.run("export dep") tc.run("create . --build=missing") # cli -> foo -> dep tc.run("install --requires=foo/1.0") assert os.path.exists(os.path.join(tc.current_folder, "sbom", "sbom.cdx.json")) def test_sbom_generation_install_path(self, hook_setup_post_generate): tc = hook_setup_post_generate tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": GenConanfile("foo", "1.0").with_requires("dep/1.0")}) tc.run("create dep") # foo -> dep tc.run("install .") assert os.path.exists(os.path.join(tc.current_folder, "sbom", "sbom.cdx.json")) def test_sbom_generation_install_path_consumer(self, hook_setup_post_generate): tc = hook_setup_post_generate tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": GenConanfile().with_requires("dep/1.0")}) tc.run("create dep") # conanfile.py -> dep tc.run("install .") assert os.path.exists(os.path.join(tc.current_folder, "sbom", "sbom.cdx.json")) def test_sbom_generation_install_path_txt(self, hook_setup_post_generate): tc = hook_setup_post_generate tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.txt": textwrap.dedent( """ [requires] dep/1.0 """ )}) tc.run("create dep") # foo -> dep tc.run("install .") assert os.path.exists(os.path.join(tc.current_folder, "sbom", "sbom.cdx.json")) def test_sbom_with_special_root_node(self, hook_setup_post_generate): # In this test, we have only one node in the subgraph, which has build context, so the number # of components after processing it is zero. tc = hook_setup_post_generate package_name = "foo" conanfile = textwrap.dedent(""" from conan import ConanFile class FooPackage(ConanFile): name = "foo" version = "1.0" package_type = "build-scripts" """) tc.save({"conanfile.py": conanfile}) tc.run("create .") create_layout = tc.created_layout() assert os.path.exists(os.path.join(create_layout.build(), "sbom", "sbom.cdx.json")) with open(os.path.join(create_layout.build(), "sbom", "sbom.cdx.json"), 'r') as file: sbom_json = json.load(file) assert package_name in sbom_json["metadata"]["component"]["name"] @pytest.mark.parametrize("name, result", [ ("None", "conan-sbom"), ('"custom-name"', "custom-name") ]) def test_sbom_generation_custom_name(self, cyclone_version, name, result): tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_generate.format(cyclone_version=cyclone_version, name=name)) tc.save({"conanfile.py": GenConanfile()}) tc.run("install .") assert os.path.exists(os.path.join(tc.current_folder, "sbom", "sbom.cdx.json")) assert f'"name": "{result}"' in tc.load( os.path.join(tc.current_folder, "sbom", "sbom.cdx.json")) def test_cyclonedx_check_content(self, cyclone_version): _sbom_hook_post_package = textwrap.dedent(""" import json import os from conan.errors import ConanException from conan.api.output import ConanOutput from conan.tools.sbom import {cyclone_version} def post_package(conanfile): sbom_cyclonedx= {cyclone_version}(conanfile) metadata_folder = conanfile.package_metadata_folder file_name = "sbom.cdx.json" with open(os.path.join(metadata_folder, file_name), 'w') as f: json.dump(sbom_cyclonedx, f, indent=4) ConanOutput().success(f"CYCLONEDX CREATED - {{conanfile.package_metadata_folder}}") """) tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, _sbom_hook_post_package.format(cyclone_version=cyclone_version)) conanfile_bar = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = 'bar' version = '1.0' author = 'conan-dev' package_type = 'application' """) conanfile_foo = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = 'foo' version = '1.0' author = 'conan-dev' package_type = 'application' def requirements(self): self.requires("bar/1.0") """) tc.save({"conanfile.py": conanfile_bar}) tc.run("create .") tc.save({"conanfile.py": conanfile_foo}) tc.run("create .") create_layout = tc.created_layout() cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") content = tc.load(cyclone_path) content_json = json.loads(content) if cyclone_version == 'cyclonedx_1_4': assert content_json["metadata"]["component"]["author"] == 'conan-dev' assert content_json["metadata"]["component"]["type"] == 'application' assert content_json["metadata"]["tools"][0] assert content_json["components"][0]["author"] == 'conan-dev' assert content_json["components"][0]["type"] == 'application' elif cyclone_version == 'cyclonedx_1_6': assert not content_json["metadata"]["component"].get("author") assert content_json["metadata"]["component"]["authors"][0]["name"] == 'conan-dev' assert content_json["metadata"]["component"]["type"] == 'application' assert content_json["metadata"]["tools"]["components"][0] assert not content_json["components"][0].get("author") assert content_json["components"][0]["authors"][0]["name"] == 'conan-dev' assert content_json["components"][0]["type"] == 'application' def test_sbom_test_requires_skipped(self, cyclone_version): tc = TestClient(light=True) hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") save(hook_path, sbom_hook_post_generate.format(cyclone_version=cyclone_version, name=None, add_build=False, add_tests=False)) tc.save({"dep/conanfile.py": GenConanfile("mydep", "1.0"), "conanfile.py": GenConanfile("foo", "1.0").with_test_requires("mydep/1.0")}) tc.run("create dep") tc.run("install .") content = tc.load("sbom/sbom.cdx.json") assert "mydep" not in content @pytest.mark.parametrize("install", ["--requires=foo/1.0", ""]) def test_sbom_deployer(self, cyclone_version, install): tc = TestClient(light=True) tc.save({"dep/conanfile.py": GenConanfile("mydep", "1.0"), "conanfile.py": GenConanfile("foo", "1.0").with_requires("mydep/1.0")}) tc.run("create dep") tc.run("create") method = { "cyclonedx_1_4": "1.4", "cyclonedx_1_6": "1.6", }.get(cyclone_version) tc.run(f"install {install} --deployer=cyclone_{method}") assert os.path.exists(os.path.join(tc.current_folder, f"sbom-cyclonedx-{method}.json")) ================================================ FILE: test/integration/settings/__init__.py ================================================ ================================================ FILE: test/integration/settings/built_type_setting_test.py ================================================ from conan.test.utils.tools import TestClient class TestBuildTypeSetting: def test_build_type(self): # https://github.com/conan-io/conan/issues/2500 client = TestClient() conanfile = """from conan import ConanFile class Pkg(ConanFile): settings = "build_type" def build(self): self.output.info("BUILD TYPE: %s" % (self.settings.build_type or "Not defined")) """ test_conanfile = """from conan import ConanFile class Pkg(ConanFile): settings = "build_type" def requirements(self): self.requires(self.tested_reference_str) def build(self): self.output.info("BUILD TYPE: %s" % (self.settings.build_type or "Not defined")) def test(self): pass """ client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile, "myprofile": ""}) # This won't fail, as it has a build_type=None, which is allowed client.run("export . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("install --requires=pkg/0.1@lasote/testing -pr=myprofile --build='*'") assert 1 == str(client.out).count("BUILD TYPE: Not defined") # test_package is totally consinstent with the regular package client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing -pr=myprofile") assert 2 == str(client.out).count("BUILD TYPE: Not defined") client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile, "myprofile": "[settings]\nbuild_type=Release"}) client.run("export . --name=pkg --version=0.1 --user=user --channel=testing") client.run("install --requires=pkg/0.1@lasote/testing -pr=myprofile --build='*'") assert 1 == str(client.out).count("BUILD TYPE: Release") client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing -pr=myprofile") assert 2 == str(client.out).count("BUILD TYPE: Release") # Explicit build_tyep=None is NOT allowed, it is not a valid value client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile, "myprofile": "[settings]\nbuild_type=None"}) client.run("export . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("install --requires=pkg/0.1@lasote/testing -pr=myprofile --build='*'", assert_error=True) assert "ERROR: Invalid setting 'None' is not a valid 'settings.build_type'" in client.out assert "Possible values are ['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel']" in \ client.out ================================================ FILE: test/integration/settings/per_package_settings_test.py ================================================ import json import textwrap from conan.test.utils.tools import TestClient, GenConanfile class TestPerPackageSetting: def test_per_package_setting(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -s os=Windows") client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1@user/testing")}) client.run("create . --name=consumer --version=0.1 --user=user --channel=testing -s os=Linux -s pkg*:os=Windows") assert "consumer/0.1@user/testing: Created package" in client.out def test_per_package_setting_build_type(self): """ comes from https://github.com/conan-io/conan/pull/9842 In Conan 1.X there was a weird behavior if you used different patterns, only first matching one win. This was broken: -s pkg*:os=Windows -s *model:build_type=Debug keeps the build_type=Release, because pkg* matches, assign os=Windows and stops """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg-model" version = "0.1" settings = "os", "build_type" def build(self): self.output.info("BUILDTYPE {}:{}!!!!".format(self.settings.os, self.settings.build_type)) """) client.save({"conanfile.py": conanfile}) # Different pattern, no issue, works now client.run("create . -s *:os=Windows -s *-model*:build_type=Debug -s build_type=Release") assert "pkg-model/0.1: BUILDTYPE Windows:Debug!!!!" in client.out client.save({"conanfile.py": GenConanfile("consumer", "0.1").with_require("pkg-model/0.1")}) client.run('create . -s *:os=Linux -s *-model*:os=Windows ' '-s "*-model*:build_type=Debug" -s build_type=Release --build=*') assert "pkg-model/0.1: BUILDTYPE Windows:Debug!!!!" in client.out assert "consumer/0.1: Created package" in client.out def test_per_package_setting_no_userchannel(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1 -s os=Windows") client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1")}) client.run("create . --name=consumer --version=0.1 -s os=Linux -s pkg*:os=Windows") assert "consumer/0.1: Created package" in client.out def test_per_package_subsetting(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler" """) client.save({"conanfile.py": conanfile}) settings = "-s os=Linux -s compiler=gcc -s compiler.version=5" client.run("create . --name=pkg --version=0.1 --user=user --channel=testing %s -s compiler.libcxx=libstdc++11" % settings) client.save({"conanfile.py": GenConanfile().with_require("pkg/0.1@user/testing")}) client.run("create . --name=consumer --version=0.1 --user=user --channel=testing %s -s compiler.libcxx=libstdc++ " "-s pkg*:compiler.libcxx=libstdc++11" % settings) assert "consumer/0.1@user/testing: Created package" in client.out def test_per_package_setting_all_packages_without_user_channel(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def configure(self): self.output.info(f"I am a {self.settings.os} pkg!!!") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg1 --version=0.1 -s os=Windows") client.run("create . --name=pkg2 --version=0.1 --user=user -s os=Linux") client.run("create . --name=pkg3 --version=0.1 --user=user --channel=channel -s os=Linux") client.save({"conanfile.py": GenConanfile().with_requires("pkg1/0.1", "pkg2/0.1@user", "pkg3/0.1@user/channel")}) client.run("install . -s os=Linux -s *@:os=Windows") assert "pkg1/0.1: I am a Windows pkg!!!" in client.out assert "pkg2/0.1@user: I am a Linux pkg!!!" in client.out assert "pkg3/0.1@user/channel: I am a Linux pkg!!!" in client.out def test_per_package_settings_target(): c = TestClient() gcc = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "gcc" version = "0.1" settings = "arch" def configure(self): self.settings_target.rm_safe("os") self.settings_target.rm_safe("compiler") self.settings_target.rm_safe("build_type") def package_id(self): self.info.settings_target = self.settings_target """) c.save({"conanfile.py": gcc}) c.run("create . -s:b arch=x86_64 -s:h arch=armv7 --build-require") pkg_id = c.created_package_id("gcc/0.1") c.run("install --tool-requires=gcc/0.1 -s:b arch=x86_64 -s arch=armv8 -s:h gcc*:arch=armv7" " --format=json") # it will not fail due to armv8, but use the binary for armv7 c.assert_listed_binary({"gcc/0.1": (pkg_id, "Cache")}, build=True) graph = json.loads(c.stdout) assert graph["graph"]["nodes"]["1"]["info"]["settings_target"] == {"arch": "armv7"} def test_per_package_settings_build(): c = TestClient() cmake = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "cmake" version = "0.1" settings = "build_type" def build(self): self.output.info(f"Building myself with {self.settings.build_type}!!") """) c.save({"conanfile.py": cmake}) c.run("export .") c.run("install --tool-requires=cmake/0.1 -s:b build_type=Release -s:b cmake*:build_type=Debug " "--build=missing") assert "cmake/0.1: Building myself with Debug!!" in c.out def test_package_settings_mixed_patterns(): # https://github.com/conan-io/conan/issues/16606 c = TestClient() profile = textwrap.dedent(""" [settings] arch=x86_64 *@test/*:build_type=Release os=Linux compiler=gcc &:compiler.version=12 compiler.libcxx = libstdc++ """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "mypkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" def build(self): self.output.info(f"BUILD_TYPE={self.settings.build_type}!") self.output.info(f"COMPILER_VERSION={self.settings.compiler.version}!") self.output.info(f"COMPILER_LIBCXX={self.settings.compiler.libcxx}!") """) c.save({"profile": profile, "conanfile.py": conanfile}) c.run("create . -pr=profile --user=test --channel=test") assert "mypkg/0.1@test/test: BUILD_TYPE=Release!" in c.out assert "mypkg/0.1@test/test: COMPILER_VERSION=12!" in c.out assert "mypkg/0.1@test/test: COMPILER_LIBCXX=libstdc++!" in c.out def test_package_settings_negate_patterns(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): version = "0.1" settings = "os" def build(self): self.output.info(f"BUILD OS={self.settings.os}!!!") """) c.save({"dep/conanfile.py": conanfile, "pkg/conanfile.py": GenConanfile().with_requires("dep1/0.1", "dep2/0.1", "dep3/0.1")}) c.run("export dep --name=dep1") c.run("export dep --name=dep2") c.run("export dep --name=dep3") c.run("install pkg --build=* -s os=Linux -s ~dep1/*:os=Macos") assert "dep1/0.1: BUILD OS=Linux!!!" in c.out assert "dep2/0.1: BUILD OS=Macos!!!" in c.out assert "dep3/0.1: BUILD OS=Macos!!!" in c.out # Order doesn't matter if using the base "os=Linux" c.run("install pkg --build=* -s ~dep1/*:os=Macos -s os=Linux") assert "dep1/0.1: BUILD OS=Linux!!!" in c.out assert "dep2/0.1: BUILD OS=Macos!!!" in c.out assert "dep3/0.1: BUILD OS=Macos!!!" in c.out # Order DOES matter if using the pattern "*:os=Linux" c.run("install pkg --build=* -s ~dep1/*:os=Macos -s *:os=Linux") assert "dep1/0.1: BUILD OS=Linux!!!" in c.out assert "dep2/0.1: BUILD OS=Linux!!!" in c.out assert "dep3/0.1: BUILD OS=Linux!!!" in c.out # dep3 comes later, works c.run("install pkg --build=* -s os=Linux -s ~dep1/*:os=Macos -s dep3/*:os=Windows") assert "dep1/0.1: BUILD OS=Linux!!!" in c.out assert "dep2/0.1: BUILD OS=Macos!!!" in c.out assert "dep3/0.1: BUILD OS=Windows!!!" in c.out # dep3 comes first, then last !dep1 pattern prevails c.run("install pkg --build=* -s os=Linux -s dep3/*:os=Windows -s ~dep1/*:os=Macos") assert "dep1/0.1: BUILD OS=Linux!!!" in c.out assert "dep2/0.1: BUILD OS=Macos!!!" in c.out assert "dep3/0.1: BUILD OS=Macos!!!" in c.out ================================================ FILE: test/integration/settings/remove_subsetting_test.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient from conan.internal.util.files import mkdir class TestRemoveSubsetting: def test_remove_options(self): # https://github.com/conan-io/conan/issues/2327 # https://github.com/conan-io/conan/issues/2781 client = TestClient() conanfile = """from conan import ConanFile class Pkg(ConanFile): options = {"opt1": [True, False], "opt2": [True, False]} default_options = {"opt1": True, "opt2": False} def config_options(self): del self.options.opt2 def build(self): assert "opt2" not in self.options self.options.opt2 """ client.save({"conanfile.py": conanfile}) build_folder = os.path.join(client.current_folder, "build") mkdir(build_folder) client.current_folder = build_folder client.run("install ..") client.run("build ..", assert_error=True) assert "ConanException: option 'opt2' doesn't exist" in client.out assert "Possible options are ['opt1']" in client.out def test_remove_setting(self): # https://github.com/conan-io/conan/issues/2327 client = TestClient() conanfile = """from conan import ConanFile class Pkg(ConanFile): settings = "os", "build_type" def configure(self): del self.settings.build_type def build(self): self.settings.build_type """ client.save({"conanfile.py": conanfile}) build_folder = os.path.join(client.current_folder, "build") mkdir(build_folder) client.current_folder = build_folder client.run("build ..", assert_error=True) assert "'settings.build_type' doesn't exist" in client.out def test_settings_and_options_rm_safe(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "build_type", "compiler" options = {"opt1": [True, False], "opt2": [True, False]} default_options = {"opt1": "True", "opt2": "False"} def configure(self): # setting self.settings.rm_safe("build_type") # sub-setting self.settings.rm_safe("compiler.version") # wrong settings self.settings.rm_safe("fake_field") self.settings.rm_safe("fake_field.version") def config_options(self): # option self.options.rm_safe("opt2") # wrong option self.options.rm_safe("opt15") def build(self): try: self.settings.build_type except Exception as exc: self.output.warning(str(exc)) try: self.settings.compiler.version except Exception as exc: self.output.warning(str(exc)) try: self.options.opt2 except Exception as exc: self.output.warning(str(exc)) assert "opt2" not in self.options """) client.save({"conanfile.py": conanfile}) client.run("install .") client.run("build .") assert "'settings.build_type' doesn't exist" in client.out assert "'settings' possible configurations are ['compiler', 'os']" in client.out assert "'settings.compiler.version' doesn't exist" in client.out assert "'settings.compiler' possible configurations are [" in client.out assert "option 'opt2' doesn't exist" in client.out assert "Possible options are ['opt1']" in client.out ================================================ FILE: test/integration/settings/settings_override_test.py ================================================ import os import pytest from conan.api.model import RecipeReference from conan.internal.paths import CONANINFO from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import load @pytest.fixture() def client(): client = TestClient() conanfile = GenConanfile("mingw", "0.1").with_settings("compiler") build_msg = """ def build(self): self.output.warning("COMPILER=> %s %s" % (self.name, str(self.settings.compiler))) """ client.save({"conanfile.py": str(conanfile) + build_msg}) client.run("export . --user=lasote --channel=testing") conanfile = GenConanfile("visual", "0.1").with_requires("mingw/0.1@lasote/testing") conanfile.with_settings("compiler") client.save({"conanfile.py": str(conanfile) + build_msg}) client.run("export . --user=lasote --channel=testing") return client def test_override(client): client.run("install --requires=visual/0.1@lasote/testing --build missing -s compiler=msvc " "-s compiler.version=191 -s compiler.runtime=dynamic " "-s mingw*:compiler='gcc' -s mingw*:compiler.libcxx='libstdc++' " "-s mingw*:compiler.version=4.8") assert "COMPILER=> mingw gcc" in client.out assert "COMPILER=> visual msvc" in client.out # CHECK CONANINFO FILE latest_rrev = client.cache.get_latest_recipe_revision( RecipeReference.loads("mingw/0.1@lasote/testing")) pkg_ids = client.cache.get_package_references(latest_rrev) latest_prev = client.cache.get_latest_package_revision(pkg_ids[0]) package_path = client.cache.pkg_layout(latest_prev).package() conaninfo = load(os.path.join(package_path, CONANINFO)) assert "compiler=gcc" in conaninfo # CHECK CONANINFO FILE package_path = client.created_layout().package() conaninfo = load(os.path.join(package_path, CONANINFO)) assert "compiler=msvc" in conaninfo assert "compiler.version=191" in conaninfo def test_non_existing_setting(client): client.run("install --requires=visual/0.1@lasote/testing --build missing -s compiler=msvc " "-s compiler.version=191 -s compiler.runtime=dynamic " "-s mingw/*:missingsetting='gcc' ", assert_error=True) assert "settings.missingsetting' doesn't exist" in client.out def test_override_in_non_existing_recipe(client): client.run("install --requires=visual/0.1@lasote/testing --build missing -s compiler=msvc " "-s compiler.version=191 -s compiler.runtime=dynamic " "-s MISSINGID:compiler='gcc' ") assert "COMPILER=> mingw msvc" in client.out assert "COMPILER=> visual msvc" in client.out def test_exclude_patterns_settings(): client = TestClient() gen = GenConanfile().with_settings("build_type") client.save({"zlib/conanfile.py": gen}) client.save({"openssl/conanfile.py": gen.with_require("zlib/1.0")}) client.save({"consumer/conanfile.py": gen.with_require("openssl/1.0")}) client.run("create zlib --name zlib --version 1.0") client.run("create openssl --name openssl --version 1.0") # We miss openssl and zlib debug packages client.run("install consumer -s build_type=Debug", assert_error=True) assert "ERROR: Missing prebuilt package for 'openssl/1.0', 'zlib/1.0'" in client.out # All except zlib are Release, the only missing is zlib debug client.run("install consumer -s build_type=Debug " " -s !zlib*:build_type=Release", assert_error=True) assert "ERROR: Missing prebuilt package for 'zlib/1.0'" # All the packages matches !potato* so all are Release client.run("install consumer -s build_type=Debug -s !potato*:build_type=Release") # All the packages except the consumer are Release, but we are creating consumer in Debug client.run("create consumer --name=consumer --version=1.0 " "-s=build_type=Debug -s=!&:build_type=Release") client.run("install --requires consumer/1.0 -s consumer/*:build_type=Debug") # Priority between package scoped settings client.run('remove consumer/*#* -c') client.run("install --reference consumer/1.0 -s build_type=Debug", assert_error=True) # Pre-check, there is no Debug package for any of them assert "ERROR: Missing prebuilt package for 'consumer/1.0', 'openssl/1.0', 'zlib/1.0'" # Pre-check there are Release packages client.run("create consumer --name=consumer --version=1.0 -s build_type=Release") # Try to install with this two scoped conditions, This is OK the right side has priority client.run("install --requires consumer/1.0 -s zlib/*:build_type=Debug -s *:build_type=Release") # Try to install with this two scoped conditions, This is ERROR the right side has priority client.run("install --requires consumer/1.0 -s *:build_type=Release -s zlib/*:build_type=Debug", assert_error=True) assert "ERROR: Missing prebuilt package for 'zlib/1.0'" in client.out # Try to install with this two scoped conditions, This is OK again, the right side has priority # The z* points to Release later, so zlib in Release client.run("install --requires consumer/1.0 -s *:build_type=Release " "-s zlib/*:build_type=Debug -s z*:build_type=Release") # Try to install with this two scoped conditions, This is OK again, the right side has priority # No package is potato, so all packages in Release client.run("install --requires consumer/1.0 -s !zlib:build_type=Debug " "-s !potato:build_type=Release") ================================================ FILE: test/integration/settings/test_disable_settings_assignment.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_disable_settings_assignment(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def generate(self): self.settings.os = "FreeBSD" """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "Tried to define 'os' setting inside recipe" in c.out ================================================ FILE: test/integration/settings/test_non_defining_settings.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_settings_not_defined_consuming(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "arch" def build(self): self.output.info(f"Building with os={self.settings.os}") self.output.info(f"Building with arch={self.settings.arch}") def package_id(self): del self.info.settings.os del self.info.settings.arch """) c.save({"conanfile.py": conanfile, "profile": ""}) c.run("create . -pr=profile -s os=Windows -s arch=armv8") assert "Building with os=Windows" in c.out assert "Building with arch=armv8" in c.out c.run("install --requires=pkg/1.0 -pr=profile") # It doesn't fail, even if settings not defined c.run("install --requires=pkg/1.0 -pr=profile -s os=Linux -s arch=x86") # It doesn't fail, even if settings different value def test_settings_undefined(): client = TestClient() client.save({ "conanfile.py": GenConanfile(name="hello", version="1.0") }) # Undefined settings field client.run("install . -s foo=None", assert_error=True) assert "'settings.foo' doesn't exist for 'settings'" in client.out client.run("install . -s foo.bar=None", assert_error=True) assert "'settings.foo' doesn't exist for 'settings'" in client.out ================================================ FILE: test/integration/settings/test_settings_possible_values.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_settings_definitions(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os" def generate(self): definition = self.settings.possible_values() assert "os" in definition assert "compiler" not in definition assert "arch" not in definition self.output.info(", ".join(definition["os"])) ios = definition["os"]["iOS"]["version"] self.output.info(f"iOS: {ios}") os_definition = self.settings.os.possible_values() ios = os_definition["iOS"]["version"] self.output.info(f"iOS2: {ios}") """) c.save({"conanfile.py": conanfile}) # New settings are there c.run("install . -s os=Linux") assert "conanfile.py: Windows, WindowsStore, WindowsCE, Linux," in c.out assert "conanfile.py: iOS: ['7.0', '7.1', '8.0'," in c.out assert "conanfile.py: iOS2: ['7.0', '7.1', '8.0'," in c.out def test_settings_definitions_compiler(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "compiler" def generate(self): definition = self.settings.compiler.version.possible_values() self.output.info("HOST: " + ", ".join(definition)) definition = self.settings_build.compiler.version.possible_values() self.output.info("BUILD: " + ", ".join(definition)) cppstds = self.settings.compiler.cppstd.possible_values() self.output.info("CPPSTDS: " + str(cppstds)) """) profile = textwrap.dedent("""\ [settings] compiler=msvc compiler.version=192 compiler.runtime=dynamic """) c.save({"conanfile.py": conanfile, "profile": profile}) # New settings are there c.run("install . -pr=profile -s:b compiler=gcc") assert "conanfile.py: HOST: 170, 180, 190, 191, 192" in c.out assert "conanfile.py: BUILD: 4.1, 4.4, 4.5, 4.6, 4.7," in c.out assert "conanfile.py: CPPSTDS: [None, '14', '17', '20', '23']" in c.out ================================================ FILE: test/integration/settings/test_settings_user.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save def test_settings_user(): c = TestClient() settings_user = textwrap.dedent("""\ os: Windows: subsystem: [new_sub] Linux: new_versions: ["a", "b", "c"] new_os: new_global: ["42", "21"] """) save(os.path.join(c.cache_folder, "settings_user.yml"), settings_user) c.save({"conanfile.py": GenConanfile().with_settings("os").with_settings("new_global")}) # New settings are there c.run("install . -s os=Windows -s os.subsystem=new_sub -s new_global=42") assert "new_global=42" in c.out assert "os.subsystem=new_sub" in c.out # Existing values of subsystem are still there c.run("install . -s os=Windows -s os.subsystem=msys2 -s new_global=42") assert "new_global=42" in c.out assert "os.subsystem=msys2" in c.out # Completely new values, not appended, but new, are there c.run("install . -s os=Linux -s os.new_versions=a -s new_global=42") assert "new_global=42" in c.out assert "os.new_versions=a" in c.out # Existing values of OSs are also there c.run("install . -s os=Macos -s new_global=42") assert "os=Macos" in c.out assert "new_global=42" in c.out def test_settings_user_subdict(): c = TestClient() settings_user = textwrap.dedent("""\ other_new: other1: other2: version: [1, 2, 3] """) save(os.path.join(c.cache_folder, "settings_user.yml"), settings_user) c.save({"conanfile.py": GenConanfile().with_settings("other_new")}) c.run("install . -s other_new=other1") assert "other_new=other1" in c.out c.run("install . -s other_new=other2 -s other_new.version=2") assert "other_new=other2" in c.out assert "other_new.version=2" in c.out def test_settings_user_convert_list_dict(): c = TestClient() settings_user = textwrap.dedent("""\ arch: x86: subarch32: a: version: ["a1", "a2"] b: variant: ["b1", "b2"] x86_64: subarch: [1, 2, 3] """) save(os.path.join(c.cache_folder, "settings_user.yml"), settings_user) c.save({"conanfile.py": GenConanfile().with_settings("arch")}) # check others are maintained c.run("install . -s arch=armv8") assert "arch=armv8" in c.out c.run("install . -s arch=x86 -s arch.subarch32=a -s arch.subarch32.version=a1") assert "arch=x86" in c.out assert "arch.subarch32=a" in c.out assert "arch.subarch32.version=a1" in c.out c.run("install . -s arch=x86_64 -s arch.subarch=2 ") assert "arch=x86_64" in c.out assert "arch.subarch=2" in c.out def test_settings_user_error(): c = TestClient() settings_user = textwrap.dedent("""\ os: Windows: libc: null """) save(os.path.join(c.cache_folder, "settings_user.yml"), settings_user) c.run("profile show", assert_error=True) assert "ERROR: Definition of settings.yml 'settings.os.libc' cannot be null" in c.out def test_settings_user_breaking_universal_binaries(): # If you had a settings_user.yml with a custom architecture wit will error # in the Apple block of CMakeToolchain # https://github.com/conan-io/conan/issues/16086#issuecomment-2059118224 c = TestClient() settings_user = textwrap.dedent("""\ arch: [universal] """) save(os.path.join(c.cache_folder, "settings_user.yml"), settings_user) c.save({"conanfile.py": GenConanfile().with_settings("os").with_settings("arch").with_generator("CMakeToolchain")}) c.run('install . -s="arch=universal"') assert "CMakeToolchain generated: conan_toolchain.cmake" in c.out ================================================ FILE: test/integration/symlinks/__init__.py ================================================ ================================================ FILE: test/integration/symlinks/symlinks_test.py ================================================ import os import platform import textwrap import pytest from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient, TestServer from conan.internal.util.files import load, rmdir, chdir, save links_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class HelloConan(ConanFile): name = "hello" version = "0.1" exports_sources = "*" def package(self): copy(self, "*", self.build_folder, self.package_folder) """) @pytest.mark.skipif(platform.system() == "Windows", reason="Requires Symlinks") def test_complete_round_trip(): """ a full round trip with a in package symlink, that is maintained all the way to the server and re-deployed in the client the symlink is relative just "link.txt" -> "midlink.txt" -> "target.txt" (local) """ c = TestClient(default_server_user=True) c.save({"conanfile.py": links_conanfile, "target.txt": "hello world!"}) os.symlink("target.txt", os.path.join(c.current_folder, "midlink.txt")) os.symlink("midlink.txt", os.path.join(c.current_folder, "link.txt")) assert c.load("link.txt") == "hello world!" c.run("create .") def checker(folder): with chdir(folder): assert os.path.exists("target.txt") assert os.readlink("link.txt") == "midlink.txt" assert os.readlink("midlink.txt") == "target.txt" assert load("link.txt") == "hello world!" ref = RecipeReference.loads("hello/0.1") checker(c.get_latest_ref_layout(ref).source()) pref = c.get_latest_package_reference(ref) pkg_layout = c.get_latest_pkg_layout(pref) checker(pkg_layout.build()) checker(pkg_layout.package()) c.run("upload * -r=default -c") c.run("remove * -c") c.save({}, clean_first=True) c.run("install --requires=hello/0.1 --deployer=full_deploy") checker(os.path.join(c.current_folder, "full_deploy", "host", "hello", "0.1")) @pytest.mark.skipif(platform.system() == "Windows", reason="Requires Symlinks") def test_complete_round_trip_broken_link(): """ same as above but with a broken one link.txt->midlink.txt->(broken) """ c = TestClient(default_server_user=True) c.save({"conanfile.py": links_conanfile}) os.symlink("target.txt", os.path.join(c.current_folder, "midlink.txt")) os.symlink("midlink.txt", os.path.join(c.current_folder, "link.txt")) c.run("create .") def checker(folder): with chdir(folder): assert not os.path.exists("target.txt") assert os.readlink("link.txt") == "midlink.txt" assert os.readlink("midlink.txt") == "target.txt" ref = RecipeReference.loads("hello/0.1") checker(c.get_latest_ref_layout(ref).source()) pref = c.get_latest_package_reference(ref) pkg_layout = c.get_latest_pkg_layout(pref) checker(pkg_layout.build()) checker(pkg_layout.package()) c.run("upload * -r=default -c") c.run("remove * -c") c.save({}, clean_first=True) c.run("install --requires=hello/0.1 --deployer=full_deploy") checker(os.path.join(c.current_folder, "full_deploy", "host", "hello", "0.1")) @pytest.mark.skipif(platform.system() == "Windows", reason="Requires Symlinks") def test_complete_round_trip_external_link(): """ same as above but with a broken one link.txt->midlink.txt->/abs/path/to/target.txt """ c = TestClient(default_server_user=True) target = os.path.join(temp_folder(), "target.txt") save(target, "foo") c.save({"conanfile.py": links_conanfile}) os.symlink(target, os.path.join(c.current_folder, "midlink.txt")) os.symlink("midlink.txt", os.path.join(c.current_folder, "link.txt")) c.run("create .") def checker(folder): with chdir(folder): assert "target.txt" not in os.listdir(".") assert not os.path.exists("target.txt") assert os.readlink("link.txt") == "midlink.txt" assert os.readlink("midlink.txt") == target assert load("link.txt") == "foo" ref = RecipeReference.loads("hello/0.1") checker(c.get_latest_ref_layout(ref).source()) pref = c.get_latest_package_reference(ref) pkg_layout = c.get_latest_pkg_layout(pref) checker(pkg_layout.build()) checker(pkg_layout.package()) c.run("upload * -r=default -c") c.run("remove * -c") c.save({}, clean_first=True) c.run("install --requires=hello/0.1 --deployer=full_deploy") checker(os.path.join(c.current_folder, "full_deploy", "host", "hello", "0.1")) @pytest.mark.skipif(platform.system() == "Windows", reason="Requires Symlinks") def test_complete_round_trip_folders(): """ similar to above, but with 2 folder symlinks and one file symlink # Reproduces issue: https://github.com/conan-io/conan/issues/5329 """ c = TestClient(default_server_user=True) c.save({"conanfile.py": links_conanfile, "src/framework/Versions/v1/headers/content": "myheader!", "src/framework/Versions/v1/file": "myfile!"}) # Add two levels of symlinks os.symlink('v1', os.path.join(c.current_folder, 'src', 'framework', 'Versions', 'Current')) os.symlink('Versions/Current/headers', os.path.join(c.current_folder, 'src', 'framework', 'headers')) os.symlink('Versions/Current/file', os.path.join(c.current_folder, 'src', 'framework', 'file')) c.run("create .") def checker(folder): with chdir(folder): assert os.readlink("src/framework/Versions/Current") == "v1" assert os.readlink("src/framework/headers") == "Versions/Current/headers" assert os.readlink("src/framework/file") == "Versions/Current/file" assert os.path.exists("src/framework/Versions/v1/headers/content") assert os.path.exists("src/framework/Versions/v1/file") assert load("src/framework/file") == "myfile!" assert load("src/framework/headers/content") == "myheader!" ref = RecipeReference.loads("hello/0.1") checker(c.get_latest_ref_layout(ref).source()) pref = c.get_latest_package_reference(ref) pkg_layout = c.get_latest_pkg_layout(pref) checker(pkg_layout.build()) checker(pkg_layout.package()) c.run("upload * -r=default -c") c.run("remove * -c") c.save({}, clean_first=True) c.run("install --requires=hello/0.1 --deployer=full_deploy") checker(os.path.join(c.current_folder, "full_deploy", "host", "hello", "0.1")) @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux") @pytest.mark.parametrize("package_files", [{"files": ["foo/bar/folder/file.txt", "foo/bar/folder/other/other_file.txt"], "symlinks": [("../file.txt", "foo/bar/folder/other/file2.txt")]}, # relative ../ symlink {"files": ["foo/bar/file/file.txt"], "symlinks": [(temp_folder(), "foo/symlink_folder")]}, # absolute symlink {"files": ["folder/file.txt"], "symlinks": [("folder", "folder2"), ("file.txt", "folder/file2.txt")]}, # single level symlink {"files": ["foo/bar/file/file.txt"], "symlinks": [("bar/file", "foo/symlink_folder"), ("foo/symlink_folder/file.txt", "file2.txt")]}, # double level symlink ]) def test_package_with_symlinks(package_files): client = TestClient(default_server_user=True) client2 = TestClient(servers=client.servers) client.save({"conanfile.py": links_conanfile}) for path in package_files["files"]: client.save({path: "foo contents"}) for link_dest, link_file in package_files["symlinks"]: os.symlink(link_dest, os.path.join(client.current_folder, link_file)) client.run("create .") def assert_folder_symlinks(base_folder): with chdir(base_folder): for f in package_files["files"]: assert os.path.exists(f) for link_dst, link in package_files["symlinks"]: assert os.readlink(link) == link_dst if os.path.isfile(link): assert load(link) == "foo contents" # Check exported sources are there ref_layout = client.exported_layout() assert_folder_symlinks(ref_layout.export_sources()) assert_folder_symlinks(ref_layout.source()) # Check files have been copied to the build pkg_layout = client.created_layout() assert_folder_symlinks(pkg_layout.build()) assert_folder_symlinks(pkg_layout.package()) client.run("upload '*' -c -r default") client.run("remove * -c") # Client 2 install client2.run("install --requires=hello/0.1 --deployer=full_deploy") ref = client2.get_latest_ref_layout(RecipeReference.loads("hello/0.1")) pref = PkgReference(ref.reference, "da39a3ee5e6b4b0d3255bfef95601890afd80709") # Check package files are there package_folder = client2.get_latest_pkg_layout(pref).package() assert_folder_symlinks(package_folder) assert_folder_symlinks(os.path.join(client2.current_folder, "full_deploy", "host", "hello", "0.1")) @pytest.mark.skipif(platform.system() == "Windows", reason="Symlinks not in Windows") def test_exports_does_not_follow_symlink(): tmp = temp_folder() linked_abs_folder = tmp save(os.path.join(tmp, "source.cpp"), "foo") client = TestClient(default_server_user=True) conanfile = GenConanfile("lib", "1.0")\ .with_package('copy(self, "*", self.source_folder, self.package_folder)')\ .with_exports_sources("*")\ .with_import("from conan.tools.files import copy") client.save({"conanfile.py": conanfile, "foo.txt": "bar"}) os.symlink(linked_abs_folder, os.path.join(client.current_folder, "linked_folder")) client.run("create . ") exports_sources_folder = client.exported_layout().export_sources() assert os.path.islink(os.path.join(exports_sources_folder, "linked_folder")) assert os.path.exists(os.path.join(exports_sources_folder, "linked_folder", "source.cpp")) # Check files have been copied to the build build_folder = client.created_layout().build() assert os.path.islink(os.path.join(build_folder, "linked_folder")) assert os.path.exists(os.path.join(build_folder, "linked_folder", "source.cpp")) # Check package files are there package_folder = client.created_layout().package() assert os.path.islink(os.path.join(package_folder, "linked_folder")) assert os.path.exists(os.path.join(package_folder, "linked_folder", "source.cpp")) # Check that the manifest doesn't contain the symlink nor the source.cpp contents = load(os.path.join(package_folder, "conanmanifest.txt")) assert "foo.txt" in contents assert "linked_folder" not in contents assert "source.cpp" not in contents # Now is a broken link, but the files are not in the cache, just a broken link rmdir(linked_abs_folder) assert not os.path.exists(os.path.join(exports_sources_folder, "linked_folder", "source.cpp")) assert not os.path.exists(os.path.join(build_folder, "linked_folder", "source.cpp")) assert not os.path.exists(os.path.join(package_folder, "linked_folder", "source.cpp")) @pytest.mark.skipif(platform.system() == "Windows", reason="Symlinks not in Windows") @pytest.mark.parametrize("uppercase", [False, True]) def test_exports_with_uppercase_symlink_folder(uppercase): # BaseFolder # |- Folder # |- source.cpp # |- Symlink (Points to "./Folder") base_dir_name = "BaseFolder" if uppercase else "basefolder" client = TestClient(default_server_user=True) conanfile = GenConanfile("lib", "1.0")\ .with_package('copy(self, "*", self.source_folder, self.package_folder, ignore_case=False)')\ .with_exports_sources(base_dir_name + "/*")\ .with_import("from conan.tools.files import copy") folder_dir_name = "Folder" if uppercase else "folder" folder_dir_path = os.path.join(base_dir_name, folder_dir_name) symlink_name = "SymLink" if uppercase else "symlink" symlink_path = os.path.join(base_dir_name, symlink_name) dummy_file_name = "source.cpp" dummy_file_path = os.path.join(folder_dir_path, dummy_file_name) client.save({"conanfile.py": conanfile, dummy_file_path: "foo"}) os.symlink(folder_dir_name, os.path.join(client.current_folder, symlink_path)) client.run("create . ") exports_sources_folder = client.exported_layout().export_sources() assert os.path.islink(os.path.join(exports_sources_folder, symlink_path)) assert os.path.exists(os.path.join(exports_sources_folder, symlink_path, dummy_file_name)) # Check files have been copied to the build build_folder = client.created_layout().build() assert os.path.islink(os.path.join(build_folder, symlink_path)) assert os.path.exists(os.path.join(build_folder, symlink_path, dummy_file_name)) # Check package files are there package_folder = client.created_layout().package() assert os.path.islink(os.path.join(package_folder, symlink_path)) assert os.path.exists(os.path.join(package_folder, symlink_path, dummy_file_name)) # Check that the manifest doesn't contain the symlink to the source.cpp contents = load(os.path.join(package_folder, "conanmanifest.txt")) assert symlink_path not in contents assert dummy_file_path in contents assert os.path.join(symlink_path, dummy_file_name) not in contents @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux") def test_package_symlinks_zero_size(): server = TestServer() client = TestClient(servers={"default": server}, inputs=["admin", "password"]) conanfile = """ import os from conan import ConanFile from conan.tools.files import save class HelloConan(ConanFile): name = "hello" version = "0.1" def package(self): # Link to file.txt and then remove it save(self, os.path.join(self.package_folder, "file.txt"), "contents") os.symlink("file.txt", os.path.join(self.package_folder, "link.txt")) """ client.save({"conanfile.py": conanfile}) # By default it is not allowed client.run("create .") p_folder = client.created_layout().download_package() # Upload, it will create the tgz client.run("upload * -r=default -c") # We can uncompress it without warns tgz = os.path.join(p_folder, "conan_package.tgz") client.run_command('gzip -d "{}"'.format(tgz)) client.run_command('tar tvf "{}"'.format(os.path.join(p_folder, "conan_package.tar"))) lines = str(client.out).splitlines() """ -rw-r--r-- 0/0 8 1970-01-01 01:00 file.txt lrw-r--r-- 0/0 0 1970-01-01 01:00 link.txt -> file.txt """ assert "link.txt" in " ".join(lines) for line in lines: assert ".txt" in line size = int([i for i in line.split(" ") if i][2]) if "link.txt" in line: assert int(size) == 0 elif "file.txt": assert int(size) > 0 ================================================ FILE: test/integration/sysroot_test.py ================================================ from conan.test.utils.tools import TestClient class TestSysroot: def test(self): client = TestClient() sysroot = """from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.cpp_info.sysroot = "HelloSysRoot" """ client.save({"conanfile.py": sysroot}) client.run("create . --name=sysroot --version=0.1 --user=user --channel=testing") conanfile = """from conan import ConanFile class Pkg(ConanFile): requires = "sysroot/0.1@user/testing" def build(self): self.output.info("PKG SYSROOT: %s" % self.dependencies["sysroot"].cpp_info.sysroot) def package_info(self): self.cpp_info.sysroot = "HelloSysRoot" """ test_conanfile = """from conan import ConanFile class Pkg(ConanFile): def requirements(self): self.requires(self.tested_reference_str) def build(self): self.output.info("Test SYSROOT: %s" % self.dependencies["sysroot"].cpp_info.sysroot) def test(self): pass """ client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") assert "pkg/0.1@user/testing: PKG SYSROOT: HelloSysRoot" in client.out assert "pkg/0.1@user/testing (test package): Test SYSROOT: HelloSysRoot" in client.out client.run("install .") ================================================ FILE: test/integration/system_reqs_test.py ================================================ from conan.test.utils.tools import TestClient base_conanfile = ''' from conan import ConanFile class TestSystemReqs(ConanFile): name = "test" version = "0.1" def system_requirements(self): self.output.info("*+Running system requirements+*") ''' class TestSystemReqs: def test_force_system_reqs_rerun(self): client = TestClient() client.save({'conanfile.py': base_conanfile}) client.run("create . ") assert "*+Running system requirements+*" in client.out client.run("install --requires=test/0.1") assert "*+Running system requirements+*" in client.out def test_local_system_requirements(self): client = TestClient() client.save({'conanfile.py': base_conanfile}) client.run("install .") assert "*+Running system requirements+*" in client.out ================================================ FILE: test/integration/test_components.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_components_cycles(): """c -> b -> a -> c""" c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestcycleConan(ConanFile): name = "testcycle" version = "1.0" def package_info(self): self.cpp_info.components["c"].requires = ["b"] self.cpp_info.components["b"].requires = ["a"] self.cpp_info.components["a"].requires = ["c"] # cycle! """) test_conanfile = GenConanfile().with_test("pass").with_generator("CMakeDeps")\ .with_settings("build_type") c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("create .", assert_error=True) out = c.out assert "ERROR: Error in generator 'CMakeDeps': error generating context for 'testcycle/1.0': " \ "There is a dependency loop in 'self.cpp_info.components' requires:" in out assert "a requires c" in out assert "b requires a" in out assert "c requires b" in out def test_components_cycle_complex(): """ Cycle: a -> b -> c -> d -> b Isolated j declaring its libs """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestcycleConan(ConanFile): name = "testcycle" version = "1.0" def package_info(self): self.cpp_info.components["a"].requires = ["b"] self.cpp_info.components["b"].requires = ["c"] self.cpp_info.components["c"].requires = ["d"] self.cpp_info.components["d"].requires = ["b"] # cycle! self.cpp_info.components["j"].libs = ["libj"] """) test_conanfile = GenConanfile().with_test("pass").with_generator("CMakeDeps") \ .with_settings("build_type") c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("create .", assert_error=True) out = c.out assert "ERROR: Error in generator 'CMakeDeps': error generating context for 'testcycle/1.0': " \ "There is a dependency loop in 'self.cpp_info.components' requires:" in out assert "a requires b" in out assert "b requires c" in out assert "c requires d" in out assert "d requires b" in out def test_components_cycles_error(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestcycleConan(ConanFile): name = "testcycle" version = "1.0" def package_info(self): self.cpp_info.components["c"].requires = ["b", "d"] self.cpp_info.components["b"].requires = ["a"] self.cpp_info.components["a"].requires = ["c"] # cycle! self.cpp_info.components["d"].includedirs = [] """) test_conanfile = GenConanfile().with_test("pass").with_generator("CMakeDeps")\ .with_settings("build_type") c.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) c.run("create .", assert_error=True) assert "ERROR: Error in generator 'CMakeDeps': error generating context for 'testcycle/1.0': " \ "There is a dependency loop in 'self.cpp_info.components' requires:" in c.out assert "a requires c" in c.out assert "b requires a" in c.out assert "c requires b" in c.out def test_components_not_required(): """ Allow requiring and building against one component, but not propagating it https://github.com/conan-io/conan/issues/12965 """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestcycleConan(ConanFile): name = "wayland" version = "1.0" requires = "expat/1.0" def package_info(self): self.cpp_info.components["wayland-scanner"].libdirs = [] """) c.save({"expat/conanfile.py": GenConanfile("expat", "1.0"), "wayland/conanfile.py": conanfile}) c.run("create expat") c.run("create wayland") assert "wayland/1.0: Created package" in c.out def test_components_overrides(): """ overrides are not direct dependencies, and as such, they don't need to be mandatory to specify in the components requires https://github.com/conan-io/conan/issues/13922 """ c = TestClient() consumer = textwrap.dedent(""" from conan import ConanFile class ConanRecipe(ConanFile): name = "app" version = "0.1" def requirements(self): self.requires("libffi/3.4.4", override=True) self.requires("glib/2.76.2") def package_info(self): self.cpp_info.requires = ["glib::glib"] """) c.save({"libffi/conanfile.py": GenConanfile("libffi"), "glib/conanfile.py": GenConanfile("glib", "2.76.2").with_requires("libffi/3.0"), "app/conanfile.py": consumer}) c.run("create libffi --version=3.0") c.run("create libffi --version=3.4.4") c.run("create glib") # This used to crash, because override was not correctly excluded c.run("create app") assert "app/0.1: Created package" in c.out def test_duplication_component_properties(): """ Regression for PR 17503 - component lists would be incorrectly aggregated """ tc = TestClient(light=True) dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.components["acomp"].set_property("prop_list", ["value1"]) self.cpp_info.components["bcomp"].set_property("prop_list", ["value2"]) self.cpp_info.components["ccomp"].set_property("prop_list", ["value3"]) """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" requires = "dep/0.1" def generate(self): # Calling this would break property lists of the last lex sorted component aggregated_components = self.dependencies["dep"].cpp_info.aggregated_components() ccomp = self.dependencies["dep"].cpp_info.components["ccomp"] self.output.info("ccomp list: " + str(ccomp.get_property("prop_list"))) """) tc.save({"dep/conanfile.py": dep, "conanfile.py": conanfile}) tc.run("create dep") tc.run("create .") # The bug would give ccomp the prop_list values of the other two components assert "pkg/0.1: ccomp list: ['value3', 'value2', 'value1']" not in tc.out assert "pkg/0.1: ccomp list: ['value3']" in tc.out ================================================ FILE: test/integration/test_components_error.py ================================================ import os import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_component_error(): # https://github.com/conan-io/conan/issues/12027 c = TestClient() t1 = textwrap.dedent(""" from conan import ConanFile class t1Conan(ConanFile): name = "t1" version = "0.1.0" package_type = "static-library" def package_info(self): self.cpp_info.components["comp1"].set_property("cmake_target_name", "t1::comp1") self.cpp_info.components["comp2"].set_property("cmake_target_name", "t1::comp2") """) t2 = textwrap.dedent(""" from conan import ConanFile class t2Conan(ConanFile): name = "t2" version = "0.1.0" requires = "t1/0.1.0" package_type = "shared-library" def package_info(self): self.cpp_info.requires.append("t1::comp1") """) t3 = textwrap.dedent(""" from conan import ConanFile class t3Conan(ConanFile): name = "t3" version = "0.1.0" requires = "t2/0.1.0" package_type = "application" generators = "CMakeDeps" settings = "os", "arch", "compiler", "build_type" """) c.save({"t1/conanfile.py": t1, "t2/conanfile.py": t2, "t3/conanfile.py": t3}) c.run("create t1") c.run("create t2") c.run("install t3") arch = c.get_default_host_profile().settings['arch'] assert 'list(APPEND t2_FIND_DEPENDENCY_NAMES )' in c.load(f"t3/t2-release-{arch}-data.cmake") assert not os.path.exists(os.path.join(c.current_folder, "t3/t1-config.cmake")) def test_verify_get_property_check_type(): c = TestClient(light=True) conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "0.1" def package_info(self): self.cpp_info.set_property("test_property", "foo") self.cpp_info.get_property("test_property", check_type=list) """) c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) assert 'The expected type for test_property is "list", but "str" was found' in c.out @pytest.mark.parametrize("component", [True, False]) def test_unused_requirement(component): """ Requires should include all listed requirements This error is known when creating the package if the requirement is consumed. """ t = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class Consumer(ConanFile): name = "wrong" version = "version" requires = "top/version", "top2/version" def package_info(self): self.cpp_info{'.components["foo"]' if component else ''}.requires = ["top::other"] """) t.save({"top/conanfile.py": GenConanfile().with_package_info({"components": {"cmp1": {"libs": ["top_cmp1"]}}}), "conanfile.py": conanfile}) t.run('create top --name=top --version=version') t.run('create top --name=top2 --version=version') t.run('create .', assert_error=True) assert "ERROR: wrong/version: package_info(): The direct dependency 'top2' is not used " \ "by any '(cpp_info/components).requires'." in t.out def test_unused_requirement_not_propagated(): # https://github.com/conan-io/conan/issues/19026 t = TestClient() conanfile = textwrap.dedent(f""" from conan import ConanFile class Consumer(ConanFile): name = "pkg" version = "0.1" requires = "header/0.1", "lib/0.1" def package_info(self): self.cpp_info.requires = ["lib::lib", "header::header"] """) t.save({"header/conanfile.py": GenConanfile("header", "0.1").with_package_type("header-library"), "lib/conanfile.py": GenConanfile("lib", "0.1").with_package_type("static-library"), "pkg/conanfile.py": conanfile, "app/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) t.run('create header') t.run('create lib') t.run('create pkg') t.run("install app") assert re.search(r"Skipped binaries(\s*)header/0.1", t.out) @pytest.mark.parametrize("component", [True, False]) def test_wrong_requirement(component): """ If we require a wrong requirement, we get a meaninful error. This error is known when creating the package if the requirement is not there. """ t = TestClient(light=True) conanfile = textwrap.dedent(f""" from conan import ConanFile class Consumer(ConanFile): name = "wrong" version = "version" requires = "top/version" def package_info(self): self.cpp_info{'.components["foo"]' if component else ''}.requires = ["top::cmp1", "other::other"] """) t.save({"top/conanfile.py": GenConanfile().with_package_info({"components": {"cmp1": {"libs": ["top_cmp1"]}}}), "conanfile.py": conanfile}) t.run('create top --name=top --version=version') t.run('create .', assert_error=True) assert "ERROR: wrong/version: package_info(): There are '(cpp_info/components).requires' " \ "that includes package 'other::', but such package is not a a direct requirement " \ "of the recipe" in t.out @pytest.mark.parametrize("component", [True, False]) def test_missing_internal(component): consumer = textwrap.dedent(f""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.cpp_info{'.components["foo"]' if component else ''}.requires = ["other", "another"] self.cpp_info{'.components["bar"]' if component else ''}.requires = ["other", "another"] """) t = TestClient(light=True) t.save({'conanfile.py': consumer}) t.run('create . --name=wrong --version=version', assert_error=True) assert "ERROR: wrong/version: package_info(): There are '(cpp_info/components).requires' " \ "to other internal components that are not defined: ['other', 'another']" in t.out def test_unused_tool_requirement(): """ Requires should include all listed requirements This error is known when creating the package if the requirement is consumed. """ top_conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.cpp_info.components["cmp1"].libs = ["top_cmp1"] self.cpp_info.components["cmp2"].libs = ["top_cmp2"] """) consumer = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): requires = "top/version" tool_requires = "top2/version" def package_info(self): self.cpp_info.requires = ["top::other"] """) t = TestClient() t.save({'top.py': top_conanfile, 'consumer.py': consumer}) t.run('create top.py --name=top --version=version') t.run('create top.py --name=top2 --version=version') t.run('create consumer.py --name=wrong --version=version') # This runs without crashing, because it is not chcking that top::other doesn't exist def test_component_double_colon_error_message(): c = TestClient() t2 = textwrap.dedent(""" from conan import ConanFile class t2Conan(ConanFile): name = "t2" version = "0.1.0" requires = "t1/0.1.0" def package_info(self): self.cpp_info.requires.append("t1::comp1::other") """) c.save({"t1/conanfile.py": GenConanfile("t1", "0.1.0"), "t2/conanfile.py": t2, "t2/test_package/conanfile.py": GenConanfile().with_settings("build_type") .with_generator("CMakeDeps") .with_test("pass")}) c.run("create t1") c.run("create t2", assert_error=True) assert "Component 't1::comp1::other' not found in 't1' package requirement" in c.out ================================================ FILE: test/integration/test_compressions.py ================================================ import json import os import sys import textwrap import pytest from conan.api.model import RecipeReference, PkgReference from conan.internal.util import load from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("compress", ["gz", "xz", "zst"]) def test_xz(compress): if compress == "zst" and sys.version_info.minor < 14: pytest.skip("Skipping zst compression tests") c = TestClient(default_server_user=True) c.save_home({"global.conf": f"core.upload:compression_format={compress}"}) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): name = "pkg" version = "0.1" exports_sources = "*.h" exports = "*.yml" def package(self): copy(self, "*.h", self.source_folder, self.package_folder) """) c.save({"conanfile.py": conanfile, "header.h": "myheader", "myfile.yml": "myyml"}) c.run("create -tf=") c.run("upload * -r=default -c --format=json") # Verify the uploaded files are all txz upload_json = json.loads(c.stdout) rrev = upload_json["default"]["pkg/0.1"]["revisions"]["4e81a0b14da7ae918cf3dba3a07578d6"] rfiles = rrev["files"] assert f"conan_export.t{compress}" in rfiles assert f"conan_sources.t{compress}" in rfiles prevs = rrev["packages"]["da39a3ee5e6b4b0d3255bfef95601890afd80709"]["revisions"] prev = prevs["13eb72928af98144fa7bf104b69663bc"] pfiles = prev["files"] assert f"conan_package.t{compress}" in pfiles # decompress should work anyway c.save_home({"global.conf": ""}) c.run("remove * -c") c.run("install --requires=pkg/0.1") # checking the recipe ref = RecipeReference.loads("pkg/0.1") rlayout = c.get_latest_ref_layout(ref) downloaded_files = os.listdir(rlayout.download_export()) assert f"conan_export.t{compress}" in downloaded_files assert f"conan_sources.t{compress}" not in downloaded_files assert "myyml" == load(os.path.join(rlayout.export(), "myfile.yml")) # checking the package pref = PkgReference(rlayout.reference, "da39a3ee5e6b4b0d3255bfef95601890afd80709") playout = c.get_latest_pkg_layout(pref) downloaded_files = os.listdir(playout.download_package()) assert f"conan_package.t{compress}" in downloaded_files assert "myheader" == load(os.path.join(playout.package(), "header.h")) # Force the build from source c.run("install --requires=pkg/0.1 --build=*") downloaded_files = os.listdir(rlayout.download_export()) assert f"conan_export.t{compress}" in downloaded_files assert f"conan_sources.t{compress}" in downloaded_files @pytest.mark.skipif(sys.version_info.minor >= 14, reason="validate zstd error in python<314") def test_unsupported_zstd(): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_package_file("myfile.h", "contents")}) c.run("create") playout = c.created_layout() c.run("upload * -r=default -c -cc core.upload:compression_format=zst", assert_error=True) assert "ERROR: The 'core.upload:compression_format=zst' is only for Python>=3.14" in c.out # Lets cheat, creating a fake zstd to test download c.run("upload * -r=default -c --dry-run") os.rename(os.path.join(playout.download_package(), "conan_package.tgz"), os.path.join(playout.download_package(), "conan_package.tzst")) c.run("upload * -r=default -c") c.run("remove * -c") c.run("install --requires=pkg/0.1", assert_error=True) assert ("ERROR: File conan_package.tzst compressed with 'zst', unsupported " "for Python<3.14") in c.out class TestDuplicatedInServerErrors: def test_duplicated_export(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "conandata.yml": ""}) c.run("export") c.run("upload * -r=default -c") c.run("remove * -c") c.run("export") c.run("upload * -r=default -c -cc core.upload:compression_format=xz --force") assert "WARN: experimental: The 'xz' compression is experimental" in c.out c.run("remove * -c") c.run("install --requires=pkg/0.1", assert_error=True) assert ("it contains more than one compressed file: " "['conan_export.tgz', 'conan_export.txz']") in c.out def test_duplicated_source(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports_sources("*.h"), "myheader.h": "content"}) c.run("export") c.run("upload * -r=default -c") c.run("remove * -c") c.run("export") c.run("upload * -r=default -c -cc core.upload:compression_format=xz --force") c.run("remove * -c") c.run("install --requires=pkg/0.1 --build=missing", assert_error=True) assert ("it contains more than one compressed file: " "['conan_sources.tgz', 'conan_sources.txz']") in c.out def test_duplicated_package(self): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create") c.run("upload * -r=default -c") c.run("remove * -c") c.run("create") c.run("upload * -r=default -c -cc core.upload:compression_format=xz --force") c.run("remove * -c") c.run("install --requires=pkg/0.1", assert_error=True) assert ("it contains more than one compressed file: " "['conan_package.tgz', 'conan_package.txz']") in c.out ================================================ FILE: test/integration/test_db_error.py ================================================ import os import shutil from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_db_error(): # https://github.com/conan-io/conan/issues/14517 c = TestClient(default_server_user=True) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1")}) c.run("create liba") c.run("install --requires=liba/0.1 --format=json", redirect_stdout="graph.json") c.run("list --graph=graph.json --format=json", redirect_stdout="installed.json") c.run("upload --list=installed.json -r=default --format=json -c", redirect_stdout="upload.json") c2 = TestClient(servers=c.servers, inputs=["admin", "password"]) shutil.copy(os.path.join(c.current_folder, "upload.json"), c2.current_folder) c2.run("download --list=upload.json -r=default --format=json") # This used to crash assert "liba/0.1: Downloaded package revision" in c2.out ================================================ FILE: test/integration/test_migrations.py ================================================ import os import textwrap from unittest.mock import patch import pytest from conan import conan_version from conan.test.utils.tools import TestClient from conan.internal.api.migrations import ClientMigrator from conan.internal.model.version import Version from conan.internal.util.files import save, load @pytest.mark.parametrize(["plugin_path", "string_replace", "new_string"], [("profile.py", "msvc", "EME_ESE_VC"), ("compatibility/compatibility.py", "conanfile", "conian_file")]) def test_migration_profile_checker_plugin(plugin_path, string_replace, new_string): t = TestClient(light=True) # Any command that checks the package cache generates the DB t.run("list") assert os.path.exists(os.path.join(t.cache_folder, "p", "cache.sqlite3")) profile_plugin_path = os.path.join(t.cache_folder, "extensions", "plugins", plugin_path) contents = load(profile_plugin_path) # Let's change the version version_txt_file_path = os.path.join(t.cache_folder, "version.txt") save(version_txt_file_path, "1.0.0") assert os.path.exists(os.path.join(t.cache_folder, "p", "cache.sqlite3")) # Do a modification to the profile plugin without changing the comment contents = contents.replace(string_replace, new_string) save(profile_plugin_path, contents) # Trigger the migrations t.run("-v") assert f"Migration: Successfully updated {os.path.basename(plugin_path)}" in t.out contents = load(profile_plugin_path) # Our changes are removed!!! assert string_replace in contents assert new_string not in contents # New client, everything new t2 = TestClient(light=True) # This generates the new plugin file t2.run("list") # Do a modification to the profile plugin but changing the comment profile_plugin_path2 = os.path.join(t2.cache_folder, "extensions", "plugins", plugin_path) contents = load(profile_plugin_path2) contents = contents.replace(string_replace, new_string) contents = contents.replace("This file was generated by Conan", "This file is from ACME corp, " "please don't touch it.") save(profile_plugin_path2, contents) # Let's change the version version_txt_file_path2 = os.path.join(t2.cache_folder, "version.txt") save(version_txt_file_path2, "1.0.0") # Trigger the migrations t2.run("list") assert "Migration: Successfully updated" not in t2.out contents = load(profile_plugin_path2) # Our Changes are kept! assert "This file is from ACME corp, " in contents assert string_replace not in contents assert new_string in contents def test_back_migrations(): t = TestClient(light=True) # add 3 migrations for number in (1, 2, 3): migration_file = os.path.join(t.cache_folder, "migrations", f"2.100.0_{number}-migrate.py") migrate = textwrap.dedent(f""" import os def migrate(cache_folder): os.remove(os.path.join(cache_folder, "file{number}.txt")) """) save(migration_file, migrate) save(os.path.join(t.cache_folder, f"file{number}.txt"), "some content") # Some older versions migrations that shouldn't be applied if we downgrade to current wrong_migration_file = os.path.join(t.cache_folder, "migrations", f"2.0_{number}-migrate.py") save(wrong_migration_file, "this is not python, it would crash") # Let's change the old version version_txt_file_path = os.path.join(t.cache_folder, "version.txt") save(version_txt_file_path, "200.0") t.run("-v") # Fire the backward migration assert f"WARN: Downgrading cache from Conan 200.0 to {conan_version}" in t.out for number in (1, 2, 3): assert f"WARN: Applying downgrade migration 2.100.0_{number}-migrate.py" in t.out assert not os.path.exists(os.path.join(t.cache_folder, f"file{number}.txt")) migration_file = os.path.join(t.cache_folder, "migrations", f"2.100.0_{number}-migrate.py") assert not os.path.exists(migration_file) def test_back_default_compatibility_migration(): t = TestClient(light=True) t.run("-v") # Fire the backward migration migration_file = os.path.join(t.cache_folder, "migrations", "2.4_1-migrate.py") assert os.path.exists(migration_file) # downgrade from a clean latest conan_version to 2.3.2 # simulate that we are in 2.3.2 and the old one is latest conan_version migrator = ClientMigrator(t.cache_folder, Version("2.3.2")) with patch('conan.api.conan_api.ClientMigrator', new=lambda *args, **kwargs: migrator): t.run("-v") # Fire the backward migration assert f"WARN: Downgrading cache from Conan {conan_version} to 2.3.2" in t.out class TestMigrationCppstdCompat: def test_migration(self): t = TestClient(light=True) t.run("-v") cppstd_compat_path = "extensions/plugins/compatibility/cppstd_compat.py" compatibility_path = "extensions/plugins/compatibility/compatibility.py" # both files exist and not modified t.save_home({"version.txt": "2.11"}) t.run("-v") assert "def cppstd_compat(conanfile)" in t.load_home(compatibility_path) assert not os.path.exists(os.path.join(t.cache_folder, cppstd_compat_path)) def test_cppstd_modified(self): t = TestClient(light=True) t.run("-v") cppstd_compat_path = "extensions/plugins/compatibility/cppstd_compat.py" compatibility_path = "extensions/plugins/compatibility/compatibility.py" # cppstd_compat modified t.save_home({"version.txt": "2.11", compatibility_path: "# This file was generated by Conan", cppstd_compat_path: "custom file content"}) t.run("-v") assert t.load_home(cppstd_compat_path) == "custom file content" # compatibility not migrated, keeps the old content assert "def cppstd_compat(conanfile)" not in t.load_home(compatibility_path) def test_compatibility_modified(self): t = TestClient(light=True) t.run("-v") cppstd_compat_path = "extensions/plugins/compatibility/cppstd_compat.py" compatibility_path = "extensions/plugins/compatibility/compatibility.py" t.save_home({"version.txt": "2.11", cppstd_compat_path: "# This file was generated by Conan POTATO", compatibility_path: "Modified file"}) t.run("-v") assert t.load_home(compatibility_path) == "Modified file" # not Removed because compatibility was modified assert "POTATO" in t.load_home(cppstd_compat_path) def test_compatibility_modified_by_conan(self): t = TestClient(light=True) t.run("-v") compatibility_path = "extensions/plugins/compatibility/compatibility.py" old = t.load_home(compatibility_path) # In old versions of Conan, assume POTATO was present in the file new = old + "\n# POTATO" t.save_home({"version.txt": "2.11", compatibility_path: new}) # But now with the newer version, Conan removed it # so the triggered migration should remove it t.run("-v") assert "Successfully updated compatibility.py" in t.out assert "# POTATO" not in t.load_home(compatibility_path) def test_compatibility_no_rewrite(self): t = TestClient(light=True) t.run("-v") # compatibility.py exists and is not modified t.save_home({"version.txt": "2.11"}) t.run("-v") assert "Successfully updated compatibility.py" not in t.out ================================================ FILE: test/integration/test_package_python_files.py ================================================ import os import textwrap from conan.api.model import RecipeReference from conan.internal.util.files import load from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID def test_package_python_files(): client = TestClient(default_server_user=True) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import copy class Pkg(ConanFile): exports_sources = "*" def package(self): copy(self, "*", self.source_folder, self.package_folder) """) client.save({"conanfile.py": conanfile, "myfile.pyc": "", "myfile.pyo": "", ".DS_Store": ""}) client.run("create . --name=pkg --version=0.1") ref_layout = client.exported_layout() pkg_layout = client.created_layout() export = ref_layout.export() export_sources = ref_layout.export_sources() assert os.path.isfile(os.path.join(export_sources, "myfile.pyc")) assert os.path.isfile(os.path.join(export_sources, "myfile.pyo")) assert os.path.isfile(os.path.join(export_sources, ".DS_Store")) manifest = load(os.path.join(export, "conanmanifest.txt")) assert "myfile.pyc" in manifest assert "myfile.pyo" in manifest assert ".DS_Store" not in manifest pkg_folder = pkg_layout.package() assert os.path.isfile(os.path.join(pkg_folder, "myfile.pyc")) assert os.path.isfile(os.path.join(pkg_folder, "myfile.pyo")) assert os.path.isfile(os.path.join(pkg_folder, ".DS_Store")) manifest = load(os.path.join(pkg_folder, "conanmanifest.txt")) assert "myfile.pyc" in manifest assert "myfile.pyo" in manifest assert ".DS_Store" not in manifest client.run("upload * -r=default --confirm") client.run("remove * -c") client.run("download pkg/0.1#*:* -r default") # The download will be in a different pkg folder now. ref = RecipeReference.loads("pkg/0.1") pref = client.get_latest_package_reference(ref, NO_SETTINGS_PACKAGE_ID) pkg_folder = client.get_latest_pkg_layout(pref).package() assert os.path.isfile(os.path.join(export_sources, "myfile.pyc")) assert os.path.isfile(os.path.join(export_sources, "myfile.pyo")) assert not os.path.isfile(os.path.join(export_sources, ".DS_Store")) manifest = load(os.path.join(export, "conanmanifest.txt")) assert "myfile.pyc" in manifest assert "myfile.pyo" in manifest assert ".DS_Store" not in manifest assert os.path.isfile(os.path.join(pkg_folder, "myfile.pyc")) assert os.path.isfile(os.path.join(pkg_folder, "myfile.pyo")) assert not os.path.isfile(os.path.join(pkg_folder, ".DS_Store")) manifest = load(os.path.join(pkg_folder, "conanmanifest.txt")) assert "myfile.pyc" in manifest assert "myfile.pyo" in manifest assert ".DS_Store" not in manifest def test_package_empty_folders(): c = TestClient(default_server_user=True) conanfile = textwrap.dedent(""" import os from conan import ConanFile class Pkg(ConanFile): def package(self): os.mkdir(os.path.join(self.package_folder, "empty_folder")) """) c.save({"conanfile.py": conanfile}) c.run("create . --name=pkg --version=0.1") pkg_folder = c.created_layout().package() assert os.path.isdir(os.path.join(pkg_folder, "empty_folder")) manifest = load(os.path.join(pkg_folder, "conanmanifest.txt")) assert "empty_folder" not in manifest c.run("upload * -r=default --confirm") c.run("remove * -c") c.run("download pkg/0.1#*:* -r default") # The download will be in a different pkg folder now. ref = RecipeReference.loads("pkg/0.1") pref = c.get_latest_package_reference(ref, NO_SETTINGS_PACKAGE_ID) pkg_folder = c.get_latest_pkg_layout(pref).package() assert os.path.isdir(os.path.join(pkg_folder, "empty_folder")) manifest = load(os.path.join(pkg_folder, "conanmanifest.txt")) assert "empty_folder" not in manifest ================================================ FILE: test/integration/test_package_vendor.py ================================================ import os import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_package_vendor(): c = TestClient() app = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy, save class App(ConanFile): name = "app" version = "0.1" package_type = "application" vendor = True requires = "pkga/0.1" def package(self): copy(self, "*", src=self.dependencies["pkga"].package_folder, dst=self.package_folder) save(self, os.path.join(self.package_folder, "app.exe"), "app") """) c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library") .with_package_file("pkga.dll", "dll"), "app/conanfile.py": app }) c.run("create pkga") c.run("create app") # -c tools.graph:vendor=build will be automatic assert "app/0.1: package(): Packaged 1 '.dll' file: pkga.dll" in c.out # we can safely remove pkga c.run("remove pkg* -c") c.run("list app:*") assert "pkga" not in c.out # The binary doesn't depend on pkga c.run("install --requires=app/0.1 --deployer=full_deploy") assert "pkga" not in c.out assert c.load("full_deploy/host/app/0.1/app.exe") == "app" assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll" # we can create a modified pkga c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library") .with_package_file("pkga.dll", "newdll")}) c.run("create pkga") # still using the re-packaged one c.run("install --requires=app/0.1 --deployer=full_deploy") assert "pkga" not in c.out assert c.load("full_deploy/host/app/0.1/app.exe") == "app" assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll" # but we can force the expansion, still not the rebuild c.run("install --requires=app/0.1 --deployer=full_deploy -c tools.graph:vendor=build") assert "pkga" in c.out assert c.load("full_deploy/host/app/0.1/app.exe") == "app" assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll" # and finally we can force the expansion and the rebuild c.run("install --requires=app/0.1 --build=app* --deployer=full_deploy " "-c tools.graph:vendor=build") assert "pkga" in c.out assert c.load("full_deploy/host/app/0.1/app.exe") == "app" assert c.load("full_deploy/host/app/0.1/pkga.dll") == "newdll" # This shoulnd't happen, no visibility over transitive dependencies of app assert not os.path.exists(os.path.join(c.current_folder, "full_deploy", "host", "pkga")) # lets remove the binary c.run("remove app:* -c") c.run("install --requires=app/0.1", assert_error=True) assert "Missing binary" in c.out c.run("install --requires=app/0.1 --build=missing", assert_error=True) assert "app/0.1: Invalid: The package 'app/0.1' is a vendoring one, needs to be built " \ "from source, but it didn't enable 'tools.graph:vendor=build'" in c.out c.run("install --requires=app/0.1 --build=missing -c tools.graph:vendor=build") assert "pkga" in c.out # it works def test_package_vendor_editable(): c = TestClient() pkgb = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy, save class App(ConanFile): name = "pkgb" version = "0.1" package_type = "shared-library" vendor = True requires = "pkga/0.1" def layout(self): self.folders.build = "build" self.cpp.build.bindirs = ["build"] def generate(self): copy(self, "*", src=self.dependencies["pkga"].package_folder, dst=self.build_folder) def build(self): save(self, os.path.join(self.build_folder, "pkgb.dll"), "dll") """) c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library") .with_package_file("bin/pkga.dll", "d"), "pkgb/conanfile.py": pkgb, "app/conanfile.py": GenConanfile("app", "0.1").with_settings("os") .with_requires("pkgb/0.1") }) c.run("create pkga") c.run("editable add pkgb") c.run("install app -s os=Linux --build=editable") assert "pkga" in c.out # The environment file of "app" doesn't have any visibility of the "pkga" paths envfile_app = c.load("app/conanrunenv.sh") assert "pkga" not in envfile_app # But the environment file needed to build "pkgb" has visibility over the "pkga" paths envfile_pkgb = c.load("pkgb/conanrunenv.sh") assert "pkga" in envfile_pkgb def test_vendor_dont_propagate_options(): c = TestClient() app = GenConanfile("app", "0.1").with_requires("pkga/0.1").with_class_attribute("vendor=True") c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_shared_option(False), "app/conanfile.py": app, "consumer/conanfile.txt": "[requires]\napp/0.1", "consumer_shared/conanfile.txt": "[requires]\napp/0.1\n[options]\n*:shared=True" }) c.run("create pkga") c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Build")}) c.run("create app") c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Cache"), "app/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) c.run("install consumer --build=app/* -c tools.graph:vendor=build") c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Cache"), "app/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) c.run("install consumer_shared --build=app/* -c tools.graph:vendor=build") c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Cache"), "app/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")}) def test_package_vendor_export_pkg(): c = TestClient() app = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import copy, save class App(ConanFile): name = "app" version = "0.1" package_type = "application" vendor = True requires = "pkga/0.1" def package(self): copy(self, "*", src=self.dependencies["pkga"].package_folder, dst=self.package_folder) save(self, os.path.join(self.package_folder, "app.exe"), "app") """) c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library") .with_package_file("pkga.dll", "dll"), "app/conanfile.py": app }) c.run("create pkga") c.run("build app") # -c tools.graph:vendor=build will be automatic c.run("export-pkg app") assert "pkga/0.1" in c.out # In the export-pkg process, dependencies are still needed assert "conanfile.py (app/0.1): package(): Packaged 1 '.dll' file: pkga.dll" in c.out # we can safely remove pkga, once the package is created, it can work without deps c.run("remove pkg* -c") c.run("list app:*") assert "pkga" not in c.out # The binary doesn't depend on pkga c.run("install --requires=app/0.1 --deployer=full_deploy") assert "pkga" not in c.out assert c.load("full_deploy/host/app/0.1/app.exe") == "app" assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll" ================================================ FILE: test/integration/test_pkg_signing.py ================================================ import os import textwrap from conan.api.model import RecipeReference from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_pkg_sign(): c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports("export/*") .with_exports_sources("export_sources/*").with_package_file("myfile", "mycontents!"), "export/file1.txt": "file1!", "export_sources/file2.txt": "file2!"}) signer = textwrap.dedent(r""" import os def sign(ref, artifacts_folder, signature_folder, **kwargs): print("Signing ref: ", ref) print("Signing folder: ", artifacts_folder) files = [] for f in sorted(os.listdir(artifacts_folder)): if os.path.isfile(os.path.join(artifacts_folder, f)): files.append(f) print("Signing files: ", sorted(files)) signature = os.path.join(signature_folder, "signature.asc") open(signature, "w").write("\n".join(files)) def verify(ref, artifacts_folder, signature_folder, files, **kwargs): print("Verifying ref: ", ref) print("Verifying folder: ", artifacts_folder) signature = os.path.join(signature_folder, "signature.asc") contents = open(signature).read() print("verifying contents", contents) for f in files: print("VERIFYING ", f) if os.path.isfile(os.path.join(artifacts_folder, f)): assert f in contents """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.run("create .") c.run("cache sign pkg/0.1") assert "Signing ref: pkg/0.1" in c.out assert "Signing ref: pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709" in c.out # Make sure it is signing the sources too assert "Signing files: ['conan_export.tgz', 'conan_sources.tgz', " \ "'conanfile.py', 'conanmanifest.txt']" in c.out assert ("WARN: deprecated: [Package sign] The signature plugin sign() function must return a " "list of signature dicts") in c.out c.run("upload * -r=default -c") assert ("WARN: deprecated: [Package sign] Implicitly signing packages in the upload command " "has been removed. Use 'conan cache sign' command before uploading instead") in c.out c.run("remove * -c") c.run("install --requires=pkg/0.1") assert "Verifying ref: pkg/0.1" in c.out assert "Verifying ref: pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709" in c.out assert "VERIFYING conanfile.py" in c.out assert "VERIFYING conan_sources.tgz" not in c.out # Sources not retrieved now # Lets force the retrieval of the sources c.run("install --requires=pkg/0.1 --build=*") assert "Verifying ref: pkg/0.1" in c.out assert "VERIFYING conanfile.py" not in c.out # It doesn't re-verify previous contents assert "VERIFYING conan_sources.tgz" in c.out def test_pkg_sign_manifest_signatures(): """Test that the sign function generates the manifest and signatures files and the verify function can access them""" c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports("export/*") .with_exports_sources("export_sources/*").with_package_file("myfile", "mycontents!"), "export/file1.txt": "file1!", "export_sources/file2.txt": "file2!"}) signer = textwrap.dedent(r""" import json import os from conan.internal.util.files import load, save # This is only for test purposes def sign(ref, artifacts_folder, signature_folder, **kwargs): save(os.path.join(signature_folder, "pkgsign-manifest.json.sig"), "") print(f"Creating signature pkgsign-manifest.json.sig for {ref}") # Return the pkgsign-signatures.json's content return [{"method": "openssl-dgst", "provider": "conan-client", "sign_artifacts": {"manifest": "pkgsign-manifest.json", "signature": "pkgsign-manifest.json.sig"}}] def verify(ref, artifacts_folder, signature_folder, files, **kwargs): manifest = load(os.path.join(signature_folder, "pkgsign-manifest.json")) manifest_content = json.loads(manifest) print(f"Manifest content:\n {manifest_content}") signatures = load(os.path.join(signature_folder, "pkgsign-signatures.json")) signatures_content = json.loads(signatures) signatures = signatures_content["signatures"] for signature in signatures_content["signatures"]: provider = signature.get("provider") method = signature.get("method") signature = signature.get("sign_artifacts", {}).get("signature") print(f"Provider: {provider}, Method: {method}, Signature: {signature}") # Verify signature here """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.run("create .") c.run("cache sign *") assert "Creating signature pkgsign-manifest.json.sig for pkg/0.1" in c.out c.run("cache verify *") assert "Manifest content:\n {'files': [{'file': 'conan_export.tgz'" in c.out assert "Checksum verified for file conanfile.py" in c.out assert "Provider: conan-client, Method: openssl-dgst, Signature: pkgsign-manifest.json.sig" assert "Manifest content:\n {'files': [{'file': 'conan_package.tgz'" in c.out assert "Checksum verified for file conan_package.tgz" in c.out def test_pkg_sign_canonical(): c = TestClient(default_server_user=True) c.save({"conanfile1.py": GenConanfile("lib1ok", "0.1") .with_exports_sources("*.txt").with_package_file("package.txt", "kk"), "conanfile2.py": GenConanfile("lib2fail", "0.1"), # will fail when installed "conanfile3.py": GenConanfile("lib3fail", "0.1"), # should always fail "sources.txt": "kk"}) c.run("create conanfile1.py") c.run("create conanfile2.py") c.run("create conanfile3.py") signer = textwrap.dedent(r""" import json import os from conan.errors import ConanException from conan.api.output import ConanOutput from conan.internal.util.files import load, save # This is only for test purposes def sign(ref, artifacts_folder, signature_folder, **kwargs): ConanOutput().info(f"Signing reference {ref}") ConanOutput().info(f"Signing folder: {artifacts_folder}") if "lib3fail" in str(ref): raise ConanException("sign failed") elif "lib2fail" in str(ref): provider = "this will fail to verify" else: provider = "conan-client" # Simulate signing the package save(os.path.join(signature_folder, "pkgsign-manifest.json.sig"), "") ConanOutput().info(f"Signature ok for {ref}") return [{"method": "dummy-method", "provider": provider, "sign_artifacts": {"manifest": "pkgsign-manifest.json", "signature": "pkgsign-manifest.json.sig"} }] def verify(ref, artifacts_folder, signature_folder, files, **kwargs): ConanOutput().info(f"Verifying reference {ref}") signatures_file_path = os.path.join(signature_folder, "pkgsign-signatures.json") if not os.path.isfile(signatures_file_path): raise ConanException("Package is not signed") if "lib3fail" in str(ref): raise ConanException(f"verify failed for {ref}") # Simulate verification signatures = json.loads(load(os.path.join(signature_folder, "pkgsign-signatures.json"))) provider = signatures["signatures"][0]["provider"] if provider != "conan-client": raise ConanException(f"Failed to verify the package {ref}") signature = signatures["signatures"][0]["sign_artifacts"]["signature"] ConanOutput().info(f"Verification ok for {ref} with signature {signature}") """) c.save_home({"extensions/plugins/sign/sign.py": signer}) # Cache verify command fails and reports if package is not signed c.run("cache verify *", assert_error=True) assert ("WARN: deprecated: [Package sign] Manifest file 'pkgsign-manifest.json' does not exist " "in signature folder") in c.out assert "ERROR: Package is not signed" in c.out # Cache sign command fails if a package fails to sign and reports it c.run("cache sign *", assert_error=True) assert textwrap.dedent(""" [Package sign] Results: lib1ok/0.1 revisions a6a4e799bb673d6e5ca4f904118d672e packages da39a3ee5e6b4b0d3255bfef95601890afd80709 revisions 76285bcb59a81071122cba04b2269b52 lib2fail/0.1 revisions 70a185be5a95af3dde25b74ae800b2f2 packages da39a3ee5e6b4b0d3255bfef95601890afd80709 revisions 0ba8627bd47edc3a501e8f0eb9a79e5e lib3fail/0.1 revisions 09ccc766ddd11c96aa78307b3f166fd6 packages da39a3ee5e6b4b0d3255bfef95601890afd80709 revisions 0ba8627bd47edc3a501e8f0eb9a79e5e ERROR: sign failed ERROR: sign failed [Package sign] Summary: OK=4, FAILED=2 """) in c.out # cache sign fails if package signing fails c.run("cache sign *", assert_error=True) assert "ERROR: sign failed" in c.out # Upload packages individually c.run("upload lib1ok* -c -r default") c.run("upload lib2fail* -c -r default") c.run("remove * -c") # Install verify command should fail if package sign verification fails c.run("install --requires lib1ok/0.1 --requires lib2fail/0.1 -r default", assert_error=True) assert "ERROR: Package 'lib2fail/0.1' not resolved: Failed to verify " \ "the package lib2fail/0.1" in c.out # If packages fail to verify signature, they should not be installed c.run("list *") assert "lib1ok" in c.out assert "lib2fail" not in c.out c.run("cache verify *") assert textwrap.dedent("""\ [Package sign] Results: lib1ok/0.1 revisions a6a4e799bb673d6e5ca4f904118d672e [Package sign] Summary: OK=1, FAILED=0 """) in c.out def test_pkg_sign_exports_sources(): """Test that the sign function generates the manifest and signatures files and the verify function can access them""" c = TestClient(default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports("export/*") .with_exports_sources("export_sources/*").with_package_file("myfile", "mycontents!"), "export/file1.txt": "file1!", "export_sources/file2.txt": "file2!"}) signer = textwrap.dedent(r""" import os from conan.internal.util.files import save # This is only for test purposes from conan.tools.files import load def sign(ref, artifacts_folder, signature_folder, **kwargs): save(os.path.join(signature_folder, "pkgsign-manifest.json.sig"), "") print(f"Creating signature pkgsign-manifest.json.sig for {ref}") # Return the pkgsign-signatures.json's content return [{"method": "openssl-dgst", "provider": "conan-client", "sign_artifacts": {"manifest": "pkgsign-manifest.json", "signature": "pkgsign-manifest.json.sig"}}] def verify(ref, artifacts_folder, signature_folder, files, **kwargs): pass """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.run("create .") c.run("cache sign pkg/0.1") assert "Creating signature pkgsign-manifest.json.sig" in c.out c.run("upload pkg/0.1 -r=default -c") c.run("remove * -c") c.run("install --requires=pkg/0.1 -r=default") assert "Checksum verified for file conan_export.tgz" in c.out assert "Checksum verified for file conan_package.tgz" in c.out c.run("install --requires=pkg/0.1 -r=default --build=pkg/0.1") assert "Checksum verified for file conan_sources.tgz" in c.out def test_pkg_sign_no_op(): c = TestClient() c.save({"conanfile1.py": GenConanfile("pkg", "0.1"), "conanfile2.py": GenConanfile("no-op", "0.1")}) signer = textwrap.dedent(r""" def sign(ref, artifacts_folder, signature_folder, **kwargs): if "pkg/0.1" in str(ref): print("Signing package", str(ref)) # Return the pkgsign-signatures.json's content return [{"method": "openssl-dgst", "provider": "conan-client", "sign_artifacts": {"manifest": "pkgsign-manifest.json", "signature": "pkgsign-manifest.json.sig"}}] else: # no-op: The package should not be signed or if it is signed, it shouldn't be resigned print("Skipping package", str(ref)) return [] """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.run("create conanfile1.py") c.run("cache sign pkg/0.1") assert "Signing package pkg/0.1" in c.out metadata_folder = c.cache.recipe_layout( RecipeReference("pkg", "0.1", revision="485dad6cb11e2fa99d9afbe44a57a164")).metadata() signatures_path = os.path.join(metadata_folder, "sign", "pkgsign-signatures.json") assert os.path.exists(signatures_path) c.run("create conanfile2.py") c.run("cache sign no-op/0.1") assert "Skipping package no-op/0.1" in c.out metadata_folder = c.cache.recipe_layout( RecipeReference("no-op", "0.1", revision="9f8243dd2f341b13df241e599326b3cb")).metadata() signatures_path = os.path.join(metadata_folder, "sign", "pkgsign-signatures.json") assert not os.path.exists(signatures_path) ================================================ FILE: test/integration/test_recipe_policies.py ================================================ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_build_policies_in_conanfile(): client = TestClient(default_server_user=True, light=True) base = GenConanfile("hello0", "1.0").with_exports("*") conanfile = str(base) + "\n build_policy = 'missing'" client.save({"conanfile.py": conanfile}) client.run("export . --user=lasote --channel=stable") # Install, it will build automatically if missing (without the --build missing option) client.run("install --requires=hello0/1.0@lasote/stable") assert "Building" in client.out # Try to do it again, now we have the package, so no build is done client.run("install --requires=hello0/1.0@lasote/stable") assert "Building" not in client.out # Try now to upload all packages, should not crash because of the "missing" build policy client.run("upload hello0/1.0@lasote/stable -r default") # --- Build policy to always --- conanfile = str(base) + "\n build_policy = 'always'" client.save({"conanfile.py": conanfile}, clean_first=True) client.run("export . --user=lasote --channel=stable") # Install, it will build automatically if missing (without the --build missing option) client.run("install --requires=hello0/1.0@lasote/stable", assert_error=True) assert "ERROR: hello0/1.0@lasote/stable: build_policy='always' has been removed" in client.out def test_build_policy_missing(): c = TestClient(default_server_user=True, light=True) conanfile = GenConanfile("pkg", "1.0").with_class_attribute('build_policy = "missing"')\ .with_class_attribute('upload_policy = "skip"') c.save({"conanfile.py": conanfile}) c.run("export .") # the --build=never has higher priority c.run("install --requires=pkg/1.0@ --build=never", assert_error=True) assert "ERROR: Missing prebuilt package for 'pkg/1.0'" in c.out c.run("install --requires=pkg/1.0@") assert "pkg/1.0: Building package from source as defined by build_policy='missing'" in c.out # If binary already there it should do nothing c.run("install --requires=pkg/1.0@") assert "pkg/1.0: Building package from source" not in c.out c.run("upload * -r=default -c") assert "Uploading package" not in c.out assert "pkg/1.0: Skipping upload of binaries, because upload_policy='skip'" in c.out ================================================ FILE: test/integration/test_source_download_password.py ================================================ import json import os import platform import sys import textwrap from shutil import copy from unittest import mock import pytest from conan.internal.api.uploader import compress_files from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.file_server import TestFileServer from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save def test_source_download_password(): c = TestClient() file_server = TestFileServer() c.servers["file_server"] = file_server save(os.path.join(file_server.store, "myfile.txt"), "hello world!") server_url = file_server.fake_url conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.files import download, load class Pkg(ConanFile): def source(self): download(self, "{server_url}/basic-auth/myfile.txt", "myfile.txt") self.output.info(f"Content: {{load(self, 'myfile.txt')}}") """) c.save({"conanfile.py": conanfile}) content = {"credentials": [{"url": server_url, "token": "password"}]} save(os.path.join(c.cache_folder, "source_credentials.json"), json.dumps(content)) c.run("source .") assert "Content: hello world!" in c.out content = {"credentials": [{"url": server_url, "user": "user", "password": "password"}]} save(os.path.join(c.cache_folder, "source_credentials.json"), json.dumps(content)) c.run("source .") assert "Content: hello world!" in c.out content = {"credentials": [{"url": server_url, "token": "{{mytk}}"}]} content = "{% set mytk = 'password' %}\n" + json.dumps(content) save(os.path.join(c.cache_folder, "source_credentials.json"), content) c.run("source .") assert "Content: hello world!" in c.out # Errors loading file for invalid in ["", "potato", {"token": "mytoken"}, {}, {"url": server_url}, {"auth": {}}, {"user": "other", "password": "pass"}]: content = {"credentials": [invalid]} save(os.path.join(c.cache_folder, "source_credentials.json"), json.dumps(content)) c.run("source .", assert_error=True) assert "Error loading 'source_credentials.json'" in c.out content = {"credentials": [{"url": server_url, "token": "mytoken2"}]} save(os.path.join(c.cache_folder, "source_credentials.json"), json.dumps(content)) c.run("source .", assert_error=True) assert "ERROR: conanfile.py: Error in source() method, line 6" in c.out assert "Authentication" in c.out def test_source_credentials_only_download(): # https://github.com/conan-io/conan/issues/16396 c = TestClient(default_server_user=True) url = c.servers["default"].fake_url content = {"credentials": [{"url": url, "token": "password stpaces"}]} save(os.path.join(c.cache_folder, "source_credentials.json"), json.dumps(content)) c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") # add_auth should never be called for regular upload/download with mock.patch("conan.internal.rest.conan_requester._SourceURLCredentials.add_auth", None): c.run("upload * -c -r=default") c.run("remove * -c") c.run("download pkg/0.1 -r=default") @pytest.mark.skipif(sys.version_info.minor < 12 or platform.system() == "Windows", reason="Extraction filters only Python 3.12, using symlinks (not Windows)") def test_blocked_malicius_tgz(): folder = temp_folder() f = os.path.join(folder, "myfile.txt") save(f, "The contents") s = os.path.join(folder, "mylink.txt") os.symlink(f, s) tgz_path = compress_files({f: f, s: s}, "myfiles.tgz", dest_dir=folder) os.remove(f) conan_file = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import get class Pkg(ConanFile): name = "pkg" version = "0.1" def source(self): get(self, "http://fake_url/myfiles.tgz") """) client = TestClient() client.save({"conanfile.py": conan_file}) with mock.patch("conan.tools.files.files.download") as mock_download: def download_zip(*args, **kwargs): # noqa copy(tgz_path, os.getcwd()) mock_download.side_effect = download_zip client.run("create . -c tools.files.unzip:filter=data", assert_error=True) assert "AbsoluteLinkError" in client.out client.save({"conanfile.py": conan_file.format("extract_filter='fully_trusted'")}) client.run("create . ") # Doesn't fail now # user conf has precedence client.save({"conanfile.py": conan_file.format("extract_filter='data'")}) client.run("create . -c tools.files.unzip:filter=fully_trusted") # Doesn't fail now ================================================ FILE: test/integration/test_timestamp_error.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_timestamp_error(): """ this test is a reproduction for # https://github.com/conan-io/conan/issues/11606 It was crashing because of multiple test_requires, some of them being BINARY_SKIP, and the prev_timestamp was not being assigned by GraphBinariesAnalizer when caching """ c = TestClient(default_server_user=True) engine = textwrap.dedent(""" from conan import ConanFile class Engine(ConanFile): name = "engine" version = "0.1" def build_requirements(self): self.test_requires("gtest/0.1") """) app = textwrap.dedent(""" from conan import ConanFile class App(ConanFile): def requirements(self): self.requires("engine/0.1") def build_requirements(self): self.test_requires("gtest/0.1") """) c.save({"gtest/conanfile.py": GenConanfile("gtest", "0.1"), "engine/conanfile.py": engine, "app/conanfile.py": app}) c.run("create gtest") c.run("create engine") c.run("upload * -r=default -c") c.run("remove * -c") c.run("install app") # This used to fail, now it is not crashing anymore assert "Finalizing install" in c.out ================================================ FILE: test/integration/tgz_macos_dot_files_test.py ================================================ import os import platform import shutil import subprocess import tempfile import textwrap import pytest from conan.internal.rest.remote_manager import uncompress_file from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires OSX") class TestTgzMacosDotFiles: @staticmethod def _test_for_metadata_in_zip_file(tgz, annotated_file, dot_file_expected): tmp_folder = tempfile.mkdtemp() try: uncompress_file(src_path=tgz, dest_folder=tmp_folder) assert os.path.exists(os.path.join(tmp_folder, annotated_file)) assert dot_file_expected == \ os.path.exists(os.path.join(tmp_folder, "._" + annotated_file)) finally: shutil.rmtree(tmp_folder) def _test_for_metadata(self, folder, annotated_file, dot_file_expected): """ We want to check if the file has metadata associated: Mac creates the ._ files at the moment of creating a tar file in order to send the metadata associated to every file. """ assert os.path.exists(os.path.join(folder, annotated_file)) tmp_folder = tempfile.mkdtemp() try: tgz = os.path.join(tmp_folder, 'compressed.tgz') subprocess.call(["tar", "-zcvf", tgz, "-C", folder, "."]) self._test_for_metadata_in_zip_file(tgz, annotated_file, dot_file_expected) finally: shutil.rmtree(tmp_folder) def test_dot_files(self): """ Check behavior related to ._ files in Macos OS Macos has the ability to store metadata associated to files. This metadata can be stored in the HFS+ (Apple native) or Unix/UFS volumes, but if the store does not have this capability it will be placed in a ._ file. So these files will automatically be created by the OS when it is creating a package in order to send this information. Nevertheless, Conan is using the Python libraries to copy, tar and untar files and this metadata is lost when using them. It can avoid some problems like #3529 but there is missing information that can be valuable at some point in time. This test is here just to be sure that the behavior is not changed without noticing. """ conanfile = textwrap.dedent("""\ from conan import ConanFile from conan.tools.files import copy class Lib(ConanFile): name = "lib" version = "version" exports_sources = "file.txt" def package(self): copy(self, "file.txt", self.source_folder, self.package_folder) """) t = TestClient(path_with_spaces=False, default_server_user=True) t.save({'conanfile.py': conanfile, 'file.txt': "content"}) def _add_macos_metadata_to_file(filepath): subprocess.call(["xattr", "-w", "name", "value", filepath]) _add_macos_metadata_to_file(os.path.join(t.current_folder, 'file.txt')) t.run("create . --user=user --channel=channel") # Check if the metadata travels through the Conan commands pref = t.get_latest_package_reference(RecipeReference.loads("lib/version@user/channel"), NO_SETTINGS_PACKAGE_ID) pkg_folder = t.get_latest_pkg_layout(pref).package() # 1) When copied to the package folder, the metadata is lost self._test_for_metadata(pkg_folder, 'file.txt', dot_file_expected=False) # 2) If we add metadata to a file, it will be there _add_macos_metadata_to_file(os.path.join(pkg_folder, 'file.txt')) self._test_for_metadata(pkg_folder, 'file.txt', dot_file_expected=True) # 3) In the upload process, the metadata is lost again export_download_folder = t.get_latest_ref_layout(pref.ref).download_export() tgz = os.path.join(export_download_folder, "conan_sources.tgz") assert not os.path.exists(tgz) t.run("upload lib/version@user/channel -r default --only-recipe") self._test_for_metadata_in_zip_file(tgz, 'file.txt', dot_file_expected=False) ================================================ FILE: test/integration/toolchains/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/apple/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/apple/test_xcodedeps.py ================================================ import os import platform import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from test.integration.toolchains.apple.test_xcodetoolchain import _get_filename from conan.test.utils.tools import TestClient _expected_dep_xconfig = [ "SYSTEM_HEADER_SEARCH_PATHS = $(inherited) $(SYSTEM_HEADER_SEARCH_PATHS_{name}_{name})", "GCC_PREPROCESSOR_DEFINITIONS = $(inherited) $(GCC_PREPROCESSOR_DEFINITIONS_{name}_{name})", "OTHER_CFLAGS = $(inherited) $(OTHER_CFLAGS_{name}_{name})", "OTHER_CPLUSPLUSFLAGS = $(inherited) $(OTHER_CPLUSPLUSFLAGS_{name}_{name})", "FRAMEWORK_SEARCH_PATHS = $(inherited) $(FRAMEWORK_SEARCH_PATHS_{name}_{name})", "LIBRARY_SEARCH_PATHS = $(inherited) $(LIBRARY_SEARCH_PATHS_{name}_{name})", "OTHER_LDFLAGS = $(inherited) $(OTHER_LDFLAGS_{name}_{name})", ] _expected_conf_xconfig = [ "SYSTEM_HEADER_SEARCH_PATHS_{name}_{name}[config={configuration}][arch={architecture}][sdk={sdk}{sdk_version}] = ", "GCC_PREPROCESSOR_DEFINITIONS_{name}_{name}[config={configuration}][arch={architecture}][sdk={sdk}{sdk_version}] = ", "OTHER_CFLAGS_{name}_{name}[config={configuration}][arch={architecture}][sdk={sdk}{sdk_version}] = ", "OTHER_CPLUSPLUSFLAGS_{name}_{name}[config={configuration}][arch={architecture}][sdk={sdk}{sdk_version}] = ", "FRAMEWORK_SEARCH_PATHS_{name}_{name}[config={configuration}][arch={architecture}][sdk={sdk}{sdk_version}] = ", "LIBRARY_SEARCH_PATHS_{name}_{name}[config={configuration}][arch={architecture}][sdk={sdk}{sdk_version}] = ", "OTHER_LDFLAGS_{name}_{name}[config={configuration}][arch={architecture}][sdk={sdk}{sdk_version}] = " ] def expected_files(current_folder, configuration, architecture, sdk_version): files = [] name = _get_filename(configuration, architecture, sdk_version) deps = ["hello", "goodbye"] files.extend( [os.path.join(current_folder, "conan_{dep}_{dep}{name}.xcconfig".format(dep=dep, name=name)) for dep in deps]) files.append(os.path.join(current_folder, "conandeps.xcconfig")) return files def check_contents(client, deps, configuration, architecture, sdk_version): for dep_name in deps: dep_xconfig = client.load("conan_{dep}_{dep}.xcconfig".format(dep=dep_name)) fname = _get_filename(configuration, architecture, sdk_version) conf_name = "conan_{}_{}{}.xcconfig".format(dep_name, dep_name, fname) assert '#include "{}"'.format(conf_name) in dep_xconfig for var in _expected_dep_xconfig: line = var.format(name=dep_name) assert line in dep_xconfig conan_conf = client.load(conf_name) for var in _expected_conf_xconfig: assert var.format(name=dep_name, configuration=configuration, architecture=architecture, sdk="macosx", sdk_version=sdk_version) in conan_conf @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") def test_generator_files(): client = TestClient() client.save({"hello.py": GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_package_info(cpp_info={"libs": ["hello"], "frameworks": ['framework_hello']})}) client.run("export hello.py --name=hello --version=0.1") client.save({"goodbye.py": GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_package_info(cpp_info={"libs": ["goodbye"], "frameworks": ['framework_goodbye']})}) client.run("export goodbye.py --name=goodbye --version=0.1") client.save({"conanfile.txt": "[requires]\nhello/0.1\ngoodbye/0.1\n"}, clean_first=True) for build_type in ["Release", "Debug"]: client.run("install . -g XcodeDeps -s build_type={} -s arch=x86_64 -s os.sdk_version=12.1 --build missing".format(build_type)) for config_file in expected_files(client.current_folder, build_type, "x86_64", "12.1"): assert os.path.isfile(config_file) conandeps = client.load("conandeps.xcconfig") assert '#include "conan_hello.xcconfig"' in conandeps assert '#include "conan_goodbye.xcconfig"' in conandeps conan_config = client.load("conan_config.xcconfig") assert '#include "conandeps.xcconfig"' in conan_config check_contents(client, ["hello", "goodbye"], build_type, "x86_64", "12.1") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") def test_generator_files_with_custom_config(): client = TestClient() client.save({"hello.py": GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_package_info(cpp_info={"libs": ["hello"]})}) client.run("export hello.py --name=hello --version=0.1") client.save({"goodbye.py": GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_package_info(cpp_info={"libs": ["goodbye"]})}) client.run("export goodbye.py --name=goodbye --version=0.1") conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.apple import XcodeDeps class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" options = {"XcodeConfigName": [None, "ANY"]} default_options = {"XcodeConfigName": None} requires = "hello/0.1", "goodbye/0.1" def generate(self): xcode = XcodeDeps(self) if self.options.get_safe("XcodeConfigName"): xcode.configuration = str(self.options.get_safe("XcodeConfigName")) xcode.generate() """) client.save({"conanfile.py": conanfile_py}) custom_config_name = "CustomConfig" for use_custom_config in [True, False]: for build_type in ["Release", "Debug"]: cli_command = "install . -s build_type={} -s arch=x86_64 -s os.sdk_version=12.1 --build missing".format(build_type) if use_custom_config: cli_command += " -o XcodeConfigName={}".format(custom_config_name) configuration_name = custom_config_name else: configuration_name = build_type client.run(cli_command) for config_file in expected_files(client.current_folder, configuration_name, "x86_64", "12.1"): assert os.path.isfile(config_file) conandeps = client.load("conandeps.xcconfig") assert '#include "conan_hello.xcconfig"' in conandeps assert '#include "conan_goodbye.xcconfig"' in conandeps conan_config = client.load("conan_config.xcconfig") assert '#include "conandeps.xcconfig"' in conan_config check_contents(client, ["hello", "goodbye"], configuration_name, "x86_64", "12.1",) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") def test_xcodedeps_aggregate_components(): client = TestClient() conanfile_py = textwrap.dedent(""" from conan import ConanFile class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.includedirs = ["liba_include"] """) client.save({"conanfile.py": conanfile_py}) client.run("create . --name=liba --version=1.0") r"""" 1 a / \ / 2 3 \ / 4 5 6 | | / \ / / 7 """ conanfile_py = textwrap.dedent(""" from conan import ConanFile class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "liba/1.0" def package_info(self): self.cpp_info.components["libb_comp1"].includedirs = ["libb_comp1"] self.cpp_info.components["libb_comp1"].libdirs = ["mylibdir"] self.cpp_info.components["libb_comp2"].includedirs = ["libb_comp2"] self.cpp_info.components["libb_comp2"].libdirs = ["mylibdir"] self.cpp_info.components["libb_comp2"].requires = ["libb_comp1"] self.cpp_info.components["libb_comp3"].includedirs = ["libb_comp3"] self.cpp_info.components["libb_comp3"].libdirs = ["mylibdir"] self.cpp_info.components["libb_comp3"].requires = ["libb_comp1", "liba::liba"] self.cpp_info.components["libb_comp4"].includedirs = ["libb_comp4"] self.cpp_info.components["libb_comp4"].libdirs = ["mylibdir"] self.cpp_info.components["libb_comp4"].requires = ["libb_comp2", "libb_comp3"] self.cpp_info.components["libb_comp5"].includedirs = ["libb_comp5"] self.cpp_info.components["libb_comp5"].libdirs = ["mylibdir"] self.cpp_info.components["libb_comp6"].includedirs = ["libb_comp6"] self.cpp_info.components["libb_comp6"].libdirs = ["mylibdir"] self.cpp_info.components["libb_comp7"].includedirs = ["libb_comp7"] self.cpp_info.components["libb_comp7"].libdirs = ["mylibdir"] self.cpp_info.components["libb_comp7"].requires = ["libb_comp4", "libb_comp5", "libb_comp6"] """) client.save({"conanfile.py": conanfile_py}) client.run("create . --name=libb --version=1.0") client.run("install --requires=libb/1.0 -g XcodeDeps") lib_entry = client.load("conan_libb.xcconfig") for index in range(1, 8): assert f"conan_libb_libb_comp{index}.xcconfig" in lib_entry component7_entry = client.load("conan_libb_libb_comp7.xcconfig") assert '#include "conan_liba.xcconfig"' in component7_entry arch_setting = client.get_default_host_profile().settings['arch'] arch = "arm64" if arch_setting == "armv8" else arch_setting component7_vars = client.load(f"conan_libb_libb_comp7_release_{arch}.xcconfig") # all of the transitive required components and the component itself are added for index in range(1, 8): assert f"libb_comp{index}" in component7_vars assert "mylibdir" in component7_vars component4_vars = client.load(f"conan_libb_libb_comp4_release_{arch}.xcconfig") # all of the transitive required components and the component itself are added for index in range(1, 5): assert f"libb_comp{index}" in component4_vars for index in range(5, 8): assert f"libb_comp{index}" not in component4_vars # folders are aggregated assert "mylibdir" in component4_vars @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") def test_xcodedeps_traits(): client = TestClient() conanfile_py = textwrap.dedent(""" from conan import ConanFile class LibConan(ConanFile): settings = "os", "compiler", "build_type", "arch" {package_info} {requirements} """) package_info = """ def package_info(self): self.cpp_info.components["cmp1"].includedirs = ["cmp1_includedir"] self.cpp_info.components["cmp2"].includedirs = ["cmp2_includedir"] self.cpp_info.components["cmp1"].libdirs = ["cmp1_libdir"] self.cpp_info.components["cmp2"].libdirs = ["cmp2_libdir"] self.cpp_info.components["cmp1"].libs = ["cmp1_lib"] self.cpp_info.components["cmp2"].libs = ["cmp2_lib"] self.cpp_info.components["cmp1"].system_libs = ["cmp1_system_lib"] self.cpp_info.components["cmp2"].system_libs = ["cmp2_system_lib"] self.cpp_info.components["cmp1"].frameworkdirs = ["cmp1_frameworkdir"] self.cpp_info.components["cmp2"].frameworkdirs = ["cmp2_frameworkdir"] self.cpp_info.components["cmp1"].frameworks = ["cmp1_framework"] self.cpp_info.components["cmp2"].frameworks = ["cmp2_framework"] self.cpp_info.components["cmp1"].defines = ["cmp1_define"] self.cpp_info.components["cmp2"].defines = ["cmp2_define"] self.cpp_info.components["cmp1"].cflags = ["cmp1_cflag"] self.cpp_info.components["cmp2"].cflags = ["cmp2_cflag"] self.cpp_info.components["cmp1"].cxxflags = ["cmp1_cxxflag"] self.cpp_info.components["cmp2"].cxxflags = ["cmp2_cxxflag"] self.cpp_info.components["cmp1"].sharedlinkflags = ["cmp1_sharedlinkflag"] self.cpp_info.components["cmp2"].sharedlinkflags = ["cmp2_sharedlinkflag"] self.cpp_info.components["cmp1"].exelinkflags = ["cmp1_exelinkflag"] self.cpp_info.components["cmp2"].exelinkflags = ["cmp2_exelinkflag"] """ client.save({"lib_a.py": conanfile_py.format(requirements="", package_info=package_info)}) client.run("create lib_a.py --name=lib_a --version=1.0") requirements = """ def requirements(self): self.requires("lib_a/1.0", headers=False) """ client.save({"lib_b.py": conanfile_py.format(requirements=requirements, package_info="")}, clean_first=True) client.run("install lib_b.py -g XcodeDeps") arch_setting = client.get_default_host_profile().settings['arch'] arch = "arm64" if arch_setting == "armv8" else arch_setting comp1_info = client.load(f"conan_lib_a_cmp1_release_{arch}.xcconfig") comp2_info = client.load(f"conan_lib_a_cmp2_release_{arch}.xcconfig") assert "cmp1_include" not in comp1_info assert "cmp2_include" not in comp2_info requirements = """ def requirements(self): self.requires("lib_a/1.0", libs=False) """ client.save({"lib_b.py": conanfile_py.format(requirements=requirements, package_info="")}, clean_first=True) client.run("install lib_b.py -g XcodeDeps") comp1_info = client.load(f"conan_lib_a_cmp1_release_{arch}.xcconfig") comp2_info = client.load(f"conan_lib_a_cmp2_release_{arch}.xcconfig") assert "cmp1_frameworkdir" not in comp1_info assert "cmp2_frameworkdir" not in comp2_info assert "-lcmp1_lib -lcmp1_system_lib -framework cmp1_framework" not in comp1_info assert "-lcmp2_lib -lcmp2_system_lib -framework cmp2_framework" not in comp2_info requirements = """ def requirements(self): self.requires("lib_a/1.0", headers=False, libs=False) """ client.save({"lib_b.py": conanfile_py.format(requirements=requirements, package_info="")}, clean_first=True) client.run("install lib_b.py -g XcodeDeps") # this changed from non-existing to existing after https://github.com/conan-io/conan/pull/15128 existing = [f"conan_lib_a_cmp1_release_{arch}.xcconfig", "conan_lib_a_cmp1.xcconfig", f"conan_lib_a_cmp2_release_{arch}.xcconfig", "conan_lib_a_cmp2.xcconfig", "conan_lib_a.xcconfig"] for file in existing: assert os.path.exists(os.path.join(client.current_folder, file)) assert '#include "conan_lib_a.xcconfig"' in client.load("conandeps.xcconfig") requirements = """ def requirements(self): self.requires("lib_a/1.0", headers=False, libs=False, run=True) """ client.save({"lib_b.py": conanfile_py.format(requirements=requirements, package_info="")}, clean_first=True) client.run("install lib_b.py -g XcodeDeps") comp1_info = client.load(f"conan_lib_a_cmp1_release_{arch}.xcconfig") comp2_info = client.load(f"conan_lib_a_cmp2_release_{arch}.xcconfig") assert "cmp1_define" not in comp1_info assert "cmp2_define" not in comp2_info assert "cmp1_cflag" not in comp1_info assert "cmp2_cflag" not in comp2_info assert "cmp1_cxxflag" not in comp1_info assert "cmp2_cxxflag" not in comp2_info assert "cmp1_sharedlinkflag" not in comp1_info assert "cmp2_sharedlinkflag" not in comp2_info assert "cmp1_exelinkflag" not in comp1_info assert "cmp2_exelinkflag" not in comp2_info @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") def test_xcodedeps_frameworkdirs(): client = TestClient() conanfile_py = textwrap.dedent(""" from conan import ConanFile class LibConan(ConanFile): name = "lib_a" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.frameworkdirs = ["lib_a_frameworkdir"] """) client.save({"conanfile.py": conanfile_py}) client.run("create .") arch_setting = client.get_default_host_profile().settings['arch'] arch = "arm64" if arch_setting == "armv8" else arch_setting client.run("install --requires=lib_a/1.0 -g XcodeDeps") lib_a_xcconfig = client.load(f"conan_lib_a_lib_a_release_{arch}.xcconfig") assert "lib_a_frameworkdir" in lib_a_xcconfig @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") def test_xcodedeps_cppinfo_requires(): """ lib_a: has four components cmp1, cmp2, cmp3, cmp4 lib_b --> uses libA cmp1 so cpp_info.requires = ["lib_a::cmp1"] lib_c --> uses libA cmp2 so cpp_info.requires = ["lib_a::cmp2"] consumer --> libB, libC """ client = TestClient() lib_a = textwrap.dedent(""" from conan import ConanFile class lib_aConan(ConanFile): name = "lib_a" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["cmp1"].includedirs = ["include"] self.cpp_info.components["cmp2"].includedirs = ["include"] self.cpp_info.components["cmp3"].includedirs = ["include"] self.cpp_info.components["cmp4"].includedirs = ["include"] """) lib = textwrap.dedent(""" from conan import ConanFile class lib_{name}Conan(ConanFile): name = "lib_{name}" version = "1.0" settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires("lib_a/1.0") def package_info(self): self.cpp_info.requires = {cppinfo_comps} """) consumer = textwrap.dedent(""" from conan import ConanFile class ConsumerConan(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "XcodeDeps" def requirements(self): self.requires("lib_b/1.0") self.requires("lib_c/1.0") """) client.save({ 'lib_a/conanfile.py': lib_a, 'lib_b/conanfile.py': lib.format(name="b", cppinfo_comps='["lib_a::cmp1"]'), 'lib_c/conanfile.py': lib.format(name="c", cppinfo_comps='["lib_a::cmp2"]'), 'consumer/conanfile.py': consumer, }) client.run("create lib_a") client.run("create lib_b") client.run("create lib_c") client.run("install consumer") """ Check that the generated lib_b and lib_c xcconfig only use the cmp1 and cmp2 components So we will only link against the components specified in the cpp_info.requires of lib_b and lib_c """ lib_b = client.load(os.path.join("consumer", "conan_lib_b_lib_b.xcconfig")) # check that nothing from other components than the specified in the cpp_info.requires # from lib_b and lib_c exist in the xcconfig that adds the includes from components assert "cmp1" in lib_b assert "cmp2" not in lib_b assert "cmp3" not in lib_b assert "cmp4" not in lib_b lib_c = client.load(os.path.join("consumer", "conan_lib_c_lib_c.xcconfig")) assert "cmp1" not in lib_c assert "cmp2" in lib_c assert "cmp3" not in lib_c assert "cmp4" not in lib_c @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") def test_dependency_of_dependency_components(): # testing: https://github.com/conan-io/conan/pull/11772 """ When a dependency of a dependency would have components, only the default name conan_dep_dep.xconfig would be included. However, this file was never generated, as they are in the form conan_dep_component.xconfig. lib_a -> lib_b -> lib_c (with components) """ client = TestClient() lib_a = GenConanfile("lib_a", "1.0").with_require("lib_b/1.0").with_settings("os", "arch", "build_type", "compiler") lib_b = GenConanfile("lib_b", "1.0").with_require("lib_c/1.0").with_settings("os", "arch", "build_type", "compiler") lib_c = textwrap.dedent(""" from conan import ConanFile class lib_aConan(ConanFile): name = "lib_c" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["cmp1"].includedirs = ["include_cmp1"] self.cpp_info.components["cmp2"].includedirs = ["include_cmp2"] """) client.save({ 'conanfile.py': lib_a, 'lib_b/conanfile.py': lib_b, 'lib_c/conanfile.py': lib_c, }) client.run("create lib_c") client.run("create lib_b") client.run("install . -g XcodeDeps") lib_b_xconfig = client.load("conan_lib_b_lib_b.xcconfig") assert '#include "conan_lib_c_cmp1.xcconfig"' in lib_b_xconfig assert '#include "conan_lib_c_cmp1.xcconfig"' in lib_b_xconfig assert '#include "conan_lib_c_lib_c.xcconfig"' not in lib_b_xconfig def test_skipped_not_included(): # https://github.com/conan-io/conan/issues/13818 client = TestClient() pkg_info = {"components": {"component": {"defines": ["SOMEDEFINE"]}}} client.save({"dep/conanfile.py": GenConanfile().with_package_type("header-library") .with_package_info(cpp_info=pkg_info), "pkg/conanfile.py": GenConanfile().with_requirement("dep/0.1") .with_package_type("library") .with_shared_option(), "consumer/conanfile.py": GenConanfile().with_requires("pkg/0.1") .with_settings("os", "build_type", "arch")}) client.run("create dep --name=dep --version=0.1") client.run("create pkg --name=pkg --version=0.1") client.run("install consumer -g XcodeDeps -s arch=x86_64 -s build_type=Release") assert re.search(r"Skipped binaries\n\s+(.*?)", client.out, re.DOTALL) dep_xconfig = client.load("consumer/conan_pkg_pkg.xcconfig") assert "conan_dep.xcconfig" not in dep_xconfig def test_correctly_handle_transitive_components(): # https://github.com/conan-io/conan/issues/14887 client = TestClient() has_components = textwrap.dedent(""" from conan import ConanFile class PkgWithComponents(ConanFile): name = 'has_components' version = '1.0' settings = 'os', 'compiler', 'arch', 'build_type' def package_info(self): self.cpp_info.components['first'].libs = ['first'] self.cpp_info.components['second'].libs = ['donottouch'] self.cpp_info.components['second'].requires = ['first'] """) uses_components = textwrap.dedent(""" from conan import ConanFile class PkgUsesComponent(ConanFile): name = 'uses_components' version = '1.0' settings = 'os', 'compiler', 'arch', 'build_type' def requirements(self): self.requires('has_components/1.0') def package_info(self): self.cpp_info.libs = ['uses_only_first'] self.cpp_info.requires = ['has_components::first'] """) consumer = textwrap.dedent(""" [requires] uses_components/1.0 """) client.save({"has_components.py": has_components, "uses_components.py": uses_components, "consumer.txt": consumer}) client.run("create has_components.py") client.run("create uses_components.py") client.run("install consumer.txt -g XcodeDeps") conandeps = client.load("conandeps.xcconfig") assert '#include "conan_has_components.xcconfig"' not in conandeps assert '#include "conan_uses_components.xcconfig"' in conandeps conan_uses_xcconfig = client.load("conan_uses_components_uses_components.xcconfig") assert '#include "conan_has_components_first.xcconfig"' in conan_uses_xcconfig assert '#include "conan_has_components_second.xcconfig"' not in conan_uses_xcconfig def test_dont_add_skipped_xcconfigs_when_required_by_components(): client = TestClient() regular_lib = textwrap.dedent(""" from conan import ConanFile class PkgWithComponents(ConanFile): name = 'regular_lib' version = '1.0' settings = 'os', 'compiler', 'arch', 'build_type' def requirements(self): self.requires('header_skip/1.0') self.requires('header_transitive/1.0', transitive_headers=True) def package_info(self): self.cpp_info.components['component'].requires = ['header_skip::header_skip', 'header_transitive::header_transitive'] """) header_transitive = textwrap.dedent(""" from conan import ConanFile class PkgUsesComponent(ConanFile): name = 'header_transitive' version = '1.0' settings = 'os', 'compiler', 'arch', 'build_type' package_type = 'header-library' def package_info(self): self.cpp_info.includedirs = ["include"] """) header_skip = textwrap.dedent(""" from conan import ConanFile class PkgUsesComponent(ConanFile): name = 'header_skip' version = '1.0' settings = 'os', 'compiler', 'arch', 'build_type' package_type = 'header-library' def package_info(self): self.cpp_info.includedirs = ["include"] """) client.save({"header_transitive.py": header_transitive, "header_skip.py": header_skip, "regular_lib.py": regular_lib}) client.run("create header_transitive.py") client.run("create header_skip.py") client.run("create regular_lib.py") client.run("install --requires=regular_lib/1.0 -g XcodeDeps") conandeps = client.load("conan_regular_lib_component.xcconfig") assert '#include "conan_header_skip.xcconfig"' not in conandeps assert '#include "conan_header_transitive.xcconfig"' in conandeps # Verify that header_skip xcconfig files are NOT generated (skipped dependency) skip_files = [f for f in os.listdir(client.current_folder) if 'header_skip' in f and f.endswith('.xcconfig')] assert len(skip_files) == 0, f"Header skip files should not be generated: {skip_files}" # Verify that header_transitive xcconfig files ARE generated (transitive dependency) transitive_files = [f for f in os.listdir(client.current_folder) if 'header_transitive' in f and f.endswith('.xcconfig')] assert len(transitive_files) > 0, f"Header transitive files should be generated: {transitive_files}" ================================================ FILE: test/integration/toolchains/apple/test_xcodetoolchain.py ================================================ import platform import re import textwrap import pytest from conan.test.utils.tools import TestClient def _get_filename(configuration, architecture, sdk_version): props = [("configuration", configuration), ("architecture", architecture), ("sdk version", sdk_version)] name = "".join("_{}".format(v) for _, v in props if v is not None and v) name = name.replace(".", "_").replace("-", "_") return name.lower() def _condition(configuration, architecture, sdk_version): sdk = "macosx{}".format(sdk_version or "*") return "[config={}][arch={}][sdk={}]".format(configuration, architecture, sdk) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.parametrize("configuration, os_version, libcxx, cppstd, arch, sdk_version, clang_cppstd", [ ("Release", "", "", "", "x86_64", "", ""), ("Debug", "", "", "", "armv8", "", ""), ("Release", "12.0", "libc++", "20", "x86_64", "", "c++20"), ("Debug", "12.0", "libc++", "20", "x86_64", "", "c++20"), ("Release", "12.0", "libc++", "20", "x86_64", "11.3", "c++20"), ("Release", "12.0", "libc++", "20", "x86_64", "", "c++20"), ]) def test_toolchain_files(configuration, os_version, cppstd, libcxx, arch, sdk_version, clang_cppstd): client = TestClient() client.save({"conanfile.txt": "[generators]\nXcodeToolchain\n"}) cmd = "install . -s build_type={}".format(configuration) cmd = cmd + " -s os.version={}".format(os_version) if os_version else cmd cmd = cmd + " -s compiler.cppstd={}".format(cppstd) if cppstd else cmd cmd = cmd + " -s os.sdk_version={}".format(sdk_version) if sdk_version else cmd cmd = cmd + " -s arch={}".format(arch) if arch else cmd client.run(cmd) arch_name = "arm64" if arch == "armv8" else arch filename = _get_filename(configuration, arch_name, sdk_version) condition = _condition(configuration, arch, sdk_version) toolchain_all = client.load("conantoolchain.xcconfig") toolchain_vars = client.load("conantoolchain{}.xcconfig".format(filename)) conan_config = client.load("conan_config.xcconfig") assert '#include "conantoolchain.xcconfig"' in conan_config assert '#include "conantoolchain{}.xcconfig"'.format(filename) in toolchain_all if libcxx: assert 'CLANG_CXX_LIBRARY{}={}'.format(condition, libcxx) in toolchain_vars if os_version: assert 'MACOSX_DEPLOYMENT_TARGET{}={}'.format(condition, os_version) in toolchain_vars if cppstd: assert 'CLANG_CXX_LANGUAGE_STANDARD{}={}'.format(condition, clang_cppstd) in toolchain_vars def test_toolchain_flags(): client = TestClient() client.save({"conanfile.txt": "[generators]\nXcodeToolchain\n"}) cmd = "install . -c 'tools.build:cxxflags=[\"flag1\"]' " \ "-c 'tools.build:defines=[\"MYDEFINITION\"]' " \ "-c 'tools.build:cflags=[\"flag2\"]' " \ "-c 'tools.build:sharedlinkflags=[\"flag3\"]' " \ "-c 'tools.build:exelinkflags=[\"flag4\"]'" client.run(cmd) conan_global_flags = client.load("conan_global_flags.xcconfig") assert "GCC_PREPROCESSOR_DEFINITIONS = $(inherited) MYDEFINITION" in conan_global_flags assert "OTHER_CFLAGS = $(inherited) flag2" in conan_global_flags assert "OTHER_CPLUSPLUSFLAGS = $(inherited) flag1" in conan_global_flags assert "OTHER_LDFLAGS = $(inherited) flag3 flag4" in conan_global_flags conan_global_file = client.load("conan_config.xcconfig") assert '#include "conan_global_flags.xcconfig"' in conan_global_file def test_flags_generated_if_only_defines(): # https://github.com/conan-io/conan/issues/16422 client = TestClient() client.save({"conanfile.txt": "[generators]\nXcodeToolchain\n"}) client.run("install . -c 'tools.build:defines=[\"MYDEFINITION\"]'") conan_global_flags = client.load("conan_global_flags.xcconfig") assert "GCC_PREPROCESSOR_DEFINITIONS = $(inherited) MYDEFINITION" in conan_global_flags conan_global_file = client.load("conan_config.xcconfig") assert '#include "conan_global_flags.xcconfig"' in conan_global_file @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.parametrize("os_name, sdk, min_version, deployment_target_flag", [ ("Macos", None, "11.0", "MACOSX_DEPLOYMENT_TARGET"), ("iOS", "iphoneos", "18.0", "IPHONEOS_DEPLOYMENT_TARGET"), ("tvOS", "appletvos", "18.4", "TVOS_DEPLOYMENT_TARGET"), ("watchOS", "watchos", "9.0", "WATCHOS_DEPLOYMENT_TARGET"), ("visionOS", "xros", "2.0", "XROS_DEPLOYMENT_TARGET"), ]) def test_xcodetoolchain_xcconfig_deplyment_target(os_name, sdk, min_version, deployment_target_flag): client = TestClient() conanfile = textwrap.dedent(f""" import os from conan import ConanFile from conan.tools.apple import XcodeToolchain from conan.tools.files import save class MyApplicationConan(ConanFile): name = "myapplication" version = "1.0" settings = "os", "compiler", "build_type", "arch" def generate(self): tc = XcodeToolchain(self) tc.generate() # Private access to get the generated xcconfig filename # It changes based on the settings, so this is the less fragile way to get it save(self, os.path.join(self.generators_folder, "name.txt"), tc._vars_xconfig_filename) """) client.save({"conanfile.py": conanfile}) settings = f"-s os={os_name} -s os.version={min_version}" if sdk: settings += f" -s os.sdk={sdk}" client.run(f"install . -s build_type=Release {settings} --build=missing") xcconfig_name = client.load("name.txt").strip() xcconfig = client.load(xcconfig_name) match = re.search(f"^{deployment_target_flag}.+={min_version}$", xcconfig, re.MULTILINE) assert match is not None ================================================ FILE: test/integration/toolchains/cmake/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/cmake/cmakeconfigdeps/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py ================================================ import re import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient new_value = "will_break_next" def test_cmakedeps_direct_deps_paths(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): name = "lib" version = "1.0" def package_info(self): self.cpp_info.includedirs = ["myincludes"] self.cpp_info.libdirs = ["mylib"] self.cpp_info.frameworkdirs = ["myframework"] """) c.save({"conanfile.py": conanfile}) c.run("create .") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake class PkgConan(ConanFile): requires = "lib/1.0" settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps" """) c.save({"conanfile.py": conanfile}, clean_first=True) c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") cmake_paths = c.load("conan_cmakedeps_paths.cmake") assert "set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)" in cmake_paths assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/bin\"", cmake_paths) # default assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/mylib\"", cmake_paths) assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/myincludes\"", cmake_paths) assert re.search(r"list\(PREPEND CMAKE_FRAMEWORK_PATH \".*/myframework\"", cmake_paths) def test_cmakedeps_transitive_paths(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): name = "liba" version = "1.0" def package_info(self): self.cpp_info.includedirs = ["includea"] self.cpp_info.libdirs = ["liba"] self.cpp_info.bindirs = ["bina"] """) c.save({"conanfile.py": conanfile}) c.run("create .") conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): name = "libb" version = "1.0" requires = "liba/1.0" def package_info(self): self.cpp_info.includedirs = ["includeb"] self.cpp_info.libdirs = ["libb"] self.cpp_info.bindirs = ["binb"] """) c.save({"conanfile.py": conanfile}) c.run("create .") conanfile = textwrap.dedent(f""" from conan import ConanFile class PkgConan(ConanFile): requires = "libb/1.0" settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps" """) c.save({"conanfile.py": conanfile}, clean_first=True) c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") cmake_paths = c.load("conan_cmakedeps_paths.cmake") assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/libb.*/p/binb\"\)", cmake_paths) assert not re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH /bina\"", cmake_paths) assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/libb.*/p/libb\" \".*/liba.*/p/liba\"\)", cmake_paths) assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/libb.*/p/includeb\" " r"\".*/liba.*/p/includea\"\)", cmake_paths) def test_cmakedeps_deployer_relative_paths(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): name = "liba" version = "1.0" def package_info(self): self.cpp_info.includedirs = ["includea"] self.cpp_info.libdirs = ["bina"] self.cpp_info.bindirs = ["bina"] crypto_module = os.path.join("share", "cmake", "crypto.cmake") self.cpp_info.set_property("cmake_build_modules", [crypto_module]) """) c.save({"conanfile.py": conanfile}) c.run("create .") conanfile_cmake = textwrap.dedent(""" import os from conan.tools.files import save from conan import ConanFile class TestConan(ConanFile): name = "libb" version = "1.0" def package(self): save(self, os.path.join(self.package_folder, "libb-config.cmake"), "") def package_info(self): self.cpp_info.set_property("cmake_find_mode", "none") """) c.save({"conanfile.py": conanfile_cmake}) c.run("create .") conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake class PkgConan(ConanFile): requires = "liba/1.0", "libb/1.0" settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps" """) c.save({"conanfile.py": conanfile}, clean_first=True) # Now with a deployment c.run(f"install . -c tools.cmake.cmakedeps:new={new_value} --deployer=full_deploy") cmake_paths = c.load("conan_cmakedeps_paths.cmake") assert 'set(libb_DIR "${CMAKE_CURRENT_LIST_DIR}/full_deploy/host/libb/1.0")' in cmake_paths assert ('set(CONAN_RUNTIME_LIB_DIRS "$<$:${CMAKE_CURRENT_LIST_DIR}' '/full_deploy/host/liba/1.0/bina>"') in cmake_paths liba_config = c.load("liba-config.cmake") assert ('include("${CMAKE_CURRENT_LIST_DIR}/full_deploy/' 'host/liba/1.0/share/cmake/crypto.cmake")') in liba_config assert ('set(liba_INCLUDE_DIRS "${CMAKE_CURRENT_LIST_DIR}/full_deploy/' 'host/liba/1.0/includea" )') in liba_config liba_targets = c.load("liba-Targets-release.cmake") assert ('set(liba_PACKAGE_FOLDER_RELEASE "${CMAKE_CURRENT_LIST_DIR}/full_deploy/' 'host/liba/1.0")') in liba_targets def test_cmakeconfigdeps_recipe(): c = TestClient() conanfile = textwrap.dedent(""" from conan.tools.cmake import CMakeConfigDeps from conan import ConanFile class TestConan(ConanFile): settings = "build_type" requires = "dep/0.1" def generate(self): deps = CMakeConfigDeps(self) deps.generate() """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "app/conanfile.py": conanfile}) c.run("create dep") c.run("install app") assert "WARN: experimental: CMakeConfigDeps is experimental" in c.out # attribute generator conanfile = textwrap.dedent(""" from conan.tools.cmake import CMakeConfigDeps from conan import ConanFile class TestConan(ConanFile): settings = "build_type" requires = "dep/0.1" generators = "CMakeConfigDeps" """) c.save({"app/conanfile.py": conanfile}, clean_first=True) c.run("install app") assert "WARN: experimental: CMakeConfigDeps is experimental" in c.out # conanfile.txt conanfile = textwrap.dedent(""" [requires] dep/0.1 [generators] CMakeConfigDeps """) c.save({"app/conanfile.txt": conanfile}, clean_first=True) c.run("install app") assert "WARN: experimental: CMakeConfigDeps is experimental" in c.out def test_system_wrappers(): c = TestClient() conanfile = textwrap.dedent(""" import os from conan.tools.files import copy from conan import ConanFile class TestConan(ConanFile): name = "lib" version = "system" package_type = "shared-library" def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.libdirs = [] self.cpp_info.system_libs = ["my_system_cool_lib"] """) c.save({"conanfile.py": conanfile}) c.run("create .") c.run(f"install --requires=lib/system -g CMakeConfigDeps " f"-c tools.cmake.cmakedeps:new={new_value}") cmake = c.load("lib-Targets-release.cmake") assert "add_library(lib::lib INTERFACE IMPORTED)" in cmake assert "set_property(TARGET lib::lib APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ ' $<$:my_system_cool_lib>)' in cmake def test_autolink_pragma(): """https://github.com/conan-io/conan/issues/10837""" c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.cpp_info.set_property("cmake_set_interface_link_directories", True) """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_test("pass") .with_settings("build_type") .with_generator("CMakeDeps")}) c.run("create . --name=pkg --version=0.1") assert "CMakeDeps: cmake_set_interface_link_directories is legacy, not necessary" in c.out c.run(f"create . --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") assert "CMakeConfigDeps: cmake_set_interface_link_directories deprecated and invalid. " \ "The package 'package_info()' must correctly define the (CPS) information" in c.out def test_consuming_cpp_info_with_components_dependency_from_same_package(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.cpp_info.components["lib"].type = 'shared-library' self.cpp_info.components["lib_extended"].type = 'shared-library' self.cpp_info.components["lib_extended"].requires = ['lib'] """) c.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_settings("build_type") .with_test("pass") .with_generator("CMakeDeps")}) c.run(f"create . --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") # it doesn't break assert "find_package(pkg)" in c.out def test_consuming_cpp_info_with_components_dependency_from_other_package(): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.components["lib"].type = 'shared-library' """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/0.1" def package_info(self): self.cpp_info.components["lib"].type = 'shared-library' self.cpp_info.components["lib"].requires = ['dep::lib'] """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": conanfile, "pkg/test_package/conanfile.py": GenConanfile().with_settings("build_type") .with_test("pass") .with_generator("CMakeDeps")}) c.run("create dep") c.run(f"create pkg --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") # it doesn't break assert "find_package(pkg)" in c.out def test_error_incorrect_component(): # https://github.com/conan-io/conan/issues/18554 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): requires = "dep/0.1" def package_info(self): self.cpp_info.requires = ['dep::lib'] """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": conanfile, "pkg/test_package/conanfile.py": GenConanfile().with_settings("build_type") .with_generator("CMakeDeps") .with_test("pass")}) c.run("create dep") c.run(f"create pkg --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert ("ERROR: Error in generator 'CMakeDeps': pkg/0.1 recipe cpp_info did .requires to " "'dep::lib' but component 'lib' not found in dep") in c.out def test_consuming_cpp_info_transitively_by_requiring_root_component(): c = TestClient() dependent_conanfile = textwrap.dedent(""" from conan import ConanFile class Dependent(ConanFile): settings = "os", "compiler", "arch", "build_type" name = 'dependent' """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def requirements(self): self.requires('dependent/0.1') def package_info(self): self.cpp_info.type = 'shared-library' self.cpp_info.requires = ['dependent::dependent'] """) test_package = textwrap.dedent(""" from conan import ConanFile class TestPkg(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "VirtualRunEnv", "CMakeDeps" def requirements(self): self.requires(self.tested_reference_str) def test(self): pass """) c.save({"dependent/conanfile.py": dependent_conanfile, "main/conanfile.py": conanfile, "main/test_package/conanfile.py": test_package}) c.run("create ./dependent/ --name=dependent --version=0.1 " f"-c tools.cmake.cmakedeps:new={new_value}") c.run(f"create ./main/ --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") def test_cmake_find_mode_deprecated(): tc = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): # Having both is ok as the user expects that config would # be generated nonetheless self.cpp_info.set_property("cmake_find_mode", "module") """) tc.save({"conanfile.py": dep}) tc.run("create .") args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" tc.run(f"install --requires=dep/0.1 {args}") assert "CMakeConfigDeps does not support module find mode" def test_build_context_deprecated(): tc = TestClient() conanfile = textwrap.dedent(""" from conan.tools.cmake import CMakeConfigDeps from conan import ConanFile class TestConan(ConanFile): settings = "build_type" def generate(self): deps = CMakeConfigDeps(self) deps.build_context_activated = ["bar"] deps.build_context_suffix = {"bar": "_BUILD"} deps.build_context_build_modules = ["myfunctions"] deps.check_components_exist = True deps.generate() """) tc.save({"conanfile.py": conanfile}) tc.run("install .") assert "WARN: deprecated: CMakeConfigDeps.build_context_activated is deprecated" in tc.out assert "WARN: deprecated: CMakeConfigDeps.build_context_suffix is deprecated" in tc.out assert "WARN: deprecated: CMakeConfigDeps.build_context_build_modules is deprecated" in tc.out assert "WARN: deprecated: CMakeConfigDeps.check_components_exist is deprecated" in tc.out def test_cmake_extra_dependencies(): tc = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_extra_dependencies", ["MyOpenMPI"]) self.cpp_info.set_property("cmake_extra_interface_libs", ["MyOpenMPILib"]) """) tc.save({"conanfile.py": dep}) tc.run("create .") args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" tc.run(f"install --requires=dep/0.1 {args}") dep = tc.load("dep-Targets-release.cmake") assert "find_dependency(MyOpenMPI REQUIRED )" in dep assert "set_property(TARGET dep::dep APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ " $<$:MyOpenMPILib>)" in dep def test_cmake_component_type_none_check(): tc = TestClient() dep = (GenConanfile("dep", "0.1") .with_package_file("lib/libmain.so", "dynamic library") .with_package_info({"components": {"main": {"libs": ["libmain.so"], "type": "'shared-library'"}}})) tc.save({"conanfile.py": dep}) tc.run("create") tc.run("install --requires=dep/0.1 -g CMakeConfigDeps") assert "None is not a valid PackageType" not in tc.out def test_cmake_extra_dependencies_components(): tc = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_extra_dependencies", ["MyOpenMPI"]) self.cpp_info.components["mycomp"].set_property("cmake_extra_interface_libs", ["MyOpenMPILib"]) self.cpp_info.components["mycomp"].libs = ["mycomplib"] self.cpp_info.components["mycomp"].type = "static-library" self.cpp_info.components["mycomp"].location = "lib/mycomp.a" """) tc.save({"conanfile.py": dep}) tc.run("create .") args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" tc.run(f"install --requires=dep/0.1 {args}") dep = tc.load("dep-Targets-release.cmake") assert "find_dependency(MyOpenMPI REQUIRED )" in dep assert "set_property(TARGET dep::mycomp APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ " $<$:MyOpenMPILib>)" in dep class TestRequiresToApp: def test_requires_to_application(self): c = TestClient() automake = GenConanfile("automake", "0.1").with_package_type("application") conanfile = (GenConanfile("libtool", "0.1").with_package_type("static-library") .with_requirement("automake/0.1")) test_package = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class TestPkg(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "CMakeDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def test(self): pass """) c.save({"automake/conanfile.py": automake, "libtool/conanfile.py": conanfile, "libtool/test_package/conanfile.py": test_package}) c.run("create automake") c.run(f"create libtool -c tools.cmake.cmakedeps:new={new_value}") targets = c.load("libtool/test_package/libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::automake" not in targets def test_requires_to_application_component(self): c = TestClient() automake = textwrap.dedent(""" from conan import ConanFile class Dependent(ConanFile): name = "automake" version = "0.1" package_type = "application" def package_info(self): self.cpp_info.components["myapp"].exe = "myapp" self.cpp_info.components["myapp"].location = "path/to/myapp" self.cpp_info.components["mylibapp"].type = "header-library" """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "libtool" version = "0.1" package_type = "static-library" def requirements(self): self.requires('automake/0.1') def package_info(self): self.cpp_info.requires = ["automake::mylibapp"] """) c.save({"automake/conanfile.py": automake, "libtool/conanfile.py": conanfile}) c.run("create automake") c.run("create libtool") c.run("install --requires=libtool/0.1 -g CMakeDeps " f"-c tools.cmake.cmakedeps:new={new_value}") targets = c.load("libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::automake" not in targets assert "# Requirement libtool::libtool -> automake::mylibapp (Full link: True)" in targets assert "$<$:automake::mylibapp>" in targets def test_requires_from_library_component(self): c = TestClient() automake = GenConanfile("automake", "0.1").with_package_type("application") conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "libtool" version = "0.1" package_type = "static-library" def requirements(self): self.requires('automake/0.1') def package_info(self): self.cpp_info.components["mycomp"].requires = ["automake::automake"] """) c.save({"automake/conanfile.py": automake, "libtool/conanfile.py": conanfile}) c.run("create automake") c.run("create libtool") c.run("install --requires=libtool/0.1 -g CMakeDeps " f"-c tools.cmake.cmakedeps:new={new_value}") targets = c.load("libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::automake" not in targets def test_requires_from_library_component_to_app_component(self): c = TestClient() automake = textwrap.dedent(""" from conan import ConanFile class Dependent(ConanFile): name = "automake" version = "0.1" def package_info(self): self.cpp_info.components["myapp"].exe = "myapp" self.cpp_info.components["myapp"].location = "path/to/myapp" self.cpp_info.components["mylibapp"].type = "header-library" """) conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "libtool" version = "0.1" package_type = "static-library" def requirements(self): self.requires('automake/0.1') def package_info(self): self.cpp_info.components["mycomp"].requires = ["automake::myapp"] """) c.save({"automake/conanfile.py": automake, "libtool/conanfile.py": conanfile}) c.run("create automake") c.run("create libtool") c.run("install --requires=libtool/0.1 -g CMakeDeps " f"-c tools.cmake.cmakedeps:new={new_value}") targets = c.load("libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::myapp" not in targets assert "automake::automake" not in targets def test_alias_cmakedeps_set_property(): tc = TestClient() tc.save({"dep/conanfile.py": textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["mycomp"].set_property("cmake_target_name", "dep::mycomponent") """), "conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps, CMake class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" requires = "dep/1.0" def generate(self): deps = CMakeDeps(self) deps.set_property("dep", "cmake_target_aliases", ["alias", "dep::other_name"]) deps.set_property("dep::mycomp", "cmake_target_aliases", ["component_alias", "dep::my_aliased_component"]) deps.generate() """)}) tc.run("create dep") tc.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") targets_data = tc.load('dep-Targets-release.cmake') assert "add_library(dep::dep" in targets_data assert "add_library(alias" in targets_data assert "add_library(dep::other_name" in targets_data assert "add_library(component_alias" in targets_data assert "add_library(dep::my_aliased_component" in targets_data def test_package_info_extra_variables(): """ Test extra_variables property - This just shows that it works, there are tests for cmaketoolchain that check the actual behavior of parsing the variables""" client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_extra_variables", {"FOO": 42, "BAR": 42, "CMAKE_GENERATOR_INSTANCE": "${GENERATOR_INSTANCE}/buildTools/", "CACHE_VAR_DEFAULT_DOC": {"value": "hello world", "cache": True, "type": "PATH"}}) """) client.save({"conanfile.py": conanfile}) client.run("create .") client.run(f"install --requires=pkg/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value} " """-c tools.cmake.cmaketoolchain:extra_variables="{'BAR': 9}" """) target = client.load("pkg-config.cmake") assert 'set(BAR' not in target assert 'set(CMAKE_GENERATOR_INSTANCE "${GENERATOR_INSTANCE}/buildTools/")' in target assert 'set(FOO 42)' in target assert 'set(CACHE_VAR_DEFAULT_DOC "hello world" CACHE PATH "CACHE_VAR_DEFAULT_DOC")' in target def test_target_defines_only(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def package_info(self): self.cpp_info.components["base"].includedirs = [] self.cpp_info.components["base"].defines = ["FOO=1"] self.cpp_info.components["comp"].includedirs = ["include"] self.cpp_info.components["comp"].requires = ["base"] """) client.save({"conanfile.py": conanfile}) client.run("create .") client.run(f"install --requires=pkg/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}") target = client.load("pkg-Targets-release.cmake") assert 'add_library(pkg::base INTERFACE IMPORTED)' in target assert "# Requirement pkg::comp -> pkg::base (Full link: True)" in target class TestLinkFeatures: def test_link_info_global_cpp_info(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.set_property("cmake_link_feature", "MYFET") """) tc.save({"conanfile.py": conanfile}) tc.run("create") dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" settings = "os", "compiler", "build_type", "arch" requires = "pkg/1.0" """) tc.save({"conanfile.py": dep}) tc.run("create") tc.run("install --requires=dep/1.0 -g CMakeConfigDeps") # The requirement should propagate the link feature info target = tc.load("dep-Targets-release.cmake") assert "# Requirement dep::dep -> pkg::pkg (Full link: True)\n# Link feature: MYFET" in target def test_link_info_local_component_from_interface(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["compA"].set_property("cmake_link_feature", "MYFET") """) tc.save({"conanfile.py": conanfile}) tc.run("create") tc.run("install --requires=pkg/1.0 -g CMakeConfigDeps") targets = tc.load("pkg-Targets-release.cmake") # The interface library created as a global target should have the requirement assert "# Requirement pkg::pkg -> pkg::compA (Full link: True)\n# Link feature: MYFET" in targets def test_link_info_local_component_to_component_require(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["compA"].set_property("cmake_link_feature", "MYFET") self.cpp_info.components["compB"].requires = ["compA"] """) tc.save({"conanfile.py": conanfile}) tc.run("create") tc.run("install --requires=pkg/1.0 -g CMakeConfigDeps") targets = tc.load("pkg-Targets-release.cmake") # The component requirement should have the link feature info assert "# Requirement pkg::compB -> pkg::compA (Full link: True)\n# Link feature: MYFET" in targets def test_link_info_lib_to_component_require(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["compA"].set_property("cmake_link_feature", "MYFET") """) tc.save({"conanfile.py": conanfile}) tc.run("create") dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" settings = "os", "compiler", "build_type", "arch" requires = "pkg/1.0" def package_info(self): self.cpp_info.requires = ["pkg::compA"] """) tc.save({"conanfile.py": dep}) tc.run("create") tc.run("install --requires=dep/1.0 -g CMakeConfigDeps") targets = tc.load("dep-Targets-release.cmake") # The requirement should have the link feature info assert "# Requirement dep::dep -> pkg::compA (Full link: True)\n# Link feature: MYFET" in targets def test_legacy_defines(): # We used not to populate this. # We do for backward compatibility with old check_symbol_exists and similar CMake code tc = TestClient() tc.save({"conanfile.py": GenConanfile("mypkg", "1.0") .with_package_info({"defines": ["MY_DEFINE", "MYVAR=1"]})}) tc.run("create") tc.run("install --requires=mypkg/1.0 -g CMakeConfigDeps") mypkg_config = tc.load("mypkg-config.cmake") assert 'set(mypkg_DEFINITIONS "-DMY_DEFINE;-DMYVAR=1" )' in mypkg_config class TestPropertiesBuildContext: def test_property_build_context(self): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeConfigDeps class PackageConan(ConanFile): name = "package" settings = "os", "arch", "compiler", "build_type" def requirements(self): self.requires("zlib/1.3.1") def generate(self): deps = CMakeConfigDeps(self) deps.set_property("zlib", "cmake_file_name", "MyZlibName") deps.generate() """) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.3.1"), "pkg/conanfile.py": conanfile}) c.run("create zlib") c.run("install pkg --build-require") assert "find_package(MyZlibName)" in c.out config = c.load("pkg/MyZlibNameConfig.cmake") assert 'set(MyZlibName_VERSION_STRING "1.3.1")' in config class TestExtraFindExtraVariants: def test_generated_dir_entries(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "1.0" def package_info(self): self.cpp_info.set_property("cmake_file_name_variants", ["HellO", "HELLO"]) """) tc.save({"conanfile.py": conanfile}) tc.run("create") tc.run("install --requires=hello/1.0 -g CMakeConfigDeps") paths_content = tc.load("conan_cmakedeps_paths.cmake") assert "set(hello_DIR" in paths_content assert "set(HellO_DIR" in paths_content assert "set(HELLO_DIR" in paths_content def test_differing_names_instead_of_case(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "1.0" def package_info(self): self.cpp_info.set_property("cmake_file_name_variants", ["Bye!"]) """) tc.save({"conanfile.py": conanfile}) tc.run("create") tc.run("install --requires=hello/1.0 -g CMakeConfigDeps", assert_error=True) assert ("'cmake_file_name_variants' property contains " "entries that differ from the default 'cmake_file_name'='hello'") in tc.out def test_consumer_dependency_name_change(self): """ If the consumer changes the dependency name via cmake_file_name, the extra casings do not get generated""" tc = TestClient() hello = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "1.0" def package_info(self): self.cpp_info.set_property("cmake_file_name_variants", ["HellO"]) """) tc.save({"hello/conanfile.py": hello}) tc.run("create hello") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeConfigDeps class Consumer(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "hello/1.0" def generate(self): deps = CMakeConfigDeps(self) deps.set_property("hello", "cmake_file_name", "greetings") deps.generate() """) tc.save({"conanfile.py": conanfile}) tc.run("install") assert ("'cmake_file_name_variants' property contains names " "with different casings than the defined name 'greetings'") in tc.out paths_content = tc.load("conan_cmakedeps_paths.cmake") assert "set(greetings_DIR" in paths_content # But the old casing names are not generated, even if they were defined in the package # they would not work assert "set(HellO_DIR" not in paths_content # The original name is not created in any case either way, as expected when cmake_file_name is used assert "set(hello_DIR" not in paths_content def test_generated_dir_none_find_mode_multi_entries(self): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = "hello" version = "1.0" settings = "build_type" def package_info(self): self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.set_property("cmake_file_name_variants", ["HellO", "HELLO"]) """) tc.save({"conanfile.py": conanfile}) tc.run("create") tc.run("create -s=build_type=Debug") tc.run("install --requires=hello/1.0 -g CMakeConfigDeps") paths_content = tc.load("conan_cmakedeps_paths.cmake") assert "set(hello_DIR" not in paths_content assert "set(HellO_DIR" not in paths_content assert "set(HELLO_DIR" not in paths_content assert "list(APPEND CONAN_hello_DIR_MULTI" in paths_content assert "list(APPEND CONAN_HellO_DIR_MULTI" in paths_content assert "list(APPEND CONAN_HELLO_DIR_MULTI" in paths_content tc.run("install --requires=hello/1.0 -g CMakeConfigDeps -s=build_type=Debug") paths_content = tc.load("conan_cmakedeps_paths.cmake") # Reading already existing MULTI variables works assert paths_content.count("list(APPEND CONAN_hello_DIR_MULTI") == 2 assert paths_content.count("list(APPEND CONAN_HellO_DIR_MULTI") == 2 assert paths_content.count("list(APPEND CONAN_HELLO_DIR_MULTI") == 2 def test_find_file_in_package(self): tc = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class HelloConan(ConanFile): name = "hello" version = "1.0" settings = "build_type" def package(self): save(self, os.path.join(self.package_folder, "HellOConfig.cmake"), "") def package_info(self): self.cpp_info.builddirs = ["."] self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.set_property("cmake_file_name_variants", ["HellO", "HELLO"]) """) tc.save({"conanfile.py": conanfile}) tc.run("create") tc.run("create -s=build_type=Debug") tc.run("install --requires=hello/1.0 -g CMakeConfigDeps") paths_content = tc.load("conan_cmakedeps_paths.cmake") assert "set(hello_DIR" in paths_content assert "set(HellO_DIR" in paths_content assert "set(HELLO_DIR" in paths_content def test_requires_only_component_target_generation(): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["compA"].includedirs = ["include"] self.cpp_info.components["compB"].includedirs = [] self.cpp_info.components["compB"].requires = ["compA"] """) tc.save({"conanfile.py": conanfile}) tc.run("create .") tc.run("install --requires=pkg/1.0 -g CMakeConfigDeps") target = tc.load("pkg-Targets-release.cmake") # An otherwise empty component is generated as a target if it requires another component # to work as an interface target for the requirement # (For example, useful when a component aggregates optional components under it) assert "add_library(pkg::compB INTERFACE" in target assert "# Requirement pkg::compB -> pkg::compA (Full link: True)" in target # And even if it's INTERFACE, the globally generated target requires it as usual assert "# Requirement pkg::pkg -> pkg::compB (Full link: True)" in target ================================================ FILE: test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient new_value = "will_break_next" @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_package_framework_needs_location(): conanfile = textwrap.dedent(f""" import os from conan import ConanFile class MyFramework(ConanFile): name = "frame" version = "1.0" settings = "os", "arch", "compiler", "build_type" package_type = 'static-library' def package_info(self): self.cpp_info.type = 'static-library' self.cpp_info.package_framework = "MyFramework" """) test_conanfile = textwrap.dedent(""" import os from conan import ConanFile class LibTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeConfigDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) """) client = TestClient() client.save({ 'test_package/conanfile.py': test_conanfile, 'conanfile.py': conanfile }) client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) assert "Error in generator 'CMakeConfigDeps': cpp_info.location missing for framework MyFramework" in client.out def test_framework_only_component_generates_target(): conanfile = textwrap.dedent(f""" import os from conan import ConanFile class MyFramework(ConanFile): name = "frame" version = "1.0" settings = "os", "arch", "compiler", "build_type" def package_info(self): self.cpp_info.frameworks = ["MyFramework"] self.cpp_info.libdirs = [] self.cpp_info.includedirs = [] """) tc = TestClient() tc.save({"conanfile.py": conanfile}) tc.run("create .") tc.run(f"install --requires=frame/1.0 -g CMakeConfigDeps -c=tools.cmake.cmakedeps:new={new_value}") targets = tc.load("frame-Targets-release.cmake") assert "add_library(frame::frame INTERFACE IMPORTED)" in targets ================================================ FILE: test/integration/toolchains/cmake/cmakedeps/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py ================================================ import os import platform import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_package_from_system(): """ If a node declares "system_package" property, the CMakeDeps generator will skip generating the -config.cmake and the other files for that node but will keep the "find_dependency" for the nodes depending on it. That will cause that cmake looks for the config files elsewhere https://github.com/conan-io/conan/issues/8919""" client = TestClient() dep2 = str(GenConanfile().with_name("dep2").with_version("1.0") .with_settings("os", "arch", "build_type")) dep2 += """ def package_info(self): self.cpp_info.set_property("cmake_find_mode", "None") self.cpp_info.set_property("cmake_file_name", "custom_dep2") """ client.save({"conanfile.py": dep2}) client.run("create .") dep1 = GenConanfile().with_name("dep1").with_version("1.0").with_require("dep2/1.0")\ .with_settings("os", "arch", "build_type") client.save({"conanfile.py": dep1}) client.run("create .") consumer = GenConanfile().with_name("consumer").with_version("1.0").\ with_require("dep1/1.0").with_generator("CMakeDeps").\ with_settings("os", "arch", "build_type") client.save({"conanfile.py": consumer}) client.run("install .") assert os.path.exists(os.path.join(client.current_folder, "dep1-config.cmake")) assert not os.path.exists(os.path.join(client.current_folder, "dep2-config.cmake")) assert not os.path.exists(os.path.join(client.current_folder, "custom_dep2-config.cmake")) host_arch = client.get_default_host_profile().settings['arch'] dep1_contents = client.load(f"dep1-release-{host_arch}-data.cmake") assert 'list(APPEND dep1_FIND_DEPENDENCY_NAMES custom_dep2)' in dep1_contents assert 'set(custom_dep2_FIND_MODE "")' in dep1_contents def test_test_package(): client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=gtest --version=1.0") client.run("create . --name=cmake --version=1.0") client.save({"conanfile.py": GenConanfile().with_tool_requires("cmake/1.0"). with_test_requires("gtest/1.0")}) client.run("export . --name=pkg --version=1.0") consumer = textwrap.dedent(r""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps" requires = "pkg/1.0" """) client.save({"conanfile.py": consumer}) client.run("install . -s:b os=Windows -s:h os=Linux -s:h compiler=gcc -s:h compiler.version=7 " "-s:h compiler.libcxx=libstdc++11 -s:h arch=x86_64 --build=missing") cmake_data = client.load("pkg-release-x86_64-data.cmake") assert "gtest" not in cmake_data def test_components_error(): # https://github.com/conan-io/conan/issues/9331 client = TestClient() conan_hello = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): settings = "os" def layout(self): pass def package_info(self): self.cpp_info.components["say"].includedirs = ["include"] """) client.save({"conanfile.py": conan_hello}) client.run("create . --name=hello --version=1.0") def test_cpp_info_component_objects(): client = TestClient() conan_hello = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "arch", "build_type" def package_info(self): self.cpp_info.components["say"].objects = ["mycomponent.o"] """) client.save({"conanfile.py": conan_hello}) client.run("create . --name=hello --version=1.0 -s arch=x86_64 -s build_type=Release") client.run("install --requires=hello/1.0@ -g CMakeDeps -s arch=x86_64 -s build_type=Release") with open(os.path.join(client.current_folder, "hello-Target-release.cmake")) as f: content = f.read() assert """set_property(TARGET hello::say APPEND PROPERTY INTERFACE_LINK_LIBRARIES $<$:${hello_hello_say_OBJECTS_RELEASE}> $<$:${hello_hello_say_LIBRARIES_TARGETS}> )""" in content # If there are componets, there is not a global cpp so this is not generated assert "hello_OBJECTS_RELEASE" not in content # But the global target is linked with the targets from the components assert "set_property(TARGET hello::hello APPEND PROPERTY INTERFACE_LINK_LIBRARIES " \ "hello::say)" in content with open(os.path.join(client.current_folder, "hello-release-x86_64-data.cmake")) as f: content = f.read() # https://github.com/conan-io/conan/issues/11862 # Global variables assert 'set(hello_OBJECTS_RELEASE "${hello_PACKAGE_FOLDER_RELEASE}/mycomponent.o")' \ in content # But component variables assert 'set(hello_hello_say_OBJECTS_RELEASE "${hello_PACKAGE_FOLDER_RELEASE}/' \ 'mycomponent.o")' in content def test_cpp_info_component_error_aggregate(): # https://github.com/conan-io/conan/issues/10176 # This test was consistently failing because "VirtualRunEnv" was not doing a "copy()" # of cpp_info before calling "aggregate_components()", and it was destructive, removing # components data client = TestClient() conan_hello = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.cpp_info.components["say"].includedirs = ["include"] """) consumer = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "hello/1.0" generators = "VirtualRunEnv", "CMakeDeps" def package_info(self): self.cpp_info.components["chat"].requires = ["hello::say"] """) test_package = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" generators = "VirtualRunEnv", "CMakeDeps" def requirements(self): self.requires(self.tested_reference_str) def test(self): pass """) client.save({"hello/conanfile.py": conan_hello, "consumer/conanfile.py": consumer, "consumer/test_package/conanfile.py": test_package}) client.run("create hello --name=hello --version=1.0") client.run("create consumer --name=consumer --version=1.0") assert "consumer/1.0 (test package): Running test()" in client.out def test_cmakedeps_cppinfo_complex_strings(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(r''' from conan import ConanFile class HelloLib(ConanFile): def package_info(self): self.cpp_info.defines.append("escape=partially \"escaped\"") self.cpp_info.defines.append("spaces=me you") self.cpp_info.defines.append("foobar=bazbuz") self.cpp_info.defines.append("answer=42") ''') client.save({"conanfile.py": conanfile}) client.run("export . --name=hello --version=1.0") client.save({"conanfile.txt": "[requires]\nhello/1.0\n"}, clean_first=True) client.run("install . --build=missing -g CMakeDeps") arch = client.get_default_host_profile().settings['arch'] deps = client.load(f"hello-release-{arch}-data.cmake") assert r"escape=partially \"escaped\"" in deps assert r"spaces=me you" in deps assert r"foobar=bazbuz" in deps assert r"answer=42" in deps def test_dependency_props_from_consumer(): client = TestClient(path_with_spaces=False) bar = textwrap.dedent(r''' from conan import ConanFile class FooConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.set_property("cmake_find_mode", "both") self.cpp_info.components["component1"].requires = [] ''') foo = textwrap.dedent(r''' from conan import ConanFile from conan.tools.cmake import CMakeDeps, cmake_layout class FooConan(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "bar/1.0" def layout(self): cmake_layout(self) def generate(self): deps = CMakeDeps(self) {set_find_mode} deps.set_property("bar", "cmake_file_name", "custom_bar_file_name") deps.set_property("bar", "cmake_module_file_name", "custom_bar_module_file_name") deps.set_property("bar", "cmake_target_name", "custom_bar_target_name") deps.set_property("bar", "cmake_module_target_name", "custom_bar_module_target_name") deps.set_property("bar::component1", "cmake_target_name", "custom_bar_component_target_name") deps.generate() ''') set_find_mode = """ deps.set_property("bar", "cmake_find_mode", {find_mode}) """ client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True) if platform.system() != "Windows": gen_folder = os.path.join(client.current_folder, "build", "Release", "generators") else: gen_folder = os.path.join(client.current_folder, "build", "generators") module_file = os.path.join(gen_folder, "module-custom_bar_module_file_nameTargets.cmake") components_module = os.path.join(gen_folder, "custom_bar_file_name-Target-release.cmake") config_file = os.path.join(gen_folder, "custom_bar_file_nameTargets.cmake") # uses cmake_find_mode set in bar: both client.run("create bar.py --name=bar --version=1.0") client.run("install foo.py") assert os.path.exists(module_file) assert os.path.exists(config_file) module_content = client.load(module_file) assert "add_library(custom_bar_module_target_name INTERFACE IMPORTED)" in module_content config_content = client.load(config_file) assert "add_library(custom_bar_target_name INTERFACE IMPORTED)" in config_content components_module_content = client.load(components_module) assert "add_library(bar_custom_bar_component_target_name_DEPS_TARGET INTERFACE IMPORTED)" in components_module_content client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'none'")), "bar.py": bar}, clean_first=True) client.run("create bar.py --name=bar --version=1.0") client.run("install foo.py") assert not os.path.exists(module_file) assert not os.path.exists(config_file) client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'module'")), "bar.py": bar}, clean_first=True) client.run("create bar.py --name=bar --version=1.0") client.run("install foo.py") assert os.path.exists(module_file) assert not os.path.exists(config_file) client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'config'")), "bar.py": bar}, clean_first=True) client.run("create bar.py --name=bar --version=1.0") client.run("install foo.py") assert not os.path.exists(module_file) assert os.path.exists(config_file) def test_props_from_consumer_build_context_activated(): client = TestClient(path_with_spaces=False) bar = textwrap.dedent(r''' from conan import ConanFile class FooConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.set_property("cmake_find_mode", "both") self.cpp_info.components["component1"].requires = [] ''') foo = textwrap.dedent(r''' from conan import ConanFile from conan.tools.cmake import CMakeDeps, cmake_layout class FooConan(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "bar/1.0" tool_requires = "bar/1.0" def layout(self): cmake_layout(self) def generate(self): deps = CMakeDeps(self) deps.build_context_activated = ["bar"] deps.build_context_suffix = {{"bar": "_BUILD"}} {set_find_mode} deps.set_property("bar", "cmake_file_name", "custom_bar_file_name") deps.set_property("bar", "cmake_module_file_name", "custom_bar_module_file_name") deps.set_property("bar", "cmake_target_name", "custom_bar_target_name") deps.set_property("bar", "cmake_module_target_name", "custom_bar_module_target_name") deps.set_property("bar::component1", "cmake_target_name", "custom_bar_component_target_name") deps.set_property("bar", "cmake_file_name", "custom_bar_build_file_name", build_context=True) deps.set_property("bar", "cmake_module_file_name", "custom_bar_build_module_file_name", build_context=True) deps.set_property("bar", "cmake_target_name", "custom_bar_build_target_name", build_context=True) deps.set_property("bar", "cmake_module_target_name", "custom_bar_build_module_target_name", build_context=True) deps.set_property("bar::component1", "cmake_target_name", "custom_bar_build_component_target_name", build_context=True) deps.generate() ''') set_find_mode = """ deps.set_property("bar", "cmake_find_mode", {find_mode}) deps.set_property("bar", "cmake_find_mode", {find_mode}, build_context=True) """ client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True) if platform.system() != "Windows": gen_folder = os.path.join(client.current_folder, "build", "Release", "generators") else: gen_folder = os.path.join(client.current_folder, "build", "generators") module_file = os.path.join(gen_folder, "module-custom_bar_module_file_nameTargets.cmake") components_module = os.path.join(gen_folder, "custom_bar_file_name-Target-release.cmake") config_file = os.path.join(gen_folder, "custom_bar_file_nameTargets.cmake") module_file_build = os.path.join(gen_folder, "module-custom_bar_build_module_file_name_BUILDTargets.cmake") components_module_build = os.path.join(gen_folder, "custom_bar_build_file_name_BUILD-Target-release.cmake") config_file_build = os.path.join(gen_folder, "custom_bar_build_file_name_BUILDTargets.cmake") # uses cmake_find_mode set in bar: both client.run("create bar.py --name=bar --version=1.0 -pr:h=default -pr:b=default") client.run("install foo.py --name=foo --version=1.0 -pr:h=default -pr:b=default") assert os.path.exists(module_file) assert os.path.exists(config_file) assert os.path.exists(module_file_build) assert os.path.exists(config_file_build) module_content = client.load(module_file) assert "add_library(custom_bar_module_target_name INTERFACE IMPORTED)" in module_content config_content = client.load(config_file) assert "add_library(custom_bar_target_name INTERFACE IMPORTED)" in config_content module_content_build = client.load(module_file_build) assert "add_library(custom_bar_build_module_target_name INTERFACE IMPORTED)" in module_content_build config_content_build = client.load(config_file_build) assert "add_library(custom_bar_build_target_name INTERFACE IMPORTED)" in config_content_build components_module_content = client.load(components_module) assert "add_library(bar_custom_bar_component_target_name_DEPS_TARGET INTERFACE IMPORTED)" in components_module_content components_module_content_build = client.load(components_module_build) assert "add_library(bar_BUILD_custom_bar_build_component_target_name_DEPS_TARGET INTERFACE IMPORTED)" in components_module_content_build client.save( {"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'none'")), "bar.py": bar}, clean_first=True) client.run("create bar.py --name=bar --version=1.0 -pr:h=default -pr:b=default") client.run("install foo.py --name=foo --version=1.0 -pr:h=default -pr:b=default") assert not os.path.exists(module_file) assert not os.path.exists(config_file) assert not os.path.exists(module_file_build) assert not os.path.exists(config_file_build) client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'module'")), "bar.py": bar}, clean_first=True) client.run("create bar.py --name=bar --version=1.0 -pr:h=default -pr:b=default") client.run("install foo.py --name=foo --version=1.0 -pr:h=default -pr:b=default") assert os.path.exists(module_file) assert not os.path.exists(config_file) assert os.path.exists(module_file_build) assert not os.path.exists(config_file_build) client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'config'")), "bar.py": bar}, clean_first=True) client.run("create bar.py --name=bar --version=1.0 -pr:h=default -pr:b=default") client.run("install foo.py --name=foo --version=1.0 -pr:h=default -pr:b=default") assert not os.path.exists(module_file) assert os.path.exists(config_file) assert not os.path.exists(module_file_build) assert os.path.exists(config_file_build) # invalidate upstream property setting a None, will use config that's the default client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="None")), "bar.py": bar}, clean_first=True) client.run("create bar.py --name=bar --version=1.0 -pr:h=default -pr:b=default") client.run("install foo.py --name=foo --version=1.0 -pr:h=default -pr:b=default") assert not os.path.exists(module_file) assert os.path.exists(config_file) assert not os.path.exists(module_file_build) assert os.path.exists(config_file_build) def test_skip_transitive_components(): """ when a transitive depenency is skipped, because its binary is not necessary (shared->static), the ``components[].requires`` clause pointing to that skipped dependency was failing with KeyError, as the dependency info was not there """ c = TestClient() pkg = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" package_type = "shared-library" requires = "dep/0.1" def package_info(self): self.cpp_info.components["mycomp"].requires = ["dep::dep"] """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_package_type("static-library"), "pkg/conanfile.py": pkg, "consumer/conanfile.py": GenConanfile().with_settings("build_type") .with_requires("pkg/0.1")}) c.run("create dep") c.run("create pkg") c.run("install consumer -g CMakeDeps -v") c.assert_listed_binary({"dep": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Skip")}) # This used to error, as CMakeDeps was raising a KeyError assert "'CMakeDeps' calling 'generate()'" in c.out def test_error_missing_config_build_context(): """ CMakeDeps was failing, not generating the zlib-config.cmake in the build context, for a test_package that both requires(example/1.0) and tool_requires(example/1.0), which depends on zlib # https://github.com/conan-io/conan/issues/12664 """ c = TestClient() example = textwrap.dedent(""" import os from conan import ConanFile class Example(ConanFile): name = "example" version = "1.0" requires = "game/1.0" generators = "CMakeDeps" settings = "build_type" def build(self): assert os.path.exists("math-config.cmake") assert os.path.exists("engine-config.cmake") assert os.path.exists("game-config.cmake") """) c.save({"math/conanfile.py": GenConanfile("math", "1.0").with_settings("build_type"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_settings("build_type") .with_require("math/1.0"), "game/conanfile.py": GenConanfile("game", "1.0").with_settings("build_type") .with_requires("engine/1.0"), "example/conanfile.py": example, # The example test_package contains already requires(self.tested_reference_str) "example/test_package/conanfile.py": GenConanfile().with_build_requires("example/1.0") .with_test("pass")}) c.run("create math") c.run("create engine") c.run("create game") # This used to crash because of the assert inside the build() method c.run("create example -pr:b=default -pr:h=default") # Now make sure we can actually build with build!=host context # The debug binaries are missing, so adding --build=missing c.run("create example -pr:b=default -pr:h=default -s:h build_type=Debug --build=missing " "--build=example") # listed as both requires and build_requires c.assert_listed_require({"example/1.0": "Cache"}) c.assert_listed_require({"example/1.0": "Cache"}, build=True) def test_using_package_module(): """ This crashed, because the profile "build" didn't have "build_type" https://github.com/conan-io/conan/issues/13209 """ c = TestClient() c.save({"conanfile.py": GenConanfile("tool", "0.1")}) c.run("create .") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler", "build_type", "arch" tool_requires = "tool/0.1" def generate(self): deps = CMakeDeps(self) deps.build_context_activated = ["tool"] deps.build_context_build_modules = ["tool"] deps.generate() """) c.save({"conanfile.py": consumer, "profile_build": "[settings]\nos=Windows"}, clean_first=True) c.run("create . -pr:b=profile_build") # it doesn't crash anymore, it used to crash assert "pkg/0.1: Created package" in c.out def test_system_libs_transitivity(): """ https://github.com/conan-io/conan/issues/13358 """ c = TestClient() system = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "system" def package_info(self): self.cpp_info.system_libs = ["m"] self.cpp_info.frameworks = ["CoreFoundation"] """) header = textwrap.dedent(""" from conan import ConanFile class Header(ConanFile): name = "header" version = "0.1" package_type = "header-library" requires = "dep/system" def package_info(self): self.cpp_info.system_libs = ["dl"] self.cpp_info.frameworks = ["CoreDriver"] """) app = textwrap.dedent("""\ from conan import ConanFile class App(ConanFile): requires = "header/0.1" settings = "build_type" generators = "CMakeDeps" """) c.save({"dep/conanfile.py": system, "header/conanfile.py": header, "app/conanfile.py": app}) c.run("create dep") c.run("create header") c.run("install app") dep = c.load("app/dep-release-data.cmake") assert "set(dep_SYSTEM_LIBS_RELEASE m)" in dep assert "set(dep_FRAMEWORKS_RELEASE CoreFoundation)" in dep app = c.load("app/header-release-data.cmake") assert "set(header_SYSTEM_LIBS_RELEASE dl)" in app assert "set(header_FRAMEWORKS_RELEASE CoreDriver)" in app class TestCMakeVersionConfigCompat: """ https://github.com/conan-io/conan/issues/13809 """ def test_cmake_version_config_compatibility(self): c = TestClient() dep = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_config_version_compat", "AnyNewerVersion") """) c.save({"conanfile.py": dep}) c.run("create .") c.run("install --requires=dep/0.1 -g CMakeDeps") dep = c.load("dep-config-version.cmake") expected = textwrap.dedent("""\ if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) set(PACKAGE_VERSION_COMPATIBLE FALSE) else() set(PACKAGE_VERSION_COMPATIBLE TRUE) if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) set(PACKAGE_VERSION_EXACT TRUE) endif() endif()""") assert expected in dep def test_cmake_version_config_compatibility_error(self): c = TestClient() dep = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_config_version_compat", "Unknown") """) c.save({"conanfile.py": dep}) c.run("create .") c.run("install --requires=dep/0.1 -g CMakeDeps", assert_error=True) assert "Unknown cmake_config_version_compat=Unknown in dep/0.1" in c.out def test_cmake_version_config_compatibility_consumer(self): c = TestClient() app = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMakeDeps class Pkg(ConanFile): settings = "build_type" requires = "dep/0.1" def generate(self): deps = CMakeDeps(self) deps.set_property("dep", "cmake_config_version_compat", "AnyNewerVersion") deps.generate() """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "app/conanfile.py": app}) c.run("create dep") c.run("install app") dep = c.load("app/dep-config-version.cmake") expected = textwrap.dedent("""\ if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) set(PACKAGE_VERSION_COMPATIBLE FALSE) else() set(PACKAGE_VERSION_COMPATIBLE TRUE) if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) set(PACKAGE_VERSION_EXACT TRUE) endif() endif()""") assert expected in dep class TestSystemPackageVersion: def test_component_version(self): c = TestClient() dep = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "system" def package_info(self): self.cpp_info.set_property("system_package_version", "1.0") self.cpp_info.components["mycomp"].set_property("component_version", "2.3") """) c.save({"conanfile.py": dep}) c.run("create .") c.run("install --requires=dep/system -g CMakeDeps -g PkgConfigDeps") dep = c.load("dep-config-version.cmake") assert 'set(PACKAGE_VERSION "1.0")' in dep dep = c.load("dep.pc") assert 'Version: 1.0' in dep dep = c.load("dep-mycomp.pc") assert 'Version: 2.3' in dep def test_component_version_consumer(self): c = TestClient() app = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMakeDeps class Pkg(ConanFile): settings = "build_type" requires = "dep/system" def generate(self): deps = CMakeDeps(self) deps.set_property("dep", "system_package_version", "1.0") deps.generate() """) c.save({"dep/conanfile.py": GenConanfile("dep", "system"), "app/conanfile.py": app}) c.run("create dep") c.run("install app") dep = c.load("app/dep-config-version.cmake") assert 'set(PACKAGE_VERSION "1.0")' in dep def test_cmakedeps_set_property_overrides(): c = TestClient() app = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.cmake import CMakeDeps class Pkg(ConanFile): settings = "build_type" requires = "dep/0.1", "other/0.1" def generate(self): deps = CMakeDeps(self) # Need the absolute path inside package dep = self.dependencies["dep"].package_folder deps.set_property("dep", "cmake_build_modules", [os.path.join(dep, "my_module1")]) deps.set_property("dep", "nosoname", True) deps.set_property("other::mycomp1", "nosoname", True) deps.generate() """) pkg_info = {"components": {"mycomp1": {"libs": ["mylib"]}}} c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_package_type("shared-library"), "other/conanfile.py": GenConanfile("other", "0.1").with_package_type("shared-library") .with_package_info(pkg_info), "app/conanfile.py": app}) c.run("create dep") c.run("create other") c.run("install app") dep = c.load("app/dep-release-data.cmake") assert 'set(dep_BUILD_MODULES_PATHS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/my_module1")' in dep assert 'set(dep_NO_SONAME_MODE_RELEASE TRUE)' in dep other = c.load("app/other-release-data.cmake") assert 'set(other_other_mycomp1_NO_SONAME_MODE_RELEASE TRUE)' in other def test_cmakedeps_set_legacy_variable_name(): client = TestClient() base_conanfile = str(GenConanfile("dep", "1.0")) conanfile = base_conanfile + """ def package_info(self): self.cpp_info.set_property("cmake_file_name", "CMakeFileName") self.cpp_info.set_property("cmake_find_mode", "both") """ client.save({"dep/conanfile.py": conanfile}) client.run("create dep") client.run("install --requires=dep/1.0 -g CMakeDeps") # Check that all the CMake variables are generated with the file_name dep_config = client.load("CMakeFileNameConfig.cmake") cmake_variables = ["VERSION_STRING", "INCLUDE_DIRS", "INCLUDE_DIR", "LIBRARIES", "DEFINITIONS"] for variable in cmake_variables: assert f"CMakeFileName_{variable}" in dep_config dep_find = client.load("FindCMakeFileName.cmake") for variable in cmake_variables: assert f"CMakeFileName_{variable}" in dep_find consumer_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "build_type", "arch", "compiler" def requirements(self): self.requires("dep/1.0") def generate(self): deps = CMakeDeps(self) deps.set_property("dep", "cmake_additional_variables_prefixes", ["PREFIX", "prefix", "PREFIX"]) deps.generate() """) client.save({"consumer/conanfile.py": consumer_conanfile}) client.run("install consumer") dep_config = client.load(os.path.join("consumer", "CMakeFileNameConfig.cmake")) for variable in cmake_variables: assert f"CMakeFileName_{variable}" in dep_config assert f"PREFIX_{variable}" in dep_config assert f"prefix_{variable}" in dep_config dep_find = client.load(os.path.join("consumer", "FindCMakeFileName.cmake")) for variable in cmake_variables: assert f"CMakeFileName_{variable}" in dep_find assert f"PREFIX_{variable}" in dep_find assert f"prefix_{variable}" in dep_find assert "set(prefix_FOUND 1)" in dep_find assert "set(PREFIX_FOUND 1)" in dep_find assert 'set(prefix_VERSION "1.0")' in dep_find assert 'set(PREFIX_VERSION "1.0")' in dep_find conanfile = base_conanfile + """ def package_info(self): self.cpp_info.set_property("cmake_file_name", "NewCMakeFileName") self.cpp_info.set_property("cmake_additional_variables_prefixes", ["PREFIX", "prefix", "PREFIX"]) """ client.save({"dep/conanfile.py": conanfile}) client.run("create dep") client.run("install --requires=dep/1.0 -g CMakeDeps") # Check that all the CMake variables are generated with the file_name and both prefixes dep_config = client.load("NewCMakeFileNameConfig.cmake") for variable in cmake_variables: assert f"NewCMakeFileName_{variable}" in dep_config assert f"PREFIX_{variable}" in dep_config assert f"prefix_{variable}" in dep_config # Check that variables are not duplicated assert dep_config.count("PREFIX_VERSION") == 1 def test_different_versions(): # https://github.com/conan-io/conan/issues/16274 c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep")}) c.run("create dep --version 1.2") c.run("create dep --version 2.3") c.run("install --requires=dep/1.2 -g CMakeDeps") config = c.load("dep-config.cmake") assert 'set(dep_VERSION_STRING "1.2")' in config c.run("install --requires=dep/2.3 -g CMakeDeps") config = c.load("dep-config.cmake") assert 'set(dep_VERSION_STRING "2.3")' in config def test_using_deployer_folder(): """ Related to: https://github.com/conan-io/conan/issues/16543 CMakeDeps was failing if --deployer-folder was used. The error looked like: conans.errors.ConanException: Error in generator 'CMakeDeps': error generating context for 'dep/1.0': mydeploy/direct_deploy/dep/include is not absolute """ c = TestClient() profile = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=15 os=Macos """) c.save({ "profile": profile, "dep/conanfile.py": GenConanfile("dep")}) c.run("create dep --version 1.0") c.run("install --requires=dep/1.0 -pr profile --deployer=direct_deploy " "--deployer-folder=mydeploy -g CMakeDeps") content = c.load("dep-release-x86_64-data.cmake") assert ('set(dep_PACKAGE_FOLDER_RELEASE "${CMAKE_CURRENT_LIST_DIR}/mydeploy/direct_deploy/dep")' in content) def test_component_name_same_package(): """ When the package and the component are the same the variables declared in data and linked to the target have to be the same. https://github.com/conan-io/conan/issues/9071 This doesn't seem to have any special treatment, in fact, last FIXME looks it could be problematic """ c = TestClient() dep = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.components["dep"].includedirs = ["myincludes"] """) c.save({"conanfile.py": dep}) c.run("create .") c.run("install --requires=dep/0.1 -g CMakeDeps -s arch=x86_64") cmake_data = c.load("dep-release-x86_64-data.cmake") assert 'set(dep_dep_dep_INCLUDE_DIRS_RELEASE ' \ '"${dep_PACKAGE_FOLDER_RELEASE}/myincludes")' in cmake_data cmake_target = c.load("dep-Target-release.cmake") assert 'add_library(dep_dep_dep_DEPS_TARGET INTERFACE IMPORTED)' in cmake_target assert 'set_property(TARGET dep::dep APPEND' in cmake_target # FIXME: Depending on itself, this doesn't look good assert 'set_property(TARGET dep::dep APPEND PROPERTY ' \ 'INTERFACE_LINK_LIBRARIES dep::dep)' in cmake_target def test_cmakedeps_set_get_property_checktype(): c = TestClient() app = textwrap.dedent("""\ from conan import ConanFile from conan.tools.cmake import CMakeDeps class Pkg(ConanFile): name = "app" version = "0.1" settings = "build_type" requires = "dep/0.1" def generate(self): deps = CMakeDeps(self) deps.set_property("dep", "foo", 1) deps.get_property("foo", self.dependencies["dep"], check_type=list) deps.generate() """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "app/conanfile.py": app}) c.run("create dep") c.run("create app", assert_error=True) assert 'The expected type for foo is "list", but "int" was found' in c.out def test_alias_cmakedeps_set_property(): tc = TestClient() tc.save({"dep/conanfile.py": textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" settings = "os", "compiler", "build_type", "arch" def package_info(self): self.cpp_info.components["mycomp"].set_property("cmake_target_name", "dep::mycomponent") """), "conanfile.py": textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeDeps, CMake class Pkg(ConanFile): name = "pkg" version = "1.0" settings = "os", "compiler", "build_type", "arch" requires = "dep/1.0" def generate(self): deps = CMakeDeps(self) deps.set_property("dep", "cmake_target_aliases", ["alias", "dep::other_name"]) deps.set_property("dep::mycomp", "cmake_target_aliases", ["component_alias", "dep::my_aliased_component"]) deps.generate() """)}) tc.run("create dep") tc.run("install .") targets_data = tc.load("depTargets.cmake") assert "add_library(dep::dep" in targets_data assert "add_library(alias" in targets_data assert "add_library(dep::other_name" in targets_data assert "add_library(component_alias" in targets_data assert "add_library(dep::my_aliased_component" in targets_data def test_package_info_extra_variables(): """ Test extra_variables property - This just shows that it works, there are tests for cmaketoolchain that check the actual behavior of parsing the variables""" client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" def package_info(self): self.cpp_info.set_property("cmake_extra_variables", {"FOO": 42, "BAR": 42, "CMAKE_GENERATOR_INSTANCE": "${GENERATOR_INSTANCE}/buildTools/", "CACHE_VAR_DEFAULT_DOC": {"value": "hello world", "cache": True, "type": "PATH"}}) """) client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=pkg/0.1 -g CMakeDeps " """-c tools.cmake.cmaketoolchain:extra_variables="{'BAR': 9}" """) target = client.load("pkg-config.cmake") assert 'set(BAR' not in target assert 'set(CMAKE_GENERATOR_INSTANCE "${GENERATOR_INSTANCE}/buildTools/")' in target assert 'set(FOO 42)' in target assert 'set(CACHE_VAR_DEFAULT_DOC "hello world" CACHE PATH "CACHE_VAR_DEFAULT_DOC")' in target ================================================ FILE: test/integration/toolchains/cmake/cmakedeps/test_cmakedeps_find_module_and_config.py ================================================ import os import textwrap import pytest from conan.tools.cmake.cmakedeps.cmakedeps import FIND_MODE_CONFIG, FIND_MODE_MODULE, \ FIND_MODE_BOTH, FIND_MODE_NONE from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("cmake_find_mode", [FIND_MODE_CONFIG, FIND_MODE_MODULE, FIND_MODE_BOTH, FIND_MODE_NONE, None]) def test_reuse_with_modules_and_config(cmake_find_mode): t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): def package_info(self): {} """) if cmake_find_mode is not None: s = 'self.cpp_info.set_property("cmake_find_mode", "{}")'.format(cmake_find_mode) conanfile = conanfile.format(s) t.save({"conanfile.py": conanfile}) t.run("create . --name=mydep --version=1.0") conanfile = GenConanfile().with_name("myapp").with_require("mydep/1.0")\ .with_generator("CMakeDeps")\ .with_settings("build_type", "os", "arch") t.save({"conanfile.py": conanfile}) t.run("install . --output-folder=install -s os=Linux -s compiler=gcc -s compiler.version=6 " "-s compiler.libcxx=libstdc++11") def exists_config(): return os.path.exists(os.path.join(t.current_folder, "install", "mydep-config.cmake")) def exists_module(): return os.path.exists(os.path.join(t.current_folder, "install", "Findmydep.cmake")) if cmake_find_mode == FIND_MODE_CONFIG or cmake_find_mode is None: # None is default "config" assert exists_config() assert not exists_module() elif cmake_find_mode == FIND_MODE_MODULE: assert not exists_config() assert exists_module() elif cmake_find_mode == FIND_MODE_BOTH: assert exists_config() assert exists_module() elif cmake_find_mode == FIND_MODE_NONE: assert not exists_config() assert not exists_module() ================================================ FILE: test/integration/toolchains/cmake/test_cmake.py ================================================ import textwrap from string import Template import pytest from conan.test.utils.tools import TestClient def test_configure_args(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure(variables={"MYVAR": "MYVALUE"}) cmake.build(cli_args=["--verbosebuild"], build_tool_args=["-something"]) cmake.test(cli_args=["--testverbose"], build_tool_args=["-testok"]) def run(self, *args, **kwargs): self.output.info("MYRUN: {}".format(*args)) """) client.save({"conanfile.py": conanfile}) client.run("create . ") # TODO: This check is ugly, because the command line is so different in each platform, # and args_to_string() is doing crazy stuff assert '-DMYVAR="MYVALUE"' in client.out assert "--verbosebuild" in client.out assert "-something" in client.out assert "--testverbose" in client.out assert "-testok" in client.out @pytest.mark.parametrize("argument, output_args", [ ("'target'", "--target target"), ("['target']", "--target target"), ("['target1', 'target2']", "--target target1 target2"), ]) def test_multiple_targets(argument, output_args): client = TestClient() conanfile_template = Template(textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Pkg(ConanFile): generators = "CMakeToolchain" name = "pkg" version = "0.1" settings = "os", "compiler", "build_type", "arch" def build(self): cmake = CMake(self) cmake.build(target=${argument}) def run(self, *args, **kwargs): self.output.info("MYRUN: {}".format(*args)) """)) conanfile = conanfile_template.substitute(argument=argument) client.save({"conanfile.py": conanfile}) client.run("create . ") assert output_args in client.out ================================================ FILE: test/integration/toolchains/cmake/test_cmaketoolchain.py ================================================ import json import os import platform import re import textwrap import pytest from unittest import mock from conan.tools.cmake.presets import load_cmake_presets from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import rmdir, load def test_cross_build(): windows_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 """) rpi_profile = textwrap.dedent(""" [settings] os=Linux compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release """) embedwin = textwrap.dedent(""" [settings] os=Windows compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "rpi": rpi_profile, "embedwin": embedwin, "windows": windows_profile}) client.run("install . --profile:build=windows --profile:host=rpi") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME Linux)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR aarch64)" in toolchain # Revert of https://github.com/conan-io/conan/pull/18559, even if it was correct # assert "set(CMAKE_TRY_COMPILE_CONFIGURATION Release)" in toolchain client.run("install . --profile:build=windows --profile:host=embedwin") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME Windows)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR ARM64)" in toolchain def test_cross_build_linux_to_macos(): linux_profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 """) macos_profile = textwrap.dedent(""" [settings] os=Macos os.version=13.1 arch=x86_64 compiler=apple-clang compiler.version=13 compiler.libcxx=libc++ build_type=Release """) client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nCMakeToolchain", "linux": linux_profile, "macos": macos_profile}) client.run("install . --profile:build=linux --profile:host=macos") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME Darwin)" in toolchain assert "set(CMAKE_SYSTEM_VERSION 22)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR x86_64)" in toolchain # Revert of https://github.com/conan-io/conan/pull/18559, even if it was correct # assert "set(CMAKE_TRY_COMPILE_CONFIGURATION Release)" in toolchain def test_cross_build_user_toolchain(): # When a user_toolchain is defined in [conf], CMakeToolchain will not generate anything # for cross-build windows_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 """) rpi_profile = textwrap.dedent(""" [settings] os=Linux compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.cmake.cmaketoolchain:user_toolchain+=rpi_toolchain.cmake """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "rpi": rpi_profile, "windows": windows_profile}) client.run("install . --profile:build=windows --profile:host=rpi") toolchain = client.load("conan_toolchain.cmake") # Fixed in https://github.com/conan-io/conan/issues/16807 expected = textwrap.dedent("""\ # Cross building if(NOT DEFINED CMAKE_SYSTEM_NAME) # It might have been defined by a user toolchain set(CMAKE_SYSTEM_NAME Linux) endif() if(NOT DEFINED CMAKE_SYSTEM_PROCESSOR) # It might have been defined by a user toolchain set(CMAKE_SYSTEM_PROCESSOR aarch64) endif()""") assert expected in toolchain def test_cross_build_user_toolchain_confs(): # When a user_toolchain is defined in [conf], but other confs are defined, they will be used windows_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 """) rpi_profile = textwrap.dedent(""" [settings] os=Linux compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.cmake.cmaketoolchain:user_toolchain+=rpi_toolchain.cmake tools.cmake.cmaketoolchain:system_name=Linux tools.cmake.cmaketoolchain:system_processor=aarch64 """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "rpi": rpi_profile, "windows": windows_profile}) client.run("install . --profile:build=windows --profile:host=rpi") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME Linux)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR aarch64)" in toolchain def test_no_cross_build(): windows_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 build_type=Release """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "windows": windows_profile}) client.run("install . --profile:build=windows --profile:host=windows") toolchain = client.load("conan_toolchain.cmake") assert "CMAKE_SYSTEM_NAME " not in toolchain assert "CMAKE_SYSTEM_PROCESSOR" not in toolchain def test_cross_arch(): # Compiling to 32bits in an architecture that runs is not full cross-compiling build_profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 """) profile_arm = textwrap.dedent(""" [settings] os=Linux arch=armv8 compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 build_type=Release """) profile_macos = textwrap.dedent(""" [settings] os=Macos arch=armv8 compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 build_type=Release """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "linux64": build_profile, "macos": profile_macos, "linuxarm": profile_arm}) client.run("install . --profile:build=linux64 --profile:host=linuxarm") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME Linux)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR aarch64)" in toolchain client.run("install . --profile:build=linux64 --profile:host=macos") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME Darwin)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR arm64)" in toolchain def test_no_cross_build_arch(): # Compiling to 32bits in an architecture that runs is not full cross-compiling build_profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 """) profile_32 = textwrap.dedent(""" [settings] os=Linux arch=x86 compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 build_type=Release """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "linux64": build_profile, "linux32": profile_32}) client.run("install . --profile:build=linux64 --profile:host=linux32") toolchain = client.load("conan_toolchain.cmake") assert "CMAKE_SYSTEM_NAME" not in toolchain assert "CMAKE_SYSTEM_PROCESSOR " not in toolchain def test_cross_build_conf(): windows_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 """) rpi_profile = textwrap.dedent(""" [settings] os=Linux compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.cmake.cmaketoolchain:system_name = Custom tools.cmake.cmaketoolchain:system_version= 42 tools.cmake.cmaketoolchain:system_processor = myarm """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "rpi": rpi_profile, "windows": windows_profile}) client.run("install . --profile:build=windows --profile:host=rpi") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME Custom)" in toolchain assert "set(CMAKE_SYSTEM_VERSION 42)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR myarm)" in toolchain def test_find_builddirs(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): def package_info(self): self.cpp_info.builddirs = ["/path/to/builddir"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=dep --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "dep/1.0@" def generate(self): cmake = CMakeToolchain(self) cmake.generate() """) client.save({"conanfile.py": conanfile}) client.run("install . ") with open(os.path.join(client.current_folder, "conan_toolchain.cmake")) as f: contents = f.read() assert "/path/to/builddir" in contents conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Conan(ConanFile): name = "mydep" version = "1.0" settings = "os", "arch", "compiler", "build_type" def build_requirements(self): self.test_requires("dep/1.0") def generate(self): cmake = CMakeToolchain(self) cmake.generate() """) client.save({"conanfile.py": conanfile}) client.run("install . ") with open(os.path.join(client.current_folder, "conan_toolchain.cmake")) as f: contents = f.read() assert "/path/to/builddir" in contents @pytest.fixture def lib_dir_setup(): client = TestClient() client.save({"conanfile.py": GenConanfile().with_generator("CMakeToolchain")}) client.run("create . --name=onelib --version=1.0") client.run("create . --name=twolib --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): requires = "onelib/1.0", "twolib/1.0" """) client.save({"conanfile.py": conanfile}) client.run("create . --name=dep --version=1.0") conanfile = (GenConanfile().with_requires("dep/1.0").with_generator("CMakeToolchain") .with_settings("os", "arch", "compiler", "build_type")) client.save({"conanfile.py": conanfile}) return client def test_runtime_lib_dirs_single_conf(lib_dir_setup): client = lib_dir_setup generator = "" is_windows = platform.system() == "Windows" if is_windows: generator = '-c tools.cmake.cmaketoolchain:generator=Ninja' client.run(f'install . -s build_type=Release {generator}') contents = client.load("conan_toolchain.cmake") pattern_lib_path = r'list\(PREPEND CMAKE_LIBRARY_PATH (.*)\)' pattern_lib_dirs = r'set\(CONAN_RUNTIME_LIB_DIRS (.*) \)' # On *nix platforms: the list in `CMAKE_LIBRARY_PATH` # is the same as `CONAN_RUNTIME_LIB_DIRS` # On windows, it's the same but with `bin` instead of `lib` cmake_library_path = re.search(pattern_lib_path, contents).group(1) conan_runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1) lib_path = cmake_library_path.replace("/p/lib", "/p/bin") if is_windows else cmake_library_path assert lib_path == conan_runtime_lib_dirs def test_runtime_lib_dirs_multiconf(lib_dir_setup): client = lib_dir_setup generator = "" if platform.system() != "Windows": generator = '-c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config"' client.run(f'install . -s build_type=Release {generator}') client.run(f'install . -s build_type=Debug {generator}') contents = client.load("conan_toolchain.cmake") pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1) assert "" in runtime_lib_dirs assert "" in runtime_lib_dirs def test_disable_package_registry(): # https://github.com/conan-io/conan/issues/19749 client = TestClient(light=True) client.save({"conanfile.txt": "[generators]\nCMakeToolchain"}) client.run("install .") toolchain = client.load("conan_toolchain.cmake") before_output_dirs, output_dirs_block = toolchain.split("########## 'output_dirs' block #############\n", 1) assert "CMAKE_EXPORT_PACKAGE_REGISTRY" not in before_output_dirs assert "cmake_policy(SET CMP0090 NEW)" in output_dirs_block assert "set(CMAKE_EXPORT_PACKAGE_REGISTRY OFF)" in toolchain @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_cmaketoolchain_cmake_system_processor_cross_apple(): """ https://github.com/conan-io/conan/pull/10434 CMAKE_SYSTEM_PROCESSOR was not set when cross-building in Mac """ client = TestClient() client.save({"hello.py": GenConanfile().with_name("hello") .with_version("1.0") .with_settings("os", "arch", "compiler", "build_type")}) profile_ios = textwrap.dedent(""" include(default) [settings] os=iOS os.version=15.4 os.sdk=iphoneos os.sdk_version=15.0 arch=armv8 """) client.save({"profile_ios": profile_ios}) client.run("install hello.py -pr:h=./profile_ios -pr:b=default -g CMakeToolchain") toolchain = client.load("conan_toolchain.cmake") assert "set(CMAKE_SYSTEM_NAME iOS)" in toolchain assert "set(CMAKE_SYSTEM_VERSION 21)" in toolchain assert "set(CMAKE_SYSTEM_PROCESSOR arm64)" in toolchain @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_apple_vars_overwrite_user_conf(): """ tools.cmake.cmaketoolchain:system_name and tools.cmake.cmaketoolchain:system_version will be overwritten by the apple block """ client = TestClient() client.save({"hello.py": GenConanfile().with_name("hello") .with_version("1.0") .with_settings("os", "arch", "compiler", "build_type")}) profile_ios = textwrap.dedent(""" include(default) [settings] os=iOS os.version=15.4 os.sdk=iphoneos os.sdk_version=15.0 arch=armv8 """) client.save({"profile_ios": profile_ios}) client.run("install hello.py -pr:h=./profile_ios -pr:b=default -g CMakeToolchain " "-c tools.cmake.cmaketoolchain:system_name=tvOS " "-c tools.cmake.cmaketoolchain:system_version=15.1 " "-c tools.cmake.cmaketoolchain:system_processor=x86_64 ") toolchain = client.load("conan_toolchain.cmake") # should set the conf values but system/version are overwritten by the apple block assert "CMAKE_SYSTEM_NAME tvOS" in toolchain assert "CMAKE_SYSTEM_NAME iOS" not in toolchain assert "CMAKE_SYSTEM_VERSION 15.1" in toolchain assert "CMAKE_SYSTEM_VERSION 15.0" not in toolchain assert "CMAKE_SYSTEM_PROCESSOR x86_64" in toolchain assert "CMAKE_SYSTEM_PROCESSOR armv8" not in toolchain def test_extra_flags_via_conf(): profile = textwrap.dedent(""" [settings] os=Linux compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.build:cxxflags=["--flag1", "--flag2"] tools.build:cflags+=["--flag3", "--flag4"] tools.build:sharedlinkflags=+["--flag5", "--flag6"] tools.build:exelinkflags=["--flag7", "--flag8"] tools.build:defines=["D1", "D2"] """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load("conan_toolchain.cmake") assert 'string(APPEND CONAN_CXX_FLAGS " --flag1 --flag2")' in toolchain assert 'string(APPEND CONAN_C_FLAGS " --flag3 --flag4")' in toolchain assert 'string(APPEND CONAN_SHARED_LINKER_FLAGS " --flag5 --flag6")' in toolchain assert 'string(APPEND CONAN_EXE_LINKER_FLAGS " --flag7 --flag8")' in toolchain assert 'add_compile_definitions( "D1" "D2")' in toolchain def test_cmaketoolchain_rcflags(): """Test that tools.build:rcflags is applied to CONAN_RC_FLAGS and CMAKE_RC_FLAGS_INIT""" profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 build_type=Release [conf] tools.build:rcflags=["/nologo", "/flag-rc"] """) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:host=profile") toolchain = client.load("conan_toolchain.cmake") assert 'string(APPEND CONAN_RC_FLAGS " /nologo /flag-rc")' in toolchain assert 'string(APPEND CMAKE_RC_FLAGS_INIT " ${CONAN_RC_FLAGS}")' in toolchain def test_bitcode_enable_flag(): profile = textwrap.dedent(""" [settings] os=Macos arch=armv8 compiler=apple-clang compiler.version=17 compiler.libcxx=libc++ build_type=Release [conf] tools.apple:enable_bitcode=True """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type") \ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load("conan_toolchain.cmake") assert 'set(BITCODE "-fembed-bitcode")' in toolchain def test_cmake_presets_binary_dir_available(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class HelloConan(ConanFile): generators = "CMakeToolchain" settings = "os", "compiler", "build_type", "arch" def layout(self): cmake_layout(self) """) client.save({"conanfile.py": conanfile}) client.run("install .") if platform.system() != "Windows": build_dir = os.path.join(client.current_folder, "build", "Release") else: build_dir = os.path.join(client.current_folder, "build") presets = load_cmake_presets(os.path.join(build_dir, "generators")) assert presets["configurePresets"][0]["binaryDir"] == build_dir @pytest.mark.parametrize("presets", ["CMakePresets.json", "CMakeUserPresets.json"]) def test_cmake_presets_shared_preset(presets): """valid user preset file is created when multiple project presets inherit from the same conan presets. """ client = TestClient() project_presets = textwrap.dedent(""" { "version": 4, "cmakeMinimumRequired": { "major": 3, "minor": 23, "patch": 0 }, "include":["ConanPresets.json"], "configurePresets": [ { "name": "debug1", "inherits": ["conan-debug"] }, { "name": "debug2", "inherits": ["conan-debug"] }, { "name": "release1", "inherits": ["conan-release"] }, { "name": "release2", "inherits": ["conan-release"] } ] }""") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout, CMakeToolchain class TestPresets(ConanFile): generators = ["CMakeDeps"] settings = "build_type" def layout(self): cmake_layout(self) def generate(self): tc = CMakeToolchain(self) tc.user_presets_path = 'ConanPresets.json' tc.generate() """) client.save({presets: project_presets, "conanfile.py": conanfile, "CMakeLists.txt": ""}) # File must exist for Conan to do Preset things. client.run("install . -s build_type=Debug") conan_presets = json.loads(client.load("ConanPresets.json")) assert len(conan_presets["configurePresets"]) == 1 assert conan_presets["configurePresets"][0]["name"] == "conan-release" def test_cmake_presets_multiconfig(): client = TestClient() profile = textwrap.dedent(""" [settings] os = Windows arch = x86_64 compiler=msvc compiler.version=193 compiler.runtime=static compiler.runtime_type=Release """) client.save({"conanfile.py": GenConanfile("mylib", "1.0"), "profile": profile}) client.run("create . -s build_type=Release --profile:h=profile") client.run("create . -s build_type=Debug --profile:h=profile") client.run("install --requires=mylib/1.0@ -g CMakeToolchain " "-s build_type=Release --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert presets["buildPresets"][0]["configuration"] == "Release" assert len(presets["testPresets"]) == 1 assert presets["testPresets"][0]["configuration"] == "Release" client.run("install --requires=mylib/1.0@ -g CMakeToolchain " "-s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 2 assert presets["buildPresets"][0]["configuration"] == "Release" assert presets["buildPresets"][1]["configuration"] == "Debug" assert len(presets["testPresets"]) == 2 assert presets["testPresets"][0]["configuration"] == "Release" assert presets["testPresets"][1]["configuration"] == "Debug" client.run("install --requires=mylib/1.0@ -g CMakeToolchain " "-s build_type=RelWithDebInfo --profile:h=profile") client.run("install --requires=mylib/1.0@ -g CMakeToolchain " "-s build_type=MinSizeRel --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 4 assert presets["buildPresets"][0]["configuration"] == "Release" assert presets["buildPresets"][1]["configuration"] == "Debug" assert presets["buildPresets"][2]["configuration"] == "RelWithDebInfo" assert presets["buildPresets"][3]["configuration"] == "MinSizeRel" assert len(presets["testPresets"]) == 4 assert presets["testPresets"][0]["configuration"] == "Release" assert presets["testPresets"][1]["configuration"] == "Debug" assert presets["testPresets"][2]["configuration"] == "RelWithDebInfo" assert presets["testPresets"][3]["configuration"] == "MinSizeRel" # Repeat one client.run("install --requires=mylib/1.0@ -g CMakeToolchain " "-s build_type=Debug --profile:h=profile") client.run("install --requires=mylib/1.0@ -g CMakeToolchain " "-s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 4 assert presets["buildPresets"][0]["configuration"] == "Release" assert presets["buildPresets"][1]["configuration"] == "Debug" assert presets["buildPresets"][2]["configuration"] == "RelWithDebInfo" assert presets["buildPresets"][3]["configuration"] == "MinSizeRel" assert len(presets["testPresets"]) == 4 assert presets["testPresets"][0]["configuration"] == "Release" assert presets["testPresets"][1]["configuration"] == "Debug" assert presets["testPresets"][2]["configuration"] == "RelWithDebInfo" assert presets["testPresets"][3]["configuration"] == "MinSizeRel" assert len(presets["configurePresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-default" def test_cmake_presets_singleconfig(): """ without defining a layout, single config always overwrites the existing CMakePresets.json """ client = TestClient() profile = textwrap.dedent(""" [settings] os = Linux arch = x86_64 compiler=gcc compiler.version=8 """) client.save({"conanfile.py": GenConanfile("mylib", "1.0"), "profile": profile}) client.run("create . -s build_type=Release --profile:h=profile") client.run("create . -s build_type=Debug --profile:h=profile") client.run("install --requires=mylib/1.0@ " "-g CMakeToolchain -s build_type=Release --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) assert len(presets["configurePresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-release" assert len(presets["buildPresets"]) == 1 assert presets["buildPresets"][0]["configurePreset"] == "conan-release" assert len(presets["testPresets"]) == 1 assert presets["testPresets"][0]["configurePreset"] == "conan-release" # This overwrites the existing profile, as there is no layout client.run("install --requires=mylib/1.0@ " "-g CMakeToolchain -s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) assert len(presets["configurePresets"]) == 1 assert presets["configurePresets"][0]["name"] == "conan-debug" assert len(presets["buildPresets"]) == 1 assert presets["buildPresets"][0]["configurePreset"] == "conan-debug" assert len(presets["testPresets"]) == 1 assert presets["testPresets"][0]["configurePreset"] == "conan-debug" # Repeat configuration, it shouldn't add a new one client.run("install --requires=mylib/1.0@ " "-g CMakeToolchain -s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) assert len(presets["configurePresets"]) == 1 def test_toolchain_cache_variables(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" options = {"enable_foobar": [True, False], "qux": ["ANY"], "number": [1,2]} default_options = {"enable_foobar": True, "qux": "baz", "number": 1} def generate(self): toolchain = CMakeToolchain(self) toolchain.cache_variables["foo"] = True toolchain.cache_variables["foo2"] = False toolchain.cache_variables["var"] = "23" toolchain.cache_variables["ENABLE_FOOBAR"] = self.options.enable_foobar toolchain.cache_variables["QUX"] = self.options.qux toolchain.cache_variables["NUMBER"] = self.options.number toolchain.cache_variables["CMAKE_SH"] = "THIS VALUE HAS PRIORITY" toolchain.cache_variables["CMAKE_POLICY_DEFAULT_CMP0091"] = "THIS VALUE HAS PRIORITY" toolchain.cache_variables["CMAKE_MAKE_PROGRAM"] = "THIS VALUE HAS NO PRIORITY" toolchain.generate() """) client.save({"conanfile.py": conanfile}) with mock.patch("platform.system", mock.MagicMock(return_value="Windows")): client.run("install . --name=mylib --version=1.0 " "-c tools.cmake.cmaketoolchain:generator='MinGW Makefiles' " "-c tools.gnu:make_program='MyMake' -c tools.build:skip_test=True") presets = json.loads(client.load("CMakePresets.json")) cache_variables = presets["configurePresets"][0]["cacheVariables"] assert cache_variables["foo"] == 'ON' assert cache_variables["foo2"] == 'OFF' assert cache_variables["var"] == '23' assert cache_variables["CMAKE_SH"] == "THIS VALUE HAS PRIORITY" assert cache_variables["CMAKE_POLICY_DEFAULT_CMP0091"] == "THIS VALUE HAS PRIORITY" assert cache_variables["CMAKE_MAKE_PROGRAM"] == "MyMake" assert cache_variables["BUILD_TESTING"] == 'OFF' assert cache_variables["ENABLE_FOOBAR"] == 'ON' assert cache_variables["QUX"] == 'baz' assert cache_variables["NUMBER"] == 1 def _format_val(val): return f'"{val}"' if isinstance(val, str) and " " in val else f"{val}" for var, value in cache_variables.items(): assert f"-D{var}={_format_val(value)}" in client.out assert "-DCMAKE_TOOLCHAIN_FILE=" in client.out assert f"-G {_format_val('MinGW Makefiles')}" in client.out client.run("install . --name=mylib --version=1.0 -c tools.gnu:make_program='MyMake'") presets = json.loads(client.load("CMakePresets.json")) cache_variables = presets["configurePresets"][0]["cacheVariables"] assert cache_variables["CMAKE_MAKE_PROGRAM"] == "MyMake" def test_variables_types(): # https://github.com/conan-io/conan/pull/10941 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): toolchain = CMakeToolchain(self) toolchain.variables["FOO"] = True toolchain.generate() """) client.save({"conanfile.py": conanfile}) client.run("install . --name=mylib --version=1.0") toolchain = client.load("conan_toolchain.cmake") assert 'set(FOO ON CACHE BOOL "Variable FOO conan-toolchain defined")' in toolchain def test_variables_escaping(): # https://github.com/conan-io/conan/issues/19638 client = TestClient() # NOTE: Users need to do explicit escaping conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): toolchain = CMakeToolchain(self) toolchain.variables["FOO"] = r"D:\new\thing\path".replace("\\", "\\\\") toolchain.variables["CMAKE_Fortran_FLAGS_INIT"] = "${CMAKE_C_FLAGS_INIT}" toolchain.variables.release["BAR"] = r"C:\new\thing\path".replace("\\", "\\\\") toolchain.generate() """) client.save({"conanfile.py": conanfile}) client.run("install .") toolchain = client.load("conan_toolchain.cmake") assert 'set(CMAKE_Fortran_FLAGS_INIT "${CMAKE_C_FLAGS_INIT}"' in toolchain assert r'set(FOO "D:\\new\\thing\\path" CACHE STRING' in toolchain assert r'set(CONAN_DEF_releaseBAR "C:\\new\\thing\\path")' in toolchain def test_android_c_library(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def configure(self): if self.settings.compiler != "msvc": del self.settings.compiler.libcxx """) client.save({"conanfile.py": conanfile}) # Settings settings = "-s arch=x86_64 -s os=Android -s os.api_level=23 -c tools.android:ndk_path=/foo" # Checking the Android variables created # Issue: https://github.com/conan-io/conan/issues/11798 client.run("install . " + settings) conan_toolchain = client.load(os.path.join(client.current_folder, "conan_toolchain.cmake")) assert "set(ANDROID_PLATFORM android-23)" in conan_toolchain assert "set(ANDROID_ABI x86_64)" in conan_toolchain assert 'include("/foo/build/cmake/android.toolchain.cmake")' in conan_toolchain client.run("create . --name=foo --version=1.0 " + settings) @pytest.mark.parametrize("cmake_legacy_toolchain", [True, False, None]) def test_android_legacy_toolchain_flag(cmake_legacy_toolchain): client = TestClient() conanfile = GenConanfile().with_settings("os", "arch")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile}) settings = "-s arch=x86_64 -s os=Android -s os.api_level=23 -c tools.android:ndk_path=/foo" expected = None if cmake_legacy_toolchain is not None: settings += f" -c tools.android:cmake_legacy_toolchain={cmake_legacy_toolchain}" expected = "ON" if cmake_legacy_toolchain else "OFF" client.run("install . " + settings) conan_toolchain = client.load(os.path.join(client.current_folder, "conan_toolchain.cmake")) if cmake_legacy_toolchain is not None: assert f"set(ANDROID_USE_LEGACY_TOOLCHAIN_FILE {expected})" in conan_toolchain else: assert "ANDROID_USE_LEGACY_TOOLCHAIN_FILE" not in conan_toolchain @pytest.mark.parametrize("cmake_legacy_toolchain", [True, False, None]) def test_android_legacy_toolchain_with_compileflags(cmake_legacy_toolchain): # https://github.com/conan-io/conan/issues/13374 client = TestClient() conanfile = GenConanfile().with_settings("os", "arch")\ .with_generator("CMakeToolchain") profile = textwrap.dedent(""" [settings] os=Android os.api_level=23 arch=armv8 [conf] tools.android:ndk_path=/foo tools.build:cflags=["-foobar"] tools.build:cxxflags=["-barfoo"] """) if cmake_legacy_toolchain is not None: profile += f"\ntools.android:cmake_legacy_toolchain={cmake_legacy_toolchain}" client.save({"conanfile.py": conanfile, "profile_host": profile}) client.run("install . -pr profile_host") warning_text = "Consider setting tools.android:cmake_legacy_toolchain to False" if cmake_legacy_toolchain is not False: assert warning_text in client.out else: assert warning_text not in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Only Windows") def test_presets_paths_normalization(): # https://github.com/conan-io/conan/issues/11795 # But then also https://github.com/conan-io/conan/issues/18434 client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo"}) client.run("install .") presets = json.loads(client.load("CMakeUserPresets.json")) assert "/" in presets["include"][0] assert "\\" not in presets["include"][0] @pytest.mark.parametrize("arch, arch_toolset", [("x86", "x86_64"), ("x86_64", "x86_64"), ("x86", "x86"), ("x86_64", "x86")]) def test_presets_ninja_msvc(arch, arch_toolset): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) """) client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo"}) configs = ["-c tools.cmake.cmaketoolchain:toolset_arch={}".format(arch_toolset), "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'", "-c tools.cmake.cmaketoolchain:generator=Ninja"] msvc = " -s compiler=msvc -s compiler.version=191 -s compiler.runtime=static " \ "-s compiler.runtime_type=Release" client.run("install . {} -s compiler.cppstd=14 {} -s arch={}".format(" ".join(configs), msvc, arch)) presets = json.loads(client.load("build/14/Release/generators/CMakePresets.json")) toolset_value = {"x86_64": "v141,host=x86_64", "x86": "v141,host=x86"}.get(arch_toolset) arch_value = {"x86_64": "x64", "x86": "x86"}.get(arch) assert presets["configurePresets"][0]["architecture"]["value"] == arch_value assert presets["configurePresets"][0]["architecture"]["strategy"] == "external" assert presets["configurePresets"][0]["toolset"]["value"] == toolset_value assert presets["configurePresets"][0]["toolset"]["strategy"] == "external" # Only for Ninja, no ninja, no values rmdir(os.path.join(client.current_folder, "build")) configs = ["-c tools.cmake.cmaketoolchain:toolset_arch={}".format(arch_toolset), "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"] client.run( "install . {} -s compiler.cppstd=14 {} -s arch={}".format(" ".join(configs), msvc, arch)) toolset_value = {"x86_64": "v141,host=x86_64", "x86": "v141,host=x86"}.get(arch_toolset) arch_value = {"x86_64": "x64", "x86": "Win32"}.get(arch) # NOTE: Win32 is different!! presets = json.loads(client.load("build/14/generators/CMakePresets.json")) assert presets["configurePresets"][0]["architecture"]["value"] == arch_value assert presets["configurePresets"][0]["architecture"]["strategy"] == "external" assert presets["configurePresets"][0]["toolset"]["value"] == toolset_value assert presets["configurePresets"][0]["toolset"]["strategy"] == "external" rmdir(os.path.join(client.current_folder, "build")) configs = ["-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'", "-c tools.cmake.cmaketoolchain:generator=Ninja"] client.run( "install . {} -s compiler.cppstd=14 {} -s arch={}".format(" ".join(configs), msvc, arch)) presets = json.loads(client.load("build/14/Release/generators/CMakePresets.json")) toolset_value = {"x86_64": "v141", "x86": "v141"}.get(arch_toolset) arch_value = {"x86_64": "x64", "x86": "x86"}.get(arch) assert presets["configurePresets"][0]["architecture"]["value"] == arch_value assert presets["configurePresets"][0]["architecture"]["strategy"] == "external" assert presets["configurePresets"][0]["toolset"]["value"] == toolset_value assert presets["configurePresets"][0]["toolset"]["strategy"] == "external" def test_pkg_config_block(): os_ = platform.system() os_ = "Macos" if os_ == "Darwin" else os_ profile = textwrap.dedent(""" [settings] os=%s arch=x86_64 [conf] tools.gnu:pkg_config=/usr/local/bin/pkg-config """ % os_) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . -pr:b profile -pr:h profile") toolchain = client.load("conan_toolchain.cmake") assert 'set(PKG_CONFIG_EXECUTABLE /usr/local/bin/pkg-config CACHE FILEPATH ' in toolchain pathsep = ":" if os_ != "Windows" else ";" pkg_config_path_set = 'set(ENV{PKG_CONFIG_PATH} "%s$ENV{PKG_CONFIG_PATH}")' % \ ("${CMAKE_CURRENT_LIST_DIR}" + pathsep) assert pkg_config_path_set in toolchain @pytest.mark.parametrize("path", ["subproject", False]) def test_user_presets_custom_location(path): client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.cmake import cmake_layout, CMakeToolchain class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): t = CMakeToolchain(self) t.user_presets_path = {} t.generate() def layout(self): cmake_layout(self) """.format('"{}"'.format(path) if isinstance(path, str) else path)) client.save({"CMakeLists.txt": "", "subproject/CMakeLists.txt": "", "subproject2/foo.txt": "", "conanfile.py": conanfile}) # We want to generate it to build the subproject client.run("install . ") if path is not False: assert not os.path.exists(os.path.join(client.current_folder, "CMakeUserPresets.json")) assert os.path.exists(os.path.join(client.current_folder, "subproject", "CMakeUserPresets.json")) else: assert not os.path.exists(os.path.join(client.current_folder, "CMakeUserPresets.json")) assert not os.path.exists(os.path.join(client.current_folder, "False", "CMakeUserPresets.json")) def test_set_cmake_lang_compilers_and_launchers(): profile = textwrap.dedent(r""" [settings] os=Windows arch=x86_64 compiler=clang compiler.version=15 compiler.libcxx=libstdc++11 [conf] tools.build:compiler_executables={"c": "/my/local/gcc", "cpp": "g++", "rc": "C:\\local\\rc.exe"} """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . -pr:b profile -pr:h profile") toolchain = client.load("conan_toolchain.cmake") assert 'set(CMAKE_C_COMPILER clang)' not in toolchain assert 'set(CMAKE_CXX_COMPILER clang++)' not in toolchain assert 'set(CMAKE_C_COMPILER "/my/local/gcc")' in toolchain assert 'set(CMAKE_CXX_COMPILER "g++")' in toolchain assert 'set(CMAKE_RC_COMPILER "C:/local/rc.exe")' in toolchain def test_cmake_layout_toolchain_folder(): """ in single-config generators, the toolchain is a different file per configuration https://github.com/conan-io/conan/issues/12827 """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) """) c.save({"conanfile.py": conanfile}) c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Release " "-s compiler.libcxx=libstdc++11") assert os.path.exists(os.path.join(c.current_folder, "build/Release/generators/conan_toolchain.cmake")) c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Debug " "-s compiler.libcxx=libstdc++11") assert os.path.exists(os.path.join(c.current_folder, "build/Debug/generators/conan_toolchain.cmake")) c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Debug " "-s compiler.libcxx=libstdc++11 -s arch=x86 " "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.arch\", \"settings.build_type\"]'") assert os.path.exists(os.path.join(c.current_folder, "build/x86-debug/generators/conan_toolchain.cmake")) c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Debug " "-s compiler.libcxx=libstdc++11 -s arch=x86 " "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.os\"]'") assert os.path.exists(os.path.join(c.current_folder, "build/linux/Debug/generators/conan_toolchain.cmake")) def test_build_folder_vars_editables(): """ when packages are in editable, they must also follow the build_folder_vars https://github.com/conan-io/conan/issues/13485 """ c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): name = "dep" version = "0.1" settings = "os", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) """) c.save({"dep/conanfile.py": conanfile, "app/conanfile.py": GenConanfile().with_requires("dep/0.1")}) c.run("editable add dep") conf = "tools.cmake.cmake_layout:build_folder_vars='[\"settings.os\", \"settings.build_type\"]'" settings = " -s os=FreeBSD -s arch=armv8 -s build_type=Debug" c.run("install app -c={} {} --build=editable".format(conf, settings)) assert os.path.exists(os.path.join(c.current_folder, "dep", "build", "freebsd-debug")) def test_set_linker_scripts(): profile = textwrap.dedent(r""" [settings] os=Windows arch=x86_64 compiler=clang compiler.version=15 compiler.libcxx=libstdc++11 [conf] tools.build:linker_scripts=["/usr/local/src/flash.ld", "C:\\local\\extra_data.ld"] """) client = TestClient(path_with_spaces=False) conanfile = GenConanfile().with_settings("os", "arch", "compiler")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . -pr:b profile -pr:h profile") toolchain = client.load("conan_toolchain.cmake") assert 'string(APPEND CONAN_EXE_LINKER_FLAGS ' \ r'" -T\"/usr/local/src/flash.ld\" -T\"C:/local/extra_data.ld\"")' in toolchain def test_test_package_layout(): """ test that the ``test_package`` folder also follows the cmake_layout and the build_folder_vars """ client = TestClient() test_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def test(self): pass """) client.save({"conanfile.py": GenConanfile("pkg", "0.1"), "test_package/conanfile.py": test_conanfile}) config = "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'" client.run(f"create . {config} -s compiler.cppstd=14") build_folder = client.created_test_build_folder("pkg/0.1") assert os.path.exists(os.path.join(client.current_folder, "test_package", build_folder)) client.run(f"create . {config} -s compiler.cppstd=17") build_folder2 = client.created_test_build_folder("pkg/0.1") assert os.path.exists(os.path.join(client.current_folder, "test_package", build_folder2)) assert build_folder != build_folder2 def test_presets_not_found_error_msg(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake class Conan(ConanFile): settings = "build_type" def build(self): CMake(self).configure() """) client.save({"conanfile.py": conanfile}) client.run("build .", assert_error=True) assert "CMakePresets.json was not found" in client.out assert "Check that you are using CMakeToolchain as generator " \ "to ensure its correct initialization." in client.out def test_recipe_build_folders_vars(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "build_type" options = {"shared": [True, False]} generators = "CMakeToolchain" def layout(self): self.folders.build_folder_vars = ["settings.os", "options.shared"] cmake_layout(self) """) client.save({"conanfile.py": conanfile}) client.run("install . -s os=Windows -s arch=armv8 -s build_type=Debug -o shared=True") presets = client.load("build/windows-shared/Debug/generators/CMakePresets.json") assert "conan-windows-shared-debug" in presets client.run("install . -s os=Linux -s arch=x86 -s build_type=Release -o shared=False") presets = client.load("build/linux-static/Release/generators/CMakePresets.json") assert "linux-static-release" in presets # CLI override has priority client.run("install . -s os=Linux -s arch=x86 -s build_type=Release -o shared=False " "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.os\"]'") presets = client.load("build/linux/Release/generators/CMakePresets.json") assert "conan-linux-release" in presets # Now we do the build in the cache, the recipe folders are still used client.run("create . -s os=Windows -s arch=armv8 -s build_type=Debug -o shared=True") build_folder = client.created_layout().build() presets = load(os.path.join(build_folder, "build/windows-shared/Debug/generators/CMakePresets.json")) assert "conan-windows-shared-debug" in presets # If we change the conf ``build_folder_vars``, it doesn't affect the cache build client.run("create . -s os=Windows -s arch=armv8 -s build_type=Debug -o shared=True " "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.os\"]'") build_folder = client.created_layout().build() presets = load(os.path.join(build_folder, "build/windows-shared/Debug/generators/CMakePresets.json")) assert "conan-windows-shared-debug" in presets def test_build_folder_vars_self_name_version(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): name = "pkg" version = "0.1" settings = "os", "build_type" generators = "CMakeToolchain" def layout(self): self.folders.build_folder_vars = ["settings.os", "self.name", "self.version"] cmake_layout(self) """) client.save({"conanfile.py": conanfile}) client.run("install . -s os=Windows -s build_type=Debug") presets = client.load("build/windows-pkg-0.1/Debug/generators/CMakePresets.json") assert "conan-windows-pkg-0.1-debug" in presets client.run("install . -s os=Linux -s build_type=Release") presets = client.load("build/linux-pkg-0.1/Release/generators/CMakePresets.json") assert "linux-pkg-0.1-release" in presets # CLI override has priority client.run("install . -s os=Linux -s build_type=Release " "-c tools.cmake.cmake_layout:build_folder_vars='[\"self.name\"]'") presets = client.load("build/pkg/Release/generators/CMakePresets.json") assert "conan-pkg-release" in presets # Now we do the build in the cache, the recipe folders are still used client.run("create . -s os=Windows -s build_type=Debug") build_folder = client.created_layout().build() presets = load(os.path.join(build_folder, "build/windows-pkg-0.1/Debug/generators/CMakePresets.json")) assert "conan-windows-pkg-0.1-debug" in presets # If we change the conf ``build_folder_vars``, it doesn't affect the cache build client.run("create . -s os=Windows -s build_type=Debug " "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.os\"]'") build_folder = client.created_layout().build() presets = load(os.path.join(build_folder, "build/windows-pkg-0.1/Debug/generators/CMakePresets.json")) assert "conan-windows-pkg-0.1-debug" in presets def test_build_folder_vars_constants_user(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class Conan(ConanFile): name = "dep" version = "0.1" settings = "os", "build_type" generators = "CMakeToolchain" def layout(self): cmake_layout(self) """) c.save({"conanfile.py": conanfile}) conf = "tools.cmake.cmake_layout:build_folder_vars='[\"const.myvalue\"]'" settings = " -s os=FreeBSD -s arch=armv8 -s build_type=Debug" c.run("install . -c={} {}".format(conf, settings)) assert "cmake --preset conan-myvalue-debug" in c.out assert os.path.exists(os.path.join(c.current_folder, "build", "myvalue", "Debug")) presets = load(os.path.join(c.current_folder, "build/myvalue/Debug/generators/CMakePresets.json")) assert "conan-myvalue-debug" in presets def test_extra_flags(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Conan(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "build_type" def generate(self): tc = CMakeToolchain(self) tc.extra_cxxflags = ["extra_cxxflags"] tc.extra_cflags = ["extra_cflags"] tc.extra_sharedlinkflags = ["extra_sharedlinkflags"] tc.extra_exelinkflags = ["extra_exelinkflags"] tc.generate() """) profile = textwrap.dedent(""" include(default) [conf] tools.build:cxxflags+=['cxxflags'] tools.build:cflags+=['cflags'] tools.build:sharedlinkflags+=['sharedlinkflags'] tools.build:exelinkflags+=['exelinkflags'] """) client.save({"conanfile.py": conanfile, "profile": profile}) client.run('install . -pr=./profile') toolchain = client.load("conan_toolchain.cmake") assert 'string(APPEND CONAN_CXX_FLAGS " extra_cxxflags cxxflags")' in toolchain assert 'string(APPEND CONAN_C_FLAGS " extra_cflags cflags")' in toolchain assert 'string(APPEND CONAN_SHARED_LINKER_FLAGS " extra_sharedlinkflags sharedlinkflags")' in toolchain assert 'string(APPEND CONAN_EXE_LINKER_FLAGS " extra_exelinkflags exelinkflags")' in toolchain def test_avoid_ovewrite_user_cmakepresets(): # https://github.com/conan-io/conan/issues/15052 c = TestClient() c.save({"conanfile.txt": "", "CMakePresets.json": "{}"}) c.run('install . -g CMakeToolchain', assert_error=True) assert "Error in generator 'CMakeToolchain': Existing CMakePresets.json not generated" in c.out assert "Use --output-folder or define a 'layout' to avoid collision" in c.out def test_presets_njobs(): c = TestClient() c.save({"conanfile.txt": ""}) c.run('install . -g CMakeToolchain -c tools.build:jobs=42') presets = json.loads(c.load("CMakePresets.json")) assert presets["buildPresets"][0]["jobs"] == 42 assert presets["testPresets"][0]["execution"]["jobs"] == 42 c.run('install . -g CMakeToolchain -c tools.build:jobs=0') presets = json.loads(c.load("CMakePresets.json")) assert "jobs" not in presets["buildPresets"][0] assert "execution" not in presets["testPresets"][0] def test_add_cmakeexe_to_presets(): c = TestClient() tool = textwrap.dedent(r""" import os from conan import ConanFile from conan.tools.files import chdir, save class Tool(ConanFile): name = "cmake" version = "3.27" settings = "os", "compiler", "arch", "build_type" def package(self): with chdir(self, self.package_folder): save(self, "bin/{}", "") """) profile = textwrap.dedent(""" include(default) [platform_tool_requires] cmake/3.27 """) consumer = textwrap.dedent(""" [tool_requires] cmake/3.27 [layout] cmake_layout """) cmake_exe = "cmake.exe" if platform.system() == "Windows" else "cmake" c.save({"tool.py": tool.format(cmake_exe), "conanfile.txt": consumer, "myprofile": profile}) c.run("create tool.py") c.run("install . -g CMakeToolchain -g CMakeDeps") presets_path = os.path.join("build", "Release", "generators", "CMakePresets.json") \ if platform.system() != "Windows" else os.path.join("build", "generators", "CMakePresets.json") presets = json.loads(c.load(presets_path)) assert cmake_exe == os.path.basename(presets["configurePresets"][0].get("cmakeExecutable")) # if we set "tools.cmake:cmake_program" that will have preference c.run("install . -g CMakeToolchain -g CMakeDeps -c tools.cmake:cmake_program='/other/path/cmake'") presets = json.loads(c.load(presets_path)) assert '/other/path/cmake' == presets["configurePresets"][0].get("cmakeExecutable") # if we have a platform_tool_requires it will not be set because it is filtered before # so it will not be in direct_build dependencies c.run("install . -g CMakeToolchain -g CMakeDeps -pr:h=./myprofile") presets = json.loads(c.load(presets_path)) assert presets["configurePresets"][0].get("cmakeExecutable") is None def test_toolchain_ends_newline(): # https://github.com/conan-io/conan/issues/15785 client = TestClient() client.save({"conanfile.py": GenConanfile()}) client.run("install . -g CMakeToolchain") toolchain = client.load("conan_toolchain.cmake") assert toolchain[-1] == "\n" def test_toolchain_and_compilers_build_context(): """ Tests how CMakeToolchain manages the build context profile if the build profile is specifying another compiler path (using conf) Issue related: https://github.com/conan-io/conan/issues/15878 """ host = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [conf] tools.build:compiler_executables={"c": "gcc", "cpp": "g++"} """) build = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=libc++ compiler.cppstd=11 [conf] tools.build:compiler_executables={"c": "clang", "cpp": "clang++"} """) tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class toolRecipe(ConanFile): name = "tool" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" def build(self): toolchain = os.path.join(self.generators_folder, "conan_toolchain.cmake") content = load(self, toolchain) assert 'set(CMAKE_C_COMPILER "clang")' in content assert 'set(CMAKE_CXX_COMPILER "clang++")' in content """) consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class consumerRecipe(ConanFile): name = "consumer" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "CMakeToolchain" tool_requires = "tool/1.0" def build(self): toolchain = os.path.join(self.generators_folder, "conan_toolchain.cmake") content = load(self, toolchain) assert 'set(CMAKE_C_COMPILER "gcc")' in content assert 'set(CMAKE_CXX_COMPILER "g++")' in content """) client = TestClient() client.save({ "host": host, "build": build, "tool/conanfile.py": tool, "consumer/conanfile.py": consumer }) client.run("export tool") client.run("create consumer -pr:h host -pr:b build --build=missing") def test_toolchain_keep_absolute_paths(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain, cmake_layout class Pkg(ConanFile): settings = "build_type" def generate(self): tc = CMakeToolchain(self) tc.absolute_paths = True tc.generate() def layout(self): cmake_layout(self) """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": ""}) c.run('install . ') user_presets = json.loads(c.load("CMakeUserPresets.json")) assert os.path.isabs(user_presets["include"][0]) presets = json.loads(c.load(user_presets["include"][0])) assert os.path.isabs(presets["configurePresets"][0]["toolchainFile"]) def test_customize_cmakeuserpresets(): # https://github.com/conan-io/conan/issues/15639 c = TestClient() c.save({"conanfile.py": GenConanfile(), "CMakeLists.txt": ""}) c.run("install . -g CMakeToolchain -of=build") old = c.load("CMakeUserPresets.json") os.remove(os.path.join(c.current_folder, "CMakeUserPresets.json")) c.run("install . -g CMakeToolchain -of=build -c tools.cmake.cmaketoolchain:user_presets=my.json") new = c.load("my.json") assert old == new # also in subfolder c.run("install . -g CMakeToolchain -of=build " "-c tools.cmake.cmaketoolchain:user_presets=mysub/my.json") assert os.path.exists(os.path.join(c.current_folder, "mysub", "my.json")) assert not os.path.exists(os.path.join(c.current_folder, "CMakeUserPresets.json")) c.run("install . -g CMakeToolchain -of=build -c tools.cmake.cmaketoolchain:user_presets=") assert not os.path.exists(os.path.join(c.current_folder, "CMakeUserPresets.json")) def test_output_dirs_gnudirs_local_default(): # https://github.com/conan-io/conan/issues/14733 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout from conan.tools.files import load class Conan(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def build(self): tc = load(self, "conan_toolchain.cmake") self.output.info(tc) """) c.save({"conanfile.py": conanfile}) c.run("create .") def _assert_install(out): assert 'set(CMAKE_INSTALL_BINDIR "bin")' in out assert 'set(CMAKE_INSTALL_SBINDIR "bin")' in out assert 'set(CMAKE_INSTALL_LIBEXECDIR "bin")' in out assert 'set(CMAKE_INSTALL_LIBDIR "lib")' in out assert 'set(CMAKE_INSTALL_INCLUDEDIR "include")' in out _assert_install(c.out) assert "CMAKE_INSTALL_PREFIX" in c.out c.run("build .") _assert_install(c.out) assert "CMAKE_INSTALL_PREFIX" not in c.out def test_output_dirs_gnudirs_local_custom(): # https://github.com/conan-io/conan/issues/14733 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout from conan.tools.files import load class Conan(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "compiler", "build_type" generators = "CMakeToolchain" def layout(self): self.cpp.package.bindirs = ["mybindir"] self.cpp.package.includedirs = ["myincludedir"] self.cpp.package.libdirs = ["mylibdir"] def build(self): tc = load(self, "conan_toolchain.cmake") self.output.info(tc) """) c.save({"conanfile.py": conanfile}) c.run("create .") def _assert_install(out): assert 'set(CMAKE_INSTALL_BINDIR "mybindir")' in out assert 'set(CMAKE_INSTALL_SBINDIR "mybindir")' in out assert 'set(CMAKE_INSTALL_LIBEXECDIR "mybindir")' in out assert 'set(CMAKE_INSTALL_LIBDIR "mylibdir")' in out assert 'set(CMAKE_INSTALL_INCLUDEDIR "myincludedir")' in out _assert_install(c.out) assert "CMAKE_INSTALL_PREFIX" in c.out c.run("build .") _assert_install(c.out) assert "CMAKE_INSTALL_PREFIX" not in c.out def test_toolchain_extra_variables(): windows_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 [conf] tools.cmake.cmaketoolchain:extra_variables={'CMAKE_GENERATOR_INSTANCE': '${GENERATOR_INSTANCE}/buildTools/', 'FOO': '42' } """) client = TestClient() client.save({"conanfile.txt": "[generators]\nCMakeToolchain", "windows": windows_profile}) # Test passing extra_variables from pro ile client.run("install . --profile:host=windows") toolchain = client.load("conan_toolchain.cmake") assert 'set(CMAKE_GENERATOR_INSTANCE "${GENERATOR_INSTANCE}/buildTools/")' in toolchain assert 'set(FOO "42")' in toolchain # Test input from command line passing dict between doble quotes client.run(textwrap.dedent(r""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'CMAKE_GENERATOR_INSTANCE': '${GENERATOR_INSTANCE}/buildTools/', 'FOO': 42.2, 'DICT': {'value': 1}, 'CACHE_VAR': {'value': 'hello world', 'cache': True, 'type': 'BOOL', 'docstring': 'test variable'}}" """) ) toolchain = client.load("conan_toolchain.cmake") assert 'set(CMAKE_GENERATOR_INSTANCE "${GENERATOR_INSTANCE}/buildTools/")' in toolchain assert 'set(FOO 42.2)' in toolchain assert 'set(DICT 1)' in toolchain assert 'set(CACHE_VAR "hello world" CACHE BOOL "test variable")' in toolchain client.run(textwrap.dedent(""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'myVar': {'value': 'hello world', 'cache': 'true'}}" """), assert_error=True) assert 'tools.cmake.cmaketoolchain:extra_variables "myVar" "cache" must be a boolean' in client.out # Test invalid force client.run(textwrap.dedent(""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'myVar': {'value': 'hello world', 'force': True}}" """), assert_error=True) assert 'tools.cmake.cmaketoolchain:extra_variables "myVar" "force" is only allowed for cache variables' in client.out client.run(textwrap.dedent(""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'myVar': {'value': 'hello world', 'cache': True, 'force': 'true'}}" """), assert_error=True) assert 'tools.cmake.cmaketoolchain:extra_variables "myVar" "force" must be a boolean' in client.out # Test invalid cache variable client.run(textwrap.dedent(""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'myVar': {'value': 'hello world', 'cache': True}}" """), assert_error=True) assert 'tools.cmake.cmaketoolchain:extra_variables "myVar" needs "type" defined for cache variable' in client.out client.run(textwrap.dedent(""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'myVar': {'value': 'hello world', 'cache': True, 'type': 'INVALID_TYPE'}}" """), assert_error=True) assert 'tools.cmake.cmaketoolchain:extra_variables "myVar" invalid type "INVALID_TYPE" for cache variable. Possible types: BOOL, FILEPATH, PATH, STRING, INTERNAL' in client.out client.run(textwrap.dedent(""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'CACHE_VAR_DEFAULT_DOC': {'value': 'hello world', 'cache': True, 'type': 'PATH'}}" """)) toolchain = client.load("conan_toolchain.cmake") assert 'set(CACHE_VAR_DEFAULT_DOC "hello world" CACHE PATH "CACHE_VAR_DEFAULT_DOC")' in toolchain client.run(textwrap.dedent(""" install . -c tools.cmake.cmaketoolchain:extra_variables="{'myVar': {'value': 'hello world', 'cache': True, 'type': 'PATH', 'docstring': 'My cache variable', 'force': True}}" """)) toolchain = client.load("conan_toolchain.cmake") assert 'set(myVar "hello world" CACHE PATH "My cache variable" FORCE)' in toolchain def test_variables_wrong_scaping(): # https://github.com/conan-io/conan/issues/16432 c = TestClient() c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), "pkg/conanfile.txt": "[tool_requires]\ntool/0.1\n[generators]\nCMakeToolchain"}) c.run("create tool") c.run("install pkg") toolchain = c.load("pkg/conan_toolchain.cmake") cache_folder = c.cache_folder.replace("\\", "/") assert f'list(PREPEND CMAKE_PROGRAM_PATH "{cache_folder}' in toolchain c.run("install pkg --deployer=full_deploy") toolchain = c.load("pkg/conan_toolchain.cmake") assert 'list(PREPEND CMAKE_PROGRAM_PATH "${CMAKE_CURRENT_LIST_DIR}/full_deploy' in toolchain def test_tricore(): # making sure the arch ``tc131`` is there c = TestClient() c.save({"conanfile.txt": "[generators]\nCMakeToolchain"}) c.run("install . -s os=baremetal -s compiler=gcc -s arch=tc131") content = c.load("conan_toolchain.cmake") assert 'set(CMAKE_SYSTEM_NAME Generic-ELF)' in content assert 'set(CMAKE_SYSTEM_PROCESSOR tricore)' in content assert 'string(APPEND CONAN_CXX_FLAGS " -mtc131")' in content assert 'string(APPEND CONAN_C_FLAGS " -mtc131")' in content assert 'string(APPEND CONAN_SHARED_LINKER_FLAGS " -mtc131")' in content assert 'string(APPEND CONAN_EXE_LINKER_FLAGS " -mtc131")' in content def test_declared_stdlib_and_passed(): client = TestClient() client.save({"conanfile.txt": "[generators]\nCMakeToolchain"}) client.run('install . -s compiler=sun-cc -s compiler.libcxx=libCstd') tc = client.load("conan_toolchain.cmake") assert 'string(APPEND CONAN_CXX_FLAGS " -library=Cstd")' in tc client.run('install . -s compiler=sun-cc -s compiler.libcxx=libstdcxx') tc = client.load("conan_toolchain.cmake") assert 'string(APPEND CONAN_CXX_FLAGS " -library=stdcxx4")' in tc def test_cmake_presets_compiler(): profile = textwrap.dedent(r""" [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=193 compiler.runtime=dynamic [conf] tools.build:compiler_executables={"c": "cl", "cpp": "cl.exe"} """) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler")\ .with_generator("CMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . -pr:b profile -pr:h profile") presets = json.loads(client.load("CMakePresets.json")) cache_variables = presets["configurePresets"][0]["cacheVariables"] # https://github.com/microsoft/vscode-cmake-tools/blob/a1ceda25ea93fc0060324de15970a8baa69addf6/src/presets/preset.ts#L1095C23-L1095C35 assert cache_variables["CMAKE_C_COMPILER"] == "cl" assert cache_variables["CMAKE_CXX_COMPILER"] == "cl.exe" @pytest.mark.parametrize( "threads, flags", [("posix", "-pthread"), ("wasm_workers", "-sWASM_WORKERS=1")], ) def test_thread_flags(threads, flags): client = TestClient() profile = textwrap.dedent(f""" [settings] arch=wasm build_type=Release compiler=emcc compiler.cppstd=17 compiler.threads={threads} compiler.libcxx=libc++ compiler.version=4.0.10 os=Emscripten """) client.save( { "conanfile.py": GenConanfile("pkg", "1.0") .with_settings("os", "arch", "compiler", "build_type") .with_generator("CMakeToolchain"), "profile": profile, } ) client.run("install . -pr=./profile") toolchain = client.load("conan_toolchain.cmake") assert f'string(APPEND CONAN_CXX_FLAGS " {flags}")' in toolchain assert f'string(APPEND CONAN_C_FLAGS " {flags}")' in toolchain assert f'string(APPEND CONAN_SHARED_LINKER_FLAGS " {flags}")' in toolchain assert f'string(APPEND CONAN_EXE_LINKER_FLAGS " {flags}")' in toolchain ================================================ FILE: test/integration/toolchains/cmake/test_cmaketoolchain_blocks.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_custom_block(): # https://github.com/conan-io/conan/issues/9998 c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMakeToolchain class Pkg(ConanFile): def generate(self): toolchain = CMakeToolchain(self) class MyBlock: template = "Hello {{myvar}}!!!" def context(self): return {"myvar": "World"} toolchain.blocks["mynewblock"] = MyBlock toolchain.generate() """) c.save({"conanfile.py": conanfile}) c.run("install .") assert "Hello World!!!" in c.load("conan_toolchain.cmake") ================================================ FILE: test/integration/toolchains/env/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/env/test_buildenv.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Fails on windows") def test_crossbuild_windows_incomplete(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler", "build_type", "arch" def build(self): # using python --version as it does not depend on shell self.run("python --version") """) build_profile = textwrap.dedent(""" [settings] arch=x86_64 """) client.save({"conanfile.py": conanfile, "build_profile": build_profile}) client.run("create . -pr:b=build_profile", assert_error=True) assert "The 'build' profile must have a 'os' declared" in client.out build_profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 """) client.save({"conanfile.py": conanfile, "build_profile": build_profile}) client.run("create . -pr:b=build_profile") assert "Python" in client.out def test_quoted_vars(): # https://github.com/conan-io/conan/issues/18761 c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "dep" version = "0.1" settings = "os" def package_info(self): v = 'MyVar:"content!! | other!!"' v = v.replace("|", "^|") if self.settings.os == "Windows" else v # escape self.buildenv_info.define("MyCustomEscapedVar", v) """) conanfile = textwrap.dedent(""" import os, platform from conan import ConanFile class ConanFileToolsTest(ConanFile): settings = "os" requires = "dep/0.1" def build(self): if platform.system() == "Windows": self.run("set MyCustomEscapedVar") else: self.run("printenv MyCustomEscapedVar") self.run("echo Hello!!!!") """) c.save({"dep/conanfile.py": dep, "consumer/conanfile.py": conanfile}) c.run("create dep") c.run("build consumer") assert 'MyVar:"content!! | other!!"' in c.out assert "Hello!!!!" in c.out ================================================ FILE: test/integration/toolchains/env/test_environment.py ================================================ import platform import textwrap from conan.test.utils.tools import TestClient # TODO: Change this test when we refactor EnvVars. The UX leaves much to be desired def test_env_and_scope_none(): """ Check scope=None does not append foo=var to conan{build|run}.{bat|sh|ps1} Issue: https://github.com/conan-io/conan/issues/17249 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.env import Environment class Pkg(ConanFile): name = "pkg" version = "0.1" settings = "os", "compiler", "build_type", "arch" def generate(self): env1 = Environment() env1.define("foo", "var") # Will not append "my_env_file" to "conanbuild.bat|sh|ps1" envvars = env1.vars(self, scope=None) envvars.save_script("my_env_file") # Let's check the apply() function with env1.vars(self, scope=None).apply(): import os assert os.environ["foo"] == "var" """) client.save({"conanfile.py": conanfile}) client.run("install .") ext = ".bat" if platform.system() == "Windows" else ".sh" assert "my_env_file" not in client.load(f"conanbuild{ext}") assert "my_env_file" not in client.load(f"conanrun{ext}") ================================================ FILE: test/integration/toolchains/env/test_virtualenv_default_apply.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture def client(): client = TestClient() conanfile = str(GenConanfile()) conanfile += """ def package_info(self): self.buildenv_info.define("Foo", "MyVar!") self.runenv_info.define("Foo", "MyRunVar!") """ client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") return client @pytest.mark.parametrize("scope", ["build", "run"]) @pytest.mark.parametrize("default_virtualenv", [True, False, None]) @pytest.mark.parametrize("cli_value", [None, "false"]) def test_virtualenv_deactivated(client, scope, default_virtualenv, cli_value): format_str = {True: f"virtual{scope}env = True", False: f"virtual{scope}env = False", None: ""}[default_virtualenv] conanfile = textwrap.dedent(""" from conan import ConanFile class ConanFileToolsTest(ConanFile): settings = "os" {} requires = "foo/1.0" """).format(format_str) client.save({"conanfile.py": conanfile}) cli_extra = f"--envs-generation={cli_value}" if cli_value is not None else "" client.run(f"install . {cli_extra}") extension = "bat" if platform.system() == "Windows" else "sh" filename = f"conan{scope}env.{extension}" filepath = os.path.join(client.current_folder, filename) exists_file = os.path.exists(filepath) should_exist = cli_value != "false" and (default_virtualenv is None or default_virtualenv) if should_exist: assert exists_file, f"File {filename} should exist in {os.listdir(client.current_folder)}" assert "Foo" in client.load(filepath) else: assert not exists_file, f"File {filename} should not exist in {os.listdir(client.current_folder)}" def test_deactivate_virtualxxxenv_attr(): conanfile = textwrap.dedent(""" from conan import ConanFile class ConanFileToolsTest(ConanFile): virtualbuildenv = False virtualrunenv = False """) c = TestClient(light=True) c.save({"conanfile.py": conanfile}) c.run(f"install ") assert os.listdir(c.current_folder) == ["conanfile.py"] def test_virtualrunenv_not_applied(client): """By default the VirtualRunEnv is not added to the list, otherwise when declaring generators = "VirtualBuildEnv", "VirtualRunEnv" will be always added""" conanfile = textwrap.dedent(""" from conan import ConanFile import platform class ConanFileToolsTest(ConanFile): settings = "os" generators = "VirtualBuildEnv", "VirtualRunEnv" requires = "foo/1.0" """) client.save({"conanfile.py": conanfile}) client.run("install . ") extension = "bat" if platform.system() == "Windows" else "sh" exists_file = os.path.exists(os.path.join(client.current_folder, "conanrun.{}".format(extension))) assert exists_file global_env = client.load("conanbuild.{}".format(extension)) assert "conanrunenv" not in global_env @pytest.mark.parametrize("explicit_declare", [True, False, None]) def test_virtualrunenv_explicit_declare(client, explicit_declare): """By default the VirtualRunEnv is not added to the list, otherwise when declaring generators = "VirtualBuildEnv", "VirtualRunEnv" will be always added""" conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.env import VirtualRunEnv import platform class ConanFileToolsTest(ConanFile): requires = "foo/1.0" def generate(self): VirtualRunEnv(self).generate({}) """).format({True: "scope='build'", False: "scope='run'", None: ""}.get(explicit_declare)) client.save({"conanfile.py": conanfile}) client.run("install . ") extension = "bat" if platform.system() == "Windows" else "sh" exists_file = os.path.exists(os.path.join(client.current_folder, "conanbuild.{}".format(extension))) assert exists_file global_env = client.load("conanbuild.{}".format(extension)) if explicit_declare: assert "conanrun" in global_env else: assert "conanrun" not in global_env ================================================ FILE: test/integration/toolchains/env/test_virtualenv_object_access.py ================================================ import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.fixture def client(): client = TestClient() conanfile = str(GenConanfile()) conanfile += """ def package_info(self): self.buildenv_info.define("Foo", "MyVar!") self.runenv_info.define("runFoo", "Value!") self.buildenv_info.append("Hello", "MyHelloValue!") """ client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") return client def test_virtualenv_object_access(client): conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.env import VirtualBuildEnv, VirtualRunEnv class ConanFileToolsTest(ConanFile): requires = "foo/1.0" def build(self): build_env = VirtualBuildEnv(self).vars() run_env = VirtualRunEnv(self).vars() self.output.warning("Foo: *{}*".format(build_env["Foo"])) self.output.warning("runFoo: *{}*".format(run_env["runFoo"])) self.output.warning("Hello: *{}*".format(build_env["Hello"])) with build_env.apply(): with run_env.apply(): self.output.warning("Applied Foo: *{}*".format(os.getenv("Foo", ""))) self.output.warning("Applied Hello: *{}*".format(os.getenv("Hello", ""))) self.output.warning("Applied runFoo: *{}*".format(os.getenv("runFoo", ""))) """) profile = textwrap.dedent(""" [buildenv] Foo+=MyFooValue """) client.save({"conanfile.py": conanfile, "profile": profile}) client.run("create . --name=app --version=1.0 --profile=profile") assert "Foo: *MyVar! MyFooValue*" assert "runFoo:* Value!*" assert "Hello:* MyHelloValue!*" assert "Applied Foo: *MyVar! MyFooValue*" assert "Applied runFoo: **" assert "Applied Hello: * MyHelloValue!*" ================================================ FILE: test/integration/toolchains/env/test_virtualenv_winbash.py ================================================ import os import platform import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient """ When we use the VirtualRunEnv and VirtualBuildEnd generators, we take information from the self.dependencies env_buildinfo and env_runinfo, but to format correctly the environment variables that are paths to run in a windows bash, we need to look at the consumer conanfile not the dependency conanfile. This is testing that in the process of aggregating the paths, these are correct. """ @pytest.fixture def client(): client = TestClient() conanfile = str(GenConanfile()) conanfile += """ def package_info(self): self.buildenv_info.define_path("AR", "c:/path/to/ar") self.buildenv_info.append_path("PATH", "c:/path/to/something") self.runenv_info.define_path("RUNTIME_VAR", "c:/path/to/exe") """ client.save({"conanfile.py": conanfile}) client.run("create . --name=foo --version=1.0") client.save_home({"global.conf": "tools.microsoft.bash:subsystem=cygwin"}) return client @pytest.mark.parametrize("win_bash", [True, False, None]) @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_virtualenv_deactivated(client, win_bash): conanfile = GenConanfile().with_settings("os").with_require("foo/1.0") if win_bash: conanfile.with_class_attribute("win_bash = True") client.save({"conanfile.py": conanfile}) client.run("install . -s:b os=Windows -s:h os=Windows") if win_bash: # Assert there is no "bat" files generated because the environment can and will be run inside # the bash assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.bat")) build_contents = client.load("conanbuildenv.sh") assert "/cygdrive/c/path/to/ar" in build_contents assert "${PATH:-}${PATH:+:}/cygdrive/c/path/to/something" in build_contents else: assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.sh")) build_contents = client.load("conanbuildenv.bat") assert "c:/path/to/ar" in build_contents assert "c:/path/to/something" in build_contents assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.sh")) run_contents = client.load("conanrunenv.bat") assert "c:/path/to/exe" in run_contents @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_nowinbash(client): """ whe the recipe doesn't define win_bash=True 2 things can happen: - No need to run in bash, things will run in normal command, with .bat - You run directly in a subsytem terminal a recipe that was not aware of that, still the files need to follow the subsystem """ conanfile = str(GenConanfile().with_settings("os") .with_generator("VirtualBuildEnv").with_generator("VirtualRunEnv") .with_require("foo/1.0")) client.save({"conanfile.py": conanfile}) client.run("install . -s:b os=Windows -s:h os=Windows") assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.sh")) build_contents = client.load("conanbuildenv.bat") assert 'set "AR=c:/path/to/ar"' in build_contents assert 'set "PATH=%PATH%;c:/path/to/something"' in build_contents assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.sh")) run_contents = client.load("conanrunenv.bat") assert 'set "RUNTIME_VAR=c:/path/to/exe"' in run_contents # Running it inside a cygwin subsystem client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -s:b os=Windows -s:h os=Windows " "-c:h tools.microsoft.bash:active=True") assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.bat")) build_contents = client.load("conanbuildenv.sh") assert 'export AR="/cygdrive/c/path/to/ar"' in build_contents assert 'export PATH="${PATH:-}${PATH:+:}/cygdrive/c/path/to/something"' in build_contents assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.bat")) run_contents = client.load("conanrunenv.sh") assert 'export RUNTIME_VAR="/cygdrive/c/path/to/exe"' in run_contents # Running it inside a msys2 subsystem, test tat it overrides client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -s:b os=Windows -s:h os=Windows " "-c tools.microsoft.bash:subsystem=msys2 -c:h tools.microsoft.bash:active=True") assert not os.path.exists(os.path.join(client.current_folder, "conanbuildenv.bat")) build_contents = client.load("conanbuildenv.sh") assert 'export AR="/c/path/to/ar"' in build_contents assert 'export PATH="${PATH:-}${PATH:+:}/c/path/to/something"' in build_contents assert not os.path.exists(os.path.join(client.current_folder, "conanrunenv.bat")) run_contents = client.load("conanrunenv.sh") assert 'export RUNTIME_VAR="/c/path/to/exe"' in run_contents ================================================ FILE: test/integration/toolchains/gnu/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/gnu/test_autotools.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_autotools_make_parameters(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools class Conan(ConanFile): settings = "os" generators = "AutotoolsDeps", "AutotoolsToolchain", "VirtualRunEnv" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}") def build(self): autotools = Autotools(self) autotools.make(makefile="MyMake", target="test", args=["-j4", "VERBOSE=1"]) autotools.install(makefile="OtherMake") """) client.save({"conanfile.py": conanfile}) client.run("build .") assert "make --file=MyMake test -j4 VERBOSE=1" in client.out assert "make --file=OtherMake install" in client.out ================================================ FILE: test/integration/toolchains/gnu/test_autotoolsdeps.py ================================================ import platform import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Autotools") def test_link_lib_correct_order(): client = TestClient() liba = GenConanfile().with_name("liba").with_version("0.1") libb = GenConanfile().with_name("libb").with_version("0.1").with_require("liba/0.1") libc = GenConanfile().with_name("libc").with_version("0.1").with_require("libb/0.1") consumer = GenConanfile().with_require("libc/0.1") client.save({"liba.py": liba, "libb.py": libb, "libc.py": libc, "consumer.py": consumer}) client.run("create liba.py") folder_a = client.created_layout().package() client.run("create libb.py") folder_b = client.created_layout().package() client.run("create libc.py") folder_c = client.created_layout().package() client.run("install consumer.py -g AutotoolsDeps") deps = client.load("conanautotoolsdeps.sh") # check the libs are added in the correct order with this regex assert re.search("export LDFLAGS.*{}.*{}.*{}".format(folder_c, folder_b, folder_a), deps) @pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Autotools") def test_cpp_info_aggregation(): profile = textwrap.dedent(""" [settings] build_type=Release arch=x86 os=Macos compiler=gcc compiler.libcxx=libstdc++11 compiler.version=7.1 compiler.cppstd=17 """) dep_conanfile = textwrap.dedent(r""" from conan import ConanFile class Dep(ConanFile): settings = "os", "arch", "compiler", "build_type" def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.includedirs.append("path/includes/{}".format(self.name)) self.cpp_info.includedirs.append("other\\include\\path\\{}".format(self.name)) # To test some path in win, to be used with MinGW make or MSYS etc self.cpp_info.libdirs = [] self.cpp_info.libdirs.append("one\\lib\\path\\{}".format(self.name)) self.cpp_info.libs = [] self.cpp_info.libs.append("{}_onelib".format(self.name)) self.cpp_info.libs.append("{}_twolib".format(self.name)) self.cpp_info.defines = [] self.cpp_info.defines.append("{}_onedefinition".format(self.name)) self.cpp_info.defines.append("{}_twodefinition".format(self.name)) self.cpp_info.cflags = ["{}_a_c_flag".format(self.name)] self.cpp_info.cxxflags = ["{}_a_cxx_flag".format(self.name)] self.cpp_info.sharedlinkflags = ["{}_shared_link_flag".format(self.name)] self.cpp_info.exelinkflags = ["{}_exe_link_flag".format(self.name)] self.cpp_info.sysroot = "/path/to/folder/{}".format(self.name) self.cpp_info.frameworks = [] self.cpp_info.frameworks.append("{}_oneframework".format(self.name)) self.cpp_info.frameworks.append("{}_twoframework".format(self.name)) self.cpp_info.system_libs = [] self.cpp_info.system_libs.append("{}_onesystemlib".format(self.name)) self.cpp_info.system_libs.append("{}_twosystemlib".format(self.name)) self.cpp_info.frameworkdirs = [] self.cpp_info.frameworkdirs.append("one/framework/path/{}".format(self.name)) """) t = TestClient() t.save({"conanfile.py": dep_conanfile, "macos": profile}) t.run("create . --name dep1 --version 1.0 --profile:host=macos") t.run("create . --name dep2 --version 1.0 --profile:host=macos") consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsDeps class HelloConan(ConanFile): requires = "dep1/1.0", "dep2/1.0" settings = "os", "arch", "build_type", "compiler" def generate(self): deps = AutotoolsDeps(self) env = deps.environment # Customize the environment env.remove("LDFLAGS", "dep2_shared_link_flag") env.append("LDFLAGS", "OtherSuperStuff") env = deps.vars() # The contents are of course modified # The topological order puts dep2 before dep1 assert env["CXXFLAGS"] == 'dep2_a_cxx_flag dep1_a_cxx_flag' assert env["CFLAGS"] == 'dep2_a_c_flag dep1_a_c_flag' assert env["LIBS"] == "-ldep2_onelib -ldep2_twolib -ldep1_onelib -ldep1_twolib "\ "-ldep2_onesystemlib -ldep2_twosystemlib "\ "-ldep1_onesystemlib -ldep1_twosystemlib" assert 'dep1_shared_link_flag dep2_exe_link_flag dep1_exe_link_flag -framework dep2_oneframework -framework dep2_twoframework ' \ '-framework dep1_oneframework -framework dep1_twoframework ' in env["LDFLAGS"] assert 'OtherSuperStuff' in env["LDFLAGS"] """) t.save({"conanfile.py": consumer}) t.run("create . --name consumer --version 1.0 --profile:host=macos") ================================================ FILE: test/integration/toolchains/gnu/test_autotoolstoolchain.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save, load def test_extra_flags_via_conf(): os_ = platform.system() os_ = "Macos" if os_ == "Darwin" else os_ profile = textwrap.dedent(""" [settings] os=%s compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.build:cxxflags=["--flag1", "--flag2"] tools.build:cflags+=["--flag3", "--flag4"] tools.build:sharedlinkflags+=["--flag5"] tools.build:exelinkflags+=["--flag6"] tools.build:defines+=["DEF1", "DEF2"] """ % os_) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("AutotoolsToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load("conanautotoolstoolchain{}".format('.bat' if os_ == "Windows" else '.sh')) if os_ == "Windows": assert 'set "CPPFLAGS=%CPPFLAGS% -DNDEBUG -DDEF1 -DDEF2"' in toolchain assert 'set "CXXFLAGS=%CXXFLAGS% -O3 --flag1 --flag2"' in toolchain assert 'set "CFLAGS=%CFLAGS% -O3 --flag3 --flag4"' in toolchain assert 'set "LDFLAGS=%LDFLAGS% --flag5 --flag6"' in toolchain else: assert 'export CPPFLAGS="${CPPFLAGS:-}${CPPFLAGS:+ }-DNDEBUG -DDEF1 -DDEF2"' in toolchain assert 'export CXXFLAGS="${CXXFLAGS:-}${CXXFLAGS:+ }-O3 --flag1 --flag2"' in toolchain assert 'export CFLAGS="${CFLAGS:-}${CFLAGS:+ }-O3 --flag3 --flag4"' in toolchain assert 'export LDFLAGS="${LDFLAGS:-}${LDFLAGS:+ }--flag5 --flag6"' in toolchain def test_extra_flags_order(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain class Conan(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "build_type" def generate(self): at = AutotoolsToolchain(self) at.extra_cxxflags = ["extra_cxxflags"] at.extra_cflags = ["extra_cflags"] at.extra_ldflags = ["extra_ldflags"] at.extra_defines = ["extra_defines"] at.generate() """) profile = textwrap.dedent(""" include(default) [conf] tools.build:cxxflags+=['cxxflags'] tools.build:cflags+=['cflags'] tools.build:sharedlinkflags+=['sharedlinkflags'] tools.build:exelinkflags+=['exelinkflags'] tools.build:defines+=['defines'] """) client.save({"conanfile.py": conanfile, "profile": profile}) client.run('install . -pr=./profile') toolchain = client.load("conanautotoolstoolchain{}".format('.bat' if platform.system() == "Windows" else '.sh')) assert '-Dextra_defines -Ddefines' in toolchain assert 'extra_cxxflags cxxflags' in toolchain assert 'extra_cflags cflags' in toolchain assert 'extra_ldflags sharedlinkflags exelinkflags' in toolchain def test_autotoolstoolchain_rcflags(): """Test that tools.build:rcflags is applied to RCFLAGS in the generated script.""" os_ = platform.system() os_ = "Macos" if os_ == "Darwin" else os_ profile = textwrap.dedent(""" [settings] os=%s arch=x86_64 compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 build_type=Release [conf] tools.build:rcflags=["--rcflag1", "--rcflag2"] """ % os_) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type").with_generator("AutotoolsToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") ext = ".bat" if os_ == "Windows" else ".sh" toolchain = client.load("conanautotoolstoolchain{}".format(ext)) assert "RCFLAGS" in toolchain assert "--rcflag1" in toolchain assert "--rcflag2" in toolchain def test_autotools_custom_environment(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain class Conan(ConanFile): settings = "os" def generate(self): at = AutotoolsToolchain(self) env = at.environment() env.define("FOO", "BAR") at.generate(env) """) client.save({"conanfile.py": conanfile}) client.run("install . -s:b os=Linux -s:h os=Linux") content = load(os.path.join(client.current_folder, "conanautotoolstoolchain.sh")) assert 'export FOO="BAR"' in content def test_linker_scripts_via_conf(): os_ = platform.system() os_ = "Macos" if os_ == "Darwin" else os_ profile = textwrap.dedent(""" [settings] os=%s compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.build:sharedlinkflags+=["--flag5"] tools.build:exelinkflags+=["--flag6"] tools.build:linker_scripts+=["/linker/scripts/flash.ld", "/linker/scripts/extra_data.ld"] """ % os_) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ .with_generator("AutotoolsToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load("conanautotoolstoolchain{}".format('.bat' if os_ == "Windows" else '.sh')) if os_ == "Windows": assert 'set "LDFLAGS=%LDFLAGS% --flag5 --flag6 -T\'/linker/scripts/flash.ld\' -T\'/linker/scripts/extra_data.ld\'"' in toolchain else: assert 'export LDFLAGS="${LDFLAGS:-}${LDFLAGS:+ }--flag5 --flag6 -T\'/linker/scripts/flash.ld\' -T\'/linker/scripts/extra_data.ld\'"' in toolchain def test_not_none_values(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain class Foo(ConanFile): name = "foo" version = "1.0" def generate(self): tc = AutotoolsToolchain(self) assert None not in tc.defines assert None not in tc.cxxflags assert None not in tc.cflags assert None not in tc.ldflags """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("install .") def test_set_prefix(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain from conan.tools.layout import basic_layout class Foo(ConanFile): name = "foo" version = "1.0" def layout(self): basic_layout(self) def generate(self): at_toolchain = AutotoolsToolchain(self, prefix="/somefolder") at_toolchain.generate() """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("install .") conanbuild = client.load(os.path.join(client.current_folder, "build", "conan", "conanbuild.conf")) assert "--prefix=/somefolder" in conanbuild assert conanbuild.count("--prefix") == 1 def test_unknown_compiler(): client = TestClient() save(client.paths.settings_path_user, "compiler:\n xlc:\n") client.save({"conanfile.py": GenConanfile().with_settings("compiler", "build_type") .with_generator("AutotoolsToolchain")}) # this used to crash, because of build_type_flags in AutotoolsToolchain returning empty string client.run("install . -s compiler=xlc") assert "conanfile.py: Generator 'AutotoolsToolchain' calling 'generate()'" in client.out def test_toolchain_and_compilers_build_context(): """ Tests how AutotoolsToolchain manages the build context profile if the build profile is specifying another compiler path (using conf) Issue related: https://github.com/conan-io/conan/issues/15878 """ host = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [conf] tools.build:compiler_executables={"c": "gcc", "cpp": "g++", "rc": "windres"} """) build = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=libc++ compiler.cppstd=11 [conf] tools.build:compiler_executables={"c": "clang", "cpp": "clang++"} """) tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class toolRecipe(ConanFile): name = "tool" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "AutotoolsToolchain" def build(self): toolchain = os.path.join(self.generators_folder, "conanautotoolstoolchain.sh") content = load(self, toolchain) assert 'export CC="clang"' in content assert 'export CXX="clang++"' in content """) consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class consumerRecipe(ConanFile): name = "consumer" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "AutotoolsToolchain" tool_requires = "tool/1.0" def build(self): toolchain = os.path.join(self.generators_folder, "conanautotoolstoolchain.sh") content = load(self, toolchain) assert 'export CC="gcc"' in content assert 'export CXX="g++"' in content assert 'export RC="windres"' in content # Issue: https://github.com/conan-io/conan/issues/15486 assert 'export CC_FOR_BUILD="clang"' in content assert 'export CXX_FOR_BUILD="clang++"' in content """) client = TestClient() client.save({ "host": host, "build": build, "tool/conanfile.py": tool, "consumer/conanfile.py": consumer }) client.run("export tool") client.run("create consumer -pr:h host -pr:b build --build=missing") def test_toolchain_crossbuild_to_android(): """ Issue related: https://github.com/conan-io/conan/issues/17441 """ build = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) host = textwrap.dedent(""" [settings] os = Android os.api_level = 21 arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=libc++ compiler.cppstd=11 [buildenv] CC=clang CXX=clang++ [conf] tools.android:ndk_path=/path/to/ndk """) consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class consumerRecipe(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "AutotoolsToolchain" def build(self): toolchain = os.path.join(self.generators_folder, "conanautotoolstoolchain.sh") content = load(self, toolchain) assert 'export CC="clang"' not in content assert 'export CXX="clang++"' not in content assert 'export LD="/path/to/ndk' in content build_env = os.path.join(self.generators_folder, "conanbuildenv-x86_64.sh") content = load(self, build_env) assert 'export CC="clang"' in content assert 'export CXX="clang++"' in content assert 'export LD=' not in content """) client = TestClient() client.save({ "host": host, "build": build, "conanfile.py": consumer }) client.run("create . -pr:h host -pr:b build") def test_conf_build_does_not_exist(): host = textwrap.dedent(""" [settings] arch=x86_64 os=Linux [conf] tools.build:compiler_executables={'c': '/usr/bin/gcc', 'cpp': '/usr/bin/g++'} """) build = textwrap.dedent(""" [settings] arch=armv8 os=Linux [conf] tools.build:compiler_executables={'c': 'x86_64-linux-gnu-gcc', 'cpp': 'x86_64-linux-gnu-g++'} """) c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "host": host, "build": build}) c.run("export .") c.run("install --requires=pkg/0.1 --build=pkg/0.1 -g AutotoolsToolchain -pr:h host -pr:b build") tc = c.load("conanautotoolstoolchain.sh") assert 'export CC_FOR_BUILD="x86_64-linux-gnu-gcc"' in tc assert 'export CXX_FOR_BUILD="x86_64-linux-gnu-g++"' in tc @pytest.mark.parametrize( "threads, flags", [("posix", "-pthread"), ("wasm_workers", "-sWASM_WORKERS=1")], ) def test_thread_flags(threads, flags): is_win = platform.system() == "Windows" client = TestClient() profile = textwrap.dedent(f""" [settings] arch=wasm build_type=Release compiler=emcc compiler.cppstd=17 compiler.threads={threads} compiler.libcxx=libc++ compiler.version=4.0.10 os=Emscripten """) client.save( { "conanfile.py": GenConanfile("pkg", "1.0") .with_settings("os", "arch", "compiler", "build_type") .with_generator("AutotoolsToolchain"), "profile": profile, } ) client.run("install . -pr=./profile") toolchain = client.load("conanautotoolstoolchain{}".format('.bat' if is_win else '.sh')) if is_win: assert f'set "CXXFLAGS=%CXXFLAGS% -stdlib=libc++ {flags}"' in toolchain assert f'set "CFLAGS=%CFLAGS% {flags}"' in toolchain assert f'set "LDFLAGS=%LDFLAGS% {flags}' in toolchain else: assert f'export CXXFLAGS="${{CXXFLAGS:-}}${{CXXFLAGS:+ }}-stdlib=libc++ {flags}"' in toolchain assert f'export CFLAGS="${{CFLAGS:-}}${{CFLAGS:+ }}{flags}"' in toolchain assert f'export LDFLAGS="${{LDFLAGS:-}}${{LDFLAGS:+ }}{flags}"' in toolchain ================================================ FILE: test/integration/toolchains/gnu/test_basic_layout.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("basic_layout, expected_path", [ ('basic_layout(self)', 'build-release'), ('basic_layout(self, build_folder="custom_build_folder")', 'custom_build_folder')]) def test_basic_layout_subproject(basic_layout, expected_path): c = TestClient() conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.layout import basic_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "AutotoolsToolchain" def layout(self): self.folders.root = ".." self.folders.subproject = "pkg" {basic_layout} """) c.save({"pkg/conanfile.py": conanfile}) c.run("install pkg") ext = "sh" if platform.system() != "Windows" else "bat" assert os.path.isfile(os.path.join(c.current_folder, "pkg", expected_path, "conan", "conanautotoolstoolchain.{}".format(ext))) def test_editable_includes(): c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile from conan.tools.layout import basic_layout class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): basic_layout(self, src_folder="src") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type", "arch") .with_requires("dep/0.1")}) c.run("editable add dep") c.run("install pkg -s arch=x86_64 -g CMakeDeps -g PkgConfigDeps -g XcodeDeps") data = c.load(f"pkg/dep-release-x86_64-data.cmake") assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/src/include")' in data pc = c.load("pkg/dep.pc") assert "includedir=${prefix}/src/include" in pc xcode = c.load("pkg/conan_dep_dep_release_x86_64.xcconfig") dep_path = os.path.join(c.current_folder, "dep", "src", "include") assert f"SYSTEM_HEADER_SEARCH_PATHS_dep_dep[config=Release][arch=x86_64][sdk=*] = \"{dep_path}\"" in xcode def test_editable_includes_previously_defined(): # if someone already defined self.cpp.source.includedirs make sure we don't overwrite # their value c = TestClient() dep = textwrap.dedent(""" from conan import ConanFile from conan.tools.layout import basic_layout class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): self.cpp.source.includedirs = ["somefolder"] basic_layout(self, src_folder="src") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type", "arch") .with_requires("dep/0.1")}) c.run("editable add dep") c.run("install pkg -s arch=x86_64 -g CMakeDeps -g PkgConfigDeps -g XcodeDeps") data = c.load(f"pkg/dep-release-x86_64-data.cmake") assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/src/somefolder")' in data pc = c.load("pkg/dep.pc") assert "includedir=${prefix}/src/somefolder" in pc xcode = c.load("pkg/conan_dep_dep_release_x86_64.xcconfig") dep_path = os.path.join(c.current_folder, "dep", "src", "somefolder") assert f"SYSTEM_HEADER_SEARCH_PATHS_dep_dep[config=Release][arch=x86_64][sdk=*] = \"{dep_path}\"" in xcode ================================================ FILE: test/integration/toolchains/gnu/test_gnutoolchain.py ================================================ import os import platform import textwrap import re import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import save, load @pytest.mark.parametrize("os_", ["Macos", "Linux", "Windows"]) def test_extra_flags_via_conf(os_): os_sdk = "tools.apple:sdk_path=/my/sdk/path" if os_ == "Macos" else "" profile = textwrap.dedent(f""" [settings] os={os_} compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.build:cxxflags=["--flag1", "--flag2"] tools.build:cflags+=["--flag3", "--flag4"] tools.build:sharedlinkflags+=["--flag5"] tools.build:exelinkflags+=["--flag6"] tools.build:defines+=["DEF1", "DEF2"] {os_sdk} """) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type") \ .with_generator("GnuToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load( "conangnutoolchain{}".format('.bat' if os_ == "Windows" else '.sh')) if os_ == "Windows": assert 'set "CPPFLAGS=%CPPFLAGS% -DNDEBUG -DDEF1 -DDEF2"' in toolchain assert 'set "CXXFLAGS=%CXXFLAGS% -O3 --flag1 --flag2"' in toolchain assert 'set "CFLAGS=%CFLAGS% -O3 --flag3 --flag4"' in toolchain assert 'set "LDFLAGS=%LDFLAGS% --flag5 --flag6"' in toolchain assert f'set "PKG_CONFIG_PATH={client.current_folder};%PKG_CONFIG_PATH%"' in toolchain else: assert os_ in ("Linux", "Macos") assert 'export CPPFLAGS="${CPPFLAGS:-}${CPPFLAGS:+ }-DNDEBUG -DDEF1 -DDEF2"' in toolchain assert 'export CXXFLAGS="${CXXFLAGS:-}${CXXFLAGS:+ }-O3 --flag1 --flag2"' in toolchain assert 'export CFLAGS="${CFLAGS:-}${CFLAGS:+ }-O3 --flag3 --flag4"' in toolchain assert 'export LDFLAGS="${LDFLAGS:-}${LDFLAGS:+ }--flag5 --flag6"' in toolchain assert (f'export PKG_CONFIG_PATH="{client.current_folder}' f'${{PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}}"') in toolchain def test_extra_flags_order(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import GnuToolchain class Conan(ConanFile): name = "pkg" version = "0.1" settings = "os", "arch", "build_type" def generate(self): at = GnuToolchain(self) at.extra_cxxflags = ["extra_cxxflags"] at.extra_cflags = ["extra_cflags"] at.extra_ldflags = ["extra_ldflags"] at.extra_defines = ["extra_defines"] at.generate() """) profile = textwrap.dedent(""" include(default) [conf] tools.build:cxxflags+=['cxxflags'] tools.build:cflags+=['cflags'] tools.build:sharedlinkflags+=['sharedlinkflags'] tools.build:exelinkflags+=['exelinkflags'] tools.build:defines+=['defines'] """) client.save({"conanfile.py": conanfile, "profile": profile}) client.run('install . -pr=./profile') toolchain = client.load( "conangnutoolchain{}".format('.bat' if platform.system() == "Windows" else '.sh')) assert '-Dextra_defines -Ddefines' in toolchain assert 'extra_cxxflags cxxflags' in toolchain assert 'extra_cflags cflags' in toolchain assert 'extra_ldflags sharedlinkflags exelinkflags' in toolchain def test_gnutoolchain_rcflags(): """Test that tools.build:rcflags is applied to RCFLAGS in the generated script.""" profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 build_type=Release [conf] tools.build:rcflags=["--rcflag1", "--rcflag2"] """) client = TestClient() conanfile = (GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_generator("GnuToolchain")) client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load("conangnutoolchain.sh") assert "RCFLAGS" in toolchain assert "--rcflag1" in toolchain assert "--rcflag2" in toolchain def test_autotools_custom_environment(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import GnuToolchain class Conan(ConanFile): settings = "os" def generate(self): at = GnuToolchain(self) env = at.extra_env env.define("FOO", "BAR") at.generate() """) client.save({"conanfile.py": conanfile}) client.run("install . -s:b os=Linux -s:h os=Linux") content = load(os.path.join(client.current_folder, "conangnutoolchain.sh")) assert 'export FOO="BAR"' in content @pytest.mark.parametrize("os_", ["Linux", "Windows"]) def test_linker_scripts_via_conf(os_): profile = textwrap.dedent(""" [settings] os=%s compiler=gcc compiler.version=6 compiler.libcxx=libstdc++11 arch=armv8 build_type=Release [conf] tools.build:sharedlinkflags+=["--flag5"] tools.build:exelinkflags+=["--flag6"] tools.build:linker_scripts+=["/linker/scripts/flash.ld", "/linker/scripts/extra_data.ld"] """ % os_) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type") \ .with_generator("GnuToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") toolchain = client.load( "conangnutoolchain{}".format('.bat' if os_ == "Windows" else '.sh')) if os_ == "Windows": assert 'set "LDFLAGS=%LDFLAGS% --flag5 --flag6 -T\'/linker/scripts/flash.ld\' -T\'/linker/scripts/extra_data.ld\'"' in toolchain else: assert 'export LDFLAGS="${LDFLAGS:-}${LDFLAGS:+ }--flag5 --flag6 -T\'/linker/scripts/flash.ld\' -T\'/linker/scripts/extra_data.ld\'"' in toolchain def test_not_none_values(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import GnuToolchain class Foo(ConanFile): name = "foo" version = "1.0" def generate(self): tc = GnuToolchain(self) assert None not in tc.defines assert None not in tc.cxxflags assert None not in tc.cflags assert None not in tc.ldflags """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("install .") def test_set_prefix(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import GnuToolchain from conan.tools.layout import basic_layout class Foo(ConanFile): name = "foo" version = "1.0" def layout(self): basic_layout(self) def generate(self): at_toolchain = GnuToolchain(self, prefix="/somefolder") at_toolchain.generate() """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("install .") conanbuild = client.load( os.path.join(client.current_folder, "build", "conan", "conanbuild.conf")) assert "--prefix=/somefolder" in conanbuild assert conanbuild.count("--prefix") == 1 def test_unknown_compiler(): client = TestClient() save(client.paths.settings_path_user, "compiler:\n xlc:\n") client.save({"conanfile.py": GenConanfile().with_settings("compiler", "build_type") .with_generator("GnuToolchain") }) # this used to crash, because of build_type_flags in GnuToolchain returning empty string client.run("install . -s compiler=xlc") assert "conanfile.py: Generator 'GnuToolchain' calling 'generate()'" in client.out def test_toolchain_and_compilers_build_context(): """ Tests how GnuToolchain manages the build context profile if the build profile is specifying another compiler path (using conf) Issue related: https://github.com/conan-io/conan/issues/15878 """ host = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [conf] tools.build:compiler_executables={"c": "gcc", "cpp": "g++", "rc": "windres"} """) build = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=libc++ compiler.cppstd=11 [conf] tools.build:compiler_executables={"c": "clang", "cpp": "clang++"} """) tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class toolRecipe(ConanFile): name = "tool" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "GnuToolchain" def build(self): toolchain = os.path.join(self.generators_folder, "conangnutoolchain.sh") content = load(self, toolchain) assert 'export CC="clang"' in content assert 'export CXX="clang++"' in content """) consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class consumerRecipe(ConanFile): name = "consumer" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "GnuToolchain" tool_requires = "tool/1.0" def build(self): toolchain = os.path.join(self.generators_folder, "conangnutoolchain.sh") content = load(self, toolchain) assert 'export CC="gcc"' in content assert 'export CXX="g++"' in content assert 'export RC="windres"' in content # Issue: https://github.com/conan-io/conan/issues/15486 assert 'export CC_FOR_BUILD="clang"' in content assert 'export CXX_FOR_BUILD="clang++"' in content """) client = TestClient() client.save({ "host": host, "build": build, "tool/conanfile.py": tool, "consumer/conanfile.py": consumer }) client.run("export tool") client.run("create consumer -pr:h host -pr:b build --build=missing") def test_autotools_crossbuild_ux(): client = TestClient() profile_build = textwrap.dedent(""" [settings] os = Macos os.version=10.11 arch = armv7 compiler = apple-clang compiler.version = 12.0 compiler.libcxx = libc++ """) profile_host = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=15 os=Macos [conf] tools.apple:sdk_path=/my/sdk/path """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import GnuToolchain from conan.tools.build import cross_building class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): tc = GnuToolchain(self) if cross_building(self): host_arch = tc.triplets_info["host"]["machine"] build_arch = tc.triplets_info["build"]["machine"] if host_arch and build_arch: tc.configure_args["--host"] = f"{host_arch}-my-triplet-host" tc.configure_args["--build"] = f"{build_arch}-my-triplet-build" tc.generate() """) client.save({"conanfile.py": conanfile, "profile_build": profile_build, "profile_host": profile_host}) client.run("install . --profile:build=profile_build --profile:host=profile_host") conanbuild = client.load("conanbuild.conf") host_flags = re.findall(r"--host=[\w-]*\b", conanbuild) build_flags = re.findall(r"--build=[\w-]*\b", conanbuild) assert len(host_flags) == 1 assert len(build_flags) == 1 assert host_flags[0] == '--host=x86_64-my-triplet-host' assert build_flags[0] == '--build=arm-my-triplet-build' def test_msvc_profile_defaults(): """ Tests how GnuToolchain manages MSVC profile and its default env variables. """ profile = textwrap.dedent("""\ [settings] os=Windows arch=x86_64 compiler=msvc compiler.version=191 compiler.runtime=dynamic build_type=Release [conf] tools.build:compiler_executables={"c": "clang", "cpp": "clang++"} # Fake installation path tools.microsoft.msbuild:installation_path={{os.getcwd()}} """) # Consumer with default values consumer = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.files import load class consumerRecipe(ConanFile): name = "consumer" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "GnuToolchain" def build(self): toolchain = os.path.join(self.generators_folder, "conangnutoolchain.bat") content = load(self, toolchain) # Default values and conf ones assert r'set "CC=clang"' in content # conf value has precedence assert r'set "CXX=clang++"' in content # conf value has precedence assert 'set "NM=dumpbin -symbols"' in content assert 'set "OBJDUMP=:"' in content assert 'set "RANLIB=:"' in content assert 'set "STRIP=:"' in content """) client = TestClient() client.save({ "profile": profile, "consumer/conanfile.py": consumer }) client.run("create consumer -pr:a profile --build=missing") # Consumer changing default values consumer = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.files import load from conan.tools.gnu import GnuToolchain class consumerRecipe(ConanFile): name = "consumer" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" def generate(self): tc = GnuToolchain(self) # Prepending compiler wrappers tc.extra_env.prepend("CC", "compile") tc.extra_env.prepend("CXX", "compile") tc.extra_env.define("OBJDUMP", "other-value") tc.extra_env.unset("RANLIB") tc.generate() def build(self): toolchain = os.path.join(self.generators_folder, "conangnutoolchain.bat") content = load(self, toolchain) # Default values assert r'set "CC=compile clang"' in content assert r'set "CXX=compile clang++"' in content assert 'set "NM=dumpbin -symbols"' in content assert 'set "OBJDUMP=other-value"' in content # redefined assert 'set "RANLIB=:"' not in content # removed assert 'set "STRIP=:"' in content """) client.save({ "consumer/conanfile.py": consumer }) client.run("create consumer -pr:a profile --build=missing") def test_conf_build_does_not_exist(): host = textwrap.dedent(""" [settings] arch=x86_64 os=Linux [conf] tools.build:compiler_executables={'c': '/usr/bin/gcc', 'cpp': '/usr/bin/g++'} """) build = textwrap.dedent(""" [settings] arch=armv8 os=Linux [conf] tools.build:compiler_executables={'c': 'x86_64-linux-gnu-gcc', 'cpp': 'x86_64-linux-gnu-g++'} """) c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1"), "host": host, "build": build}) c.run("export .") c.run("install --requires=pkg/0.1 --build=pkg/0.1 -g GnuToolchain -pr:h host -pr:b build") tc = c.load("conangnutoolchain.sh") assert 'export CC_FOR_BUILD="x86_64-linux-gnu-gcc"' in tc assert 'export CXX_FOR_BUILD="x86_64-linux-gnu-g++"' in tc @pytest.mark.parametrize("toolchain", ["GnuToolchain", "AutotoolsToolchain"]) def test_conf_extra_apple_flags(toolchain): host = textwrap.dedent(""" [settings] arch=x86_64 os=Macos [conf] tools.apple:enable_bitcode = True tools.apple:enable_arc = True tools.apple:enable_visibility = True """) c = TestClient() c.save({"conanfile.txt": f"[generators]\n{toolchain}", "host": host}) c.run("install . -pr:a host") f = "conanautotoolstoolchain.sh" if toolchain == "AutotoolsToolchain" else "conangnutoolchain.sh" tc = c.load(f) assert 'export CXXFLAGS="${CXXFLAGS:-}${CXXFLAGS:+ }-fembed-bitcode -fvisibility=default"' in tc assert 'export CFLAGS="${CFLAGS:-}${CFLAGS:+ }-fembed-bitcode -fvisibility=default"' in tc assert 'export LDFLAGS="${LDFLAGS:-}${LDFLAGS:+ }-fembed-bitcode -fvisibility=default"' in tc assert 'export OBJCFLAGS="${OBJCFLAGS:-}${OBJCFLAGS:+ }-fobjc-arc"' in tc assert 'export OBJCXXFLAGS="${OBJCXXFLAGS:-}${OBJCXXFLAGS:+ }-fobjc-arc"' in tc c.run("install . -pr:a host -s build_type=Debug") tc = c.load(f) assert 'export CXXFLAGS="${CXXFLAGS:-}${CXXFLAGS:+ }-fembed-bitcode-marker -fvisibility=default"' in tc assert 'export CFLAGS="${CFLAGS:-}${CFLAGS:+ }-fembed-bitcode-marker -fvisibility=default"' in tc assert 'export LDFLAGS="${LDFLAGS:-}${LDFLAGS:+ }-fembed-bitcode-marker -fvisibility=default"' in tc assert 'export OBJCFLAGS="${OBJCFLAGS:-}${OBJCFLAGS:+ }-fobjc-arc"' in tc assert 'export OBJCXXFLAGS="${OBJCXXFLAGS:-}${OBJCXXFLAGS:+ }-fobjc-arc"' in tc host = textwrap.dedent(""" [settings] arch=x86_64 os=Macos [conf] tools.apple:enable_bitcode = False tools.apple:enable_arc = False tools.apple:enable_visibility = False """) c.save({"host": host}) c.run("install . -pr:a host") tc = c.load(f) assert 'CXXFLAGS="${CXXFLAGS:-}${CXXFLAGS:+ }-fvisibility=hidden -fvisibility-inlines-hidden"' in tc assert 'CFLAGS="${CFLAGS:-}${CFLAGS:+ }-fvisibility=hidden -fvisibility-inlines-hidden"' in tc assert 'LDFLAGS="${LDFLAGS:-}${LDFLAGS:+ }-fvisibility=hidden -fvisibility-inlines-hidden"' in tc assert 'export OBJCFLAGS="${OBJCFLAGS:-}${OBJCFLAGS:+ }-fno-objc-arc"' in tc assert 'export OBJCXXFLAGS="${OBJCXXFLAGS:-}${OBJCXXFLAGS:+ }-fno-objc-arc"' in tc def test_toolchain_crossbuild_to_android(): """ Issue related: https://github.com/conan-io/conan/issues/17441 """ build = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux """) host = textwrap.dedent(""" [settings] os = Android os.api_level = 21 arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=libc++ compiler.cppstd=11 [buildenv] CC=clang CXX=clang++ [conf] tools.android:ndk_path=/path/to/ndk """) consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class consumerRecipe(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" generators = "GnuToolchain" def build(self): toolchain = os.path.join(self.generators_folder, "conangnutoolchain.sh") content = load(self, toolchain) assert 'export CC="clang"' not in content assert 'export CXX="clang++"' not in content assert 'export LD="/path/to/ndk' in content assert 'export STRIP="/path/to/ndk' in content assert 'export RANLIB="/path/to/ndk' in content assert 'export AS="/path/to/ndk' in content assert 'export AR="/path/to/ndk' in content assert 'export ADDR2LINE="/path/to/ndk' in content assert 'export NM="/path/to/ndk' in content assert 'export OBJCOPY="/path/to/ndk' in content assert 'export OBJDUMP="/path/to/ndk' in content assert 'export READELF="/path/to/ndk' in content assert 'export ELFEDIT="/path/to/ndk' in content build_env = os.path.join(self.generators_folder, "conanbuildenv-x86_64.sh") content = load(self, build_env) assert 'export CC="clang"' in content assert 'export CXX="clang++"' in content assert 'export LD=' not in content assert 'export STRIP="/path/to/ndk' not in content assert 'export RANLIB="/path/to/ndk' not in content assert 'export AS="/path/to/ndk' not in content assert 'export AR="/path/to/ndk' not in content assert 'export ADDR2LINE="/path/to/ndk' not in content assert 'export NM="/path/to/ndk' not in content assert 'export OBJCOPY="/path/to/ndk' not in content assert 'export OBJDUMP="/path/to/ndk' not in content assert 'export READELF="/path/to/ndk' not in content assert 'export ELFEDIT="/path/to/ndk' not in content """) client = TestClient() client.save({ "host": host, "build": build, "conanfile.py": consumer }) client.run("create . -pr:h host -pr:b build") @pytest.mark.parametrize( "threads, flags", [("posix", "-pthread"), ("wasm_workers", "-sWASM_WORKERS=1")], ) def test_thread_flags(threads, flags): client = TestClient() profile = textwrap.dedent(f""" [settings] arch=wasm build_type=Release compiler=emcc compiler.cppstd=17 compiler.threads={threads} compiler.libcxx=libc++ compiler.version=4.0.10 os=Emscripten """) client.save( { "conanfile.py": GenConanfile("pkg", "1.0") .with_settings("os", "arch", "compiler", "build_type") .with_generator("GnuToolchain"), "profile": profile, } ) client.run("install . -pr=./profile") is_win = platform.system() == "Windows" toolchain = client.load( "conangnutoolchain{}".format('.bat' if is_win else '.sh')) if is_win: assert f'set "CXXFLAGS=%CXXFLAGS% -stdlib=libc++ {flags}"' in toolchain assert f'set "CFLAGS=%CFLAGS% {flags}"' in toolchain assert f'set "LDFLAGS=%LDFLAGS% {flags}' in toolchain else: assert f'export CXXFLAGS="${{CXXFLAGS:-}}${{CXXFLAGS:+ }}-stdlib=libc++ {flags}"' in toolchain assert f'export CFLAGS="${{CFLAGS:-}}${{CFLAGS:+ }}{flags}"' in toolchain assert f'export LDFLAGS="${{LDFLAGS:-}}${{LDFLAGS:+ }}{flags}"' in toolchain ================================================ FILE: test/integration/toolchains/gnu/test_makedeps.py ================================================ import platform import pytest import os import textwrap import pathlib from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.tools.gnu.makedeps import CONAN_MAKEFILE_FILENAME def test_make_dirs_with_abs_path(): """ MakeDeps should support absolute paths when cppinfp """ conanfile = textwrap.dedent(""" import os from conan import ConanFile class TestMakeDirsConan(ConanFile): name = "mylib" version = "0.1" def package_info(self): self.cpp_info.frameworkdirs = [] libname = "mylib" fake_dir = os.path.join("/", "my_absoulte_path", "fake") include_dir = os.path.join(fake_dir, libname, "include") lib_dir = os.path.join(fake_dir, libname, "lib") lib_dir2 = os.path.join(self.package_folder, "lib2") self.cpp_info.includedirs = [include_dir] self.cpp_info.libdirs = [lib_dir, lib_dir2] self.cpp_info.set_property("my_prop", "my prop value") self.cpp_info.set_property("my_prop_with_newline", "my\\nprop") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=mylib/0.1@ -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) prefix = pathlib.Path(client.current_folder).drive if platform.system() == "Windows" else "" assert 'CONAN_NAME_MYLIB = mylib' in makefile_content assert 'CONAN_VERSION_MYLIB = 0.1' in makefile_content assert f'CONAN_LIB_DIRS_MYLIB = \\\n\t$(CONAN_LIB_DIR_FLAG){prefix}/my_absoulte_path/fake/mylib/lib \\\n\t$(CONAN_LIB_DIR_FLAG)$(CONAN_ROOT_MYLIB)/lib2' in makefile_content assert f'CONAN_INCLUDE_DIRS_MYLIB = $(CONAN_INCLUDE_DIR_FLAG){prefix}/my_absoulte_path/fake/mylib/include' in makefile_content assert 'CONAN_BIN_DIRS_MYLIB = $(CONAN_BIN_DIR_FLAG)$(CONAN_ROOT_MYLIB)/bin' in makefile_content assert 'CONAN_PROPERTY_MYLIB_MY_PROP = my prop value' in makefile_content assert 'CONAN_PROPERTY_MYLIB_MY_PROP_WITH_NEWLINE' not in makefile_content assert "WARN: Skipping propery 'my_prop_with_newline' because it contains newline" in client.stderr lines = makefile_content.splitlines() for line_no, line in enumerate(lines): includedir_pattern = "CONAN_INCLUDE_DIRS_MYLIB = $(CONAN_INCLUDE_DIR_FLAG)" if line.startswith(includedir_pattern): assert os.path.isabs(line[len(includedir_pattern):]) assert line.endswith("include") elif line.startswith("\t$(CONAN_LIB_DIR_FLAG)") and 'my_absoulte_path' in line: assert os.path.isabs(line[len("\t$(CONAN_LIB_DIR_FLAG)"):-2]) assert line.endswith("lib \\") elif line.startswith("\t$(CONAN_LIB_DIR_FLAG)") and line.endswith('lib2'): assert "\t$(CONAN_LIB_DIR_FLAG)$(CONAN_ROOT_MYLIB)/lib2" in line def test_make_empty_dirs(): """ MakeDeps should support cppinfo empty dirs """ conanfile = textwrap.dedent(""" import os from conan import ConanFile class TestMakeDepsConan(ConanFile): name = "mylib" version = "0.1" def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.libdirs = [] self.cpp_info.bindirs = [] self.cpp_info.libs = [] self.cpp_info.frameworkdirs = [] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=mylib/0.1@ -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) assert 'CONAN_ROOT_MYLIB' in makefile_content assert 'SYSROOT' not in makefile_content assert 'CONAN_INCLUDE_DIRS' not in makefile_content assert 'CONAN_LIB_DIRS' not in makefile_content assert 'CONAN_BIN_DIRS' not in makefile_content assert 'CONAN_LIBS' not in makefile_content assert 'CONAN_FRAMEWORK_DIRS' not in makefile_content assert 'CONAN_PROPERTY' not in makefile_content def test_libs_and_system_libs(): """ MakeDeps should support cppinfo system_libs with regular libs """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class TestMakeDepsConan(ConanFile): name = "mylib" version = "0.1" def package(self): save(self, os.path.join(self.package_folder, "lib", "file"), "") def package_info(self): self.cpp_info.libs = ["mylib1", "mylib2"] self.cpp_info.system_libs = ["system_lib1", "system_lib2"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=mylib/0.1@ -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) assert "CONAN_LIBS_MYLIB = \\\n\t$(CONAN_LIB_FLAG)mylib1 \\\n\t$(CONAN_LIB_FLAG)mylib2" in makefile_content assert "CONAN_SYSTEM_LIBS_MYLIB = \\\n\t$(CONAN_SYSTEM_LIB_FLAG)system_lib1 \\\n\t$(CONAN_SYSTEM_LIB_FLAG)system_lib2" in makefile_content assert "CONAN_LIBS = $(CONAN_LIBS_MYLIB)" in makefile_content assert "CONAN_SYSTEM_LIBS = $(CONAN_SYSTEM_LIBS_MYLIB)" in makefile_content def test_multiple_include_and_lib_dirs(): """ MakeDeps should support cppinfo multiple include directories """ conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class TestMakeDepsConan(ConanFile): def package(self): for p in ["inc1", "inc2", "inc3/foo", "lib1", "lib2"]: save(self, os.path.join(self.package_folder, p, "file"), "") def package_info(self): self.cpp_info.includedirs = ["inc1", "inc2", "inc3/foo"] self.cpp_info.libdirs = ["lib1", "lib2"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client.run("install --requires=pkg/0.1@ -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) assert "CONAN_INCLUDE_DIRS_PKG = \\\n" \ "\t$(CONAN_INCLUDE_DIR_FLAG)$(CONAN_ROOT_PKG)/inc1 \\\n" \ "\t$(CONAN_INCLUDE_DIR_FLAG)$(CONAN_ROOT_PKG)/inc2 \\\n" \ "\t$(CONAN_INCLUDE_DIR_FLAG)$(CONAN_ROOT_PKG)/inc3/foo\n" in makefile_content assert "CONAN_LIB_DIRS_PKG = \\\n" \ "\t$(CONAN_LIB_DIR_FLAG)$(CONAN_ROOT_PKG)/lib1 \\\n" \ "\t$(CONAN_LIB_DIR_FLAG)$(CONAN_ROOT_PKG)/lib2\n" in makefile_content assert "CONAN_INCLUDE_DIRS = $(CONAN_INCLUDE_DIRS_PKG)\n" in makefile_content assert "CONAN_LIB_DIRS = $(CONAN_LIB_DIRS_PKG)\n" in makefile_content def test_make_with_public_deps_and_component_requires(): """ Testing a complex structure like: * lib/0.1 - Components: "cmp1" * other/0.1 * second/0.1 - Requires: "lib/0.1" - Components: "mycomponent", "myfirstcomp" + "mycomponent" requires "lib::cmp1" + "myfirstcomp" requires "mycomponent" * third/0.1 - Requires: "second/0.1", "other/0.1" """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.cpp_info.components["cmp1"].libs = ["libcmp1"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=lib --version=0.1") client.save({"conanfile.py": GenConanfile("other", "0.1").with_package_file("file.h", "0.1")}) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class TestMakeDepsConan(ConanFile): requires = "lib/0.1" def package_info(self): self.cpp_info.components["mycomponent"].requires.append("lib::cmp1") self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") self.cpp_info.components["myfirstcomp"].set_property("my_prop", "my prop value") self.cpp_info.components["myfirstcomp"].set_property("my_prop_with_newline", "my\\nprop") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=second --version=0.1") client.save({"conanfile.py": GenConanfile("third", "0.1").with_package_file("file.h", "0.1") .with_require("second/0.1") .with_require("other/0.1")}, clean_first=True) client.run("create .") client2 = TestClient(cache_folder=client.cache_folder) conanfile = textwrap.dedent(""" [requires] third/0.1 [generators] MakeDeps """) client2.save({"conanfile.txt": conanfile}) client2.run("install .") makefile_content = client2.load(CONAN_MAKEFILE_FILENAME) assert "CONAN_DEPS = \\\n" \ "\tthird \\\n" \ "\tsecond \\\n" \ "\tlib \\\n" \ "\tother\n" in makefile_content assert 'CONAN_REQUIRES_SECOND = \\\n' \ '\t$(CONAN_REQUIRES_SECOND_MYCOMPONENT) \\\n' \ '\t$(CONAN_REQUIRES_SECOND_MYFIRSTCOMP)\n' in makefile_content assert 'SYSROOT' not in makefile_content assert 'CONAN_REQUIRES_SECOND_MYFIRSTCOMP = mycomponent\n' in makefile_content assert 'CONAN_LIBS_LIB = $(CONAN_LIBS_LIB_CMP1)\n' in makefile_content assert 'CONAN_COMPONENTS_LIB = cmp1\n' in makefile_content assert 'CONAN_LIBS_LIB_CMP1 = $(CONAN_LIB_FLAG)libcmp1\n' in makefile_content assert 'CONAN_REQUIRES = $(CONAN_REQUIRES_SECOND)\n' in makefile_content assert 'CONAN_LIBS = $(CONAN_LIBS_LIB)\n' in makefile_content assert 'CONAN_PROPERTY_SECOND_MYFIRSTCOMP_MY_PROP = my prop value\n' in makefile_content assert 'CONAN_PROPERTY_SECOND_MYFIRSTCOMP_MY_PROP_WITH_NEWLINE' not in makefile_content assert "WARN: Skipping propery 'my_prop_with_newline' because it contains newline" in client2.stderr def test_make_with_public_deps_and_component_requires_second(): """ Testing another complex structure like: * other/0.1 - Components: "cmp1", "cmp2", "cmp3" + "cmp1" (it shouldn't be affected by "other") + "cmp3" (it shouldn't be affected by "other") + "cmp3" requires "cmp1" * pkg/0.1 - Requires: "other/0.1" -> "other::cmp1" """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.cpp_info.components["cmp1"].libs = ["other_cmp1"] self.cpp_info.components["cmp2"].libs = ["other_cmp2"] self.cpp_info.components["cmp3"].requires.append("cmp1") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=other --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class TestMakeDepsConan(ConanFile): requires = "other/1.0" def package_info(self): self.cpp_info.requires = ["other::cmp1"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client2 = TestClient(cache_folder=client.cache_folder) conanfile = textwrap.dedent(""" [requires] pkg/0.1 [generators] MakeDeps """) client2.save({"conanfile.txt": conanfile}) client2.run("install .") make_content = client2.load(CONAN_MAKEFILE_FILENAME) assert 'CONAN_REQUIRES_PKG = other::cmp1\n' in make_content assert 'CONAN_REQUIRES_OTHER = $(CONAN_REQUIRES_OTHER_CMP3)\n' in make_content assert 'CONAN_REQUIRES_OTHER_CMP3 = cmp1\n' in make_content assert 'CONAN_COMPONENTS_OTHER = \\\n\tcmp1 \\\n\tcmp2 \\\n\tcmp3\n' in make_content assert 'CONAN_LIBS_OTHER = \\\n\t$(CONAN_LIBS_OTHER_CMP1) \\\n\t$(CONAN_LIBS_OTHER_CMP2)\n' in make_content assert 'CONAN_LIBS_OTHER_CMP1 = $(CONAN_LIB_FLAG)other_cmp1\n' in make_content assert 'CONAN_LIBS_OTHER_CMP2 = $(CONAN_LIB_FLAG)other_cmp2\n' in make_content assert 'CONAN_LIBS = $(CONAN_LIBS_OTHER)\n' in make_content assert 'CONAN_REQUIRES = \\\n\t$(CONAN_REQUIRES_PKG) \\\n\t$(CONAN_REQUIRES_OTHER)\n' in make_content def test_makedeps_with_test_requires(): """ MakeDeps has to create any test requires to be declared on the recipe. """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestMakeDeps(ConanFile): def package_info(self): self.cpp_info.libs = [f"lib{self.name}"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=app --version=1.0") client.run("create . --name=test --version=1.0") # Create library having build and test requires conanfile = textwrap.dedent(""" from conan import ConanFile class HelloLib(ConanFile): def build_requirements(self): self.tool_requires('app/1.0') self.test_requires('test/1.0') """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -g MakeDeps") assert 'CONAN_DEPS = test\n' in client.load(CONAN_MAKEFILE_FILENAME) assert 'app' not in client.load(CONAN_MAKEFILE_FILENAME) def test_makedeps_with_editable_layout(): """ The MakeDeps should be supported with editable layour mode """ client = TestClient() dep = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): self.cpp.source.includedirs = ["include"] def package_info(self): self.cpp_info.libs = ["mylib"] """) client.save({"dep/conanfile.py": dep, "dep/include/header.h": "", "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1")}) client.run("create dep") client.run("editable add dep") with client.chdir("pkg"): client.run("install . -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) assert 'CONAN_LIBS_DEP = $(CONAN_LIB_FLAG)mylib\n' in makefile_content assert 'CONAN_INCLUDE_DIRS_DEP = $(CONAN_INCLUDE_DIR_FLAG)$(CONAN_ROOT_DEP)/include\n' in makefile_content def test_makedeps_tool_requires(): """ Testing if MakeDeps are created for tool requires """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestMakeDepsConan(ConanFile): def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name tool --version 1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class TestMakeDepsConan(ConanFile): def package_info(self): self.cpp_info.components["cmp1"].libs = ["other_cmp1"] self.cpp_info.components["cmp2"].libs = ["other_cmp2"] self.cpp_info.components["cmp3"].requires.append("cmp1") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name other --version 1.0") conanfile = textwrap.dedent(""" [tool_requires] tool/1.0 other/1.0 [generators] MakeDeps """) client.save({"conanfile.txt": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default") make_content = client.load(CONAN_MAKEFILE_FILENAME) # TODO: MakeDeps should support tool_requires in the future assert "CONAN_LIBS_TOOL" not in make_content assert "CONAN_NAME_OTHER" not in make_content @pytest.mark.parametrize("pattern, result, expected", [("libs = []", False, 'SYSROOT'), ("sysroot = ['/foo/bar/sysroot']", True, 'CONAN_SYSROOT_PACKAGE = /foo/bar/sysroot') if platform.system() != "Windows" else ("sysroot = ['C:/my_sysroot']", True, 'CONAN_SYSROOT_PACKAGE = C:/my_sysroot')]) def test_makefile_sysroot(pattern, result, expected): """ The MakeDeps should not enforce sysroot in case not defined """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class TestMakeDepsConan(ConanFile): def package_info(self): self.cpp_info.{{ pattern }} """) client.save({"conanfile.py": conanfile.replace("{{ pattern }}", pattern)}) client.run("create . --name package --version 0.1.0") client.run("install --requires=package/0.1.0 -pr:h default -pr:b default -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) assert (expected in makefile_content) == result def test_makefile_reference(): """ The MakeDeps should generate the correct package reference as variable """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Testing(ConanFile): pass """) client.save({"conanfile.py": conanfile}) # official packages / client.run("create . --name package --version 0.1.0") client.run("install --requires=package/0.1.0 -pr:h default -pr:b default -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) assert 'CONAN_REFERENCE_PACKAGE = package/0.1.0\n' in makefile_content # custom packages /@/ client.run("create . --name package --version 0.1.0 --user user --channel channel") client.run("install --requires=package/0.1.0@user/channel -pr:h default -pr:b default -g MakeDeps") makefile_content = client.load(CONAN_MAKEFILE_FILENAME) assert 'CONAN_REFERENCE_PACKAGE = package/0.1.0@user/channel\n' in makefile_content ================================================ FILE: test/integration/toolchains/gnu/test_pkgconfigdeps.py ================================================ import glob import os import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import load def get_requires_from_content(content): for line in content.splitlines(): if "Requires:" in line: return line return "" def test_pkg_config_dirs(): # https://github.com/conan-io/conan/issues/2756 conanfile = textwrap.dedent(""" import os from conan import ConanFile class PkgConfigConan(ConanFile): name = "mylib" version = "0.1" def package_info(self): self.cpp_info.frameworkdirs = [] self.cpp_info.filter_empty = False libname = "mylib" fake_dir = os.path.join("/", "my_absoulte_path", "fake") include_dir = os.path.join(fake_dir, libname, "include") lib_dir = os.path.join(fake_dir, libname, "lib") lib_dir1 = os.path.join(self.package_folder, "lib2") self.cpp_info.includedirs = [include_dir] self.cpp_info.libdirs = [lib_dir, lib_dir1] self.cpp_info.bindirs = ["mybin"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=mylib/0.1@ -g PkgConfigDeps") pc_path = os.path.join(client.current_folder, "mylib.pc") assert os.path.exists(pc_path) is True pc_content = load(pc_path) assert 'Name: mylib' in pc_content assert 'Description: Conan package: mylib' in pc_content assert 'Version: 0.1' in pc_content assert 'Libs: -L"${libdir}" -L"${libdir1}"' in pc_content assert 'Cflags: -I"${includedir}"' in pc_content # https://github.com/conan-io/conan/pull/13623 assert 'bindir=${prefix}/mybin' in pc_content def assert_is_abs(path): assert os.path.isabs(path) is True for line in pc_content.splitlines(): if line.startswith("includedir="): assert_is_abs(line[len("includedir="):]) assert line.endswith("include") elif line.startswith("libdir="): assert_is_abs(line[len("libdir="):]) assert line.endswith("lib") elif line.startswith("libdir1="): assert "${prefix}/lib2" in line def test_empty_dirs(): # Adding in package_info all the empty directories conanfile = textwrap.dedent(""" import os from conan import ConanFile class PkgConfigConan(ConanFile): name = "mylib" version = "0.1" def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.libdirs = [] self.cpp_info.bindirs = [] self.cpp_info.libs = [] self.cpp_info.frameworkdirs = [] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=mylib/0.1@ -g PkgConfigDeps") pc_path = os.path.join(client.current_folder, "mylib.pc") assert os.path.exists(pc_path) is True pc_content = load(pc_path) expected = textwrap.dedent(""" Name: mylib Description: Conan package: mylib Version: 0.1""") assert "\n".join(pc_content.splitlines()[1:]) == expected def test_system_libs(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class PkgConfigConan(ConanFile): name = "mylib" version = "0.1" def package(self): save(self, os.path.join(self.package_folder, "lib", "file"), "") def package_info(self): self.cpp_info.libs = ["mylib1", "mylib2"] self.cpp_info.system_libs = ["system_lib1", "system_lib2"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") client.run("install --requires=mylib/0.1@ -g PkgConfigDeps") pc_content = client.load("mylib.pc") assert 'Libs: -L"${libdir}" -lmylib1 -lmylib2 -lsystem_lib1 -lsystem_lib2' in pc_content def test_multiple_include(): # https://github.com/conan-io/conan/issues/7056 conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os class PkgConfigConan(ConanFile): def package(self): for p in ["inc1", "inc2", "inc3/foo", "lib1", "lib2"]: save(self, os.path.join(self.package_folder, p, "file"), "") def package_info(self): self.cpp_info.includedirs = ["inc1", "inc2", "inc3/foo"] self.cpp_info.libdirs = ["lib1", "lib2"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client.run("install --requires=pkg/0.1@ -g PkgConfigDeps") pc_content = client.load("pkg.pc") assert "includedir=${prefix}/inc1" in pc_content assert "includedir1=${prefix}/inc2" in pc_content assert "includedir2=${prefix}/inc3/foo" in pc_content assert "libdir=${prefix}/lib1" in pc_content assert "libdir1=${prefix}/lib2" in pc_content assert 'Libs: -L"${libdir}" -L"${libdir1}"' in pc_content assert 'Cflags: -I"${includedir}" -I"${includedir1}" -I"${includedir2}"' in pc_content def test_custom_content(): # https://github.com/conan-io/conan/issues/7661 conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save import os import textwrap class PkgConfigConan(ConanFile): def package(self): save(self, os.path.join(self.package_folder, "include" ,"file"), "") save(self, os.path.join(self.package_folder, "lib" ,"file"), "") def package_info(self): custom_content = textwrap.dedent(\""" bindir=${prefix}/my/bin/folder fakelibdir=${prefix}/my/lib/folder datadir=${prefix}/share schemasdir=${datadir}/mylib/schemas \""") self.cpp_info.set_property("pkg_config_custom_content", custom_content) self.cpp_info.includedirs = ["include"] self.cpp_info.libdirs = ["lib"] """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client.run("install --requires=pkg/0.1@ -g PkgConfigDeps") pc_content = client.load("pkg.pc") prefix = pc_content.splitlines()[0] expected = textwrap.dedent(f"""\ {prefix} libdir=${{prefix}}/lib includedir=${{prefix}}/include bindir=${{prefix}}/my/bin/folder fakelibdir=${{prefix}}/my/lib/folder datadir=${{prefix}}/share schemasdir=${{datadir}}/mylib/schemas Name: pkg Description: Conan package: pkg Version: 0.1 Libs: -L"${{libdir}}" Cflags: -I"${{includedir}}" """) assert expected == pc_content def test_custom_content_and_version_components(): conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.components["mycomponent"].set_property("pkg_config_custom_content", "componentdir=${prefix}/mydir") self.cpp_info.components["mycomponent"].set_property("component_version", "19.8.199") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client.run("install --requires=pkg/0.1@ -g PkgConfigDeps") pc_content = client.load("pkg-mycomponent.pc") assert "componentdir=${prefix}/mydir" in pc_content assert "Version: 19.8.199" in pc_content # Now with lockfile # https://github.com/conan-io/conan/issues/16197 lockfile = textwrap.dedent(""" { "version": "0.5", "requires": [ "pkg/0.1#9a5fed2bf506fd28817ddfbc92b07fc1" ] } """) client.save({"conan.lock": lockfile}) client.run("install --requires=pkg/0.1 -g PkgConfigDeps --lockfile=conan.lock") pc_content = client.load("pkg-mycomponent.pc") assert "componentdir=${prefix}/mydir" in pc_content assert "Version: 19.8.199" in pc_content def test_custom_version(): # https://github.com/conan-io/conan/issues/16197 conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.set_property("system_package_version", "19.8.199") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client.run("install --requires=pkg/0.1 -g PkgConfigDeps") pc_content = client.load("pkg.pc") assert "Version: 19.8.199" in pc_content # Now with lockfile # https://github.com/conan-io/conan/issues/16197 lockfile = textwrap.dedent(""" { "version": "0.5", "requires": [ "pkg/0.1#0fe93a852dd6a177bca87cb2d4491a18" ] } """) client.save({"conan.lock": lockfile}) client.run("install --requires=pkg/0.1 -g PkgConfigDeps --lockfile=conan.lock") pc_content = client.load("pkg.pc") assert "Version: 19.8.199" in pc_content def test_pkg_with_public_deps_and_component_requires(): """ Testing a complex structure like: * first/0.1 - Global pkg_config_name == "myfirstlib" - Components: "cmp1" * other/0.1 * second/0.1 - Requires: "first/0.1" - Components: "mycomponent", "myfirstcomp" + "mycomponent" requires "first::cmp1" + "myfirstcomp" requires "mycomponent" * third/0.1 - Requires: "second/0.1", "other/0.1" Expected file structure after running PkgConfigDeps as generator: - other.pc - myfirstlib-cmp1.pc - myfirstlib.pc - second-mycomponent.pc - second-myfirstcomp.pc - second.pc - third.pc """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.cpp_info.set_property("pkg_config_name", "myfirstlib") self.cpp_info.components["cmp1"].libs = ["libcmp1"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=first --version=0.1") client.save({"conanfile.py": GenConanfile("other", "0.1").with_package_file("file.h", "0.1")}) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): requires = "first/0.1" def package_info(self): self.cpp_info.components["mycomponent"].requires.append("first::cmp1") self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=second --version=0.1") client.save({"conanfile.py": GenConanfile("third", "0.1").with_package_file("file.h", "0.1") .with_require("second/0.1") .with_require("other/0.1")}, clean_first=True) client.run("create .") client2 = TestClient(cache_folder=client.cache_folder) conanfile = textwrap.dedent(""" [requires] third/0.1 [generators] PkgConfigDeps """) client2.save({"conanfile.txt": conanfile}) client2.run("install .") pc_content = client2.load("third.pc") # Originally posted: https://github.com/conan-io/conan/issues/9939 assert "Requires: second other" == get_requires_from_content(pc_content) pc_content = client2.load("second.pc") assert "Requires: second-mycomponent second-myfirstcomp" == get_requires_from_content(pc_content) pc_content = client2.load("second-mycomponent.pc") assert "Requires: myfirstlib-cmp1" == get_requires_from_content(pc_content) pc_content = client2.load("second-myfirstcomp.pc") assert "Requires: second-mycomponent" == get_requires_from_content(pc_content) pc_content = client2.load("myfirstlib.pc") assert "Requires: myfirstlib-cmp1" == get_requires_from_content(pc_content) pc_content = client2.load("other.pc") assert "" == get_requires_from_content(pc_content) def test_pkg_with_public_deps_and_component_requires_2(): """ Testing another complex structure like: * other/0.1 - Global pkg_config_name == "fancy_name" - Components: "cmp1", "cmp2", "cmp3" + "cmp1" pkg_config_name == "component1" (it shouldn't be affected by "fancy_name") + "cmp3" pkg_config_name == "component3" (it shouldn't be affected by "fancy_name") + "cmp3" requires "cmp1" * pkg/0.1 - Requires: "other/0.1" -> "other::cmp1" Expected file structure after running PkgConfigDeps as generator: - component1.pc - component3.pc - other-cmp2.pc - other.pc - pkg.pc """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Recipe(ConanFile): def package_info(self): self.cpp_info.set_property("pkg_config_name", "fancy_name") self.cpp_info.components["cmp1"].libs = ["other_cmp1"] self.cpp_info.components["cmp1"].set_property("pkg_config_name", "component1") self.cpp_info.components["cmp2"].libs = ["other_cmp2"] self.cpp_info.components["cmp3"].requires.append("cmp1") self.cpp_info.components["cmp3"].set_property("pkg_config_name", "component3") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=other --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): requires = "other/1.0" def package_info(self): self.cpp_info.requires = ["other::cmp1"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client2 = TestClient(cache_folder=client.cache_folder) conanfile = textwrap.dedent(""" [requires] pkg/0.1 [generators] PkgConfigDeps """) client2.save({"conanfile.txt": conanfile}) client2.run("install .") pc_content = client2.load("pkg.pc") assert "Requires: component1" == get_requires_from_content(pc_content) pc_content = client2.load("fancy_name.pc") assert "Requires: component1 fancy_name-cmp2 component3" == get_requires_from_content(pc_content) assert client2.load("component1.pc") assert client2.load("fancy_name-cmp2.pc") pc_content = client2.load("component3.pc") assert "Requires: component1" == get_requires_from_content(pc_content) def test_pkg_config_name_full_aliases(): """ Testing a simpler structure but paying more attention into several aliases. Expected file structure after running PkgConfigDeps as generator: - compo1.pc - compo1_alias.pc - pkg_alias1.pc - pkg_alias2.pc - pkg_other_name.pc - second-mycomponent.pc - second.pc """ client = TestClient() conanfile = textwrap.dedent(""" import textwrap from conan import ConanFile class Recipe(ConanFile): def package_info(self): custom_content = textwrap.dedent(\""" datadir=${prefix}/share schemasdir=${datadir}/mylib/schemas \""") self.cpp_info.set_property("pkg_config_name", "pkg_other_name") self.cpp_info.set_property("pkg_config_aliases", ["pkg_alias1", "pkg_alias2"]) # Custom content only added to root pc file -> pkg_other_name.pc self.cpp_info.set_property("pkg_config_custom_content", custom_content) self.cpp_info.components["cmp1"].libs = ["libcmp1"] self.cpp_info.components["cmp1"].set_property("pkg_config_name", "compo1") self.cpp_info.components["cmp1"].set_property("pkg_config_aliases", ["compo1_alias"]) """) client.save({"conanfile.py": conanfile}) client.run("create . --name=first --version=0.3") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): requires = "first/0.3" def package_info(self): self.cpp_info.components["mycomponent"].requires.append("first::cmp1") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=second --version=0.2") conanfile = textwrap.dedent(""" [requires] second/0.2 [generators] PkgConfigDeps """) client.save({"conanfile.txt": conanfile}, clean_first=True) client.run("install .") pc_content = client.load("compo1.pc") prefix = pc_content.splitlines()[0] assert "Description: Conan component: compo1" in pc_content assert "Requires" not in pc_content pc_content = client.load("compo1_alias.pc") content = textwrap.dedent(f"""\ Name: compo1_alias Description: Alias compo1_alias for compo1 Version: 0.3 Requires: compo1 """) assert content == pc_content pc_content = client.load("pkg_other_name.pc") content = textwrap.dedent(f"""\ {prefix} libdir=${{prefix}}/lib includedir=${{prefix}}/include bindir=${{prefix}}/bin datadir=${{prefix}}/share schemasdir=${{datadir}}/mylib/schemas Name: pkg_other_name Description: Conan package: pkg_other_name Version: 0.3 Libs: -L"${{libdir}}" Cflags: -I"${{includedir}}" Requires: compo1 """) assert content == pc_content pc_content = client.load("pkg_alias1.pc") content = textwrap.dedent(f"""\ Name: pkg_alias1 Description: Alias pkg_alias1 for pkg_other_name Version: 0.3 Requires: pkg_other_name """) assert content == pc_content pc_content = client.load("pkg_alias2.pc") content = textwrap.dedent(f"""\ Name: pkg_alias2 Description: Alias pkg_alias2 for pkg_other_name Version: 0.3 Requires: pkg_other_name """) assert content == pc_content pc_content = client.load("second-mycomponent.pc") assert "Requires: compo1" == get_requires_from_content(pc_content) def test_components_and_package_pc_creation_order(): """ Testing if the root package PC file name matches with any of the components one, the first one is not going to be created. Components have more priority than root package. Issue related: https://github.com/conan-io/conan/issues/10341 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.set_property("pkg_config_name", "OpenCL") self.cpp_info.components["_opencl-headers"].set_property("pkg_config_name", "OpenCL") self.cpp_info.components["_opencl-other"].set_property("pkg_config_name", "OtherCL") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=opencl --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): requires = "opencl/1.0" def package_info(self): self.cpp_info.components["comp"].set_property("pkg_config_name", "pkgb") self.cpp_info.components["comp"].requires.append("opencl::_opencl-headers") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=pkgb --version=1.0") conanfile = textwrap.dedent(""" [requires] pkgb/1.0 [generators] PkgConfigDeps """) client.save({"conanfile.txt": conanfile}, clean_first=True) client.run("install .") pc_files = [os.path.basename(i) for i in glob.glob(os.path.join(client.current_folder, '*.pc'))] pc_files.sort() # Let's check all the PC file names created just in case assert pc_files == ['OpenCL.pc', 'OtherCL.pc', 'pkgb.pc'] pc_content = client.load("OpenCL.pc") assert "Name: OpenCL" in pc_content assert "Description: Conan component: OpenCL" in pc_content assert "Requires:" not in pc_content pc_content = client.load("pkgb.pc") assert "Requires: OpenCL" in get_requires_from_content(pc_content) def test_pkgconfigdeps_with_test_requires(): """ PkgConfigDeps has to create any test requires declared on the recipe. Related issue: https://github.com/conan-io/conan/issues/11376 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): def package_info(self): self.cpp_info.libs = ["lib%s"] """) with client.chdir("app"): client.save({"conanfile.py": conanfile % "app"}) client.run("create . --name=app --version=1.0") with client.chdir("test"): client.save({"conanfile.py": conanfile % "test"}) client.run("create . --name=test --version=1.0") # Create library having build and test requires conanfile = textwrap.dedent(""" from conan import ConanFile class HelloLib(ConanFile): def build_requirements(self): self.test_requires('app/1.0') self.test_requires('test/1.0') """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -g PkgConfigDeps") assert "Description: Conan package: test" in client.load("test.pc") assert "Description: Conan package: app" in client.load("app.pc") def test_with_editable_layout(): """ https://github.com/conan-io/conan/issues/11435 """ client = TestClient() dep = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): self.cpp.source.includedirs = ["include"] def package_info(self): self.cpp_info.libs = ["mylib"] """) client.save({"dep/conanfile.py": dep, "dep/include/header.h": "", "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1")}) client.run("create dep") client.run("editable add dep") with client.chdir("pkg"): client.run("install . -g PkgConfigDeps") pc = client.load("dep.pc") assert 'Libs: -L"${libdir}" -lmylib' in pc assert 'includedir=' in pc assert 'Cflags: -I"${includedir}"' in pc def test_tool_requires(): """ Testing if PC files are created for tool requires if build_context_activated/_suffix is used. Issue related: https://github.com/conan-io/conan/issues/11710 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name tool --version 1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.set_property("pkg_config_name", "libother") self.cpp_info.components["cmp1"].libs = ["other_cmp1"] self.cpp_info.components["cmp1"].set_property("pkg_config_name", "component1") self.cpp_info.components["cmp2"].libs = ["other_cmp2"] self.cpp_info.components["cmp3"].requires.append("cmp1") self.cpp_info.components["cmp3"].set_property("pkg_config_name", "component3") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name other --version 1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class PkgConfigConan(ConanFile): name = "demo" version = "1.0" def build_requirements(self): self.build_requires("tool/1.0") self.build_requires("other/1.0") def generate(self): tc = PkgConfigDeps(self) tc.build_context_activated = ["other", "tool"] tc.build_context_suffix = {"tool": "_bt", "other": "_bo"} tc.generate() """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default") pc_files = [os.path.basename(i) for i in glob.glob(os.path.join(client.current_folder, '*.pc'))] pc_files.sort() # Let's check all the PC file names created just in case assert pc_files == ['component1_bo.pc', 'component3_bo.pc', 'libother_bo-cmp2.pc', 'libother_bo.pc', 'tool_bt.pc'] pc_content = client.load("tool_bt.pc") assert "Name: tool_bt" in pc_content pc_content = client.load("libother_bo.pc") assert "Name: libother_bo" in pc_content assert "Requires: component1_bo libother_bo-cmp2 component3_bo" == get_requires_from_content(pc_content) pc_content = client.load("component1_bo.pc") assert "Name: component1_bo" in pc_content pc_content = client.load("libother_bo-cmp2.pc") assert "Name: libother_bo-cmp2" in pc_content pc_content = client.load("component3_bo.pc") assert "Name: component3_bo" in pc_content assert "Requires: component1_bo" == get_requires_from_content(pc_content) def test_tool_requires_not_created_if_no_activated(): """ Testing if there are no PC files created in no context are activated """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name tool --version 1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): name = "demo" version = "1.0" generators = "PkgConfigDeps" def build_requirements(self): self.build_requires("tool/1.0") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default") pc_files = [os.path.basename(i) for i in glob.glob(os.path.join(client.current_folder, '*.pc'))] pc_files.sort() assert pc_files == [] def test_tool_requires_error_if_no_build_suffix(): """ Testing if same dependency exists in both require and build require (without suffix) """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name tool --version 1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class PkgConfigConan(ConanFile): name = "demo" version = "1.0" def requirements(self): self.requires("tool/1.0") def build_requirements(self): self.build_requires("tool/1.0") def generate(self): tc = PkgConfigDeps(self) tc.build_context_activated = ["tool"] tc.generate() """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default", assert_error=True) assert "The packages ['tool'] exist both as 'require' and as 'build require'" in client.out def test_error_missing_pc_build_context(): """ PkgConfigDeps was failing, not generating the zlib.pc in the build context, for a test_package that both requires(example/1.0) and tool_requires(example/1.0), which depends on zlib # https://github.com/conan-io/conan/issues/12664 """ c = TestClient() example = textwrap.dedent(""" import os from conan import ConanFile class Example(ConanFile): name = "example" version = "1.0" requires = "game/1.0" generators = "PkgConfigDeps" settings = "build_type" def build(self): assert os.path.exists("math.pc") assert os.path.exists("engine.pc") assert os.path.exists("game.pc") """) c.save({"math/conanfile.py": GenConanfile("math", "1.0").with_settings("build_type"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_settings("build_type") .with_require("math/1.0"), "game/conanfile.py": GenConanfile("game", "1.0").with_settings("build_type") .with_requires("engine/1.0"), "example/conanfile.py": example, # With ``with_test()`` it already generates a requires(example/1.0) "example/test_package/conanfile.py": GenConanfile().with_build_requires("example/1.0") .with_test("pass")}) c.run("create math") c.run("create engine") c.run("create game") # This used to crash because of the assert inside the build() method c.run("create example -pr:b=default -pr:h=default") # Now make sure we can actually build with build!=host context # The debug binaries are missing, so adding --build=missing c.run("create example -pr:b=default -pr:h=default -s:h build_type=Debug --build=missing " "--build=example") c.assert_listed_require({"example/1.0": "Cache"}) c.assert_listed_require({"example/1.0": "Cache"}, build=True) class TestPCGenerationBuildContext: """ https://github.com/conan-io/conan/issues/14920 """ def test_pc_generate(self): c = TestClient() tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class Example(ConanFile): name = "tool" version = "1.0" requires = "wayland/1.0" tool_requires = "wayland/1.0" def generate(self): deps = PkgConfigDeps(self) deps.build_context_activated = ["wayland", "dep"] deps.build_context_suffix = {"wayland": "_BUILD", "dep": "_BUILD"} deps.generate() def build(self): assert os.path.exists("wayland.pc") assert os.path.exists("wayland_BUILD.pc") assert os.path.exists("dep.pc") assert os.path.exists("dep_BUILD.pc") """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_package_type("shared-library"), "wayland/conanfile.py": GenConanfile("wayland", "1.0").with_requires("dep/1.0"), "tool/conanfile.py": tool, "app/conanfile.py": GenConanfile().with_tool_requires("tool/1.0")}) c.run("export dep") c.run("export wayland") c.run("export tool") c.run("install app --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail # Deprecation warning! assert "PkgConfigDeps.build_context_suffix attribute has been deprecated" in c.out # Now make sure we can actually build with build!=host context c.run("install app -s:h build_type=Debug --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail def test_pc_generate_components(self): c = TestClient() tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class Example(ConanFile): name = "tool" version = "1.0" requires = "wayland/1.0" tool_requires = "wayland/1.0" def generate(self): deps = PkgConfigDeps(self) deps.build_context_activated = ["wayland", "dep"] deps.build_context_suffix = {"wayland": "_BUILD", "dep": "_BUILD"} deps.generate() def build(self): assert os.path.exists("wayland.pc") assert os.path.exists("wayland-client.pc") assert os.path.exists("wayland-server.pc") assert os.path.exists("wayland_BUILD.pc") assert os.path.exists("wayland_BUILD-client.pc") assert os.path.exists("wayland_BUILD-server.pc") assert os.path.exists("dep.pc") assert os.path.exists("dep_BUILD.pc") # Issue: https://github.com/conan-io/conan/issues/12342 # Issue: https://github.com/conan-io/conan/issues/14935 assert not os.path.exists("build/wayland.pc") assert not os.path.exists("build/wayland-client.pc") assert not os.path.exists("build/wayland-server.pc") assert not os.path.exists("build/dep.pc") """) wayland = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "wayland" version = "1.0" requires = "dep/1.0" def package_info(self): self.cpp_info.components["client"].libs = [] self.cpp_info.components["server"].libs = [] """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_package_type("shared-library"), "wayland/conanfile.py": wayland, "tool/conanfile.py": tool, "app/conanfile.py": GenConanfile().with_tool_requires("tool/1.0")}) c.run("export dep") c.run("export wayland") c.run("export tool") c.run("install app --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail # Now make sure we can actually build with build!=host context c.run("install app -s:h build_type=Debug --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail @pytest.mark.parametrize("build_folder_name", ["build", ""]) def test_pc_generate_components_in_build_context_folder(self, build_folder_name): c = TestClient() tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class Example(ConanFile): name = "tool" version = "1.0" requires = "wayland/1.0" tool_requires = "wayland/1.0" def generate(self): deps = PkgConfigDeps(self) deps.build_context_activated = ["wayland", "dep"] deps.build_context_folder = "{build_folder_name}" deps.generate() def build(self): assert os.path.exists("wayland.pc") assert os.path.exists("wayland-client.pc") assert os.path.exists("wayland-server.pc") assert os.path.exists("dep.pc") # Issue: https://github.com/conan-io/conan/issues/12342 # Issue: https://github.com/conan-io/conan/issues/14935 if "{build_folder_name}": assert os.path.exists("{build_folder_name}/wayland.pc") assert os.path.exists("{build_folder_name}/wayland-client.pc") assert os.path.exists("{build_folder_name}/wayland-server.pc") assert os.path.exists("{build_folder_name}/dep.pc") """.format(build_folder_name=build_folder_name)) wayland = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "wayland" version = "1.0" requires = "dep/1.0" def package_info(self): self.cpp_info.components["client"].libs = [] self.cpp_info.components["server"].libs = [] """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_package_type("shared-library"), "wayland/conanfile.py": wayland, "tool/conanfile.py": tool, "app/conanfile.py": GenConanfile().with_tool_requires("tool/1.0")}) c.run("export dep") c.run("export wayland") c.run("export tool") c.run("install app --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail # Now make sure we can actually build with build!=host context c.run("install app -s:h build_type=Debug --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail @pytest.mark.parametrize("build_folder_name", ["build", ""]) def test_pkg_config_deps_set_in_build_context_folder(self, build_folder_name): c = TestClient() tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class Example(ConanFile): name = "tool" version = "1.0" requires = "wayland/1.0" tool_requires = "wayland/1.0" def generate(self): deps = PkgConfigDeps(self) deps.set_property("wayland", "pkg_config_name", "waylandx264") deps.build_context_activated = ["wayland", "dep"] deps.build_context_folder = "{build_folder_name}" deps.generate() def build(self): assert os.path.exists("waylandx264.pc") assert not os.path.exists("wayland.pc") if "{build_folder_name}": assert os.path.exists("{build_folder_name}/waylandx264.pc") assert not os.path.exists("{build_folder_name}/wayland.pc") """.format(build_folder_name=build_folder_name)) wayland = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "wayland" version = "1.0" requires = "dep/1.0" def package_info(self): self.cpp_info.components["client"].libs = [] self.cpp_info.components["server"].libs = [] """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_package_type("shared-library"), "wayland/conanfile.py": wayland, "tool/conanfile.py": tool, "app/conanfile.py": GenConanfile().with_tool_requires("tool/1.0")}) c.run("create dep") c.run("create wayland") c.run("create tool") c.run("install app --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail # Now make sure we can actually build with build!=host context c.run("install app -s:h build_type=Debug --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail def test_tool_requires_error_if_folder_and_suffix(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name tool --version 1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class PkgConfigConan(ConanFile): name = "demo" version = "1.0" def requirements(self): self.requires("tool/1.0") def build_requirements(self): self.build_requires("tool/1.0") def generate(self): tc = PkgConfigDeps(self) tc.build_context_activated = ["tool"] tc.build_context_folder = "build" tc.build_context_suffix = {"tool": "_bt"} tc.generate() """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default", assert_error=True) assert ("It's not allowed to define both PkgConfigDeps.build_context_folder " "and PkgConfigDeps.build_context_suffix (deprecated).") in client.out def test_pkg_config_deps_and_private_deps(): """ Testing that no errors are raised when the dependency tree has a private one in the middle of it. Issue related: https://github.com/conan-io/conan/issues/15311 """ client = TestClient() client.save({"conanfile.py": GenConanfile("private", "0.1")}) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class ConsumerConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps" name = "pkg" version = "0.1" def requirements(self): self.requires("private/0.1", visible=False) """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class ConsumerConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PkgConfigDeps" def requirements(self): self.requires("pkg/0.1") """) client.save({"conanfile.py": conanfile}, clean_first=True) # Now, it passes and creates the pc files correctly (the skipped one is not created) client.run("install .") assert "Requires:" not in client.load("pkg.pc") def test_using_deployer_folder(): """ Testing that the absolute path is kept as the prefix instead of the relative path. Issue related: https://github.com/conan-io/conan/issues/16543 """ client = TestClient() client.save({"dep/conanfile.py": GenConanfile("dep", "0.1")}) client.run("create dep/conanfile.py") client.run("install --requires=dep/0.1 --deployer=direct_deploy " "--deployer-folder=mydeploy -g PkgConfigDeps") content = client.load("dep.pc") prefix_base = client.current_folder.replace('\\', '/') assert f"prefix={prefix_base}/mydeploy/direct_deploy/dep" in content assert "libdir=${prefix}/lib" in content assert "includedir=${prefix}/include" in content assert "bindir=${prefix}/bin" in content def test_pkg_config_deps_set_property(): c = TestClient() app = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.gnu import PkgConfigDeps class Pkg(ConanFile): settings = "build_type" requires = "dep/0.1", "other/0.1" def generate(self): pc = PkgConfigDeps(self) pc.set_property("dep", "pkg_config_name", "depx264") pc.set_property("other::mycomp1", "nosoname", True) pc.set_property("other::mycomp1", "pkg_config_name", "new_other_comp") pc.generate() """) pkg_info = {"components": {"mycomp1": {"libs": ["mylib"]}}} c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_package_type("shared-library"), "other/conanfile.py": GenConanfile("other", "0.1").with_package_type("shared-library") .with_package_info(pkg_info), "app/conanfile.py": app}) c.run("create dep") c.run("create other") c.run("install app") assert not os.path.exists(os.path.join(c.current_folder, "app", "dep.pc")) dep = c.load("app/depx264.pc") assert 'Name: depx264' in dep other = c.load("app/other.pc") assert 'Name: other' in other other_mycomp1 = c.load("app/new_other_comp.pc") assert 'Name: new_other_comp' in other_mycomp1 assert other.split("\n")[0] == other_mycomp1.split("\n")[0] def test_pkg_with_duplicated_component_requires(): """ Testing that even having duplicated component requires, the PC does not include them. Issue: https://github.com/conan-io/conan/issues/18283 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.components["mycomponent"].libs = [] self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") # Duplicate one self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=mylib --version=0.1") client.save({"conanfile.py": GenConanfile("pkg", "0.1").with_require("mylib/0.1")}, clean_first=True) client.run("install . -g PkgConfigDeps") pc_content = client.load("mylib-myfirstcomp.pc") assert "Requires: mylib-mycomponent" == get_requires_from_content(pc_content) def test_pkg_skip_component(): conanfile_a = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): name = "pkg_a" version = "0.1" def package_info(self): self.cpp_info.set_property("pkg_config_name", "none") """) conanfile_b = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): name = "pkg_b" version = "0.1" requires = "pkg_a/0.1" def package_info(self): self.cpp_info.components["cmp1"].set_property("pkg_config_name", "b-cmp1") """) conanfile_c = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): name = "pkg_c" version = "0.1" requires = "pkg_b/0.1" def package_info(self): self.cpp_info.components["cmp2"].set_property("pkg_config_name", "none") """) tc = TestClient(light=True) tc.save({"a/conanfile.py": conanfile_a, "b/conanfile.py": conanfile_b, "c/conanfile.py": conanfile_c}) tc.run("create a") tc.run("create b") tc.run("create c") tc.run("install --requires=pkg_c/0.1 --generator=PkgConfigDeps -of=out") install_contents = os.listdir(os.path.join(tc.current_folder, "out")) assert "pkg_a.pc" not in install_contents assert "pkg_b.pc" in install_contents pkg_b_content = tc.load(os.path.join("out", "pkg_b.pc")) pkg_b_requires = get_requires_from_content(pkg_b_content) assert "b-cmp1" in pkg_b_requires assert "pkg_a" not in pkg_b_requires assert "none" not in pkg_b_requires assert "b-cmp1.pc" in install_contents b_cmp1_content = tc.load(os.path.join("out", "b-cmp1.pc")) assert "Requires:" not in b_cmp1_content assert "pkg_c.pc" in install_contents # Components can not skip the PC file creation assert "none.pc" in install_contents ================================================ FILE: test/integration/toolchains/google/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/google/test_bazeldeps.py ================================================ import os import pathlib import platform import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.tools.files import load def test_bazel(): # https://github.com/conan-io/conan/issues/10471 dep = textwrap.dedent(""" from conan import ConanFile class ExampleConanIntegration(ConanFile): name = "dep" version = "0.1" def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.libs = [] """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.google import BazelToolchain, BazelDeps class ExampleConanIntegration(ConanFile): generators = 'BazelDeps', 'BazelToolchain' requires = 'dep/0.1', """) c = TestClient() c.save({"dep/conanfile.py": dep, "consumer/conanfile.py": conanfile}) c.run("create dep") c.run("install consumer") assert "conanfile.py: Generator 'BazelToolchain' calling 'generate()'" in c.out def test_bazel_relative_paths(): # https://github.com/conan-io/conan/issues/10476 conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.google import BazelToolchain, BazelDeps class ExampleConanIntegration(ConanFile): generators = 'BazelDeps', 'BazelToolchain' requires = 'dep/0.1' def layout(self): self.folders.generators = "conandeps" """) c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "consumer/conanfile.py": conanfile}) c.run("create dep") c.run("install consumer") assert "conanfile.py: Generator 'BazelToolchain' calling 'generate()'" in c.out build_file = c.load("consumer/conandeps/dep/BUILD.bazel") expected = textwrap.dedent("""\ # Components precompiled libs # Root package precompiled libs # Components libraries declaration # Package library declaration cc_library( name = "dep", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], ) # Filegroup library declaration filegroup( name = "dep_binaries", srcs = glob([ "bin/**", ], allow_empty = True ), visibility = ["//visibility:public"], ) """) assert build_file == expected def test_bazel_exclude_folders(): # https://github.com/conan-io/conan/issues/11081 dep = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class ExampleConanIntegration(ConanFile): name = "dep" version = "0.1" def package(self): save(self, os.path.join(self.package_folder, "lib", "mymath", "otherfile.a"), "") save(self, os.path.join(self.package_folder, "lib", "libmymath.a"), "") def package_info(self): self.cpp_info.libs = ["mymath"] """) c = TestClient() c.save({"dep/conanfile.py": dep}) c.run("create dep") c.run("install --requires=dep/0.1 -g BazelDeps") build_file = c.load("dep/BUILD.bazel") assert 'static_library = "lib/libmymath.a"' in build_file def test_bazeldeps_and_tool_requires(): """ Testing that direct build requires are not included in dependencies BUILD.bazel files Issues related: * https://github.com/conan-io/conan/issues/12444 * https://github.com/conan-io/conan/issues/12236 """ c = TestClient() tool = textwrap.dedent(""" import os from conan import ConanFile class ToolConanfile(ConanFile): name = "tool" version = "0.1" def package_info(self): self.cpp_info.libs = ["mymath"] """) c.save({"tool/conanfile.py": tool}) c.run("create tool") dep = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class ExampleConanIntegration(ConanFile): name = "dep" version = "0.1" def build_requirements(self): self.tool_requires("tool/0.1") def package(self): save(self, os.path.join(self.package_folder, "lib", "mymath", "otherfile.a"), "") save(self, os.path.join(self.package_folder, "lib", "libmymath.a"), "") def package_info(self): self.cpp_info.libs = ["mymath"] self.cpp_info.bindirs = ["bin1", "bin2"] """) c.save({"dep/conanfile.py": dep}) c.run("export dep") c.run("install --requires=dep/0.1 -g BazelDeps --build=missing") build_file = c.load("dep/BUILD.bazel") expected = textwrap.dedent("""\ # Components precompiled libs # Root package precompiled libs cc_import( name = "mymath_precompiled", static_library = "lib/libmymath.a", ) # Components libraries declaration # Package library declaration cc_library( name = "dep", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":mymath_precompiled", ], ) # Filegroup library declaration filegroup( name = "dep_binaries", srcs = glob([ "bin1/**", "bin2/**", ], allow_empty = True ), visibility = ["//visibility:public"], ) """) assert expected == build_file def test_pkg_with_public_deps_and_component_requires(): """ Testing a complex structure like: * first/0.1 - Global bazel_target_name == "myfirstlib" - Components: "cmp1" * other/0.1 * second/0.1 - Requires: "first/0.1" - Components: "mycomponent", "myfirstcomp" + "mycomponent" requires "first::cmp1" + "myfirstcomp" requires "mycomponent" * third/0.1 - Requires: "second/0.1", "other/0.1" Expected file structure after running BazelDeps as generator: - second/BUILD.bazel - third/BUILD.bazel - other/BUILD.bazel - myfirstlib/BUILD.bazel """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Recipe(ConanFile): def package(self): # Saving an empty lib dest_lib = os.path.join(self.package_folder, "lib", "libcmp1.a") save(self, dest_lib, "") def package_info(self): self.cpp_info.set_property("bazel_target_name", "myfirstlib") self.cpp_info.components["cmp1"].libs = ["libcmp1"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=first --version=0.1") client.save({"conanfile.py": GenConanfile("other", "0.1").with_package_file("file.h", "0.1")}) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgBazelConan(ConanFile): requires = "first/0.1" def package_info(self): self.cpp_info.components["mycomponent"].requires.append("first::cmp1") self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=second --version=0.1") client.save({"conanfile.py": GenConanfile("third", "0.1").with_package_file("file.h", "0.1") .with_require("second/0.1") .with_require("other/0.1")}, clean_first=True) client.run("create .") client2 = TestClient(cache_folder=client.cache_folder) conanfile = textwrap.dedent(""" [requires] third/0.1 [generators] BazelDeps """) client2.save({"conanfile.txt": conanfile}) client2.run("install .") content = client2.load("third/BUILD.bazel") assert textwrap.dedent("""\ cc_library( name = "third", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort "@second//:second", "@other//:other", ], )""") in content content = client2.load("second/BUILD.bazel") assert textwrap.dedent("""\ # Components libraries declaration cc_library( name = "second-mycomponent", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort "@first//:myfirstlib-cmp1", ], ) cc_library( name = "second-myfirstcomp", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":second-mycomponent", ], ) # Package library declaration cc_library( name = "second", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":second-mycomponent", ":second-myfirstcomp", "@first//:myfirstlib", ], )""") in content content = client2.load("first/BUILD.bazel") assert textwrap.dedent("""\ # Components precompiled libs cc_import( name = "libcmp1_precompiled", static_library = "lib/libcmp1.a", ) # Root package precompiled libs # Components libraries declaration cc_library( name = "myfirstlib-cmp1", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":libcmp1_precompiled", ], ) # Package library declaration cc_library( name = "myfirstlib", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":myfirstlib-cmp1", ], )""") in content content = client2.load("other/BUILD.bazel") assert "deps =" not in content def test_pkg_with_public_deps_and_component_requires_2(): """ Testing another complex structure like: * other/0.1 - Global bazel_target_name == "fancy_name" - Components: "cmp1", "cmp2", "cmp3" + "cmp1" bazel_target_name == "component1" (it shouldn't be affected by "fancy_name") + "cmp3" bazel_target_name == "component3" (it shouldn't be affected by "fancy_name") + "cmp3" requires "cmp1" * pkg/0.1 - Requires: "other/0.1" -> "other::cmp1" Expected file structure after running BazelDeps as generator: - fancy_name/BUILD.bazel (components: "component1", "fancy_name-cmp2", "component3") - pkg/BUILD.bazel """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Recipe(ConanFile): def package(self): # Saving an empty lib dest_lib = os.path.join(self.package_folder, "lib", "libother_cmp1.a") dest_lib2 = os.path.join(self.package_folder, "lib", "libother_cmp2.a") save(self, dest_lib, "") save(self, dest_lib2, "") def package_info(self): self.cpp_info.set_property("bazel_target_name", "fancy_name") self.cpp_info.components["cmp1"].libs = ["other_cmp1"] self.cpp_info.components["cmp1"].set_property("bazel_target_name", "component1") self.cpp_info.components["cmp2"].libs = ["other_cmp2"] self.cpp_info.components["cmp3"].requires.append("cmp1") self.cpp_info.components["cmp3"].set_property("bazel_target_name", "component3") """) client.save({"conanfile.py": conanfile}) client.run("create . --name=other --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgBazelConan(ConanFile): requires = "other/1.0" def package_info(self): self.cpp_info.requires = ["other::cmp1"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=pkg --version=0.1") client2 = TestClient(cache_folder=client.cache_folder) conanfile = textwrap.dedent(""" [requires] pkg/0.1 [generators] BazelDeps """) client2.save({"conanfile.txt": conanfile}) client2.run("install .") content = client2.load("pkg/BUILD.bazel") assert textwrap.dedent("""\ # Package library declaration cc_library( name = "pkg", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort "@other//:component1", ], )""") in content content = client2.load("other/BUILD.bazel") assert textwrap.dedent("""\ # Components precompiled libs cc_import( name = "other_cmp1_precompiled", static_library = "lib/libother_cmp1.a", ) cc_import( name = "other_cmp2_precompiled", static_library = "lib/libother_cmp2.a", ) # Root package precompiled libs # Components libraries declaration cc_library( name = "component1", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":other_cmp1_precompiled", ], ) cc_library( name = "fancy_name-cmp2", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":other_cmp2_precompiled", ], ) cc_library( name = "component3", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":component1", ], ) # Package library declaration cc_library( name = "fancy_name", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":component1", ":fancy_name-cmp2", ":component3", ], )""") in content def test_pkgconfigdeps_with_test_requires(): """ BazelDeps has to create any declared test requirements on the recipe. """ client = TestClient() conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Recipe(ConanFile): def package(self): # Saving an empty lib dest_lib = os.path.join(self.package_folder, "lib", "liblib{0}.a") save(self, dest_lib, "") def package_info(self): self.cpp_info.libs = ["lib{0}"] """) with client.chdir("app"): client.save({"conanfile.py": conanfile.format("app")}) client.run("create . --name=app --version=1.0") with client.chdir("test"): client.save({"conanfile.py": conanfile.format("test")}) client.run("create . --name=test --version=1.0") # Create library having build and test requires conanfile = textwrap.dedent(""" from conan import ConanFile class HelloLib(ConanFile): def build_requirements(self): self.test_requires('app/1.0') self.test_requires('test/1.0') """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -g BazelDeps") expected = textwrap.dedent("""\ # Root package precompiled libs cc_import( name = "lib{0}_precompiled", static_library = "lib/liblib{0}.a", ) # Components libraries declaration # Package library declaration cc_library( name = "{0}", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":lib{0}_precompiled", ], )""") assert expected.format("test") in client.load("test/BUILD.bazel") assert expected.format("app") in client.load("app/BUILD.bazel") def test_with_editable_layout(): """ Testing if editable mode is working for a Bazel project (using BazelDeps and bazel_layout) """ client = TestClient() dep = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.google import bazel_layout from conan.tools.files import save class Dep(ConanFile): name = "dep" version = "0.1" def layout(self): bazel_layout(self, target_folder="main") self.cpp.source.includedirs = ["include"] def package_info(self): self.cpp_info.libs = ["mylib"] """) client.save({"dep/conanfile.py": dep, "dep/include/header.h": "", "dep/bazel-bin/main/libmylib.a": "", "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/0.1")}) client.run("create dep") client.run("editable add dep --name=dep --version=0.1") recipes_folder = client.current_folder.replace("\\", "/") with client.chdir("pkg"): client.run("install . -g BazelDeps") # TODO: Remove when dropped Bazel 6.x compatibility content = client.load("dependencies.bzl") assert textwrap.dedent(f"""\ def load_conan_dependencies(): native.new_local_repository( name="dep", path="{recipes_folder}/dep", build_file="{recipes_folder}/pkg/dep/BUILD.bazel", )""") in content # Bazel 7.x content = client.load("conan_deps_module_extension.bzl") assert textwrap.dedent(f"""\ def _load_dependencies_impl(mctx): conan_dependency_repo( name = "dep", package_path = "{recipes_folder}/dep", build_file_path = "{recipes_folder}/pkg/dep/BUILD.bazel", )""") in content content = client.load("dep/BUILD.bazel") assert pathlib.Path(client.current_folder, "conan_deps_repo_rules.bzl").exists() assert textwrap.dedent("""\ cc_import( name = "mylib_precompiled", static_library = "bazel-bin/main/libmylib.a", ) # Components libraries declaration # Package library declaration cc_library( name = "dep", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":mylib_precompiled", ], )""") in content def test_tool_requires(): """ Testing if BUILD files are created for tool requires if build_context_activated=True is used. """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save class PkgBazelConan(ConanFile): def package(self): import os libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(libdirs, "libtool.lib"), "") def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=tool --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save class PkgBazelConan(ConanFile): def package(self): import os libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(libdirs, "other_cmp1.lib"), "") save(self, os.path.join(libdirs, "other_cmp2.lib"), "") def package_info(self): self.cpp_info.set_property("bazel_target_name", "libother") self.cpp_info.set_property("bazel_repository_name", "other-repo") self.cpp_info.components["cmp1"].libs = ["other_cmp1"] self.cpp_info.components["cmp1"].set_property("bazel_target_name", "component1") self.cpp_info.components["cmp2"].libs = ["other_cmp2"] self.cpp_info.components["cmp3"].requires.append("cmp1") self.cpp_info.components["cmp3"].set_property("bazel_target_name", "component3") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=other --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.google import BazelDeps class PkgBazelConan(ConanFile): name = "demo" version = "1.0" def build_requirements(self): self.tool_requires("tool/1.0") self.tool_requires("other/1.0") def generate(self): tc = BazelDeps(self) tc.build_context_activated = ["other", "tool"] tc.generate() """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default") assert 'name = "tool"' in client.load("build-tool/BUILD.bazel") assert textwrap.dedent("""\ # Components precompiled libs cc_import( name = "other_cmp1_precompiled", static_library = "lib/other_cmp1.lib", ) cc_import( name = "other_cmp2_precompiled", static_library = "lib/other_cmp2.lib", ) # Root package precompiled libs # Components libraries declaration cc_library( name = "component1", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":other_cmp1_precompiled", ], ) cc_library( name = "libother-cmp2", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":other_cmp2_precompiled", ], ) cc_library( name = "component3", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":component1", ], ) # Package library declaration cc_library( name = "libother", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":component1", ":libother-cmp2", ":component3", ], )""") in client.load("build-other-repo/BUILD.bazel") # TODO: Remove when dropped Bazel 6.x compatibility # Let's check if the names used in the dependencies.bzl are correct content = client.load("dependencies.bzl") assert 'name="build-other-repo"' in content # build context + bazel_repository_name prop assert 'name="build-tool"' in content # build context + package reference name # Bazel 7.x # Let's check if the names used in the conan_deps_repo_rules.bzl are correct content = client.load("conan_deps_module_extension.bzl") assert 'name = "build-other-repo"' in content # build context + bazel_repository_name prop assert 'name = "build-tool"' in content # build context + package reference name def test_tool_requires_not_created_if_no_activated(): """ Testing if there are no BUILD files created in no context are activated """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save class PkgBazelConan(ConanFile): def package(self): import os libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(libdirs, "libtool.lib"), "") def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=tool --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile class PkgBazelConan(ConanFile): name = "demo" version = "1.0" generators = "BazelDeps" def build_requirements(self): self.tool_requires("tool/1.0") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default") assert not os.path.exists(os.path.join(client.current_folder, "tool")) def test_tool_requires_raise_exception_if_exist_both_require_and_build_one(): """ Testing if same dependency exists in both host and build context """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import save class PkgBazelConan(ConanFile): def package(self): import os libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(libdirs, "libtool.lib"), "") def package_info(self): self.cpp_info.libs = ["libtool"] """) client.save({"conanfile.py": conanfile}) client.run("create . --name=tool --version=1.0") conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.google import BazelDeps class PkgBazelConan(ConanFile): name = "demo" version = "1.0" def requirements(self): self.requires("tool/1.0") def build_requirements(self): self.tool_requires("tool/1.0") def generate(self): tc = BazelDeps(self) tc.build_context_activated = ["tool"] tc.generate() """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("install . -pr:h default -pr:b default") # Different Bazel repository names, same target names assert 'name = "tool"' in client.load("build-tool/BUILD.bazel") assert 'name = "tool"' in client.load("tool/BUILD.bazel") def test_error_missing_bazel_build_files_in_build_context(): """ BazelDeps should generate BUILD files in the build context for a test_package that both requires(example/1.0) and tool_requires(example/1.0), which depends on other requirements * Issue related: https://github.com/conan-io/conan/issues/12664 """ c = TestClient() example = textwrap.dedent(""" import os from conan import ConanFile class Example(ConanFile): name = "example" version = "1.0" requires = "game/1.0" generators = "BazelDeps" settings = "build_type" def build(self): context = "build-" if self.context == "build" else "" assert os.path.exists(os.path.join(f"{context}math", "BUILD.bazel")) assert os.path.exists(os.path.join(f"{context}engine", "BUILD.bazel")) assert os.path.exists(os.path.join(f"{context}game", "BUILD.bazel")) """) c.save({"math/conanfile.py": GenConanfile("math", "1.0").with_settings("build_type"), "engine/conanfile.py": GenConanfile("engine", "1.0").with_settings("build_type") .with_require("math/1.0"), "game/conanfile.py": GenConanfile("game", "1.0").with_settings("build_type") .with_requires("engine/1.0"), "example/conanfile.py": example, # With ``with_test()`` it already generates a requires(example/1.0) "example/test_package/conanfile.py": GenConanfile().with_build_requires("example/1.0") .with_test("pass")}) c.run("create math") c.run("create engine") c.run("create game") # This used to crash because of the assert inside the build() method c.run("create example -pr:b=default -pr:h=default") # Now make sure we can actually build with build!=host context # The debug binaries are missing, so adding --build=missing c.run("create example -pr:b=default -pr:h=default -s:h build_type=Debug --build=missing " "--build=example") c.assert_listed_require({"example/1.0": "Cache"}) c.assert_listed_require({"example/1.0": "Cache"}, build=True) class TestBazelGenerationBuildContext: """ https://github.com/conan-io/conan/issues/15764 """ def test_bazel_generate(self): c = TestClient() tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.google import BazelDeps class Example(ConanFile): name = "tool" version = "1.0" requires = "wayland/1.0" tool_requires = "wayland/1.0" def generate(self): tc = BazelDeps(self) tc.build_context_activated = ["wayland", "dep"] tc.generate() def build(self): # Build context assert os.path.exists(os.path.join("build-wayland", "BUILD.bazel")) # Revisit this after investigation -> build_req = conanfile.dependencies.direct_build # assert os.path.exists(os.path.join("build-dep", "BUILD.bazel")) # Host context assert os.path.exists(os.path.join("wayland", "BUILD.bazel")) assert os.path.exists(os.path.join("dep", "BUILD.bazel")) """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_package_type("shared-library"), "wayland/conanfile.py": GenConanfile("wayland", "1.0").with_requires("dep/1.0"), "tool/conanfile.py": tool, "app/conanfile.py": GenConanfile().with_tool_requires("tool/1.0")}) c.run("export dep") c.run("export wayland") c.run("export tool") c.run("install app --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail # Now make sure we can actually build with build!=host context c.run("install app -s:h build_type=Debug --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail def test_bazel_generate_components(self): c = TestClient() tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.google import BazelDeps class Example(ConanFile): name = "tool" version = "1.0" requires = "wayland/1.0" tool_requires = "wayland/1.0" def generate(self): tc = BazelDeps(self) tc.build_context_activated = ["wayland", "dep"] tc.generate() def build(self): # Build context assert os.path.exists(os.path.join("build-wayland", "BUILD.bazel")) # Revisit this after investigation -> build_req = conanfile.dependencies.direct_build # assert os.path.exists(os.path.join("build-dep", "BUILD.bazel")) # Host context assert os.path.exists(os.path.join("wayland", "BUILD.bazel")) assert os.path.exists(os.path.join("dep", "BUILD.bazel")) """) wayland = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): name = "wayland" version = "1.0" requires = "dep/1.0" def package_info(self): self.cpp_info.components["client"].libs = [] self.cpp_info.components["server"].libs = [] """) c.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_package_type("shared-library"), "wayland/conanfile.py": wayland, "tool/conanfile.py": tool, "app/conanfile.py": GenConanfile().with_tool_requires("tool/1.0")}) c.run("export dep") c.run("export wayland") c.run("export tool") c.run("install app --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail # Now make sure we can actually build with build!=host context c.run("install app -s:h build_type=Debug --build=missing") assert "Install finished successfully" in c.out # the asserts in build() didn't fail def test_shared_windows_find_libraries(): """ Testing the ``_get_libs`` mechanism in Windows, the shared libraries and their import ones are correctly found. Note: simulating dependencies with openssl, libcurl, and zlib packages: zlib: - (zlib package) bin/zlib1.dll AND lib/zdll.lib libcurl: - (curl component) bin/libcurl.dll AND lib/libcurl_imp.lib openssl: - (crypto component) bin/libcrypto-3-x64.dll AND lib/libcrypto.lib - (ssl component) bin/libssl-3-x64.dll AND lib/libssl.lib Issue: https://github.com/conan-io/conan/issues/16691 """ c = TestClient() zlib = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Example(ConanFile): name = "zlib" version = "1.0" options = {"shared": [True, False]} default_options = {"shared": False} def package(self): bindirs = os.path.join(self.package_folder, "bin") libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(bindirs, "zlib1.dll"), "") save(self, os.path.join(libdirs, "zdll.lib"), "") def package_info(self): self.cpp_info.libs = ["zdll"] """) libiconv = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Example(ConanFile): name = "libiconv" version = "1.0" options = {"shared": [True, False]} default_options = {"shared": False} def package(self): bindirs = os.path.join(self.package_folder, "bin") libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(bindirs, "charset-1.dll"), "") save(self, os.path.join(bindirs, "iconv-2.dll"), "") save(self, os.path.join(libdirs, "charset.lib"), "") save(self, os.path.join(libdirs, "iconv.lib"), "") def package_info(self): self.cpp_info.libs = ["iconv", "charset"] """) openssl = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "openssl" version = "1.0" options = {"shared": [True, False]} default_options = {"shared": False} def package(self): bindirs = os.path.join(self.package_folder, "bin") libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(bindirs, "libcrypto-3-x64.dll"), "") save(self, os.path.join(bindirs, "libssl-3-x64.dll"), "") save(self, os.path.join(libdirs, "libcrypto.lib"), "") save(self, os.path.join(libdirs, "libssl.lib"), "") def package_info(self): self.cpp_info.components["crypto"].libs = ["libcrypto"] self.cpp_info.components["ssl"].libs = ["libssl"] """) libcurl = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Example(ConanFile): name = "libcurl" version = "1.0" options = {"shared": [True, False]} default_options = {"shared": False} def package(self): bindirs = os.path.join(self.package_folder, "bin") libdirs = os.path.join(self.package_folder, "lib") save(self, os.path.join(bindirs, "libcurl.dll"), "") save(self, os.path.join(libdirs, "libcurl_imp.lib"), "") def package_info(self): self.cpp_info.components["curl"].libs = ["libcurl_imp"] """) consumer = textwrap.dedent(""" [requires] zlib/1.0 openssl/1.0 libcurl/1.0 libiconv/1.0 [options] *:shared=True """) c.save({"conanfile.txt": consumer, "zlib/conanfile.py": zlib, "openssl/conanfile.py": openssl, "libcurl/conanfile.py": libcurl, "libiconv/conanfile.py": libiconv }) c.run("export-pkg zlib -o:a shared=True") c.run("export-pkg openssl -o:a shared=True") c.run("export-pkg libcurl -o:a shared=True") c.run("export-pkg libiconv -o:a shared=True") c.run("install . -g BazelDeps") libcurl_bazel_build = load(None, os.path.join(c.current_folder, "libcurl", "BUILD.bazel")) zlib_bazel_build = load(None, os.path.join(c.current_folder, "zlib", "BUILD.bazel")) openssl_bazel_build = load(None, os.path.join(c.current_folder, "openssl", "BUILD.bazel")) libiconv_bazel_build = load(None, os.path.join(c.current_folder, "libiconv", "BUILD.bazel")) libcurl_expected = textwrap.dedent("""\ # Components precompiled libs cc_import( name = "libcurl_imp_precompiled", shared_library = "bin/libcurl.dll", interface_library = "lib/libcurl_imp.lib", ) """) openssl_expected = textwrap.dedent("""\ # Components precompiled libs cc_import( name = "libcrypto_precompiled", shared_library = "bin/libcrypto-3-x64.dll", interface_library = "lib/libcrypto.lib", ) cc_import( name = "libssl_precompiled", shared_library = "bin/libssl-3-x64.dll", interface_library = "lib/libssl.lib", ) """) zlib_expected = textwrap.dedent("""\ cc_import( name = "zdll_precompiled", shared_library = "bin/zlib1.dll", interface_library = "lib/zdll.lib", ) """) iconv_expected = textwrap.dedent("""\ cc_import( name = "iconv_precompiled", shared_library = "bin/iconv-2.dll", interface_library = "lib/iconv.lib", ) """) charset_expected = textwrap.dedent("""\ cc_import( name = "charset_precompiled", shared_library = "bin/charset-1.dll", interface_library = "lib/charset.lib", ) """) assert libcurl_expected in libcurl_bazel_build assert zlib_expected in zlib_bazel_build assert openssl_expected in openssl_bazel_build assert iconv_expected in libiconv_bazel_build and charset_expected in libiconv_bazel_build def test_pkg_with_duplicated_component_requires(): """ Testing that even having duplicated component requires, the PC does not include them. Issue: https://github.com/conan-io/conan/issues/18283 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.components["mycomponent"].libs = [] self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") # Duplicate one self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=mylib --version=0.1") client.save({"conanfile.py": GenConanfile("pkg", "0.1").with_require("mylib/0.1")}, clean_first=True) client.run("install . -g BazelDeps") build_content = load(None, os.path.join(client.current_folder, "mylib", "BUILD.bazel")) myfirstcomp_expected = textwrap.dedent("""\ cc_library( name = "mylib-myfirstcomp", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], visibility = ["//visibility:public"], deps = [ # do not sort ":mylib-mycomponent", ], ) """) assert myfirstcomp_expected in build_content @pytest.mark.skipif(platform.system() == "Windows", reason="Unix paths only") def test_apple_frameworks_and_frameworkdirs(): """ Testing that Apple frameworks are included as linkopts Issue: https://github.com/conan-io/conan/issues/18748 """ client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class PkgConfigConan(ConanFile): def package_info(self): self.cpp_info.frameworks = ["CoreFoundation", "Cocoa"] self.cpp_info.frameworkdirs = ["/my/path/to/frw1"] """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . --name=mylib --version=0.1") client.save({"conanfile.py": GenConanfile("pkg", "0.1").with_require("mylib/0.1")}, clean_first=True) client.run("install . -g BazelDeps") build_content = load(None, os.path.join(client.current_folder, "mylib", "BUILD.bazel")) build_file_expected = textwrap.dedent("""\ cc_library( name = "mylib", hdrs = glob([ "include/**", ], allow_empty = True ), includes = [ "include", ], linkopts = [ "-framework", "CoreFoundation", "-framework", "Cocoa", "-F", "/my/path/to/frw1", ], visibility = ["//visibility:public"], ) """) assert build_file_expected in build_content def test_shared_libs_and_unix_includes_rpath(): """ Testing the RPATH flag is added to the BUILD.bazel file if these conditions are given: shared library and UNIX systems. Issue: https://github.com/conan-io/conan/issues/19190 Issue: https://github.com/conan-io/conan/issues/19135 """ client = TestClient() csm = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "csm" version = "1.0" settings = "os" options = {"shared": [True, False]} default_options = {"shared": False} def package(self): if self.settings.os == "Windows": save(self, os.path.join(self.package_folder, "bin", "csmapi.dll"), "") save(self, os.path.join(self.package_folder, "lib", "csmapi.lib"), "") else: save(self, os.path.join(self.package_folder, "lib", "libcsmapi.so"), "") def package_info(self): self.cpp_info.libs = ["csmapi"] """) consumer = textwrap.dedent(""" [requires] csm/1.0 [options] *:shared=True """) client.save({"conanfile.txt": consumer, "csm/conanfile.py": csm}) client.run("export-pkg csm -o '*:shared=True' -s 'os=Windows'") client.run("export-pkg csm -o '*:shared=True' -s 'os=Linux'") client.run("install . -g BazelDeps -s 'os=Windows'") build_content = load(None, os.path.join(client.current_folder, "csm", "BUILD.bazel")) assert '"-Wl,-rpath,' not in build_content client.run("install . -g BazelDeps -s 'os=Linux'") build_content = load(None, os.path.join(client.current_folder, "csm", "BUILD.bazel")) assert re.search('"-Wl,-rpath,.*/p/lib"', build_content.replace("\\", "/")) ================================================ FILE: test/integration/toolchains/google/test_bazeltoolchain.py ================================================ import os import textwrap import pytest from conan.tools.files import load from conan.tools.google import BazelToolchain from conan.test.utils.tools import TestClient @pytest.fixture(scope="module") def conanfile(): return textwrap.dedent(""" from conan import ConanFile from conan.tools.google import BazelToolchain, bazel_layout class ExampleConanIntegration(ConanFile): settings = "os", "arch", "build_type", "compiler" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} generators = 'BazelToolchain' def layout(self): bazel_layout(self) """) def test_default_bazel_toolchain(conanfile): profile = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=13.0 os=Macos """) c = TestClient() c.save({"conanfile.py": conanfile, "profile": profile}) c.run("install . -pr profile") content = load(c, os.path.join(c.current_folder, "conan", BazelToolchain.bazelrc_name)) assert "build:conan-config --cxxopt=-std=gnu++17" in content assert "build:conan-config --force_pic=True" in content assert "build:conan-config --dynamic_mode=off" in content assert "build:conan-config --compilation_mode=opt" in content def test_bazel_toolchain_and_flags(conanfile): profile = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=13.0 os=Macos [options] shared=True [conf] tools.build:cxxflags=["--flag1", "--flag2"] tools.build:cflags+=["--flag3", "--flag4"] tools.build:sharedlinkflags+=["--flag5"] tools.build:exelinkflags+=["--flag6"] tools.build:linker_scripts+=["myscript.sh"] """) c = TestClient() c.save({"conanfile.py": conanfile, "profile": profile}) c.run("install . -pr profile") content = load(c, os.path.join(c.current_folder, "conan", BazelToolchain.bazelrc_name)) assert "build:conan-config --conlyopt=--flag3 --conlyopt=--flag4" in content assert "build:conan-config --cxxopt=-std=gnu++17 --cxxopt=--flag1 --cxxopt=--flag2" in content assert "build:conan-config --linkopt=--flag5 --linkopt=--flag6 --linkopt=-T'myscript.sh'" in content assert "build:conan-config --force_pic=True" not in content assert "build:conan-config --dynamic_mode=fully" in content assert "build:conan-config --compilation_mode=opt" in content def test_bazel_toolchain_and_cross_compilation(conanfile): profile = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=13.0 os=Macos """) profile_host = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=13.0 os=Macos """) c = TestClient() c.save({"conanfile.py": conanfile, "profile": profile, "profile_host": profile_host}) c.run("install . -pr:b profile -pr:h profile_host") content = load(c, os.path.join(c.current_folder, "conan", BazelToolchain.bazelrc_name)) assert "build:conan-config --cpu=darwin_arm64" in content def test_toolchain_attributes_and_conf_priority(): """ Tests that all the attributes are appearing correctly in the conan_bzl.rc even defining some conf variables """ profile = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=13.0 os=Macos [conf] tools.build:cxxflags=["--flag1"] tools.build:cflags+=["--flag3"] tools.build:sharedlinkflags+=["--linkflag5"] tools.build:exelinkflags+=["--linkflag6"] """) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.google import BazelToolchain class ExampleConanIntegration(ConanFile): settings = "os", "arch", "build_type", "compiler" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def generate(self): bz = BazelToolchain(self) bz.copt = ["copt1"] bz.conlyopt = ["conly1"] bz.cxxopt = ["cxxopt1"] bz.linkopt = ["linkopt1"] bz.force_pic = True bz.dynamic_mode = "auto" bz.compilation_mode = "fastbuild" bz.compiler = "gcc" bz.cpu = "armv8" bz.crosstool_top = "my_crosstool" bz.generate() """) c = TestClient() c.save({"conanfile.py": conanfile, "profile": profile}) c.run("install . -pr profile") content = load(c, os.path.join(c.current_folder, BazelToolchain.bazelrc_name)) expected = textwrap.dedent("""\ # Automatic bazelrc file created by Conan build:conan-config --copt=copt1 build:conan-config --conlyopt=conly1 --conlyopt=--flag3 build:conan-config --cxxopt=-std=gnu++17 --cxxopt=cxxopt1 --cxxopt=--flag1 build:conan-config --linkopt=linkopt1 --linkopt=--linkflag5 --linkopt=--linkflag6 build:conan-config --force_pic=True build:conan-config --dynamic_mode=auto build:conan-config --compilation_mode=fastbuild build:conan-config --compiler=gcc build:conan-config --cpu=armv8 build:conan-config --crosstool_top=my_crosstool""") assert expected == content ================================================ FILE: test/integration/toolchains/intel/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/intel/test_intel_cc.py ================================================ import os import platform import textwrap import pytest from conan.test.utils.tools import TestClient conanfile = textwrap.dedent("""\ [generators] IntelCC """) intelprofile = textwrap.dedent("""\ [settings] os=%s arch=x86_64 compiler=intel-cc compiler.mode=dpcpp compiler.version=2021.3 compiler.libcxx=libstdc++ build_type=Release [conf] tools.intel:installation_path=%s """) def get_intel_cc_generator_file(os_, installation_path, filename): profile = intelprofile % (os_, installation_path) client = TestClient() client.save({ "conanfile.txt": conanfile, "intelprofile": profile, }) client.run("install . -pr intelprofile") return client.load(filename) @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_intel_cc_generator_windows(): os_ = "Windows" installation_path = "C:\\Program Files (x86)\\Intel\\oneAPI" conanintelsetvars = get_intel_cc_generator_file(os_, installation_path, "conanintelsetvars.bat") expected = textwrap.dedent("""\ @echo off call "C:\\Program Files (x86)\\Intel\\oneAPI\\setvars.bat" intel64 """) assert conanintelsetvars == expected @pytest.mark.skipif(platform.system() != "Linux", reason="Requires Linux") def test_intel_cc_generator_linux(): os_ = "Linux" installation_path = "/opt/intel/oneapi" conanintelsetvars = get_intel_cc_generator_file(os_, installation_path, "conanintelsetvars.sh") expected = '. "/opt/intel/oneapi/setvars.sh" intel64' assert conanintelsetvars == expected def test_avoid_generation(): # The empty string for tools.intel:installation_path avoids the generation profile = intelprofile % ("Linux", "") client = TestClient() client.save({ "conanfile.txt": conanfile, "intelprofile": profile, }) client.run("install . -pr intelprofile") files = os.listdir(client.current_folder) assert "conanintelsetvars" not in ",".join(files) ================================================ FILE: test/integration/toolchains/meson/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/meson/test_mesontoolchain.py ================================================ import os import platform import re import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.tools.meson import MesonToolchain def test_apple_meson_keep_user_custom_flags(): default = textwrap.dedent(""" [settings] os=Macos arch=x86_64 compiler=apple-clang compiler.version=12.0 compiler.libcxx=libc++ build_type=Release """) cross = textwrap.dedent(""" [settings] os = iOS os.version = 10.0 os.sdk = iphoneos arch = armv8 compiler = apple-clang compiler.version = 12.0 compiler.libcxx = libc++ [conf] tools.apple:sdk_path=/my/sdk/path """) _conanfile_py = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class App(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): tc = MesonToolchain(self) # Customized apple flags tc.apple_arch_flag = ['-arch', 'myarch'] tc.apple_isysroot_flag = ['-isysroot', '/other/sdk/path'] tc.apple_min_version_flag = ['-otherminversion=10.7'] tc.generate() """) t = TestClient() t.save({"conanfile.py": _conanfile_py, "build_prof": default, "host_prof": cross}) t.run("install . -pr:h host_prof -pr:b build_prof") # Checking that the global conanbuild aggregator includes conanbuildenv-xxx file # it should have been generated by implicit VirtualBuildEnv generator env_file = t.load("conanbuild.sh") assert "conanbuildenv" in env_file content = t.load(MesonToolchain.cross_filename) assert "c_args = ['-isysroot', '/other/sdk/path', '-arch', 'myarch', '-otherminversion=10.7']" in content assert "c_link_args = ['-isysroot', '/other/sdk/path', '-arch', 'myarch', '-otherminversion=10.7']" in content assert "cpp_args = ['-isysroot', '/other/sdk/path', '-arch', 'myarch', '-otherminversion=10.7', '-stdlib=libc++']" in content assert "cpp_link_args = ['-isysroot', '/other/sdk/path', '-arch', 'myarch', '-otherminversion=10.7', '-stdlib=libc++']" in content def test_apple_meson_cross_building_subsystem(): """ Issue related: https://github.com/conan-io/conan/issues/17873 """ default = textwrap.dedent(""" [settings] os=Macos arch=x86_64 compiler=apple-clang compiler.version=12.0 compiler.libcxx=libc++ build_type=Release """) cross = textwrap.dedent(""" [settings] os = iOS os.version = 10.0 os.sdk = iphoneos arch = armv8 compiler = apple-clang compiler.version = 12.0 compiler.libcxx = libc++ [conf] tools.apple:sdk_path=/my/sdk/path """) t = TestClient() t.save({"conanfile.py": GenConanfile(name="app", version="1.0") .with_settings("os", "arch", "compiler", "build_type") .with_generator("MesonToolchain"), "build": default, "host": cross}) t.run("install . -pr:h host -pr:b build") content = t.load(MesonToolchain.cross_filename) machines_settings = textwrap.dedent("""\ [build_machine] system = 'darwin' subsystem = 'macos' cpu_family = 'x86_64' cpu = 'x86_64' endian = 'little' [host_machine] system = 'darwin' subsystem = 'ios' cpu_family = 'aarch64' cpu = 'armv8' endian = 'little'""") assert machines_settings in content # Let's check that it does not appear if cross-compiling to other non-Apple-OS cross = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=13 os=Linux """) t.save({"host": cross}) t.run("install . -pr:h host -pr:b build") content = t.load(MesonToolchain.cross_filename) assert "subsystem =" not in content def test_extra_flags_via_conf(): profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++ build_type=Release [buildenv] CFLAGS=-flag0 -other=val CXXFLAGS=-flag0 -other=val LDFLAGS=-flag0 -other=val [conf] tools.build:cxxflags=["-flag1", "-flag2"] tools.build:cflags=["-flag3", "-flag4"] tools.build:sharedlinkflags+=["-flag5"] tools.build:exelinkflags+=["-flag6"] # Issue related: https://github.com/conan-io/conan/issues/16169 tools.build:defines=["define1=0"] """) t = TestClient() t.save({"conanfile.txt": "[generators]\nMesonToolchain", "profile": profile}) t.run("install . -pr:h=profile -pr:b=profile") content = t.load(MesonToolchain.native_filename) assert "cpp_args = ['-flag0', '-other=val', '-m64', '-flag1', '-flag2', '-Ddefine1=0', '-D_GLIBCXX_USE_CXX11_ABI=0']" in content assert "c_args = ['-flag0', '-other=val', '-m64', '-flag3', '-flag4', '-Ddefine1=0']" in content assert "c_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6']" in content assert "cpp_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6']" in content def test_extra_flags_via_toolchain(): profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++ build_type=Release [buildenv] CFLAGS=-flag0 -other=val CXXFLAGS=-flag0 -other=val LDFLAGS=-flag0 -other=val """) t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.extra_cxxflags = ["-flag1", "-flag2"] tc.extra_cflags = ["-flag3", "-flag4"] tc.extra_ldflags = ["-flag5", "-flag6"] tc.extra_defines = ["define1=0"] tc.generate() """) t.save({"conanfile.py": conanfile, "profile": profile}) t.run("install . -pr:h=profile -pr:b=profile") content = t.load(MesonToolchain.native_filename) assert "cpp_args = ['-flag0', '-other=val', '-m64', '-flag1', '-flag2', '-Ddefine1=0', '-D_GLIBCXX_USE_CXX11_ABI=0']" in content assert "c_args = ['-flag0', '-other=val', '-m64', '-flag3', '-flag4', '-Ddefine1=0']" in content assert "c_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6']" in content assert "cpp_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6']" in content def test_custom_arch_flag_via_toolchain(): t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.arch_flag = "-mmy-flag" tc.generate() """) t.save({"conanfile.py": conanfile}) t.run("install .") content = t.load(MesonToolchain.native_filename) assert re.search(r"c_args =.+-mmy-flag.+", content) assert re.search(r"c_link_args =.+-mmy-flag.+", content) assert re.search(r"cpp_args =.+-mmy-flag.+", content) assert re.search(r"cpp_link_args =.+-mmy-flag.+", content) def test_linker_scripts_via_conf(): profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++ build_type=Release [buildenv] LDFLAGS=-flag0 -other=val [conf] tools.build:sharedlinkflags+=["-flag5"] tools.build:exelinkflags+=["-flag6"] tools.build:linker_scripts+=["/linker/scripts/flash.ld", "/linker/scripts/extra_data.ld"] """) t = TestClient() t.save({"conanfile.txt": "[generators]\nMesonToolchain", "profile": profile}) t.run("install . -pr:b=profile -pr=profile") content = t.load(MesonToolchain.native_filename) assert ("c_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6', " "'-T/linker/scripts/flash.ld', '-T/linker/scripts/extra_data.ld']") in content assert ("cpp_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6', " "'-T/linker/scripts/flash.ld', '-T/linker/scripts/extra_data.ld']") in content def test_correct_quotes(): profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++11 build_type=Release """) t = TestClient() t.save({"conanfile.txt": "[generators]\nMesonToolchain", "profile": profile}) t.run("install . -pr:h=profile -pr:b=profile") content = t.load(MesonToolchain.native_filename) assert "cpp_std = 'c++17'" in content assert "backend = 'ninja'" in content assert "buildtype = 'release'" in content def test_c_std(): profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=9 compiler.cstd=11 build_type=Release """) t = TestClient() t.save({"conanfile.py": GenConanfile().with_settings("os", "compiler", "build_type", "arch") .with_generator("MesonToolchain") .with_class_attribute("languages='C'"), "profile": profile}) t.run("install . -pr:h=profile -pr:b=profile") content = t.load(MesonToolchain.native_filename) assert "c_std = 'c11'" in content assert "backend = 'ninja'" in content assert "buildtype = 'release'" in content def test_deactivate_nowrap(): # https://github.com/conan-io/conan/issues/10671 t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.project_options.pop("wrap_mode") tc.generate() """) t.save({"conanfile.py": conanfile}) t.run("install .") content = t.load(MesonToolchain.native_filename) assert "wrap_mode " not in content assert "nofallback" not in content @pytest.mark.skipif(platform.system() != "Windows", reason="requires Win") @pytest.mark.parametrize("build_type,runtime,vscrt", [ ("Debug", "dynamic", "mdd"), ("Debug", "static", "mtd"), ("Release", "dynamic", "md"), ("Release", "static", "mt") ]) def test_clang_cl_vscrt(build_type, runtime, vscrt): profile = textwrap.dedent(f""" [settings] os=Windows arch=x86_64 build_type={build_type} compiler=clang compiler.runtime={runtime} compiler.runtime_version=v143 compiler.version=16 [conf] tools.cmake.cmaketoolchain:generator=Visual Studio 17 [buildenv] CC=clang-cl CXX=clang-cl """) t = TestClient() t.save({"conanfile.txt": "[generators]\nMesonToolchain", "profile": profile}) t.run("install . -pr:h=profile -pr:b=profile") content = t.load(MesonToolchain.native_filename) assert f"b_vscrt = '{vscrt}'" in content def test_env_vars_from_build_require(): conanfile = textwrap.dedent(""" from conan import ConanFile import os class HelloConan(ConanFile): name = 'hello_compiler' version = '1.0' def package_info(self): self.buildenv_info.define("CC", "CC_VALUE") self.buildenv_info.define("CC_LD", "CC_LD_VALUE") self.buildenv_info.define("CXX", "CXX_VALUE") self.buildenv_info.define("CXX_LD", "CXX_LD_VALUE") self.buildenv_info.define("AR", "AR_VALUE") self.buildenv_info.define("STRIP", "STRIP_VALUE") self.buildenv_info.define("AS", "AS_VALUE") self.buildenv_info.define("WINDRES", "WINDRES_VALUE") self.buildenv_info.define("PKG_CONFIG", "PKG_CONFIG_VALUE") self.buildenv_info.define("LD", "LD_VALUE") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create .") conanfile = textwrap.dedent(""" from conan import ConanFile class HelloConan(ConanFile): name = 'consumer' version = '1.0' generators = "MesonToolchain" settings = "os", "arch", "compiler", "build_type" def requirements(self): self.tool_requires("hello_compiler/1.0", ) """) # Now, let's check how all the build env variables are applied at consumer side client.save({"conanfile.py": conanfile}) client.run("install . -pr:h=default -pr:b=default") content = client.load("conan_meson_native.ini") assert "c = 'CC_VALUE'" in content assert "cpp = 'CXX_VALUE'" in content assert "ld = 'LD_VALUE'" in content assert "c_ld = 'CC_LD_VALUE'" in content assert "cpp_ld = 'CXX_LD_VALUE'" in content assert "ar = 'AR_VALUE'" in content assert "strip = 'STRIP_VALUE'" in content assert "as = 'AS_VALUE'" in content assert "windres = 'WINDRES_VALUE'" in content assert "pkgconfig = 'PKG_CONFIG_VALUE'" in content assert "pkg-config = 'PKG_CONFIG_VALUE'" in content def test_check_c_cpp_ld_list_formats(): # Issue related: https://github.com/conan-io/conan/issues/14028 profile = textwrap.dedent(""" [settings] os=Windows arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++11 build_type=Release [buildenv] CC=aarch64-poky-linux-gcc -mcpu=cortex-a53 -march=armv8-a+crc+crypto CXX=aarch64-poky-linux-g++ -mcpu=cortex-a53 -march=armv8-a+crc+crypto LD=aarch64-poky-linux-ld --sysroot=/opt/sysroots/cortexa53-crypto-poky-linux """) t = TestClient() t.save({"conanfile.txt": "[generators]\nMesonToolchain", "profile": profile}) t.run("install . -pr:h=profile -pr:b=profile") content = t.load(MesonToolchain.native_filename) assert "c = ['aarch64-poky-linux-gcc', '-mcpu=cortex-a53', '-march=armv8-a+crc+crypto']" in content assert "cpp = ['aarch64-poky-linux-g++', '-mcpu=cortex-a53', '-march=armv8-a+crc+crypto']" in content assert "ld = ['aarch64-poky-linux-ld', '--sysroot=/opt/sysroots/cortexa53-crypto-poky-linux']" in content def test_check_pkg_config_paths(): # Issue: https://github.com/conan-io/conan/issues/12342 # Issue: https://github.com/conan-io/conan/issues/14935 t = TestClient() t.save({"conanfile.txt": "[generators]\nMesonToolchain"}) t.run("install .") content = t.load(MesonToolchain.native_filename) assert f"pkg_config_path = '{t.current_folder}'" in content assert f"build.pkg_config_path = " not in content conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.build_pkg_config_path = os.path.join(self.generators_folder, "build") tc.generate() """) t.save({"conanfile.py": conanfile}, clean_first=True) t.run("install .") content = t.load(MesonToolchain.native_filename) base_folder = t.current_folder assert f"pkg_config_path = '{base_folder}'" in content assert f"build.pkg_config_path = '{os.path.join(base_folder, 'build')}'" in content def test_toolchain_and_compilers_build_context(): """ Tests how MesonToolchain manages the build context profile if the build profile is specifying another compiler path (using conf). It should create both native and cross files. Issue related: https://github.com/conan-io/conan/issues/15878 """ host = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [conf] tools.build:compiler_executables={"c": "gcc", "cpp": "g++"} """) build = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=libc++ compiler.cppstd=11 [conf] tools.build:compiler_executables={"c": "clang", "cpp": "clang++"} """) tool = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class toolRecipe(ConanFile): name = "tool" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "MesonToolchain" def build(self): toolchain = os.path.join(self.generators_folder, "conan_meson_native.ini") content = load(self, toolchain) assert "c = 'clang'" in content assert "cpp = 'clang++'" in content """) consumer = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class consumerRecipe(ConanFile): name = "consumer" version = "1.0" # Binary configuration settings = "os", "compiler", "build_type", "arch" generators = "MesonToolchain" tool_requires = "tool/1.0" def build(self): toolchain = os.path.join(self.generators_folder, "conan_meson_cross.ini") content = load(self, toolchain) assert "c = 'gcc'" in content assert "cpp = 'g++'" in content """) client = TestClient() client.save({ "host": host, "build": build, "tool/conanfile.py": tool, "consumer/conanfile.py": consumer }) client.run("export tool") client.run("create consumer -pr:h host -pr:b build --build=missing") def test_subproject_options(): t = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.subproject_options["subproject1"] = [{"option1": "enabled"}, {"option2": "disabled"}] tc.subproject_options["subproject2"] = [{"option3": "enabled"}] tc.subproject_options["subproject2"].append({"option4": "disabled"}) tc.generate() """) t.save({"conanfile.py": conanfile}) t.run("install .") content = t.load(MesonToolchain.native_filename) assert "[subproject1:project options]" in content assert "[subproject2:project options]" in content assert "option1 = 'enabled'" in content assert "option2 = 'disabled'" in content assert "option3 = 'enabled'" in content assert "option4 = 'disabled'" in content def test_native_attribute(): """ Tests that native file only has the build context (not as a build require) content. """ host = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [buildenv] STRIP=False PKG_CONFIG=/usr/bin/pkg-config [conf] tools.build:compiler_executables={"c": "gcc", "cpp": "g++"} """) build = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=clang compiler.version=12 compiler.libcxx=libc++ compiler.cppstd=11 [buildenv] STRIP=True PKG_CONFIG=/usr/lib/meson/pkgconfig [conf] tools.build:compiler_executables={"c": "clang", "cpp": "clang++"} """) client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.generate() # We're cross-building, no need to check it tc = MesonToolchain(self, native=True) tc.generate() """) client.save({"host": host, "build": build, "conanfile.py": conanfile}) client.run("install . -pr:h host -pr:b build") native_content = client.load(MesonToolchain.native_filename) cross_content = client.load(MesonToolchain.cross_filename) expected_native = textwrap.dedent(""" [binaries] c = 'clang' cpp = 'clang++' strip = 'True' pkgconfig = '/usr/lib/meson/pkgconfig' pkg-config = '/usr/lib/meson/pkgconfig' """) expected_cross = textwrap.dedent(""" [binaries] c = 'gcc' cpp = 'g++' strip = 'False' pkgconfig = '/usr/bin/pkg-config' pkg-config = '/usr/bin/pkg-config' """) assert expected_native in native_content assert "[host_machine]" not in native_content assert "[build_machine]" not in native_content assert expected_cross in cross_content assert "[build_machine]" in cross_content assert "[host_machine]" in cross_content def test_native_attribute_error(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.generate() tc = MesonToolchain(self, native=True) tc.generate() """) client.save({"conanfile.py": conanfile}) client.run("install .", assert_error=True) assert "You can only pass native=True if you're cross-building" in client.out def test_compiler_path_with_spaces(): profile = textwrap.dedent(""" include(default) [conf] tools.build:compiler_executables={"c":"c compiler path with spaces", "cpp":"cpp compiler path with spaces"} """) client = TestClient() client.save( {"conanfile.py": GenConanfile().with_generator("MesonToolchain").with_settings("compiler"), "meson-profile": profile}) client.run("install . -pr:h=meson-profile") conan_meson_native = client.load("conan_meson_native.ini") assert "c = 'c compiler path with spaces'" in conan_meson_native assert "cpp = 'cpp compiler path with spaces'" in conan_meson_native def test_meson_sysroot_app(): """Testing when users pass tools.build:sysroot on the profile with Meson * The generated conan_meson_cross.ini does not fill the "sys_root" property (see https://github.com/conan-io/conan/issues/16468) * It adds the compiler flags with --sysroot. When cross-building, Meson needs both compiler_executables in the config, otherwise it will fail when running setup. """ sysroot = "/my/new/sysroot/path" client = TestClient() profile = textwrap.dedent(f""" [settings] os = Macos arch = armv8 compiler = apple-clang compiler.version = 13.0 compiler.libcxx = libc++ [conf] tools.build:sysroot={sysroot} tools.build:verbosity=verbose tools.compilation:verbosity=verbose tools.apple:sdk_path=/my/sdk/path """) profile_build = textwrap.dedent(f""" [settings] os = Macos arch = x86_64 compiler = apple-clang compiler.version = 13.0 compiler.libcxx = libc++ """) client.save({"conanfile.py": GenConanfile(name="hello", version="0.1") .with_settings("os", "arch", "compiler", "build_type") .with_generator("MesonToolchain"), "build": profile_build, "host": profile}) client.run("install . -pr:h host -pr:b build") # Check the meson configuration file conan_meson = client.load("conan_meson_cross.ini") assert f"sys_root = '{sysroot}'\n" not in conan_meson assert re.search(r"c_args =.+--sysroot={}.+".format(sysroot), conan_meson) assert re.search(r"c_link_args =.+--sysroot={}.+".format(sysroot), conan_meson) assert re.search(r"cpp_args =.+--sysroot={}.+".format(sysroot), conan_meson) assert re.search(r"cpp_link_args =.+--sysroot={}.+".format(sysroot), conan_meson) def test_cross_x86_64_to_x86(): """ https://github.com/conan-io/conan/issues/17261 """ c = TestClient() c.save({"conanfile.py": GenConanfile().with_settings("os", "compiler", "arch", "build_type")}) c.run("install . -g MesonToolchain -s arch=x86 -s:b arch=x86_64") assert not os.path.exists(os.path.join(c.current_folder, MesonToolchain.native_filename)) cross = c.load(MesonToolchain.cross_filename) assert "cpu = 'x86_64'" in cross # This is the build machine assert "cpu = 'x86'" in cross # This is the host machine def test_cross_x86_64_to_riscv32(): """ https://github.com/conan-io/conan/issues/18490 """ c = TestClient() c.save({"conanfile.py": GenConanfile().with_settings("os", "compiler", "arch", "build_type")}) c.run("install . -g MesonToolchain -s os=Linux -s arch=riscv32 -s:b arch=x86_64") assert not os.path.exists(os.path.join(c.current_folder, MesonToolchain.native_filename)) cross = c.load(MesonToolchain.cross_filename) assert "cpu = 'x86_64'" in cross # This is the build machine assert "cpu = 'riscv32'" in cross # This is the host machine def test_cross_x86_64_to_riscv64(): """ https://github.com/conan-io/conan/issues/18490 """ c = TestClient() c.save({"conanfile.py": GenConanfile().with_settings("os", "compiler", "arch", "build_type")}) c.run("install . -g MesonToolchain -s os=Linux -s arch=riscv64 -s:b arch=x86_64") assert not os.path.exists(os.path.join(c.current_folder, MesonToolchain.native_filename)) cross = c.load(MesonToolchain.cross_filename) assert "cpu = 'x86_64'" in cross # This is the build machine assert "cpu = 'riscv64'" in cross # This is the host machine def test_conf_extra_apple_flags(): host = textwrap.dedent(""" [settings] arch=x86_64 os=Macos compiler=apple-clang compiler.version=14.0 [conf] tools.apple:enable_bitcode = True tools.apple:enable_arc = True tools.apple:enable_visibility = True """) c = TestClient() c.save({"conanfile.txt": f"[generators]\nMesonToolchain", "host": host}) c.run("install . -pr:a host") f = "conan_meson_native.ini" tc = c.load(f) for flags in ["c_args", "cpp_args", "c_link_args", "cpp_link_args"]: assert f"{flags} = ['-m64', '-fembed-bitcode', '-fvisibility=default']" in tc for flags in ["objcpp_args", "objc_args"]: assert f"{flags} = ['-fobjc-arc', '-m64', '-fembed-bitcode', '-fvisibility=default']" in tc c.run("install . -pr:a host -s build_type=Debug") tc = c.load(f) for flags in ["c_args", "cpp_args", "c_link_args", "cpp_link_args"]: assert f"{flags} = ['-m64', '-fembed-bitcode-marker', '-fvisibility=default']" in tc for flags in ["objcpp_args", "objc_args"]: assert f"{flags} = ['-fobjc-arc', '-m64', '-fembed-bitcode-marker', '-fvisibility=default']" in tc host = textwrap.dedent(""" [settings] arch=x86_64 os=Macos compiler=apple-clang compiler.version=14.0 [conf] tools.apple:enable_bitcode = False tools.apple:enable_arc = False tools.apple:enable_visibility = False """) c.save({"host": host}) c.run("install . -pr:a host") tc = c.load(f) for flags in ["c_args", "cpp_args", "c_link_args", "cpp_link_args"]: assert f"{flags} = ['-m64', '-fvisibility=hidden', '-fvisibility-inlines-hidden']" in tc for flags in ["objcpp_args", "objc_args"]: assert f"{flags} = ['-fno-objc-arc', '-m64', '-fvisibility=hidden', '-fvisibility-inlines-hidden']" in tc @pytest.mark.parametrize( "threads, flags", [("posix", "-pthread"), ("wasm_workers", "-sWASM_WORKERS=1")], ) def test_thread_flags(threads, flags): client = TestClient() profile = textwrap.dedent(f""" [settings] arch=wasm build_type=Release compiler=emcc compiler.cppstd=17 compiler.threads={threads} compiler.libcxx=libc++ compiler.version=4.0.10 os=Emscripten """) client.save( { "conanfile.py": GenConanfile("pkg", "1.0") .with_settings("os", "arch", "compiler", "build_type") .with_generator("MesonToolchain"), "profile": profile, } ) client.run("install . -pr=./profile") toolchain = client.load("conan_meson_cross.ini") assert f"c_args = ['{flags}']" in toolchain assert f"c_link_args = ['{flags}']" in toolchain assert f"cpp_args = ['{flags}', '-stdlib=libc++']" in toolchain assert f"cpp_link_args = ['{flags}', '-stdlib=libc++']" in toolchain def test_new_public_attributes(): host = textwrap.dedent(f""" [settings] arch=armv8 build_type=Release compiler=gcc compiler.cppstd=gnu17 compiler.libcxx=libstdc++11 compiler.version=11 os=Linux [conf] tools.meson.mesontoolchain:backend=xcode """) client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.meson import MesonToolchain class Pkg(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): tc = MesonToolchain(self) tc.backend = "vs2022" # conf has more prio tc.b_staticpic = True tc.buildtype = "Debug" tc.default_library = "shared" tc.cpp_std="c++20" tc.c_std="c20" tc.b_vscrt="MD" tc.generate() """) client.save({"conanfile.py": conanfile, "host": host}) client.run("install . -pr:a host") content = client.load(MesonToolchain.native_filename) expected = textwrap.dedent("""\ buildtype = 'Debug' default_library = 'shared' b_vscrt = 'MD' b_ndebug = 'true' b_staticpic = true cpp_std = 'c++20' c_std = 'c20' backend = 'xcode' """) assert expected in content def test_needs_exe_wrapper(): """ Tests needs_exe_wrapper depends on `can_run()` function instead of simply checking the cross_building() one. Issue: https://github.com/conan-io/conan/issues/19217 """ host = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=16 os=Macos [conf] tools.apple:sdk_path=/my/sdk/path tools.build.cross_building:can_run=True """) build = textwrap.dedent(""" [settings] arch=armv8 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.libcxx=libc++ compiler.version=16 os=Macos """) client = TestClient() client.save({ "host": host, "build": build, "conanfile.py": GenConanfile("pkg", "1.0") .with_settings("os", "arch", "compiler", "build_type") .with_generator("MesonToolchain") }) client.run("install . -pr:h host -pr:b build") content = client.load(MesonToolchain.cross_filename) assert "needs_exe_wrapper = false" in content ================================================ FILE: test/integration/toolchains/microsoft/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/microsoft/test_msbuild_toolchain.py ================================================ import platform import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") @pytest.mark.parametrize( "compiler, version", [ ("msvc", "190"), ("msvc", "191"), ("clang", "16") ], ) @pytest.mark.parametrize("runtime", ["dynamic", "static"]) @pytest.mark.parametrize("runtime_type", ["Release", "Debug"]) def test_toolchain_win(compiler, version, runtime, runtime_type): client = TestClient(path_with_spaces=False) settings = {"compiler": compiler, "compiler.version": version, "compiler.cppstd": "14", "compiler.runtime": runtime, "compiler.runtime_type": runtime_type, "build_type": "Release", "arch": "x86_64"} if compiler == "clang": settings["compiler.runtime_version"] = "v144" # Build the profile according to the settings provided settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import MSBuildToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def generate(self): msbuild = MSBuildToolchain(self) msbuild.properties["IncludeExternals"] = "true" msbuild.generate() """) client.save({"conanfile.py": conanfile}) client.run(f"install . {settings} -c tools.microsoft.msbuild:installation_path=") props = client.load("conantoolchain_release_x64.props") assert "true" in props assert "stdcpp14" in props if compiler == "msvc": if version == "190": assert "v140" in props elif version == "191": assert "v141" in props elif compiler == "clang": assert "ClangCl" in props if runtime == "dynamic": if runtime_type == "Release": assert "MultiThreadedDLL" in props else: assert "MultiThreadedDebugDLL" in props else: if runtime_type == "Release": assert "MultiThreaded" in props else: assert "MultiThreadedDebug" in props ================================================ FILE: test/integration/toolchains/microsoft/test_msbuilddeps.py ================================================ import glob import os import platform import textwrap from xml.dom import minidom import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize( "arch,exp_platform", [ ("x86", "Win32"), ("x86_64", "x64"), ("armv7", "ARM"), ("armv8", "ARM64"), ], ) def test_msbuilddeps_maps_architecture_to_platform(arch, exp_platform): client = TestClient() client.run("new msbuild_lib -d name=hello -d version=0.1") client.run(f"install . -g MSBuildDeps -s arch={arch} -pr:b=default") toolchain = client.load(os.path.join("conan", "conantoolchain.props")) expected_import = f"""""" assert expected_import in toolchain @pytest.mark.parametrize( "build_type,arch,configuration,exp_platform", [ ("Release", "x86", "Release - Test", "Win32"), ("Debug", "x86_64", "Debug - Test", "x64"), ], ) @pytest.mark.parametrize( "config_key,platform_key", [ ("GlobalConfiguration", "GlobalPlatform"), (None, None), ], ) def test_msbuilddeps_import_condition(build_type, arch, configuration, exp_platform, config_key, platform_key): client = TestClient() app = textwrap.dedent(f""" from conan import ConanFile from conan.tools.microsoft import MSBuildDeps class App(ConanFile): requires = ("lib/0.1") settings = "arch", "build_type" options = {{"configuration": ["ANY"], "platform": ["Win32", "x64"]}} def generate(self): ms = MSBuildDeps(self) ms.configuration_key = "{config_key}" ms.configuration = self.options.configuration ms.platform_key = "{platform_key}" ms.platform = self.options.platform ms.generate() """) # Remove custom set of keys to test default values app = app.replace(f'ms.configuration_key = "{config_key}"', "") if config_key is None else app app = app.replace(f'ms.platform_key = "{platform_key}"', "") if platform_key is None else app config_key_expected = "Configuration" if config_key is None else config_key platform_key_expected = "Platform" if platform_key is None else platform_key client.save({"app/conanfile.py": app, "lib/conanfile.py": GenConanfile("lib", "0.1").with_package_type("header-library")}) client.run("create lib") client.run(f'install app -s build_type={build_type} -s arch={arch} -o *:platform={exp_platform} -o *:configuration="{configuration}"') assert os.path.exists(os.path.join(client.current_folder, "app", "conan_lib.props")) dep = client.load(os.path.join(client.current_folder, "app", "conan_lib.props")) expected_import = f"""""" assert expected_import in dep def test_msbuilddeps_format_names(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" name = "pkg.name-more+" version = "1.0" def package_info(self): self.cpp_info.components["libmpdecimal++"].libs = ["libmp++"] self.cpp_info.components["mycomp.some-comp+"].libs = ["mylib"] self.cpp_info.components["libmpdecimal++"].requires = ["mycomp.some-comp+"] """) c.save({"conanfile.py": conanfile}) c.run("create . -s arch=x86_64") # Issue: https://github.com/conan-io/conan/issues/11822 c.run("install --require=pkg.name-more+/1.0@ -g MSBuildDeps -s build_type=Release -s arch=x86_64") # Checking that MSBuildDeps builds correctly the XML file # loading all .props and xml parse them to check no errors pkg_more = c.load("conan_pkg_name-more_.props") assert "$(conan_pkg_name-more__libmpdecimal___props_imported)" in pkg_more assert "$(conan_pkg_name-more__mycomp_some-comp__props_imported)" in pkg_more some_comp = c.load("conan_pkg_name-more__mycomp_some-comp_.props") assert "" in some_comp libmpdecimal = c.load("conan_pkg_name-more__libmpdecimal__.props") assert "" in libmpdecimal libmpdecimal_release = c.load("conan_pkg_name-more__libmpdecimal___release_x64.props") assert "$(conan_pkg_name-more__mycomp_some-comp__props_imported)" in libmpdecimal_release counter = 0 for f in os.listdir(c.current_folder): if f.endswith(".props"): content = c.load(f) minidom.parseString(content) counter += 1 assert counter == 8 @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") class TestMSBuildDepsSkips: # https://github.com/conan-io/conan/issues/15624 def test_msbuilddeps_skipped_deps(self): c = TestClient() c.save({"liba/conanfile.py": GenConanfile("liba", "0.1").with_package_type("header-library"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_package_type("static-library") .with_requires("liba/0.1"), "app/conanfile.py": GenConanfile().with_requires("libb/0.1") .with_settings("arch", "build_type")}) c.run("create liba") c.run("create libb") c.run("install app -g MSBuildDeps") assert not os.path.exists(os.path.join(c.current_folder, "app", "conan_liba.props")) assert os.path.exists(os.path.join(c.current_folder, "app", "conan_libb.props")) libb = c.load("app/conan_libb.props") assert "conan_liba" not in libb libb = c.load("app/conan_libb_release_x64.props") assert "conan_liba" not in libb libb = c.load("app/conan_libb_vars_release_x64.props") assert "conan_liba" not in libb def test_msbuilddeps_skipped_deps_components(self): c = TestClient() libb = textwrap.dedent(""" from conan import ConanFile class Libb(ConanFile): name = "libb" version = "0.1" package_type = "static-library" requires = "liba/0.1" def package_info(self): self.cpp_info.components["mycomp"].libs = ["mycomplib"] self.cpp_info.components["mycomp"].requires = ["liba::liba"] """) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1").with_package_type("header-library"), "libb/conanfile.py": libb, "app/conanfile.py": GenConanfile().with_requires("libb/0.1") .with_settings("arch", "build_type")}) c.run("create liba") c.run("create libb") c.run("install app -g MSBuildDeps") assert not os.path.exists(os.path.join(c.current_folder, "app", "conan_liba.props")) assert os.path.exists(os.path.join(c.current_folder, "app", "conan_libb.props")) libb = c.load("app/conan_libb.props") assert "conan_liba" not in libb libb = c.load("app/conan_libb_mycomp.props") assert "conan_liba" not in libb libb = c.load("app/conan_libb_mycomp_release_x64.props") assert "conan_liba" not in libb libb = c.load("app/conan_libb_mycomp_vars_release_x64.props") assert "conan_liba" not in libb @pytest.mark.skipif(platform.system() != "Windows", reason="MSBuildDeps broken with POSIX paths") @pytest.mark.parametrize("withdepl", [False, True]) def test_msbuilddeps_relocatable(withdepl): c = TestClient() c.save({ "libh/conanfile.py": GenConanfile("libh", "0.1").with_package_type("header-library"), "libs/conanfile.py": GenConanfile("libs", "0.2").with_package_type("static-library") .with_requires("libh/0.1"), "libd/conanfile.py": GenConanfile("libd", "0.3").with_package_type("shared-library"), "app/conanfile.py": GenConanfile().with_requires("libh/0.1", "libs/0.2", "libd/0.3") .with_settings("arch", "build_type"), }) c.run("create libh") c.run("create libs") c.run("create libd") c.run("install app -g MSBuildDeps" + (" -d full_deploy" if withdepl else "")) for dep in ["libh", "libs", "libd"]: text = c.load(f"app/conan_{dep}_vars_release_x64.props") marker = f"Conan{dep}RootFolder" value = text.split(f"<{marker}>")[1].split(f"")[0] if withdepl: # path should be relative, since artifacts are moved along with project prefix = '$(MSBuildThisFileDirectory)\\' assert value.startswith(prefix) tail = value[len(prefix):] assert not os.path.isabs(tail) else: # path should be absolute, since conan cache does not move with project assert os.path.isabs(value) assert '$(' not in value if withdepl: # extra checks: no absolute paths allowed anywhere in props propsfiles = glob.glob(os.path.join(c.current_folder, "app/*.props")) assert len(propsfiles) > 0 for fn in propsfiles: text = c.load(fn) text = text.replace('\\', '/') folder = c.current_folder.replace('\\', '/') assert folder not in text def test_msbuilddeps_consume_meson(): c = TestClient() pkga = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import save class Pkg(ConanFile): name = "pkga" version = "0.1" package_type = "static-library" def package(self): save(self, os.path.join(self.package_folder, "lib", "libpkga.a"), "") def package_info(self): self.cpp_info.libs = ["pkga"] """) c.save({"pkga/conanfile.py": pkga, "app/conanfile.py": GenConanfile().with_requires("pkga/0.1") .with_settings("arch", "build_type")}) c.run("create pkga -s arch=x86_64") c.run("install app -g MSBuildDeps -s arch=x86_64") deps = c.load("app/conan_pkga_vars_release_x64.props") assert "libpkga.a;" in deps def test_build_requires_transitives_with_components(): c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep", "0.1") .with_package_type("shared-library") .with_package_info(cpp_info={ "components": { "core": {"libs": ["depcore"]}, "extra": {"requires": ["core"]} } }), "tool/conanfile.py": GenConanfile("tool", "0.1") .with_requires("dep/0.1") .with_package_info(cpp_info={ "components": { "a": {"requires": ["dep::core"]}, "b": {"requires": ["dep::dep"]} } }), "consumer/conanfile.py": GenConanfile().with_settings("os", "compiler", "build_type", "arch") .with_build_requires("tool/0.1")}) c.run("create dep -s arch=x86_64") c.run("create tool -s arch=x86_64") c.run("install consumer -g MSBuildDeps -of=. -s arch=x86_64") dep_extra = c.load("conan_dep_build_extra_release_x64.props") assert "conan_dep_build_core.props" in dep_extra assert "conan_dep_core.props" not in dep_extra tool_a = c.load("conan_tool_build_a_release_x64.props") assert "conan_dep_build_core.props" in tool_a assert "conan_dep_core.props" not in tool_a tool_b = c.load("conan_tool_build_b_release_x64.props") assert "conan_dep_build.props" in tool_b assert "conan_dep.props" not in tool_b ================================================ FILE: test/integration/toolchains/microsoft/test_msbuildtoolchain.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient def test_msbuildtoolchain_props_with_extra_flags(): """ Simple test checking that conantoolchain_release_x64.props is adding all the expected flags and preprocessor definitions """ profile = textwrap.dedent("""\ include(default) [settings] arch=x86_64 [conf] tools.build:cxxflags=["--flag1", "--flag2"] tools.build:cflags+=["--flag3", "--flag4"] tools.build:sharedlinkflags+=["--flag5"] tools.build:exelinkflags+=["--flag6"] tools.build:defines+=["DEF1", "DEF2"] """) client = TestClient() client.run("new msbuild_lib -d name=hello -d version=0.1") client.save({ "myprofile": profile }) # Local flow works client.run("install . -pr myprofile") toolchain = client.load(os.path.join("conan", "conantoolchain_release_x64.props")) expected_cl_compile = """ DEF1;DEF2;%(PreprocessorDefinitions) --flag1 --flag2 --flag3 --flag4 %(AdditionalOptions)""" expected_link = """ --flag5 --flag6 %(AdditionalOptions) """ expected_resource_compile = """ DEF1;DEF2;%(PreprocessorDefinitions) """ assert expected_cl_compile in toolchain assert expected_link in toolchain assert expected_resource_compile in toolchain def test_msbuildtoolchain_rcflags(): """Test that tools.build:rcflags is applied to ResourceCompile AdditionalOptions""" profile = textwrap.dedent("""\ include(default) [settings] arch=x86_64 [conf] tools.build:rcflags=["/flag-rc1", "/flag-rc2"] """) client = TestClient() client.run("new msbuild_lib -d name=hello -d version=0.1") client.save({"myprofile": profile}) client.run("install . -pr myprofile") toolchain = client.load(os.path.join("conan", "conantoolchain_release_x64.props")) expected_resource_compile = ("/flag-rc1 /flag-rc2 %(AdditionalOptions)" "") assert expected_resource_compile in toolchain ================================================ FILE: test/integration/toolchains/microsoft/test_nmakedeps.py ================================================ import platform import re import textwrap import pytest from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows") def test_nmakedeps(): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "arch", "compiler", "build_type" name = "test-nmakedeps" version = "1.0" def package_info(self): self.cpp_info.components["pkg-1"].libs = ["pkg-1"] self.cpp_info.components["pkg-1"].defines = ["TEST_DEFINITION1"] self.cpp_info.components["pkg-1"].system_libs = ["ws2_32"] self.cpp_info.components["pkg-2"].libs = ["pkg-2"] self.cpp_info.components["pkg-2"].defines = ["TEST_DEFINITION2=0"] self.cpp_info.components["pkg-2"].requires = ["pkg-1"] self.cpp_info.components["pkg-3"].libs = ["pkg-3"] self.cpp_info.components["pkg-3"].defines = ["TEST_DEFINITION3="] self.cpp_info.components["pkg-3"].requires = ["pkg-1", "pkg-2"] self.cpp_info.components["pkg-4"].libs = ["pkg-4"] self.cpp_info.components["pkg-4"].defines = ["TEST_DEFINITION4=foo", "TEST_DEFINITION5=__declspec(dllexport)", "TEST_DEFINITION6=foo bar", "TEST_DEFINITION7=7"] """) client.save({"conanfile.py": conanfile}) client.run("create . -s arch=x86_64") client.run("install --requires=test-nmakedeps/1.0" " -g NMakeDeps -s build_type=Release -s arch=x86_64") # Checking that NMakeDeps builds correctly .bat file bat_file = client.load("conannmakedeps.bat") # Checking that defines are added to CL for flag in ( r'/D"TEST_DEFINITION1"', '/D"TEST_DEFINITION2#0"', r'/D"TEST_DEFINITION3#"', '/D"TEST_DEFINITION4#"foo""', r'/D"TEST_DEFINITION5#"__declspec\(dllexport\)""', r'/D"TEST_DEFINITION6#"foo bar""', r'/D"TEST_DEFINITION7#7"' ): assert re.search(fr'set "CL=%CL%.*\s{flag}(?:\s|")', bat_file) # Checking that libs and system libs are added to _LINK_ for flag in (r"pkg-1\.lib", r"pkg-2\.lib", r"pkg-3\.lib", r"pkg-4\.lib", r"ws2_32\.lib"): assert re.search(fr'set "_LINK_=%_LINK_%.*\s{flag}(?:\s|")', bat_file) ================================================ FILE: test/integration/toolchains/microsoft/test_nmaketoolchain.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() != "Windows", reason="NMake toolchain is Windows-only") def test_nmaketoolchain_rcflags(): """Test that tools.build:rcflags is applied to RCFLAGS in the NMake toolchain environment.""" profile = textwrap.dedent("""\ include(default) [settings] arch=x86_64 [conf] tools.build:rcflags=["/nologo", "/flag-rc1"] """) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type").with_generator("NMakeToolchain") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . -pr profile") script = client.load("conannmaketoolchain.bat") assert "RCFLAGS" in script assert "/nologo" in script or "nologo" in script assert "/flag-rc1" in script or "flag-rc1" in script ================================================ FILE: test/integration/toolchains/microsoft/test_vs_layout.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient def test_vs_layout_subproject(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import vs_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "MSBuildToolchain" def layout(self): self.folders.root = ".." self.folders.subproject = "pkg" vs_layout(self) """) c.save({"pkg/conanfile.py": conanfile}) c.run("install pkg") assert os.path.isfile(os.path.join(c.current_folder, "pkg", "conan", "conantoolchain.props")) def test_vs_layout_error(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import vs_layout class Pkg(ConanFile): settings = "os", "compiler", "arch" def layout(self): vs_layout(self) """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "The 'vs_layout' requires the 'build_type' setting" in c.out conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import vs_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type" def layout(self): vs_layout(self) """) c.save({"conanfile.py": conanfile}) c.run("install .", assert_error=True) assert "The 'vs_layout' requires the 'arch' setting" in c.out conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import vs_layout class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def layout(self): vs_layout(self) """) c.save({"conanfile.py": conanfile}) c.run("install . -s arch=riscv64", assert_error=True) assert "The 'vs_layout' doesn't work with the arch 'riscv64'" in c.out assert "Accepted architectures: 'x86', 'x86_64', 'armv7', 'armv8'" in c.out ================================================ FILE: test/integration/toolchains/microsoft/vcvars_test.py ================================================ import platform import textwrap import os import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() not in ["Windows"], reason="Requires Windows") @pytest.mark.parametrize("scope", ["build", "run", None]) def test_vcvars_generator(scope): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.microsoft import VCVars class TestConan(ConanFile): settings = "os", "compiler", "arch", "build_type" def generate(self): VCVars(self).generate({}) """.format('scope="{}"'.format(scope) if scope else "")) client.save({"conanfile.py": conanfile}) client.run('install . -s os=Windows -s compiler="msvc" -s compiler.version=191 ' '-s compiler.cppstd=14 -s compiler.runtime=static') assert os.path.exists(os.path.join(client.current_folder, "conanvcvars.bat")) assert r"VC\Auxiliary\Build\vcvarsall.bat" in client.load("conanvcvars.bat") bat_contents = client.load("conanbuild.bat") if scope in ("build", None): assert "conanvcvars.bat" in bat_contents @pytest.mark.skipif(platform.system() not in ["Windows"], reason="Requires Windows") def test_vcvars_generator_skip(): """ tools.microsoft.msbuild:installation_path=disabled avoids creation of conanvcvars.bat """ client = TestClient() client.save({"conanfile.py": GenConanfile().with_generator("VCVars") .with_settings("os", "compiler", "arch", "build_type"), "profile": 'include(default)\n[conf]\ntools.microsoft.msbuild:installation_path='}) client.run('install . -c tools.microsoft.msbuild:installation_path=""') assert not os.path.exists(os.path.join(client.current_folder, "conanvcvars.bat")) client.run('install . -pr=profile') assert not os.path.exists(os.path.join(client.current_folder, "conanvcvars.bat")) @pytest.mark.skipif(platform.system() not in ["Linux"], reason="Requires Linux") def test_vcvars_generator_skip_on_linux(): """ Skip creation of conanvcvars.bat on Linux build systems """ client = TestClient() client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s os=Windows -s compiler=msvc -s compiler.version=193 ' '-s compiler.runtime=dynamic') assert not os.path.exists(os.path.join(client.current_folder, "conanvcvars.bat")) @pytest.mark.skipif(platform.system() not in ["Windows"], reason="Requires Windows") def test_vcvars_generator_string(): client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s os=Windows -s compiler="msvc" -s compiler.version=191 ' '-s compiler.cppstd=14 -s compiler.runtime=static') assert os.path.exists(os.path.join(client.current_folder, "conanvcvars.bat")) @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_vcvars_platform_x86(): # https://github.com/conan-io/conan/issues/11144 client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s os=Windows -s compiler="msvc" -s compiler.version=193 ' '-s compiler.cppstd=14 -s compiler.runtime=static -s:b arch=x86') vcvars = client.load("conanvcvars.bat") assert 'vcvarsall.bat" x86_amd64' in vcvars @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_vcvars_winsdk_version(): client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s os=Windows -s compiler=msvc -s compiler.version=193 ' '-s compiler.cppstd=14 -s compiler.runtime=static ' '-c tools.microsoft:winsdk_version=10.0') vcvars = client.load("conanvcvars.bat") assert 'vcvarsall.bat" amd64 10.0 -vcvars_ver=14.3' in vcvars @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_vcvars_compiler_update(): client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s os=Windows -s compiler=msvc -s compiler.version=193 ' '-s compiler.cppstd=14 -s compiler.runtime=static ' '-s compiler.update=3') vcvars = client.load("conanvcvars.bat") assert 'vcvarsall.bat" amd64 -vcvars_ver=14.33' in vcvars @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_vcvars_conf_msvc_update(): client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s os=Windows -s compiler=msvc -s compiler.version=193 ' '-s compiler.cppstd=14 -s compiler.runtime=static ' '-c tools.microsoft:msvc_update=8.29910') vcvars = client.load("conanvcvars.bat") assert 'vcvarsall.bat" amd64 -vcvars_ver=14.38.29910' in vcvars @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_vcvars_armv8_windows_store(): client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s:b os=Windows -s compiler="msvc" -s compiler.version=194 ' '-s compiler.cppstd=14 -s compiler.runtime=static -s:h arch=armv8 ' '-s:h os=WindowsStore -s:h os.version=10.0') vcvars = client.load("conanvcvars.bat") assert 'vcvarsall.bat" amd64_arm64' in vcvars @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") def test_vcvars_clang_visual2026(): client = TestClient(path_with_spaces=False) client.save({"conanfile.txt": "[generators]\nVCVars"}) client.run('install . -s:b os=Windows -s compiler=clang -s compiler.version=20 ' '-s compiler.cppstd=14 -s compiler.runtime=static -s arch=x86_64 ' '-s compiler.runtime_version=v145 ' # Using a known existing path to avoid auto-detection via vswhere '-c tools.microsoft.msbuild:installation_path=C:/') vcvars = client.load("conanvcvars.bat") assert '-vcvars_ver=14.5' in vcvars ================================================ FILE: test/integration/toolchains/premake/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/premake/test_premake.py ================================================ import textwrap from conan.test.utils.tools import TestClient def test_premake_args_without_toolchain(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.premake import Premake class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}!!") def build(self): premake = Premake(self) premake.luafile = "myproject.lua" premake.arguments = {"myarg": "myvalue"} premake.configure() """) c.save({"conanfile.py": conanfile}) c.run( "build . -s compiler=msvc -s compiler.version=193 -s compiler.runtime=dynamic" ) assert ( 'conanfile.py: Running premake5 --file="myproject.lua" vs2022 --myarg=myvalue!!' in c.out ) def test_premake_build_without_toolchain(): tc = TestClient() conanfile = textwrap.dedent( """ from conan import ConanFile from conan.tools.premake import Premake, PremakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}!!") def build(self): premake = Premake(self) premake.configure() premake.build(workspace="App") """ ) tc.save({"conanfile.py": conanfile}) tc.run( "build . -s compiler=msvc -s compiler.version=193 -s compiler.runtime=dynamic", assert_error=True, ) assert "Premake.build() method requires PremakeToolchain to work properly" in tc.out def test_premake_build(): tc = TestClient() conanfile = textwrap.dedent( """ from conan import ConanFile from conan.tools.premake import Premake, PremakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PremakeToolchain" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}!!") def build(self): premake = Premake(self) premake.configure() premake.build(workspace="App") """ ) tc.save({"conanfile.py": conanfile}) tc.run( "build . -s compiler=gcc -s compiler.version=13 -s compiler.libcxx=libstdc++ -s arch=x86_64 -c tools.build:jobs=4", ) assert 'conanfile.premake5.lua" gmake --arch=x86_64!!' in tc.out assert "Running make config=release all -j4!!" in tc.out def test_premake_build_with_targets(): tc = TestClient() conanfile = textwrap.dedent( """ from conan import ConanFile from conan.tools.premake import Premake, PremakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PremakeToolchain" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}!!") def build(self): premake = Premake(self) premake.configure() premake.build(workspace="App", targets=["app", "test"]) """ ) tc.save({"conanfile.py": conanfile}) tc.run( "build . -s compiler=gcc -s compiler.version=13 -s compiler.libcxx=libstdc++ -s arch=armv8 -c tools.build:jobs=42", ) assert 'conanfile.premake5.lua" gmake --arch=arm64!!' in tc.out assert "Running make config=release app test -j42!!" in tc.out def test_premake_msbuild_platform(): tc = TestClient() windows_profile = textwrap.dedent(""" [settings] arch=x86_64 build_type=Release compiler=msvc compiler.cppstd=14 compiler.runtime=dynamic compiler.version=194 os=Windows """) conanfile = textwrap.dedent( """ from conan import ConanFile from conan.tools.premake import Premake, PremakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PremakeToolchain" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}!!") def build(self): premake = Premake(self) premake.configure() premake.build(workspace="App", msbuild_platform="Win64") """ ) tc.save({"conanfile.py": conanfile, "win": windows_profile}) tc.run("build . -pr win") assert 'conanfile.premake5.lua" vs2022 --arch=x86_64!!' in tc.out assert 'Running msbuild.exe "App.sln" -p:Configuration="Release" -p:Platform="Win64"!!' in tc.out def test_premake_build_with_custom_configuration(): tc = TestClient() conanfile = textwrap.dedent( """ from conan import ConanFile from conan.tools.premake import Premake, PremakeDeps, PremakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "PremakeToolchain" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}!!") def generate(self): deps = PremakeDeps(self) deps.configuration = "ReleaseDLL" def build(self): premake = Premake(self) premake.configure() premake.build(workspace="App", configuration="ReleaseDLL") """ ) tc.save({"conanfile.py": conanfile}) tc.run( "build . -s compiler=gcc -s compiler.version=13 -s compiler.libcxx=libstdc++ -s arch=armv8 -c tools.build:jobs=4", ) assert 'conanfile.premake5.lua" gmake --arch=arm64!!' in tc.out assert "Running make config=releasedll all -j4!!" in tc.out ================================================ FILE: test/integration/toolchains/premake/test_premakedeps.py ================================================ import textwrap import pytest import os from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def assert_vars_file(client, configuration): contents = client.load(f"conan_pkg.name-more+_vars_{configuration}_x86_64.premake5.lua") assert f'include "conanutils.premake5.lua"' in contents assert f't_conandeps = {{}}' in contents assert f't_conandeps["{configuration}_x86_64"] = {{}}' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"] = {{}}' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["includedirs"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["libdirs"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["bindirs"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["libs"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["system_libs"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["defines"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["cxxflags"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["cflags"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["sharedlinkflags"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["exelinkflags"]' in contents assert f't_conandeps["{configuration}_x86_64"]["pkg.name-more+"]["frameworks"]' in contents assert f'if conandeps == nil then conandeps = {{}} end' in contents assert f'conan_premake_tmerge(conandeps, t_conandeps)' in contents def test_premakedeps(): # Create package client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" name = "pkg.name-more+" version = "1.0" def package_info(self): self.cpp_info.components["libmpdecimal++"].libs = ["libmp++"] self.cpp_info.components["mycomp.some-comp+"].libs = ["mylib"] self.cpp_info.components["libmpdecimal++"].requires = ["mycomp.some-comp+"] """) client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create . -s arch=x86_64 -s build_type=Debug") client.run("create . -s arch=x86_64 -s build_type=Release") # Run conan client.run("install --require=pkg.name-more+/1.0@ -g PremakeDeps -s arch=x86_64 -s build_type=Debug") client.run("install --require=pkg.name-more+/1.0@ -g PremakeDeps -s arch=x86_64 -s build_type=Release") # Assert root lua file contents = client.load("conandeps.premake5.lua") assert 'include "conan_pkg.name-more+.premake5.lua"' in contents assert 'function conan_setup_build(conf, pkg)' in contents assert 'function conan_setup_link(conf, pkg)' in contents assert 'function conan_setup(conf, pkg)' in contents # Assert package root file contents = client.load("conan_pkg.name-more+.premake5.lua") assert 'include "conan_pkg.name-more+_vars_debug_x86_64.premake5.lua"' in contents assert 'include "conan_pkg.name-more+_vars_release_x86_64.premake5.lua"' in contents # Assert package per configuration files assert_vars_file(client, 'debug') assert_vars_file(client, 'release') def test_premakedeps_link_order(): client = TestClient() profile = textwrap.dedent( """ [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++ build_type=Release """) client.save( { "liba/conanfile.py": GenConanfile("liba") .with_settings("os", "compiler", "build_type", "arch") .with_version("1.0") .with_generator("PremakeDeps") .with_generator("PremakeToolchain") .with_package_info(cpp_info={"libs": ["liba"]}), "libb/conanfile.py": GenConanfile("libb") .with_settings("os", "compiler", "build_type", "arch") .with_version("1.0") .with_requires("liba/1.0") .with_generator("PremakeDeps") .with_package_info( cpp_info={ "components": { "libb1": {"requires": ["liba::liba"]}, "libb2": {"requires": ["libb1"]}, } } ), "libc/conanfile.py": GenConanfile("libc") .with_settings("os", "compiler", "build_type", "arch") .with_version("1.0") .with_requires("libb/1.0") .with_generator("PremakeDeps") .with_package_info(cpp_info={"libs": ["libc"]}), "consumer/conanfile.py": GenConanfile("consumer") .with_settings("os", "compiler", "build_type", "arch") .with_version("1.0") .with_requires("libc/1.0") .with_generator("PremakeDeps"), "profile": profile } ) client.run("create liba -pr profile") client.run("create libb -pr profile") client.run("create libc -pr profile") client.run("install consumer -pr profile") contents = client.load("consumer/conanconfig.premake5.lua") assert 'include "conanconfig_release_x86_64.premake5.lua"' in contents contents = client.load("consumer/conanconfig_release_x86_64.premake5.lua") # Check correct order of dependencies: more dependent libs should be linked first assert 't_conan_deps_order["release_x86_64"] = {"libc", "libb", "liba"}' in contents # Check previous configurations are preserved when installing new configuration client.run("install consumer -pr profile -s build_type=Debug --build=missing") contents = client.load("consumer/conanconfig.premake5.lua") assert 'include "conanconfig_release_x86_64.premake5.lua"' in contents assert 'include "conanconfig_debug_x86_64.premake5.lua"' in contents contents = client.load("consumer/conanconfig_debug_x86_64.premake5.lua") assert 't_conan_deps_order["debug_x86_64"] = {"libc", "libb", "liba"}' in contents @pytest.mark.parametrize("transitive_headers", [True, False]) @pytest.mark.parametrize("transitive_libs", [True, False]) @pytest.mark.parametrize("brotli_package_type", ["unknown", "static-library", "shared-library"]) @pytest.mark.parametrize("lib_package_type", ["unknown", "static-library", "shared-library"]) def test_premakedeps_traits(transitive_headers, transitive_libs, brotli_package_type, lib_package_type): client = TestClient() profile = textwrap.dedent( """ [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++ build_type=Release """) client.save( { "brotli/conanfile.py": GenConanfile("brotli", "1.0") .with_package_type(brotli_package_type) .with_package_info( {"libs": ["brotlienc2"]} ), "lib/conanfile.py": GenConanfile("lib", "1.0") .with_package_type(lib_package_type) .with_requirement( "brotli/1.0", transitive_headers=transitive_headers, transitive_libs=transitive_libs, ), "conanfile.py": GenConanfile("app", "1.0") .with_settings("os", "compiler", "build_type", "arch") .with_require("lib/1.0") .with_generator("PremakeDeps"), "profile": profile, } ) client.run("create brotli -pr profile") brotli_layout = client.created_layout() client.run("create lib -pr profile") client.run("install . -pr profile") if not transitive_headers and not transitive_libs and brotli_package_type != "shared-library": assert not os.path.exists(os.path.join(client.current_folder, "conan_brotli_vars_release_x86_64.premake5.lua")) return brotli_vars = client.load("conan_brotli_vars_release_x86_64.premake5.lua") if transitive_libs: assert 't_conandeps["release_x86_64"]["brotli"]["libs"] = {"brotlienc2"}' in brotli_vars else: assert 't_conandeps["release_x86_64"]["brotli"]["libs"] = {}' in brotli_vars if transitive_headers: include_path = os.path.join(brotli_layout.package(), "include").replace("\\", "/") assert f't_conandeps["release_x86_64"]["brotli"]["includedirs"] = {{"{include_path}"}}' in brotli_vars else: assert 't_conandeps["release_x86_64"]["brotli"]["includedirs"] = {}' in brotli_vars ================================================ FILE: test/integration/toolchains/premake/test_premaketoolchain.py ================================================ import textwrap from conan.test.assets.genconanfile import GenConanfile import pytest from conan.test.utils.tools import TestClient from conan.tools.premake.toolchain import PremakeToolchain def test_extra_flags_via_conf(): profile = textwrap.dedent( """ [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=9 compiler.cppstd=17 compiler.libcxx=libstdc++ build_type=Release [conf] tools.build:cxxflags=["-flag1", "-flag2"] tools.build:cflags=["-flag3", "-flag4"] tools.build:sharedlinkflags+=["-flag5"] tools.build:exelinkflags+=["-flag6"] tools.build:defines=["define1=0"] """ ) tc = TestClient() tc.save( { "conanfile.txt": "[generators]\nPremakeToolchain", "profile": profile, } ) tc.run("install . -pr:a=profile") content = tc.load(PremakeToolchain.filename) assert 'cppdialect "c++17"' in content assert ( """ filter { files { "**.c" } } buildoptions { "-flag3", "-flag4", "-m64" } filter {} """ in content ) assert ( """ filter { files { "**.cpp", "**.cxx", "**.cc" } } buildoptions { "-flag1", "-flag2", "-m64" } filter {} """ in content ) assert 'linkoptions { "-flag5", "-flag6", "-m64" }' in content assert 'defines { "define1=0", "_GLIBCXX_USE_CXX11_ABI=0" }' in content def test_premaketoolchain_rcflags(): """Test that tools.build:rcflags is applied to buildoptions for **.rc files.""" profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=9 compiler.libcxx=libstdc++ build_type=Release [conf] tools.build:rcflags=["-rcflag1", "-rcflag2"] """) tc = TestClient() tc.save({ "conanfile.txt": "[generators]\nPremakeToolchain", "profile": profile, }) tc.run("install . -pr:a=profile") content = tc.load(PremakeToolchain.filename) assert '**.rc' in content assert "-rcflag1" in content assert "-rcflag2" in content def test_project_configuration(): tc = TestClient(path_with_spaces=False) profile = textwrap.dedent( """ [settings] arch=armv8 build_type=Release compiler=apple-clang compiler.cppstd=gnu17 compiler.cstd=gnu11 compiler.libcxx=libc++ compiler.version=17 os=Macos """ ) conanfile = textwrap.dedent( """ from conan import ConanFile from conan.tools.layout import basic_layout from conan.tools.premake import Premake, PremakeDeps, PremakeToolchain import os class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" name = "pkg" version = "1.0" options = {"fPIC": [True, False]} default_options = {"fPIC": True} def layout(self): basic_layout(self, src_folder="src") def generate(self): tc = PremakeToolchain(self) tc.extra_defines = ["VALUE=2"] tc.extra_cflags = ["-Wextra"] tc.extra_cxxflags = ["-Wall", "-Wextra"] tc.extra_ldflags = ["-lm"] tc.project("main").extra_defines = ["TEST=False"] tc.project("test").disable = True tc.project("main").extra_cxxflags = ["-FS"] tc.generate() """ ) tc.save({"conanfile.py": conanfile, "profile": profile}) tc.run("install . -pr:a profile") toolchain = tc.load("build-release/conan/conantoolchain.premake5.lua") assert 'pic "On"' in toolchain assert ( """ filter { files { "**.c" } } buildoptions { "-Wextra" } filter {} """ in toolchain ) assert ( """ filter { files { "**.cpp", "**.cxx", "**.cc" } } buildoptions { "-stdlib=libc++", "-Wall", "-Wextra" } filter {} """ in toolchain ) assert 'linkoptions { "-lm" }' in toolchain assert 'defines { "VALUE=2" }' in toolchain assert 'linkoptions { "-Wl,-rpath,@loader_path" }' in toolchain assert ( textwrap.dedent( """ project "main" -- CXX flags retrieved from CXXFLAGS environment, conan.conf(tools.build:cxxflags), extra_cxxflags and compiler settings filter { files { "**.cpp", "**.cxx", "**.cc" } } buildoptions { "-stdlib=libc++", "-FS" } filter {} -- Defines retrieved from DEFINES environment, conan.conf(tools.build:defines) and extra_defines defines { "TEST=False" } """ ) in toolchain ) assert ( textwrap.dedent( """ project "test" kind "None" """ ) in toolchain ) @pytest.mark.parametrize( "threads, flags", [("posix", "-pthread"), ("wasm_workers", "-sWASM_WORKERS=1")], ) def test_thread_flags(threads, flags): client = TestClient() profile = textwrap.dedent(f""" [settings] arch=wasm64 build_type=Release compiler=emcc compiler.cppstd=17 compiler.threads={threads} compiler.libcxx=libc++ compiler.version=4.0.10 os=Emscripten """) client.save( { "conanfile.py": GenConanfile("pkg", "1.0") .with_settings("os", "arch", "compiler", "build_type") .with_generator("PremakeToolchain"), "profile": profile, } ) client.run("install . -pr=./profile") toolchain = client.load("conantoolchain.premake5.lua") assert ( """ filter { files { "**.c" } } buildoptions { "-sMEMORY64=1", "%s" } filter {} """ % flags in toolchain ) assert ( """ filter { files { "**.cpp", "**.cxx", "**.cc" } } buildoptions { "-stdlib=libc++", "-sMEMORY64=1", "%s" } filter {} """ % flags in toolchain ) assert 'linkoptions { "-sMEMORY64=1", "%s" }' % flags in toolchain ================================================ FILE: test/integration/toolchains/qbs/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/qbs/test_qbsdeps.py ================================================ import json import os import platform import pytest import textwrap from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.internal.util.files import load def test_empty_package(): # Checks default values generated by conan for cpp_info client = TestClient() client.save({'conanfile.py': GenConanfile("mylib", "0.1")}) client.run('create .') client.run('install --requires=mylib/0.1@ -g QbsDeps') module_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mylib.json') module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'mylib' assert module_content.get('version') == '0.1' package_dir = module_content['package_dir'] cpp_info = module_content['cpp_info'] package_dir = package_dir.replace("/", "\\") if platform.system() == "Windows" else package_dir assert cpp_info.get('includedirs') == [os.path.join(package_dir, 'include')] assert cpp_info.get('libdirs') == [os.path.join(package_dir, 'lib')] assert cpp_info.get('bindirs') == [os.path.join(package_dir, 'bin')] assert cpp_info.get('libs') == [] assert cpp_info.get('frameworkdirs') == [] assert cpp_info.get('frameworks') == [] assert cpp_info.get('defines') == [] assert cpp_info.get('cflags') == [] assert cpp_info.get('cxxflags') == [] assert module_content.get('settings') == {} assert module_content.get('options') == {} def test_empty_dirs(): # Checks that we can override default values with empty directories conanfile = textwrap.dedent(''' from conan import ConanFile class Recipe(ConanFile): name = 'mylib' version = '0.1' def package_info(self): self.cpp_info.includedirs = [] self.cpp_info.libdirs = [] self.cpp_info.bindirs = [] self.cpp_info.libs = [] self.cpp_info.frameworkdirs = [] ''') client = TestClient() client.save({'conanfile.py': conanfile}) client.run('create .') client.run('install --requires=mylib/0.1@ -g QbsDeps') module_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mylib.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'mylib' assert module_content.get('version') == '0.1' assert 'cpp_info' in module_content cpp_info = module_content['cpp_info'] assert cpp_info.get('includedirs') == [] assert cpp_info.get('libdirs') == [] assert cpp_info.get('bindirs') == [] assert cpp_info.get('libs') == [] assert cpp_info.get('frameworkdirs') == [] assert cpp_info.get('frameworks') == [] assert cpp_info.get('defines') == [] assert cpp_info.get('cflags') == [] assert cpp_info.get('cxxflags') == [] assert module_content.get('settings') == {} assert module_content.get('options') == {} def test_pkg_config_name(): # Checks we can override module name using the "pkg_config_name" property conanfile = textwrap.dedent(''' from conan import ConanFile class Recipe(ConanFile): name = 'mylib' version = '0.1' def package_info(self): self.cpp_info.set_property("pkg_config_name", "myfirstlib") ''') client = TestClient() client.save({'conanfile.py': conanfile}) client.run('create .') client.run('install --requires=mylib/0.1@ -g QbsDeps') module_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'myfirstlib.json') assert os.path.exists(module_path) @pytest.mark.parametrize('host_os, arch, build_type', [ ('Linux', 'x86_64', 'Debug'), ('Linux', 'x86_64', 'Release'), ('Linux', 'armv8', 'Debug'), ('Windows', 'x86_64', 'Debug'), ('Macos', 'armv8', 'Release'), ]) def test_settings(host_os, arch, build_type): conanfile = textwrap.dedent(''' from conan import ConanFile class Recipe(ConanFile): settings = "os", "arch", "build_type" name = "mylib" version = "0.1" ''') client = TestClient() client.save({'conanfile.py': conanfile}) common_cmd = '-s:h os={os} -s:h arch={arch} -s:h build_type={build_type}'.format( os=host_os, arch=arch, build_type=build_type ) client.run('create . ' + common_cmd) client.run('install --requires=mylib/0.1@ -g QbsDeps ' + common_cmd) module_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mylib.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert 'settings' in module_content # Qbs only cares about os and arch (and maybe build type) assert module_content['settings'].get('os') == host_os assert module_content['settings'].get('arch') == arch assert module_content['settings'].get('build_type') == build_type @pytest.mark.parametrize('shared', ['False', 'True']) def test_options(shared): conanfile = textwrap.dedent(''' from conan import ConanFile class Recipe(ConanFile): options = {"shared": [True, False]} default_options = {"shared": False} name = 'mylib' version = '0.1' ''') client = TestClient() client.save({'conanfile.py': conanfile}) common_cmd = '-o:h shared={shared}'.format(shared=shared) client.run('create . ' + common_cmd) client.run('install --requires=mylib/0.1@ -g QbsDeps ' + common_cmd) module_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mylib.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert 'options' in module_content assert module_content['options'].get('shared') == shared def test_components(): """ Checks a package with multiple components. Here we test component name and version override as well as the defaults. """ conanfile = textwrap.dedent(''' from conan import ConanFile class Recipe(ConanFile): name = 'mylib' version = '0.1' def package_info(self): self.cpp_info.components["mycomponent1"].set_property("pkg_config_name", "mycomponent") self.cpp_info.components["mycomponent2"].set_property("component_version", "19.8.199") ''') client = TestClient() client.save({'conanfile.py': conanfile}) client.run('create .') client.run('install --requires=mylib/0.1@ -g QbsDeps') module1_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mycomponent.json') assert os.path.exists(module1_path) module1_content = json.loads(load(module1_path)) assert module1_content.get('package_name') == 'mylib' assert module1_content.get('version') == '0.1' assert module1_content.get('dependencies') == [] module2_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mycomponent2.json') assert os.path.exists(module2_path) module2_content = json.loads(load(module2_path)) assert module2_content.get('package_name') == 'mylib' assert module2_content.get('version') == '19.8.199' assert module2_content.get('dependencies') == [] main_module_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mylib.json') assert os.path.exists(main_module_path) main_module_content = json.loads(load(main_module_path)) assert main_module_content.get('package_name') == 'mylib' assert main_module_content.get('version') == '0.1' assert main_module_content.get('dependencies') == [ {"name": "mycomponent", "version": "0.1"}, # name overriden, version default {"name": "mycomponent2", "version": "19.8.199"} # name default, version overriden ] def test_cpp_info_requires(): """ Testing a complex structure like: * first/0.1 - Global pkg_config_name == "myfirstlib" - Components: "cmp1" * other/0.1 * second/0.2 - Requires: "first/0.1" - Components: "mycomponent", "myfirstcomp" + "mycomponent" requires "first::cmp1" + "myfirstcomp" requires "mycomponent" * third/0.4 - Requires: "second/0.2", "other/0.1" Expected file structure after running QbsDeps as generator: - other.json - myfirstlib-cmp1.json - myfirstlib.json - second-mycomponent.json - second-myfirstcomp.json - second.json - third.json """ client = TestClient() # first conanfile = textwrap.dedent(''' from conan import ConanFile class Recipe(ConanFile): name = "first" version = "0.1" def package_info(self): self.cpp_info.set_property("pkg_config_name", "myfirstlib") self.cpp_info.components["cmp1"].libs = ["libcmp1"] ''') client.save({"conanfile.py": conanfile}) client.run("create .") # other client.save({"conanfile.py": GenConanfile("other", "0.1")}, clean_first=True) client.run("create .") # second conanfile = textwrap.dedent(''' from conan import ConanFile class PkgConfigConan(ConanFile): name = "second" version = "0.2" requires = "first/0.1" def package_info(self): self.cpp_info.components["mycomponent"].requires.append("first::cmp1") self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") ''') client.save({"conanfile.py": conanfile}, clean_first=True) client.run("create .") # third client.save({"conanfile.py": GenConanfile("third", "0.3").with_require("second/0.2") .with_require("other/0.1")}, clean_first=True) client.run("create .") client2 = TestClient(cache_folder=client.cache_folder) conanfile = textwrap.dedent(""" [requires] third/0.3 other/0.1 [generators] QbsDeps """) client2.save({"conanfile.txt": conanfile}) client2.run("install .") # first module_path = os.path.join(client2.current_folder, 'conan-qbs-deps', 'cmp1.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'first' assert module_content.get('version') == '0.1' assert module_content.get('dependencies') == [] module_path = os.path.join(client2.current_folder, 'conan-qbs-deps', 'myfirstlib.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'first' assert module_content.get('version') == '0.1' assert module_content.get('dependencies') == [{'name': 'cmp1', 'version': '0.1'}] # other module_path = os.path.join(client2.current_folder, 'conan-qbs-deps', 'other.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'other' assert module_content.get('version') == '0.1' assert module_content.get('dependencies') == [] # second.mycomponent module_path = os.path.join(client2.current_folder, 'conan-qbs-deps', 'mycomponent.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'second' assert module_content.get('version') == '0.2' assert module_content.get('dependencies') == [{'name': 'cmp1', 'version': '0.1'}] # second.myfirstcomp module_path = os.path.join(client2.current_folder, 'conan-qbs-deps', 'myfirstcomp.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'second' assert module_content.get('version') == '0.2' assert module_content.get('dependencies') == [{'name': 'mycomponent', 'version': '0.2'}] # second module_path = os.path.join(client2.current_folder, 'conan-qbs-deps', 'second.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'second' assert module_content.get('version') == '0.2' assert module_content.get('dependencies') == [ {'name': 'mycomponent', 'version': '0.2'}, {'name': 'myfirstcomp', 'version': '0.2'} ] # third module_path = os.path.join(client2.current_folder, 'conan-qbs-deps', 'third.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'third' assert module_content.get('version') == '0.3' assert module_content.get('dependencies') == [ {'name': 'second', 'version': '0.2'}, {'name': 'other', 'version': '0.1'} ] # see https://github.com/conan-io/conan/issues/10341 def test_components_conflict(): """ If component has the same name as the root package, skip root package """ conanfile = textwrap.dedent(''' from conan import ConanFile class Recipe(ConanFile): name = 'mylib' version = '0.1' def package_info(self): self.cpp_info.set_property("pkg_config_name", "mycoollib") self.cpp_info.components["_mycoollib"].defines = ["MAGIC_DEFINE"] self.cpp_info.components["_mycoollib"].set_property("pkg_config_name", "mycoollib") ''') client = TestClient() client.save({'conanfile.py': conanfile}) client.run('create .') client.run('install --requires=mylib/0.1@ -g QbsDeps') module_path = os.path.join(client.current_folder, 'conan-qbs-deps', 'mycoollib.json') assert os.path.exists(module_path) module_content = json.loads(load(module_path)) assert module_content.get('package_name') == 'mylib' assert module_content.get('version') == '0.1' assert 'cpp_info' in module_content cpp_info = module_content['cpp_info'] assert cpp_info.get('defines') == ["MAGIC_DEFINE"] ================================================ FILE: test/integration/toolchains/qbs/test_qbsprofile.py ================================================ import os import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() == "Windows", reason="QbsProfile requires host compiler (gcc) to be found") def test_qbsprofile_rcflags(): """Test that tools.build:rcflags is applied to cpp.rcFlags in qbs_settings.txt.""" profile = textwrap.dedent(""" [settings] os=Linux arch=x86_64 compiler=gcc compiler.version=9 compiler.libcxx=libstdc++ build_type=Release [conf] tools.build:rcflags=["-rcflag1", "-rcflag2"] """) client = TestClient() conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type").with_generator("QbsProfile") client.save({"conanfile.py": conanfile, "profile": profile}) client.run("install . --profile:build=profile --profile:host=profile") settings_path = os.path.join(client.current_folder, "qbs_settings.txt") assert os.path.exists(settings_path) content = client.load("qbs_settings.txt") assert "cpp.rcFlags" in content assert "-rcflag1" in content assert "-rcflag2" in content ================================================ FILE: test/integration/toolchains/scons/__init__.py ================================================ ================================================ FILE: test/integration/toolchains/scons/test_scondeps.py ================================================ import re import textwrap from conan.test.utils.tools import TestClient def test_sconsdeps(): dep = textwrap.dedent(""" from conan import ConanFile class ExampleConanIntegration(ConanFile): name = "{dep}" version = "0.1" def package_info(self): self.cpp_info.includedirs = ["{dep}_includedir"] self.cpp_info.libdirs = ["{dep}_libdir"] self.cpp_info.bindirs = ["{dep}_bindir"] self.cpp_info.libs = ["{dep}_lib"] self.cpp_info.frameworks = ["{dep}_frameworks"] self.cpp_info.frameworkdirs = ["{dep}_frameworkdirs"] self.cpp_info.defines = ["{dep}_defines"] self.cpp_info.cxxflags = ["{dep}_cxxflags"] self.cpp_info.cflags = ["{dep}_cflags"] self.cpp_info.sharedlinkflags = ["{dep}_sharedlinkflags"] self.cpp_info.exelinkflags = ["{dep}_exelinkflags"] """) consumer = textwrap.dedent(""" from conan import ConanFile from conan.tools.layout import basic_layout class ExampleConanIntegration(ConanFile): generators = 'SConsDeps' requires = 'dep1/0.1', 'dep2/0.1' """) c = TestClient() c.save({"dep1/conanfile.py": dep.format(dep="dep1"), "dep2/conanfile.py": dep.format(dep="dep2"), "consumer/conanfile.py": consumer}) c.run("create dep1") c.run("create dep2") c.run("install consumer") sconsdeps = c.load("consumer/SConscript_conandeps") # remove all cache paths from the output but the last component def clean_paths(text): text = text.replace("\\", "/") pattern = r"'[A-Za-z]?[:]?[/]?[^']+/([^'/]+)'" return re.sub(pattern, r"'\1'", text) expected_content = [""" "conandeps" : { "CPPPATH" : ['dep2_includedir', 'dep1_includedir'], "LIBPATH" : ['dep2_libdir', 'dep1_libdir'], "BINPATH" : ['dep2_bindir', 'dep1_bindir'], "LIBS" : ['dep2_lib', 'dep1_lib'], "FRAMEWORKS" : ['dep2_frameworks', 'dep1_frameworks'], "FRAMEWORKPATH" : ['dep2_frameworkdirs', 'dep1_frameworkdirs'], "CPPDEFINES" : ['dep2_defines', 'dep1_defines'], "CXXFLAGS" : ['dep2_cxxflags', 'dep1_cxxflags'], "CCFLAGS" : ['dep2_cflags', 'dep1_cflags'], "SHLINKFLAGS" : ['dep2_sharedlinkflags', 'dep1_sharedlinkflags'], "LINKFLAGS" : ['dep2_exelinkflags', 'dep1_exelinkflags'], }, """, """ "dep1" : { "CPPPATH" : ['dep1_includedir'], "LIBPATH" : ['dep1_libdir'], "BINPATH" : ['dep1_bindir'], "LIBS" : ['dep1_lib'], "FRAMEWORKS" : ['dep1_frameworks'], "FRAMEWORKPATH" : ['dep1_frameworkdirs'], "CPPDEFINES" : ['dep1_defines'], "CXXFLAGS" : ['dep1_cxxflags'], "CCFLAGS" : ['dep1_cflags'], "SHLINKFLAGS" : ['dep1_sharedlinkflags'], "LINKFLAGS" : ['dep1_exelinkflags'], }, "dep1_version" : "0.1", """, """ "dep2" : { "CPPPATH" : ['dep2_includedir'], "LIBPATH" : ['dep2_libdir'], "BINPATH" : ['dep2_bindir'], "LIBS" : ['dep2_lib'], "FRAMEWORKS" : ['dep2_frameworks'], "FRAMEWORKPATH" : ['dep2_frameworkdirs'], "CPPDEFINES" : ['dep2_defines'], "CXXFLAGS" : ['dep2_cxxflags'], "CCFLAGS" : ['dep2_cflags'], "SHLINKFLAGS" : ['dep2_sharedlinkflags'], "LINKFLAGS" : ['dep2_exelinkflags'], }, "dep2_version" : "0.1", """] clean_sconsdeps = clean_paths(sconsdeps) for block in expected_content: assert block in clean_sconsdeps ================================================ FILE: test/integration/toolchains/test_raise_on_universal_binaries.py ================================================ import platform import textwrap import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("toolchain", ["MesonToolchain", "BazelToolchain"]) def test_create_universal_binary(toolchain): client = TestClient() conanfile = (GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_generator(toolchain)) client.save({"conanfile.py": conanfile}) client.run('create . --name=foo --version=1.0 -s="arch=armv8|armv8.3|x86_64"', assert_error=True) assert (f"Error in generator '{toolchain}': " f"Universal binaries not supported by toolchain.") in client.out @pytest.mark.parametrize("toolchain", ["AutotoolsToolchain", "GnuToolchain"]) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_toolchain_universal_binary_support(toolchain): """Test that toolchain now supports universal binaries on macOS""" client = TestClient() conanfile = (GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_generator(toolchain)) client.save({"conanfile.py": conanfile}) client.run('install . --name=foo --version=1.0 -s="arch=armv8|x86_64"') script_name = f"conan{toolchain.lower()}.sh" toolchain_content = client.load(script_name) assert "-arch arm64" in toolchain_content assert "-arch x86_64" in toolchain_content # Verify isysroot flag is NOT present when sdk_path is not configured assert "-isysroot" not in toolchain_content @pytest.mark.parametrize("toolchain", ["AutotoolsToolchain", "GnuToolchain"]) def test_toolchain_universal_binary_non_macos(toolchain): """Test that toolchain still raises error for universal binaries on non-macOS""" client = TestClient() conanfile = (GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_generator(toolchain)) client.save({"conanfile.py": conanfile}) # This should still raise an error on non-macOS platforms client.run('create . --name=foo --version=1.0 -s="os=Linux" -s="arch=armv8|x86_64"', assert_error=True) assert "Universal arch 'armv8|x86_64' is only supported in Apple OSes" in client.out @pytest.mark.parametrize("toolchain", ["AutotoolsToolchain", "GnuToolchain"]) @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_toolchain_universal_binary_with_sdk_path(toolchain): """Test that toolchain sets isysroot when sdk_path is configured for universal binaries""" client = TestClient() conanfile = (GenConanfile().with_settings("os", "arch", "compiler", "build_type") .with_generator(toolchain)) client.save({"conanfile.py": conanfile}) client.run('install . --name=foo --version=1.0 -s="arch=armv8|x86_64" -c="tools.apple:sdk_path=mysdkpath"') script_name = f"conan{toolchain.lower()}.sh" toolchain_content = client.load(script_name) assert "-arch arm64" in toolchain_content assert "-arch x86_64" in toolchain_content assert "-isysroot" in toolchain_content assert "mysdkpath" in toolchain_content def test_create_universal_binary_test_package_folder(): # https://github.com/conan-io/conan/issues/18820 # While multi-arch is Darwin specific, this was a cmake_layout issue, so it can be # tested in any platform c = TestClient() test_conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import cmake_layout class mylibraryTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): self.requires(self.tested_reference_str) def layout(self): cmake_layout(self) def test(self): pass """) c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_settings("arch"), "test_package/conanfile.py": test_conanfile}) c.run('create . -s="arch=armv8|x86_64"') c.run("list *:*") assert "arch: armv8|x86_64" in c.out ================================================ FILE: test/integration/toolchains/test_toolchain_namespaces.py ================================================ import os import textwrap from conan.tools.build import load_toolchain_args, CONAN_TOOLCHAIN_ARGS_FILE from conan.test.utils.tools import TestClient def test_autotools_namespace(): client = TestClient() namespace = "somename" conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.gnu import AutotoolsToolchain, Autotools class Conan(ConanFile): settings = "os", "arch", "compiler", "build_type" def generate(self): autotools = AutotoolsToolchain(self, namespace='{0}') autotools.configure_args = ['a', 'b'] autotools.make_args = ['c', 'd'] autotools.generate() def build(self): autotools = Autotools(self, namespace='{0}') self.output.info(autotools._configure_args) self.output.info(autotools._make_args) """.format(namespace)) client.save({"conanfile.py": conanfile}) client.run("install .") assert os.path.isfile(os.path.join(client.current_folder, "{}_{}".format(namespace, CONAN_TOOLCHAIN_ARGS_FILE))) content = load_toolchain_args(generators_folder=client.current_folder, namespace=namespace) at_configure_args = content.get("configure_args") at_make_args = content.get("make_args") client.run("build .") assert at_configure_args in client.out assert at_make_args in client.out ================================================ FILE: test/integration/tools/__init__.py ================================================ ================================================ FILE: test/integration/tools/conan_version_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient from conan import __version__ def test_conan_version(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan import conan_version from conan.tools.scm import Version from conan import __version__ class pkg(ConanFile): def generate(self): assert __version__ == str(conan_version) assert isinstance(conan_version, Version) print(f"current version: {conan_version}") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("install .") assert f"current version: {__version__}" in client.out ================================================ FILE: test/integration/tools/cppstd_minimum_version_test.py ================================================ import pytest from textwrap import dedent from conan.test.utils.tools import TestClient class TestCppStdMinimumVersion: CONANFILE = dedent(""" import os from conan import ConanFile from conan.tools.build import check_min_cppstd, valid_min_cppstd class Fake(ConanFile): name = "fake" version = "0.1" settings = "compiler" def validate(self): check_min_cppstd(self, "17", False) self.output.info("valid standard") assert valid_min_cppstd(self, "17", False) """) PROFILE = dedent(""" [settings] compiler=gcc compiler.version=9 compiler.libcxx=libstdc++ {} """) @pytest.fixture(autouse=True) def setup(self): self.client = TestClient() self.client.save({"conanfile.py": TestCppStdMinimumVersion.CONANFILE}) @pytest.mark.parametrize("cppstd", ["17", "gnu17"]) def test_cppstd_from_settings(self, cppstd): profile = TestCppStdMinimumVersion.PROFILE.replace("{}", "compiler.cppstd=%s" % cppstd) self.client.save({"myprofile": profile}) self.client.run("create . --user=user --channel=channel -pr myprofile") assert "valid standard" in self.client.out @pytest.mark.parametrize("cppstd", ["11", "gnu11"]) def test_invalid_cppstd_from_settings(self, cppstd): profile = TestCppStdMinimumVersion.PROFILE.replace("{}", "compiler.cppstd=%s" % cppstd) self.client.save({"myprofile": profile}) self.client.run("create . --user=user --channel=channel -pr myprofile", assert_error=True) assert "Invalid: Current cppstd (%s) is lower than the required C++ standard (17)." \ % cppstd in self.client.out def test_header_only_check_min_cppstd(): """ Check that for a header only package you can check self.info in the validate Related to: https://github.com/conan-io/conan/issues/11786 """ conanfile = dedent(""" import os from conan import ConanFile from conan.tools.build import check_min_cppstd class Fake(ConanFile): name = "fake" version = "0.1" settings = "compiler" def package_id(self): self.info.clear() def validate(self): check_min_cppstd(self, "11") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . -s compiler.cppstd=14") client.run("install --require=fake/0.1@ -s compiler.cppstd=14") assert "fake/0.1: Already installed!" in client.out def test_validate_build_check_min_cppstd(): """ Check the case that a package needs certain cppstd to build but can be consumed with a lower cppstd or even not cppstd defined at all """ conanfile = dedent(""" import os from conan import ConanFile from conan.tools.build import check_min_cppstd class Fake(ConanFile): name = "fake" version = "0.1" settings = "compiler" def validate_build(self): check_min_cppstd(self, "17") def validate(self): print("validated") def build(self): print("built") def package_id(self): del self.info.settings.compiler.cppstd """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("create . -s compiler.cppstd=14", assert_error=True) assert "fake/0.1: Cannot build for this configuration: " \ "Current cppstd (14) is lower than the required C++ standard (17)." in client.out client.run("create . -s compiler.cppstd=17") client.run("install --require=fake/0.1@") assert "fake/0.1: Already installed!" in client.out assert "validated" in client.out ================================================ FILE: test/integration/tools/cpu_count_test.py ================================================ import textwrap from conan.test.utils.tools import TestClient class TestNJobs: def test_cpu_count_override(self): client = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.build import build_jobs class Conan(ConanFile): name = "hello0" version = "0.1" def build(self): self.output.warning("CPU COUNT=> %s" % build_jobs(self)) """) client.save({"conanfile.py": conanfile}) client.run("create . -c tools.build:jobs=5") assert "CPU COUNT=> 5" in client.out ================================================ FILE: test/integration/tools/file_tools_test.py ================================================ import os import textwrap from conan.test.utils.tools import TestClient def test_file_tools(): conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.files import rmdir, mkdir class pkg(ConanFile): def layout(self): self.folders.generators = "gen" def generate(self): mkdir(self, "folder1") mkdir(self, "folder2") rmdir(self, "folder2") """) client = TestClient() client.save({"conanfile.py": conanfile}) client.run("install . ") assert os.path.exists(os.path.join(client.current_folder, "gen", "folder1")) assert not os.path.exists(os.path.join(client.current_folder, "gen", "folder2")) ================================================ FILE: test/integration/tools/fix_symlinks_test.py ================================================ import os import platform import textwrap import pytest from conan.api.model import RecipeReference from conan.test.utils.tools import TestClient @pytest.mark.skipif(platform.system() == "Windows", reason="symlink need admin privileges") class TestFixSymlinks: name_ref = RecipeReference.loads("name/version") conanfile = textwrap.dedent(""" import os import shutil from conan import ConanFile from conan.tools.files import save from conan.tools.files.symlinks import (absolute_to_relative_symlinks, remove_external_symlinks, remove_broken_symlinks) class Recipe(ConanFile): def build(self): save(self, os.path.join(self.build_folder, "build.txt"), "contents") def package(self): os.symlink('/dev/null', os.path.join(self.package_folder, "black_hole")) # Files: Symlink to file outside the package os.symlink(os.path.join(self.build_folder, "build.txt"), os.path.join(self.package_folder, "outside_symlink.txt")) # Files: A regular file with symlinks to it save(self, os.path.join(self.package_folder, "regular.txt"), "contents") os.symlink(os.path.join(self.package_folder, "regular.txt"), os.path.join(self.package_folder, "absolute_symlink.txt")) os.symlink("regular.txt", os.path.join(self.package_folder, "relative_symlink.txt")) # Files: A broken symlink save(self, os.path.join(self.package_folder, "file.txt"), "contents") os.symlink(os.path.join(self.package_folder, "file.txt"), os.path.join(self.package_folder, "broken.txt")) os.unlink(os.path.join(self.package_folder, "file.txt")) # Folder: Symlink outside package os.symlink(self.build_folder, os.path.join(self.package_folder, "outside_folder")) # Folder: a regular folder and symlinks to it save(self, os.path.join(self.package_folder, "folder", "file.txt"), "contents") os.symlink(os.path.join(self.package_folder, "folder"), os.path.join(self.package_folder, "absolute")) os.symlink("folder", os.path.join(self.package_folder, "relative")) # Folder: broken symlink save(self, os.path.join(self.package_folder, "tmp", "file.txt"), "contents") os.symlink(os.path.join(self.package_folder, "tmp"), os.path.join(self.package_folder, "broken_folder")) shutil.rmtree(os.path.join(self.package_folder, "tmp")) # os.symlink(os.path.join(self.package_folder, "folder", "file.txt"), os.path.join(self.package_folder, "abs_to_file_in_folder.txt")) # --> Run the tools absolute_to_relative_symlinks(self, self.package_folder) remove_external_symlinks(self, self.package_folder) remove_broken_symlinks(self, self.package_folder) """) def test_error_reported(self): t = TestClient() t.save({'conanfile.py': self.conanfile}) t.run("create . --name=name --version=version") package_folder = t.created_layout().package() assert sorted(os.listdir(package_folder)) == ['abs_to_file_in_folder.txt', 'absolute', 'absolute_symlink.txt', 'conaninfo.txt', 'conanmanifest.txt', 'folder', 'regular.txt', 'relative', 'relative_symlink.txt'] # All the links in the package_folder are relative and contained into it for (dirpath, dirnames, filenames) in os.walk(package_folder): for filename in filenames: filename = os.path.join(dirpath, filename) if os.path.islink(filename): rel_path = str(os.readlink(filename)) assert not os.path.exists(os.path.abspath(rel_path)) assert not rel_path.startswith('..') for dirname in dirnames: dirname = os.path.join(dirpath, dirname) if os.path.islink(dirname): rel_path = str(os.readlink(dirname)) assert not os.path.exists(os.path.abspath(rel_path)) assert not rel_path.startswith('..') ================================================ FILE: test/integration/tools/ros/__init__.py ================================================ ================================================ FILE: test/integration/tools/ros/test_rosenv.py ================================================ import os import textwrap import platform import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient def test_rosenv(): """ Test that the ROSEnv generator generates the environment files and that the environment variables are correctly set. """ client = TestClient() conanfile3 = textwrap.dedent(''' [requires] [generators] CMakeDeps CMakeToolchain ROSEnv ''') client.save({ "conanfile3.txt": conanfile3 }) if platform.system() == "Windows": conanrosenv_file = "conanrosenv.bat" conanrosenv_build = "conanrosenv-build.bat" build_type = "Release" cmd_source = "call" cmd_env = "set" else: conanrosenv_file = "conanrosenv.sh" conanrosenv_build = "conanrosenv-build.sh" build_type = '"Release"' cmd_source = "." cmd_env = "env" client.run("install conanfile3.txt --output-folder install/conan") assert f"Generated ROSEnv Conan file: {conanrosenv_file}" in client.out install_folder = os.path.join(client.current_folder, "install", "conan") conanrosenv_path = os.path.join(install_folder, conanrosenv_file) assert os.path.exists(conanrosenv_path) conanrosenvbuild_path = os.path.join(install_folder, conanrosenv_build) assert os.path.exists(conanrosenvbuild_path) toolchain_path = os.path.join(client.current_folder, "install", "conan", "conan_toolchain.cmake") content = client.load(conanrosenvbuild_path) if platform.system() != "Windows": assert f'CMAKE_TOOLCHAIN_FILE="{toolchain_path}"' in content else: assert f'CMAKE_TOOLCHAIN_FILE={toolchain_path}' in content assert f'CMAKE_BUILD_TYPE={build_type}' in content client.run_command(f'{cmd_source} "{conanrosenv_path}" && {cmd_env}') assert f"CMAKE_TOOLCHAIN_FILE={toolchain_path}" in client.out assert "CMAKE_BUILD_TYPE=Release" in client.out @pytest.mark.skipif(platform.system() == "Windows", reason="Uses UNIX commands") def test_rosenv_shared_libraries(): """ Test that the library paths env vars are set up correctly so that the executables built with colcon can found the shared libraries of conan packages """ client = TestClient() c1 = GenConanfile("lib1", "1.0").with_shared_option(False).with_package_file("lib/lib1", "lib-content") c2 = GenConanfile("lib2", "1.0").with_shared_option(False).with_requirement("lib1/1.0").with_package_file("lib/lib2", "lib-content") c3 = textwrap.dedent(''' [requires] lib2/1.0 [generators] CMakeDeps CMakeToolchain ROSEnv ''') client.save({ "conanfile1.py": c1, "conanfile2.py": c2, "conanfile3.txt": c3 }) client.run("create conanfile1.py -o *:shared=True") client.run("create conanfile2.py -o *:shared=True") client.run("install conanfile3.txt -o *:shared=True --output-folder install/conan") conanrosenv_path = os.path.join(client.current_folder, "install", "conan", "conanrosenv.sh") client.run_command(f". \"{conanrosenv_path}\" && env") environment_content = client.out client.run( "cache path lib1/1.0#58723f478a96866dcbd9456d8eefd7c4:1744785cb24e3bdca70e27041dc5abd20476f947") lib1_lib_path = os.path.join(client.out.strip(), "lib") assert lib1_lib_path in environment_content client.run( "cache path lib2/1.0#4b7a6063ba107d770458ce10385beb52:5c3c2e56259489f7ffbc8e494921eda4b747ef21") lib2_lib_path = os.path.join(client.out.strip(), "lib") assert lib2_lib_path in environment_content ================================================ FILE: test/integration/tools/system/__init__.py ================================================ ================================================ FILE: test/integration/tools/system/package_manager_test.py ================================================ import platform from unittest.mock import MagicMock, patch from unittest import mock import pytest from unittest.mock import PropertyMock from conan.tools.system.package_manager import Apt, Apk, Dnf, Yum, Brew, Pkg, PkgUtil, Chocolatey, \ Zypper, PacMan, _SystemPackageManagerTool from conan.errors import ConanException from conan.internal.model.settings import Settings from conan.test.utils.mocks import ConanFileMock, MockSettings @pytest.mark.parametrize("system, tool", [ ("Linux", "apt-get"), ("Windows", "choco"), ("Darwin", "brew"), ("Solaris", "pkgutil"), ]) @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux") def test_package_manager_platform(system, tool): with mock.patch("platform.system", return_value=system): with mock.patch("distro.id", return_value=''): with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" conanfile = ConanFileMock() conanfile.settings = Settings() manager = _SystemPackageManagerTool(conanfile) assert tool == manager.get_default_tool() @pytest.mark.skipif(platform.system() != "Windows", reason="Only Windows") def test_msys2(): with mock.patch("platform.system", return_value="Windows"): with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" conanfile = ConanFileMock() conanfile.settings = Settings({"os": {"Windows": {"subsystem": ["msys2"]}}}) conanfile.conf.define("tools.microsoft.bash:subsystem", "msys2") manager = _SystemPackageManagerTool(conanfile) assert manager.get_default_tool() == "choco" conanfile.settings.os = "Windows" conanfile.settings.os.subsystem = "msys2" manager = _SystemPackageManagerTool(conanfile) assert manager.get_default_tool() == "pacman" @pytest.mark.parametrize("distro, tool", [ ("ubuntu", "apt-get"), ("debian", "apt-get"), ("linuxmint", "apt-get"), ("pidora", "yum"), ("rocky", "dnf"), ("almalinux", "dnf"), ("oracle", "dnf"), ("fedora", "dnf"), ("nobara", "dnf"), ("arch", "pacman"), ("opensuse", "zypper"), ("sles", "zypper"), ("opensuse", "zypper"), ("opensuse-tumbleweed", "zypper"), ("opensuse-leap", "zypper"), ("opensuse-next_version", "zypper"), ("freebsd", "pkg"), ("alpine", "apk"), ('altlinux', "apt-get"), ("astra", 'apt-get'), ('elbrus', 'apt-get'), ('pop', "apt-get"), ]) @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux") def test_package_manager_distro(distro, tool): with mock.patch("platform.system", return_value="Linux"): with mock.patch("distro.id", return_value=distro): with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" conanfile = ConanFileMock() conanfile.settings = Settings() manager = _SystemPackageManagerTool(conanfile) assert tool == manager.get_default_tool() @pytest.mark.parametrize("sudo, sudo_askpass, expected_str", [ (True, True, "sudo -A "), (True, False, "sudo "), (False, True, ""), (False, False, ""), ]) def test_sudo_str(sudo, sudo_askpass, expected_str): conanfile = ConanFileMock() conanfile.settings = Settings() conanfile.conf.define("tools.system.package_manager:sudo", sudo) conanfile.conf.define("tools.system.package_manager:sudo_askpass", sudo_askpass) with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" apt = Apt(conanfile) assert apt.sudo_str == expected_str @pytest.mark.parametrize("recommends, recommends_str", [ (False, "--no-install-recommends "), (True, ""), ]) def test_apt_install_recommends(recommends, recommends_str): conanfile = ConanFileMock() conanfile.settings = Settings() conanfile.conf.define("tools.system.package_manager:tool", "apt-get") conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" apt = Apt(conanfile) apt.install(["package1", "package2"], recommends=recommends, check=False) assert apt._conanfile.command == "apt-get install -y {}package1 package2".format(recommends_str) @pytest.mark.parametrize("tool_class", [Apk, Apt, Yum, Dnf, Brew, Pkg, PkgUtil, Chocolatey, PacMan, Zypper]) def test_tools_install_mode_check(tool_class): conanfile = ConanFileMock() conanfile.settings = Settings() conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) with pytest.raises(ConanException) as exc_info: def fake_check(*args, **kwargs): return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): tool.install(["package1", "package2"]) assert exc_info.value.args[0] == "System requirements: 'package1, package2' are missing but " \ "can't install because tools.system.package_manager:mode is " \ "'check'.Please update packages manually or set " \ "'tools.system.package_manager:mode' to 'install' in the [conf] " \ "section of the profile, or in the command line using " \ "'-c tools.system.package_manager:mode=install'" @pytest.mark.parametrize("tool_class, result", [ (Apk, "apk update"), (Apt, "apt-get update"), (Yum, "yum check-update -y"), (Dnf, "dnf check-update -y"), (Brew, "brew update"), (Pkg, "pkg update"), (PkgUtil, "pkgutil --catalog"), (Chocolatey, "choco outdated"), (PacMan, "pacman -Syyu --noconfirm"), (Zypper, "zypper --non-interactive ref"), ]) def test_tools_update_mode_install(tool_class, result): conanfile = ConanFileMock() conanfile.settings = Settings() conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) for mode in ["check", "install"]: conanfile.conf.define("tools.system.package_manager:mode", mode) with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) tool.update() if mode == "install": assert tool._conanfile.command == result else: # does not run the update when mode check assert tool._conanfile.command is None @pytest.mark.parametrize("tool_class, result", [ (Yum, "yum check-update -y"), (Dnf, "dnf check-update -y"), ]) def test_dnf_yum_return_code_100(tool_class, result): # https://github.com/conan-io/conan/issues/11661 conanfile = ConanFileMock() conanfile.settings = Settings() conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, quiet=False): assert command == result return 100 if "check-update" in command else 0 conanfile.run = fake_run tool.update() # check that some random return code fails with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, quiet=False): return 55 if "check-update" in command else 0 conanfile.run = fake_run with pytest.raises(ConanException) as exc_info: tool.update() assert f"Command '{result}' failed" == str(exc_info.value) @pytest.mark.parametrize("tool_class, arch_host, result", [ # Install host package and not cross-compile -> do not add host architecture (Apk, 'x86_64', 'apk add --no-cache package1 package2'), (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1 package2'), (Yum, 'x86_64', 'yum install -y package1 package2'), (Dnf, 'x86_64', 'dnf install -y package1 package2'), (Brew, 'x86_64', 'brew install package1 package2'), (Pkg, 'x86_64', 'pkg install -y package1 package2'), (PkgUtil, 'x86_64', 'pkgutil --install --yes package1 package2'), (Chocolatey, 'x86_64', 'choco install --yes package1 package2'), (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'), (Zypper, 'x86_64', 'zypper --non-interactive in package1 package2'), # Install host package and cross-compile -> add host architecture (Apt, 'x86', 'apt-get install -y --no-install-recommends package1:i386 package2:i386'), (Yum, 'x86', 'yum install -y package1.i?86 package2.i?86'), (Dnf, 'x86', 'dnf install -y package1.i?86 package2.i?86'), (Brew, 'x86', 'brew install package1 package2'), (Pkg, 'x86', 'pkg install -y package1 package2'), (PkgUtil, 'x86', 'pkgutil --install --yes package1 package2'), (Chocolatey, 'x86', 'choco install --yes package1 package2'), (PacMan, 'x86', 'pacman -S --noconfirm package1-lib32 package2-lib32'), (Zypper, 'x86', 'zypper --non-interactive in package1 package2'), ]) def test_tools_install_mode_install_different_archs(tool_class, arch_host, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": arch_host}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) def fake_check(*args, **kwargs): return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): tool.install(["package1", "package2"]) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, arch_host, result", [ # Install host package and not cross-compile -> do not add host architecture (Apk, 'x86_64', 'apk add --no-cache package1=0.1 package2=0.2'), (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'), (Yum, 'x86_64', 'yum install -y package1-0.1 package2-0.2'), (Dnf, 'x86_64', 'dnf install -y package1-0.1 package2-0.2'), (Brew, 'x86_64', 'brew install package1@0.1 package2@0.2'), (Pkg, 'x86_64', 'pkg install -y package1-0.1 package2-0.2'), (PkgUtil, 'x86_64', 'pkgutil --install --yes package1@0.1 package2@0.2'), (Chocolatey, 'x86_64', 'choco install --yes package1 --version 0.1 package2 --version 0.2'), (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'), (Zypper, 'x86_64', 'zypper --non-interactive in package1=0.1 package2=0.2'), # Install host package and cross-compile -> add host architecture (Apt, 'x86', 'apt-get install -y --no-install-recommends package1:i386=0.1 package2:i386=0.2'), (Yum, 'x86', 'yum install -y package1-0.1.i?86 package2-0.2.i?86'), (Dnf, 'x86', 'dnf install -y package1-0.1.i?86 package2-0.2.i?86'), (Brew, 'x86', 'brew install package1@0.1 package2@0.2'), (Pkg, 'x86', 'pkg install -y package1-0.1 package2-0.2'), (PkgUtil, 'x86', 'pkgutil --install --yes package1@0.1 package2@0.2'), (Chocolatey, 'x86', 'choco install --yes package1 --version 0.1 package2 --version 0.2'), (PacMan, 'x86', 'pacman -S --noconfirm package1-lib32 package2-lib32'), (Zypper, 'x86', 'zypper --non-interactive in package1=0.1 package2=0.2'), ]) def test_tools_install_mode_install_different_archs_with_version(tool_class, arch_host, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": arch_host}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) def fake_check(*args, **kwargs): return ["package1=0.1", "package2=0.2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): tool.install(["package1=0.1", "package2=0.2"]) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, arch_host, result", [ # Install build machine package and not cross-compile -> do not add host architecture (Apk, 'x86_64', 'apk add --no-cache package1 package2'), (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1 package2'), (Yum, 'x86_64', 'yum install -y package1 package2'), (Dnf, 'x86_64', 'dnf install -y package1 package2'), (Brew, 'x86_64', 'brew install package1 package2'), (Pkg, 'x86_64', 'pkg install -y package1 package2'), (PkgUtil, 'x86_64', 'pkgutil --install --yes package1 package2'), (Chocolatey, 'x86_64', 'choco install --yes package1 package2'), (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'), (Zypper, 'x86_64', 'zypper --non-interactive in package1 package2'), # Install build machine package and cross-compile -> do not add host architecture (Apt, 'x86', 'apt-get install -y --no-install-recommends package1 package2'), (Yum, 'x86', 'yum install -y package1 package2'), (Dnf, 'x86', 'dnf install -y package1 package2'), (Brew, 'x86', 'brew install package1 package2'), (Pkg, 'x86', 'pkg install -y package1 package2'), (PkgUtil, 'x86', 'pkgutil --install --yes package1 package2'), (Chocolatey, 'x86', 'choco install --yes package1 package2'), (PacMan, 'x86', 'pacman -S --noconfirm package1 package2'), (Zypper, 'x86', 'zypper --non-interactive in package1 package2'), ]) def test_tools_install_mode_install_to_build_machine_arch(tool_class, arch_host, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": arch_host}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) def fake_check(*args, **kwargs): return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): tool.install(["package1", "package2"], host_package=False) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, arch_host, result", [ # Install build machine package and not cross-compile -> do not add host architecture (Apk, 'x86_64', 'apk add --no-cache package1=0.1 package2=0.2'), (Apt, 'x86_64', 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'), (Yum, 'x86_64', 'yum install -y package1-0.1 package2-0.2'), (Dnf, 'x86_64', 'dnf install -y package1-0.1 package2-0.2'), (Brew, 'x86_64', 'brew install package1@0.1 package2@0.2'), (Pkg, 'x86_64', 'pkg install -y package1-0.1 package2-0.2'), (PkgUtil, 'x86_64', 'pkgutil --install --yes package1@0.1 package2@0.2'), (Chocolatey, 'x86_64', 'choco install --yes package1 --version 0.1 package2 --version 0.2'), (PacMan, 'x86_64', 'pacman -S --noconfirm package1 package2'), (Zypper, 'x86_64', 'zypper --non-interactive in package1=0.1 package2=0.2'), # Install build machine package and cross-compile -> do not add host architecture (Apt, 'x86', 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'), (Yum, 'x86', 'yum install -y package1-0.1 package2-0.2'), (Dnf, 'x86', 'dnf install -y package1-0.1 package2-0.2'), (Brew, 'x86', 'brew install package1@0.1 package2@0.2'), (Pkg, 'x86', 'pkg install -y package1-0.1 package2-0.2'), (PkgUtil, 'x86', 'pkgutil --install --yes package1@0.1 package2@0.2'), (Chocolatey, 'x86', 'choco install --yes package1 --version 0.1 package2 --version 0.2'), (PacMan, 'x86', 'pacman -S --noconfirm package1 package2'), (Zypper, 'x86', 'zypper --non-interactive in package1=0.1 package2=0.2'), ]) def test_tools_install_mode_install_to_build_machine_arch_with_version(tool_class, arch_host, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": arch_host}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) def fake_check(*args, **kwargs): return ["package1=0.1", "package2=0.2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): tool.install(["package1=0.1", "package2=0.2"], host_package=False) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, result", [ # cross-compile but arch_names=None -> do not add host architecture # https://github.com/conan-io/conan/issues/12320 because the package is archless (Apt, 'apt-get install -y --no-install-recommends package1 package2'), (Yum, 'yum install -y package1 package2'), (Dnf, 'dnf install -y package1 package2'), (PacMan, 'pacman -S --noconfirm package1 package2'), ]) def test_tools_install_archless(tool_class, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": "x86"}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile, arch_names={}) def fake_check(*args, **kwargs): return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): tool.install(["package1", "package2"]) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, result", [ # cross-compile but arch_names=None -> do not add host architecture # https://github.com/conan-io/conan/issues/12320 because the package is archless (Apt, 'apt-get install -y --no-install-recommends package1=0.1 package2=0.2'), (Yum, 'yum install -y package1-0.1 package2-0.2'), (Dnf, 'dnf install -y package1-0.1 package2-0.2'), (PacMan, 'pacman -S --noconfirm package1 package2'), ]) def test_tools_install_archless_with_version(tool_class, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": "x86"}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) conanfile.conf.define("tools.system.package_manager:mode", "install") with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile, arch_names={}) def fake_check(*args, **kwargs): return ["package1=0.1", "package2=0.2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): tool.install(["package1=0.1", "package2=0.2"]) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, result", [ (Apk, 'apk info -e package'), (Apt, r"dpkg-query -W -f='${Architecture}\n' package | grep -qEx '(amd64|all)'"), (Yum, 'rpm -q package'), (Dnf, 'rpm -q package'), (Brew, 'test -n "$(brew ls --versions package)"'), (Pkg, 'pkg info package'), (PkgUtil, 'test -n "`pkgutil --list package`"'), (Chocolatey, 'choco list --exact package | findstr /c:"1 packages installed."'), (PacMan, 'pacman -Qi package'), (Zypper, 'rpm -q package'), ]) def test_tools_check(tool_class, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) tool.check(["package"]) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, result", [ (Apk, 'apk info package | grep "0.1"'), (Apt, r"dpkg-query -W -f='${Architecture} ${Version}\n' package | grep -qEx '(amd64|all) 0.1'"), (Yum, 'rpm -q package-0.1'), (Dnf, 'rpm -q package-0.1'), (Brew, 'brew list --versions package | grep "0.1"'), (Pkg, 'pkg info package | grep "Version: 0.1"'), (PkgUtil, 'test -n "`pkgutil --list package`"'), (Chocolatey, 'choco list --local-only package | findstr /i "0.1"'), (PacMan, 'pacman -Qi package'), (Zypper, 'rpm -q package-0.1'), ]) def test_tools_check_with_version(tool_class, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) tool.check(["package=0.1"]) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, result", [ (Apt, r"dpkg-query -W -f='${Architecture}\n' package | grep -qEx '(amd64|all)'"), ]) @pytest.mark.parametrize("arch_host", [ 'x86_64', 'x86', ]) def test_tools_apt_check_install_to_build_machine_arch(tool_class, arch_host, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": arch_host}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) tool.check(["package"], host_package=False) assert tool._conanfile.command == result @pytest.mark.parametrize("tool_class, result", [ (Apt, r"dpkg-query -W -f='${Architecture} ${Version}\n' package | grep -qEx '(amd64|all) 0.1'"), ]) @pytest.mark.parametrize("arch_host", [ 'x86_64', 'x86', ]) def test_tools_apt_check_install_to_build_machine_arch_with_version(tool_class, arch_host, result): conanfile = ConanFileMock() conanfile.settings = MockSettings({"arch": arch_host}) conanfile.settings_build = MockSettings({"arch": "x86_64"}) conanfile.conf.define("tools.system.package_manager:tool", tool_class.tool_name) with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: context_mock.return_value = "host" tool = tool_class(conanfile) tool.check(["package=0.1"], host_package=False) assert tool._conanfile.command == result ================================================ FILE: test/integration/workspace/__init__.py ================================================ ================================================ FILE: test/integration/workspace/test_workspace.py ================================================ import json import os import shutil import textwrap import pytest from conan.api.model import RecipeReference from conan.api.subapi.workspace import WorkspaceAPI from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.mocks import ConanFileMock from conan.test.utils.scm import create_local_git_repo from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient from conan.internal.util.files import save_files from conan.tools.files import replace_in_file WorkspaceAPI.TEST_ENABLED = "will_break_next" class TestWorkspaceRoot: def test_workspace_root(self): c = TestClient(light=True) # Just check the root command works c.run("workspace root", assert_error=True) assert "ERROR: Workspace not defined, please create" in c.out # error, conanws/ folder does not contain conanws.[py | yml] c.save({"conanws/test.txt": ""}) with c.chdir("conanws"): c.run("workspace root", assert_error=True) assert "ERROR: Workspace not defined, please create" in c.out c.save({"conanws.yml": ""}) c.run("workspace root") assert c.current_folder in c.stdout # error, empty .py c.save({"conanws.py": ""}, clean_first=True) c.run("workspace root", assert_error=True) assert "Error loading conanws.py" in c.out assert "No subclass of Workspace" in c.out c.save({"conanws.yml": ""}, clean_first=True) c.run("workspace root") assert c.current_folder in c.stdout class TestAddRemove: def test_add(self): c = TestClient(light=True) c.save({"conanws.yml": "", "dep1/conanfile.py": GenConanfile("dep1", "0.1"), "dep2/conanfile.py": GenConanfile("dep2", "0.1"), "dep3/conanfile.py": GenConanfile("dep3", "0.1")}) c.run("workspace add dep1") assert "Reference 'dep1/0.1' added to workspace" in c.out c.run("workspace info") assert "dep1/0.1" in c.out assert "dep2" not in c.out c.run("editable list") # No editables in global assert "dep1" not in c.out assert "dep2" not in c.out c.run("workspace add dep2") assert "Reference 'dep2/0.1' added to workspace" in c.out c.run("workspace info") assert "dep1/0.1" in c.out assert "dep2/0.1" in c.out with c.chdir(temp_folder()): # If we move to another folder, outside WS, no editables c.run("editable list") assert "dep1" not in c.out assert "dep2" not in c.out c.run("workspace info") assert "dep1/0.1" in c.out assert "dep2/0.1" in c.out c.run("workspace remove dep1") c.run("workspace info") assert "dep1/0.1" not in c.out assert "dep2/0.1" in c.out c.run("workspace remove dep2") c.run("workspace info") assert "dep1/0.1" not in c.out assert "dep2/0.1" not in c.out def test_add_from_outside(self): c = TestClient(light=True) c.save({"sub/conanws.yml": "", "sub/dep1/conanfile.py": GenConanfile("dep1", "0.1"), "sub/dep2/conanfile.py": GenConanfile("dep2", "0.1")}) with c.chdir("sub"): c.run("workspace add dep1") assert "Reference 'dep1/0.1' added to workspace" in c.out c.run("workspace add dep2") assert "Reference 'dep2/0.1' added to workspace" in c.out c.run("workspace info") assert "dep1/0.1" in c.out assert "dep2/0.1" in c.out assert c.load_home("editable_packages.json") is None c.run("editable list") assert "dep1" not in c.out assert "dep2" not in c.out assert c.load_home("editable_packages.json") is None with c.chdir("sub"): c.run("editable add dep1") assert c.load_home("editable_packages.json") is not None c.run("editable list") assert "dep1/0.1" in c.out assert "dep2/0.1" not in c.out c.run("workspace info") assert "dep1" in c.out assert "dep2" in c.out c.run("editable list") assert "dep1/0.1" in c.out assert "dep2" not in c.out def test_add_modified(self): # https://github.com/conan-io/conan/issues/18942 c = TestClient(light=True) c.save({"conanws.yml": "", "dep1/conanfile.py": GenConanfile("dep1", "0.1")}) c.run("workspace add dep1") assert "Reference 'dep1/0.1' added to workspace" in c.out c.run("workspace info") assert "dep1/0.1" in c.out c.save({"dep1/conanfile.py": GenConanfile("dep1", "0.2")}) c.run("workspace add dep1") assert "Package dep1 already exists, updating its reference" in c.out assert "Reference 'dep1/0.2' added to workspace" in c.out c.run("workspace info") assert "dep1/0.2" in c.out assert "dep1/0.1" not in c.out @pytest.mark.parametrize("api", [False, True]) def test_dynamic_editables(self, api): c = TestClient(light=True) conanfile = textwrap.dedent(""" import os from conan import ConanFile from conan.tools.files import load class Lib(ConanFile): def set_name(self): self.name = load(self, os.path.join(self.recipe_folder, "name.txt")) def set_version(self): self.version = load(self, os.path.join(self.recipe_folder, "version.txt")) """) if not api: workspace = textwrap.dedent("""\ import os from conan import Workspace class MyWorkspace(Workspace): def packages(self): result = [] for f in os.listdir(self.folder): if os.path.isdir(os.path.join(self.folder, f)): full_path = os.path.join(self.folder, f, "name.txt") name = open(full_path).read().strip() version = open(os.path.join(self.folder, f, "version.txt")).read().strip() result.append({"path": f, "ref": f"{name}/{version}"}) return result """) else: workspace = textwrap.dedent("""\ import os from conan import Workspace class MyWorkspace(Workspace): def packages(self): result = [] for f in os.listdir(self.folder): if os.path.isdir(os.path.join(self.folder, f)): conanfile = self.load_conanfile(f) result.append({"path": f, "ref": f"{conanfile.name}/{conanfile.version}"}) return result """) c.save({"conanws.py": workspace, "dep1/conanfile.py": conanfile, "dep1/name.txt": "pkg", "dep1/version.txt": "2.1"}) c.run("workspace info --format=json") info = json.loads(c.stdout) assert info["packages"] == [{"ref": "pkg/2.1", "path": "dep1"}] c.save({"dep1/name.txt": "other", "dep1/version.txt": "14.5"}) c.run("workspace info --format=json") info = json.loads(c.stdout) assert info["packages"] == [{"ref": "other/14.5", "path": "dep1"}] c.run("install --requires=other/14.5") # Doesn't fail assert "other/14.5 - Editable" in c.out with c.chdir("dep1"): c.run("install --requires=other/14.5") # Doesn't fail assert "other/14.5 - Editable" in c.out def test_api_dynamic_version_run(self): # https://github.com/conan-io/conan/issues/17306 c = TestClient(light=True) conanfile = textwrap.dedent(""" from io import StringIO from conan import ConanFile class Lib(ConanFile): name= "pkg" def set_version(self): my_buf = StringIO() self.run('echo 2.1', stdout=my_buf) self.version = my_buf.getvalue().strip() """) workspace = textwrap.dedent("""\ import os from conan import Workspace class MyWorkspace(Workspace): def packages(self): conanfile = self.load_conanfile("dep1") return [{"path": "dep1", "ref": f"{conanfile.name}/{conanfile.version}"}] """) c.save({"conanws.py": workspace, "dep1/conanfile.py": conanfile}) c.run("workspace info --format=json") info = json.loads(c.stdout) assert info["packages"] == [{"ref": "pkg/2.1", "path": "dep1"}] c.run("install --requires=pkg/2.1") # it will not fail def test_api_dynamic_any_version(self): # For workspace, the version of the conanfile can be ignored if the # workspace define a different version. c = TestClient(light=True) workspace = textwrap.dedent("""\ import os from conan import Workspace class MyWorkspace(Workspace): def packages(self): return [{"path": "dep1", "ref": "pkg/1.2.3"}] """) c.save({"conanws.py": workspace, "dep1/conanfile.py": GenConanfile("pkg", "0.1")}) c.run("workspace info --format=json") info = json.loads(c.stdout) assert info["packages"] == [{"ref": "pkg/1.2.3", "path": "dep1"}] c.run("install --requires=pkg/1.2.3") # it will not fail def test_replace_requires(self): c = TestClient(light=True) c.save({"conanws.yml": "", "pkga/conanfile.py": GenConanfile("pkga", "0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/develop"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkgb/0.1", "pkga/0.1"), "myreplaces": "[replace_requires]\npkga/develop: pkga/0.1"}) c.run("workspace add pkga") c.run("workspace add pkgb") c.run("workspace add pkgc") c.run("workspace info --format=json") info = json.loads(c.stdout) assert info["packages"] == [{'path': 'pkga', 'ref': 'pkga/0.1'}, {'path': 'pkgb', 'ref': 'pkgb/0.1'}, {'path': 'pkgc', 'ref': 'pkgc/0.1'}] c.run("install --requires=pkgc/0.1", assert_error=True) assert "Version conflict: Conflict between pkga/develop and pkga/0.1 in the graph" in c.out # it will not fail c.run("install --requires=pkgc/0.1 -pr=default -pr=myreplaces") assert "pkga/0.1 - Editable" in c.out assert "pkga/develop: pkga/0.1" in c.out def test_error_uppercase(self): c = TestClient(light=True) c.save({"conanws.yml": "", "conanfile.py": GenConanfile("Pkg", "0.1")}) c.run("workspace add .", assert_error=True) assert "ERROR: Conan packages names 'Pkg/0.1' must be all lowercase" in c.out c.save({"conanfile.py": GenConanfile()}) c.run("workspace add . --name=Pkg --version=0.1", assert_error=True) assert "ERROR: Conan packages names 'Pkg/0.1' must be all lowercase" in c.out def test_add_open_error(self): c = TestClient(light=True) c.save({"conanws.yml": "", "dep/conanfile.py": GenConanfile("dep", "0.1")}) c.run("workspace add dep") c.run("workspace open dep/0.1", assert_error=True) assert "ERROR: Can't open a dependency that is already an editable: dep/0.1" in c.out def test_remove_product(self): c = TestClient(light=True) c.save({"conanws.yml": "", "mydeppkg/conanfile.py": GenConanfile("mydeppkg", "0.1")}) c.run("workspace add mydeppkg") c.run("workspace remove mydeppkg") c.run("workspace info") assert "mydeppkg" not in c.out def test_remove_removed_folder(self): c = TestClient(light=True) c.save({"conanws.yml": "", "mydeppkg/conanfile.py": GenConanfile("mydeppkg", "0.1")}) c.run("workspace add mydeppkg") # If we now remove the folder shutil.rmtree(os.path.join(c.current_folder, "mydeppkg")) # It can still be removed by path, even if the path doesn't exist c.run("workspace remove mydeppkg") assert "Removed from workspace: mydeppkg" in c.out c.run("workspace info") assert "mydeppkg" not in c.out def test_custom_add_remove(self): c = TestClient(light=True) workspace = textwrap.dedent("""\ import os from conan import Workspace class MyWorkspace(Workspace): def name(self): return "myws" def add(self, ref, path, *args, **kwargs): self.output.info(f"Adding {ref} at {path}") super().add(ref, path, *args, **kwargs) def remove(self, path, *args, **kwargs): self.output.info(f"Removing {path}") return super().remove(path, *args, **kwargs) """) c.save({"conanws.py": workspace, "dep/conanfile.py": GenConanfile("dep", "0.1")}) c.run("workspace add dep") assert "Workspace 'myws': Adding dep/0.1" in c.out c.run("workspace info") assert "dep/0.1" in c.out c.run("workspace remove dep") assert "Workspace 'myws': Removing" in c.out c.run("workspace info") assert "dep/0.1" not in c.out def test_remove_non_existing_error(self): c = TestClient(light=True) c.save({"conanws.yml": ""}) c.run("workspace remove kk", assert_error=True) assert "ERROR: No editable package to remove from this path: kk" in c.out class TestOpenAdd: def test_without_git(self): t = TestClient(default_server_user=True, light=True) t.save({"conanfile.py": GenConanfile("pkg", "0.1")}) t.run("create .") t.run("upload * -r=default -c") c = TestClient(servers=t.servers, light=True) c.run(f"workspace open pkg/0.1") assert "name = 'pkg'" in c.load("pkg/conanfile.py") # The add should work the same c2 = TestClient(servers=t.servers, light=True) c2.save({"conanws.yml": ""}) c2.run(f"workspace add --ref=pkg/0.1") assert "name = 'pkg'" in c2.load("pkg/conanfile.py") c2.run("workspace info") assert "pkg/0.1" in c2.out def test_without_git_export_sources(self): t = TestClient(default_server_user=True, light=True) t.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports_sources("*.txt"), "CMakeLists.txt": "mycmake"}) t.run("create .") t.run("upload * -r=default -c") c = TestClient(servers=t.servers) c.run("workspace open pkg/0.1") assert "name = 'pkg'" in c.load("pkg/conanfile.py") assert "mycmake" in c.load("pkg/CMakeLists.txt") def test_workspace_git_scm(self): folder = temp_folder() conanfile = textwrap.dedent(""" from conan import ConanFile from conan.tools.scm import Git class Pkg(ConanFile): name = "pkg" version = "0.1" def export(self): git = Git(self) git.coordinates_to_conandata() """) url, commit = create_local_git_repo(files={"conanfile.py": conanfile}, folder=folder, branch="mybranch") t1 = TestClient(default_server_user=True, light=True) t1.run_command('git clone "file://{}" .'.format(url)) t1.run("create .") t1.run("upload * -r=default -c") c = TestClient(servers=t1.servers, light=True) c.run("workspace open pkg/0.1") assert c.load("pkg/conanfile.py") == conanfile c2 = TestClient(servers=t1.servers, light=True) c2.save({"conanws.yml": ""}) c2.run(f"workspace add --ref=pkg/0.1") assert 'name = "pkg"' in c2.load("pkg/conanfile.py") c2.run("workspace info") assert "pkg/0.1" in c2.out def test_workspace_build_editables(self): c = TestClient(light=True) c.save({"conanws.yml": ""}) c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_build_msg("BUILD PKGA!"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_build_msg("BUILD PKGB!") .with_requires("pkga/0.1")}) c.run("workspace add pkga") c.run("workspace add pkgb") c.run("install --requires=pkgb/0.1 --build=editable") c.assert_listed_binary({"pkga/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "EditableBuild"), "pkgb/0.1": ("47a5f20ec8fb480e1c5794462089b01a3548fdc5", "EditableBuild")}) assert "pkga/0.1: WARN: BUILD PKGA!" in c.out assert "pkgb/0.1: WARN: BUILD PKGB!" in c.out class TestComplete: def test_complete_add(self): c = TestClient(light=True) c.save({"conanws.yml": "", "pkga/conanfile.py": GenConanfile("pkga", "0.1"), "pkgx/conanfile.py": GenConanfile("pkgx", "0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkga/0.1", "pkgx/0.1"), "pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requires("pkgb/0.1", "pkgc/0.1")}) c.run("workspace complete") # Does nothing, but it doesn't fail assert "There are no packages in this workspace, nothing to complete" in c.out c.run("workspace info") assert "packages: (empty)" in c.out for pkg in ("pkga", "pkgx", "pkgb", "pkgc", "pkgd"): c.run(f"export {pkg}") c.run("workspace add pkgd") c.run("workspace complete") # Does nothing, but it doesn't fail assert "There are no intermediate packages to add to the workspace" in c.out c.run("workspace info") assert "pkgd/0.1" in c.out assert "pkga/0.1" not in c.out c.run("workspace add pkga") c.run("workspace install --build=missing", assert_error=True) assert "Workspace definition error. Package pkgb/0.1 in the Conan cache" in c.out c.run("workspace info") assert "pkgb/0.1" not in c.out assert "pkgc/0.1" not in c.out c.run("workspace complete") c.run("workspace info") assert "pkgb/0.1" in c.out assert "pkgc/0.1" in c.out assert "pkgx/0.1" not in c.out def test_complete_open(self): c = TestClient(light=True) c.save({"pkgx/conanfile.py": GenConanfile("pkgx", "0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/0.1"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkga/0.1", "pkgx/0.1")}) for pkg in ("pkgx", "pkgb", "pkgc"): c.run(f"export {pkg}") c.save({"conanws.yml": "", "pkga/conanfile.py": GenConanfile("pkga", "0.1"), "pkgd/conanfile.py": GenConanfile("pkgd", "0.1").with_requires("pkgb/0.1", "pkgc/0.1")}, clean_first=True) c.run("workspace add pkgd") c.run("workspace add pkga") c.run("workspace install --build=missing", assert_error=True) assert "Workspace definition error. Package pkgb/0.1 in the Conan cache" in c.out c.run("workspace info") assert "pkgb/0.1" not in c.out assert "pkgc/0.1" not in c.out c.run("workspace complete") c.run("workspace info") assert "pkgb/0.1" in c.out assert "pkgc/0.1" in c.out assert "pkgx/0.1" not in c.out class TestWorkspaceBuild: def test_build(self): c = TestClient(light=True) c.save({"conanws.yml": ""}) c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_build_msg("BUILD PKGA!"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_build_msg("BUILD PKGB!") .with_requires("pkga/0.1")}) c.run("workspace add pkga") c.run("workspace add pkgb") c.run("workspace build") c.assert_listed_binary({"pkga/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "EditableBuild")}) assert "conanfile.py (pkga/0.1): WARN: BUILD PKGA!" in c.out assert "conanfile.py (pkgb/0.1): WARN: BUILD PKGB!" in c.out # It is also possible to build a specific package by path # equivalent to ``conan build --build=editable`` # This can be done even if it is not a product c.run("workspace build --pkg=pkgc/*", assert_error=True) assert "ERROR: There are no selected packages defined in the workspace" in c.out c.run("workspace build --pkg=pkgb/*") c.assert_listed_binary({"pkga/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "EditableBuild")}) assert "conanfile.py (pkga/0.1): WARN: BUILD PKGA!" in c.out assert "conanfile.py (pkgb/0.1): WARN: BUILD PKGB!" in c.out c.run("workspace build --pkg=pkga/*") assert "conanfile.py (pkga/0.1): Calling build()" in c.out assert "conanfile.py (pkga/0.1): WARN: BUILD PKGA!" in c.out def test_error_if_no_products(self): c = TestClient(light=True) c.save({"conanws.yml": ""}) c.run("workspace build", assert_error=True) assert "ERROR: There are no selected packages defined in the workspace" in c.out def test_build_external_missing(self): c = TestClient(light=True) c.save({"conanws.yml": ""}) c.save({"mymath/conanfile.py": GenConanfile("mymath", "0.1").with_build_msg("MyMATH!!"), "pkga/conanfile.py": GenConanfile("pkga", "0.1").with_build_msg("BUILD PKGA!") .with_requires("mymath/0.1"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_build_msg("BUILD PKGB!") .with_requires("pkga/0.1")}) c.run("export mymath") c.run("workspace add pkga") c.run("workspace add pkgb") c.run("workspace build", assert_error=True) assert "ERROR: Missing prebuilt package for 'mymath/0.1'" in c.out c.run("workspace build --build=missing") assert "Workspace building external mymath/0.1" in c.out def test_build_dynamic_name_version(self): conanfile = textwrap.dedent("""\ from conan import ConanFile class MyConan(ConanFile): def build(self): self.output.info(f"Building {self.name} AND {self.version}!!!") """) workspace = textwrap.dedent("""\ import os from conan import Workspace class MyWorkspace(Workspace): def packages(self): result = [] for f in os.listdir(self.folder): if os.path.isdir(os.path.join(self.folder, f)): result.append({"path": f, "ref": f"{f}/0.1", "output_folder": f"{f}/myout"}) return result """) c = TestClient(light=True) c.save({"conanws.py": workspace, "pkga/conanfile.py": conanfile}) c.run("workspace info") assert "pkga/0.1" in c.out c.run("workspace build") assert "metadata" in os.listdir(os.path.join(c.current_folder, "pkga", "myout")) assert "conanfile.py (pkga/0.1): Building pkga AND 0.1!!!" in c.out def test_build_with_external_editable_python_requires(self): c = TestClient(light=True) c.save({"conanws.yml": "", "ext/conanfile.py": GenConanfile("ext", "0.1").with_package_type("python-require"), "pkga/conanfile.py": GenConanfile("pkga", "0.1").with_python_requires("ext/0.1") }) c.run("editable add ext") c.run("workspace add pkga") c.run("workspace build") c.assert_listed_binary({"pkga/0.1": ("8bca2eae7cd0d6b6da8d14f8c86069d89d265bd4", "EditableBuild")}) c.assert_listed_require({"ext/0.1": "Editable"}, python=True) def test_build_with_external_editable(self): c = TestClient(light=True) c.save({"conanws.yml": "", "ext/conanfile.py": GenConanfile("ext", "0.1").with_build_msg("BUILD EXT!"), "pkga/conanfile.py": GenConanfile("pkga", "0.1").with_requires("ext/0.1") }) c.run("editable add ext") c.run("workspace add pkga") c.run("workspace build") assert "ext/0.1: WARN: BUILD EXT!" in c.out c.assert_listed_binary({"pkga/0.1": ("38f8a554b1b3c2cbb44321f0e731b3863359126c", "EditableBuild"), "ext/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "EditableBuild")}) class TestNew: def test_new(self): # Very basic workspace for testing c = TestClient(light=True) c.run("new workspace") assert 'name = "liba"' in c.load("liba/conanfile.py") c.run("workspace info") assert "liba/0.1" in c.out assert "libb/0.1" in c.out assert "app1/0.1" in c.out def test_new_dep(self): c = TestClient(light=True) c.run("new workspace -d requires=dep/0.1") assert 'self.requires("dep/0.1")' in c.load("liba/conanfile.py") assert 'name = "liba"' in c.load("liba/conanfile.py") c.run("workspace info") assert "liba/0.1" in c.out assert "libb/0.1" in c.out assert "app1/0.1" in c.out class TestMeta: def test_install(self): c = TestClient() c.save({"dep/conanfile.py": GenConanfile()}) c.run("create dep --name=dep1 --version=0.1") c.run("create dep --name=dep2 --version=0.1") c.save({"conanws.yml": "", "liba/conanfile.py": GenConanfile("liba", "0.1").with_requires("dep1/0.1", "dep2/0.1"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requires("liba/0.1", "dep1/0.1")}, clean_first=True) c.run("workspace add liba") c.run("workspace add libb") c.run("workspace super-install -g CMakeDeps -g CMakeToolchain -of=build " "--envs-generation=false") assert "Packages build order:\n liba/0.1: liba\n libb/0.1: libb" in c.out assert "Workspace conanws.py not found in the workspace folder, using default" in c.out files = os.listdir(os.path.join(c.current_folder, "build")) assert "conan_toolchain.cmake" in files assert "dep1-config.cmake" in files assert "dep2-config.cmake" in files assert "liba-config.cmake" not in files assert "libb-config.cmake" not in files assert "conanbuild.bat" not in files assert "conanbuild.sh" not in files def test_conanfilews_custom(self): c = TestClient() conanfilews = textwrap.dedent(""" from conan import ConanFile from conan import Workspace class MyWs(ConanFile): settings = "arch", "build_type" generators = "MSBuildDeps" def generate(self): self.run("echo myhello world!!!!") class Ws(Workspace): def root_conanfile(self): return MyWs """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "conanws.py": conanfilews}) c.run("workspace add dep") c.run("workspace super-install -of=build") assert "myhello world!!!!" in c.out files = os.listdir(os.path.join(c.current_folder, "build")) assert "conandeps.props" in files def test_conanfilews_errors(self): c = TestClient() conanfilews = textwrap.dedent(""" from conan import ConanFile from conan import Workspace class MyWs(ConanFile): requires = "dep/0.1" class Ws(Workspace): def root_conanfile(self): return MyWs """) c.save({"conanws.yml": "conanfilews: myconanfilews.py", "dep/conanfile.py": GenConanfile("dep", "0.1"), "conanws.py": conanfilews}) c.run("workspace super-install", assert_error=True) assert "ERROR: There are no selected packages defined in the workspace" in c.out c.run("workspace add dep") c.run("workspace super-install", assert_error=True) assert "ERROR: Conanfile in conanws.py shouldn't have 'requires'" in c.out def test_install_partial(self): # If we want to install only some part of the workspace c = TestClient() c.save({"dep/conanfile.py": GenConanfile()}) c.run("create dep --name=dep1 --version=0.1") c.run("create dep --name=dep2 --version=0.1") c.save({"conanws.yml": "", "liba/conanfile.py": GenConanfile("liba", "0.1").with_requires("dep1/0.1"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requires("liba/0.1"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requires("libb/0.1", "dep2/0.1")}, clean_first=True) c.run("workspace add liba") c.run("workspace add libb") c.run("workspace add libc") for arg in ("--pkg=libb/*", "--pkg=libb/* --pkg=liba/*"): c.run(f"workspace super-install {arg} -g CMakeDeps -of=build") assert "dep1/0.1" in c.out assert "dep2/0.1" not in c.out assert "libc/0.1" not in c.out files = os.listdir(os.path.join(c.current_folder, "build")) assert "dep1-config.cmake" in files assert "dep2-config.cmake" not in files def test_workspace_options(self): c = TestClient() conanfilews = textwrap.dedent(""" from conan import ConanFile from conan import Workspace class MyWs(ConanFile): settings = "arch", "build_type" options = {"myoption": [1, 2, 3]} def generate(self): self.output.info(f"Generating with my option {self.options.myoption}!!!!") class Ws(Workspace): def root_conanfile(self): return MyWs """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "conanws.py": conanfilews}) c.run("workspace add dep") c.run("workspace super-install -of=build") assert "conanws.py base project Conanfile: Generating with my option None!!!!" in c.out c.run("workspace super-install -of=build -o *:myoption=1") assert "conanws.py base project Conanfile: Generating with my option 1!!!!" in c.out def test_workspace_common_shared_options(self): c = TestClient() conanfilews = textwrap.dedent(""" from conan import ConanFile from conan import Workspace class MyWs(ConanFile): settings = "os" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} implements = "auto_shared_fpic" def generate(self): self.output.info(f"OS={self.settings.os}!!!!") self.output.info(f"shared={self.options.shared}!!!!") self.output.info(f"fPIC={self.options.get_safe('fPIC')}!!!!") class Ws(Workspace): def root_conanfile(self): return MyWs """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "conanws.py": conanfilews}) c.run("workspace add dep") c.run("workspace super-install -of=build -s os=Windows") assert "conanws.py base project Conanfile: OS=Windows!!!!" in c.out assert "conanws.py base project Conanfile: shared=False!!!!" in c.out assert "conanws.py base project Conanfile: fPIC=None!!!!" in c.out c.run("workspace super-install -of=build -s os=Linux -o *:shared=True") assert "conanws.py base project Conanfile: OS=Linux!!!!" in c.out assert "conanws.py base project Conanfile: shared=True!!!!" in c.out assert "conanws.py base project Conanfile: fPIC=None!!!!" in c.out c.run("workspace super-install -of=build -s os=Linux -o *:shared=False") assert "conanws.py base project Conanfile: OS=Linux!!!!" in c.out assert "conanws.py base project Conanfile: shared=False!!!!" in c.out assert "conanws.py base project Conanfile: fPIC=True!!!!" in c.out def test_workspace_pkg_options(self): c = TestClient() conanfilews = textwrap.dedent(""" from conan import ConanFile from conan import Workspace class MyWs(ConanFile): settings = "arch", "build_type" def generate(self): for pkg, dep in self.workspace_packages.items(): for k, v in dep.options.items(): self.output.info(f"Generating with opt {pkg}:{k}={v}!!!!") class Ws(Workspace): def root_conanfile(self): return MyWs """) c.save({"dep/conanfile.py": GenConanfile("dep", "0.1").with_option("myoption", [1, 2, 3]), "conanws.py": conanfilews}) c.run("workspace add dep") c.run("workspace super-install -of=build") assert "project Conanfile: Generating with opt dep/0.1:myoption=None!!!!" in c.out c.run("workspace super-install -of=build -o *:myoption=1") assert "project Conanfile: Generating with opt dep/0.1:myoption=1!!!!" in c.out def test_workspace_pkg_definitions(self): c = TestClient() conanfilews = textwrap.dedent(""" from conan import ConanFile from conan import Workspace from conan.tools.cmake import CMakeToolchain class MyWs(ConanFile): settings = "arch", "build_type" def generate(self): tc = CMakeToolchain(self) for ref, dep in self.workspace_packages.items(): dep.configure_toolchain(tc) tc.generate() class Ws(Workspace): def root_conanfile(self): return MyWs """) dep = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.2.3" def configure_toolchain(self, tc): tc.preprocessor_definitions["DEP_SOME_DEFINITION"] = self.version def generate(self): tc = CMakeToolchain(self) self.configure_toolchain(tc) tc.generate() """) pkg = textwrap.dedent(""" from conan import ConanFile class Dep(ConanFile): name = "pkg" version = "2.3.4" def configure_toolchain(self, tc): tc.preprocessor_definitions["SOME_PKG_DEFINE"] = self.version def generate(self): tc = CMakeToolchain(self) self.configure_toolchain(tc) tc.generate() """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg, "conanws.py": conanfilews}) c.run("workspace add dep") c.run("workspace add pkg") c.run("workspace super-install") tc = c.load("conan_toolchain.cmake") assert 'add_compile_definitions("SOME_PKG_DEFINE=2.3.4")' in tc assert 'add_compile_definitions("DEP_SOME_DEFINITION=1.2.3")' in tc def test_intermediate_non_editable(self): c = TestClient(light=True) c.save({"liba/conanfile.py": GenConanfile("liba", "0.1"), "libb/conanfile.py": GenConanfile("libb", "0.1").with_requires("liba/0.1"), "libc/conanfile.py": GenConanfile("libc", "0.1").with_requires("libb/0.1")}) c.run("workspace init") c.run("workspace add liba") c.run("export libb") c.run("workspace add libc") c.run("workspace super-install", assert_error=True) assert ("Workspace definition error. Package libb/0.1 in the Conan cache " "has dependencies to packages in the workspace: [liba/0.1]") in c.out def test_install_create_use_lockfile(self): c = TestClient() c.save({"dep/conanfile.py": GenConanfile(), "ws/conanws.yml": "", "ws/liba/conanfile.py": GenConanfile("liba").with_requires("dep1/[*]", "dep2/[*]"), "ws/libb/conanfile.py": GenConanfile("libb").with_requires("liba/[*]", "dep1/[*]")}, ) c.run("create dep --name=dep1 --version=0.1") c.run("create dep --name=dep2 --version=0.1") with c.chdir("ws"): c.run("workspace add liba --version=0.1") c.run("workspace add libb --version=0.1") c.run("workspace super-install --lockfile-out=ws.lock") lock = json.loads(c.load("ws.lock")) refs = [RecipeReference.loads(r).repr_notime() for r in lock["requires"]] # Liba and libb are NOT in the lockfile for this assert refs == ['libb/0.1', 'liba/0.1', 'dep2/0.1#4d670581ccb765839f2239cc8dff8fbd', 'dep1/0.1#4d670581ccb765839f2239cc8dff8fbd'] c.run("create dep --name=dep1 --version=0.2") c.run("create dep --name=dep2 --version=0.2") with c.chdir("ws"): c.run("workspace super-install --lockfile=ws.lock") assert "dep1/0.1" in c.out assert "dep2/0.1" in c.out assert "dep1/0.2" not in c.out assert "dep2/0.2" not in c.out # bump the workspace versions themselves c.run("workspace add liba --version=0.2") assert "Workspace 'ws': WARN: Package liba already exists, updating" in c.out c.run("workspace add libb --version=0.2") c.run("workspace super-install --lockfile=ws.lock", assert_error=True) assert "ERROR: Requirement 'liba/0.2' not in lockfile 'requires'" in c.out c.run("workspace super-install --lockfile=ws.lock --lockfile-partial " "--lockfile-clean --lockfile-out=ws2.lock") assert "dep1/0.1" in c.out assert "dep2/0.1" in c.out lock = json.loads(c.load("ws2.lock")) refs = [RecipeReference.loads(r).repr_notime() for r in lock["requires"]] # Liba and libb are NOT in the lockfile for this assert refs == ['libb/0.2', 'liba/0.2', 'dep2/0.1#4d670581ccb765839f2239cc8dff8fbd', 'dep1/0.1#4d670581ccb765839f2239cc8dff8fbd'] def test_deployers_json(self): c = TestClient() dep = textwrap.dedent(""" import os from conan.tools.files import save from conan import ConanFile class Dep(ConanFile): name = "dep" version = "1.0" def package(self): save(self, os.path.join(self.package_folder, "myfile.txt"), "content") """) pkg = textwrap.dedent(""" import os from conan.tools.files import save from conan import ConanFile class Dep(ConanFile): name = "pkg" version = "1.0" requires = "dep/1.0" def package(self): save(self, os.path.join(self.package_folder, "myfile.txt"), "content") """) c.save({"dep/conanfile.py": dep, "pkg/conanfile.py": pkg}) c.run("workspace init") c.run("create dep") c.run("workspace add pkg") c.run("workspace super-install --format=json --deployer=full_deploy") graph = json.loads(c.stdout) # Only 1 node, pkg is not a node in the graph! assert graph["graph"]["nodes"]["1"]["name"] == "dep" deploy_folder = os.path.join(c.current_folder, "full_deploy", "host") folders = os.listdir(deploy_folder) assert "dep" in folders assert "pkg" not in folders assert os.path.isfile(os.path.join(deploy_folder, "dep", "1.0", "myfile.txt")) def test_customize_generator_per_package(self): # https://github.com/conan-io/conan/issues/19326 c = TestClient(light=True) pkg = GenConanfile("pkg", "0.1") workspace = textwrap.dedent("""\ from conan import Workspace, ConanFile class MyWs(ConanFile): pass class Ws(Workspace): def root_conanfile(self): return MyWs """) c.save({"pkg/conanfile.py": pkg, "conanws.py": workspace}) c.run("workspace add pkg") c.run("workspace super-install -c acme*:tools.cmake.cmaketoolchain:generator=Ninja") # It no longer crashes assert "Install finished successfully" in c.out def test_workspace_with_local_recipes_index(): c3i_folder = temp_folder() recipes_folder = os.path.join(c3i_folder, "recipes") zlib_config = textwrap.dedent(""" versions: "1.2.11": folder: all """) save_files(recipes_folder, {"zlib/config.yml": zlib_config, "zlib/all/conanfile.py": str(GenConanfile("zlib")), "zlib/all/conandata.yml": ""}) c = TestClient(light=True) c.save({".conanrc": 'conan_home=deps\n'}) c.run(f'remote add local "{c3i_folder}"') c.run("list zlib/1.2.11#* -r=local") assert "zlib/1.2.11" in c.out # It doesn't crash c.run("list zlib/1.2.11#*") assert "zlib/1.2.11" not in c.out class TestClean: def test_clean(self): # Using cmake_layout, we can clean the build folders c = TestClient() c.save({"conanws.yml": "name: my_workspace"}) pkga = GenConanfile("pkga", "0.1").with_settings("build_type") pkgb = GenConanfile("pkgb", "0.1").with_requires("pkga/0.1").with_settings("build_type") c.save({"pkga/conanfile.py": pkga, "pkgb/conanfile.py": pkgb, "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkgb/0.1")}) c.run("workspace add pkga -of=build/pkga") c.run("workspace add pkgb -of=build/pkgb") c.run("workspace add pkgc") c.run("workspace build") assert os.path.exists(os.path.join(c.current_folder, "build", "pkga")) assert os.path.exists(os.path.join(c.current_folder, "build", "pkgb")) c.run("workspace clean") assert "Workspace 'my_workspace': Removing pkga/0.1 output folder" in c.out assert "Workspace 'my_workspace': Removing pkgb/0.1 output folder" in c.out assert "Editable pkgc/0.1 doesn't have an output_folder defined" in c.out assert not os.path.exists(os.path.join(c.current_folder, "build", "pkga")) assert not os.path.exists(os.path.join(c.current_folder, "build", "pkgb")) def test_custom_clean(self): conanfilews = textwrap.dedent(""" from conan import Workspace class Ws(Workspace): def name(self): return "my_workspace" def clean(self): self.output.info("MY CLEAN!!!!") """) c = TestClient() c.save({"conanws.py": conanfilews}) c.run("workspace clean") assert "Workspace 'my_workspace': MY CLEAN!!!" in c.out def test_relative_paths(): # This is using the meta-project c = TestClient() c.run("new cmake_lib -d name=liba --output=liba") c.run("new cmake_exe -d name=app1 -d requires=liba/0.1 --output=app1") c.run("new cmake_lib -d name=libb --output=other/libb") c.run("new cmake_exe -d name=app2 -d requires=libb/0.1 --output=other/app2") c.run("workspace init mywks") c.run("workspace init otherwks") # cd mywks with c.chdir("mywks"): c.run("workspace add ../liba") c.run("workspace add ../app1") c.run("workspace info") expected = textwrap.dedent("""\ packages - path: ../liba ref: liba/0.1 - path: ../app1 ref: app1/0.1 """) assert expected in c.out c.run("graph info --requires=app1/0.1") c.assert_listed_require({"app1/0.1": "Editable", "liba/0.1": "Editable"}) # cd otherwks with c.chdir("otherwks"): c.run("workspace add ../other/libb") c.run("workspace add ../other/app2") c.run("workspace info") expected = textwrap.dedent("""\ packages - path: ../other/libb ref: libb/0.1 - path: ../other/app2 ref: app2/0.1 """) assert expected in c.out c.run("graph info --requires=app2/0.1") c.assert_listed_require({"app2/0.1": "Editable", "libb/0.1": "Editable"}) def test_host_build_require(): c = TestClient() protobuf = textwrap.dedent("""\ from conan import ConanFile from conan.tools.files import save, copy class Protobuf(ConanFile): name = "protobuf" version = "0.1" settings = "os" def layout(self): self.folders.build = f"mybuild/folder_{self.settings.os}" self.cpp.build.bindirs = ["."] def build(self): save(self, "myprotobuf.txt", f"MYOS={self.settings.os}!!!") def package(self): copy(self, "myprotobuf.txt", src=self.build_folder, dst=self.package_folder) def package_info(self): self.cpp_info.bindirs = ["."] """) app = textwrap.dedent("""\ import os from conan import ConanFile from conan.tools.files import load class Protobuf(ConanFile): name = "app" version = "0.1" requires = "protobuf/0.1" tool_requires = "protobuf/0.1" def build(self): binhost = self.dependencies.host["protobuf"].cpp_info.bindir binbuild = self.dependencies.build["protobuf"].cpp_info.bindir host = load(self, os.path.join(binhost, "myprotobuf.txt")) build = load(self, os.path.join(binbuild, "myprotobuf.txt")) self.output.info(f"BUILDING WITH BINHOST: {host}") self.output.info(f"BUILDING WITH BINBUILD: {build}") """) c.save({"protobuf/conanfile.py": protobuf, "app/conanfile.py": app}) c.run("workspace init .") c.run("workspace add protobuf") c.run("workspace add app") c.run("install app --build=editable -s:b os=Linux -s:h os=Windows") assert c.load("protobuf/mybuild/folder_Linux/myprotobuf.txt") == "MYOS=Linux!!!" assert c.load("protobuf/mybuild/folder_Windows/myprotobuf.txt") == "MYOS=Windows!!!" shutil.rmtree(os.path.join(c.current_folder, "protobuf", "mybuild")) assert not os.path.exists(os.path.join(c.current_folder, "protobuf", "mybuild")) c.run("workspace build -s:b os=Linux -s:h os=Windows") assert "conanfile.py (app/0.1): BUILDING WITH BINHOST: MYOS=Windows!!!" in c.out assert "conanfile.py (app/0.1): BUILDING WITH BINBUILD: MYOS=Linux!!!" in c.out assert c.load("protobuf/mybuild/folder_Linux/myprotobuf.txt") == "MYOS=Linux!!!" assert c.load("protobuf/mybuild/folder_Windows/myprotobuf.txt") == "MYOS=Windows!!!" shutil.rmtree(os.path.join(c.current_folder, "protobuf", "mybuild")) assert not os.path.exists(os.path.join(c.current_folder, "protobuf", "mybuild")) c.run("workspace create -s:b os=Linux -s:h os=Windows") assert "app/0.1: BUILDING WITH BINHOST: MYOS=Windows!!!" in c.out assert "app/0.1: BUILDING WITH BINBUILD: MYOS=Linux!!!" in c.out class TestCreate: def test_create(self): c = TestClient(light=True, default_server_user=True) c.save({"conanfile.py": GenConanfile("zlib", "0.1").with_build_msg("BUILD ZLIB!")}) c.run("export .") c.save({"conanws.yml": ""}, clean_first=True) c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_requires("zlib/0.1") .with_build_msg("BUILD PKGA!"), "pkga/test_package/conanfile.py": GenConanfile().with_test("pass"), "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_build_msg("BUILD PKGB!") .with_requires("pkga/0.1"), "pkgb/test_package/conanfile.py": GenConanfile().with_test("pass"), "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_build_msg("BUILD PKGC!") .with_requires("pkgb/0.1"), "pkgc/test_package/conanfile.py": GenConanfile().with_test("pass"), }) c.run("workspace add pkga") c.run("workspace add pkgb") c.run("workspace add pkgc") c.run("workspace create --build=zlib/*") assert str(c.out).count("zlib/0.1: WARN: BUILD ZLIB!") == 1 assert str(c.out).count("pkga/0.1: WARN: BUILD PKGA!") == 1 assert str(c.out).count("pkgb/0.1: WARN: BUILD PKGB!") == 1 assert str(c.out).count("pkgc/0.1: WARN: BUILD PKGC!") == 1 assert "pkga/0.1 (test package): Running test()" in c.out assert "pkgb/0.1 (test package): Running test()" in c.out assert "pkgc/0.1 (test package): Running test()" in c.out # A second conan workspace create will not re-build c.run("workspace create") assert "WARN: BUILD ZLIB!" not in c.out assert "WARN: BUILD PKGA!" not in c.out assert "WARN: BUILD PKGB!" not in c.out assert "WARN: BUILD PKBC!" not in c.out assert "pkga/0.1 (test package): Running test()" not in c.out assert "pkgb/0.1 (test package): Running test()" not in c.out assert "pkgc/0.1 (test package): Running test()" not in c.out c.run("upload zlib/* -r=default -c") c.run("remove zlib/* -c") c.run("remove pkgc/* -c") c.run("workspace create") assert "WARN: BUILD ZLIB!" not in c.out assert "pkgc/0.1: WARN: BUILD PKGC!" in c.out def test_create_dynamic_name(self): workspace = textwrap.dedent("""\ import os from conan import Workspace class MyWorkspace(Workspace): def packages(self): result = [] for f in os.listdir(self.folder): if os.path.isdir(os.path.join(self.folder, f)): result.append({"path": f, "ref": f"{f}/0.1"}) return result """) c = TestClient(light=True) c.save({"conanws.py": workspace, "pkga/conanfile.py": GenConanfile()}) c.run("workspace info") assert "pkga/0.1" in c.out c.run("workspace create") assert "Workspace create pkga/0.1" in c.out def test_host_build_require(self): c = TestClient() protobuf = textwrap.dedent("""\ from conan import ConanFile class Protobuf(ConanFile): name = "protobuf" version = "0.1" settings = "os" def build(self): self.output.info(f"Building for: {self.settings.os}!!!") """) app = textwrap.dedent("""\ from conan import ConanFile class Protobuf(ConanFile): name = "app" version = "0.1" requires = "protobuf/0.1" tool_requires = "protobuf/0.1" """) c.save({"protobuf/conanfile.py": protobuf, "app/conanfile.py": app}) c.run("workspace init .") c.run("workspace add protobuf") c.run("workspace add app") c.run("workspace create -s:b os=Linux -s:h os=Windows") assert "protobuf/0.1: Building for: Windows!!!" in c.out assert "protobuf/0.1: Building for: Linux!!!" in c.out class TestSource: def test_source(self): c = TestClient(light=True) c.save({"conanws.yml": ""}) pkga = textwrap.dedent("""\ from conan import ConanFile class TestPackage(ConanFile): name = "pkga" version = "0.1" def source(self): self.output.info("Executing SOURCE!!!") """) pkgb = textwrap.dedent("""\ from conan import ConanFile class TestPackage(ConanFile): name = "pkgb" version = "0.1" requires = "pkga/0.1" def source(self): self.output.info("Executing SOURCE!!!") """) pkgc = textwrap.dedent("""\ from conan import ConanFile class TestPackage(ConanFile): name = "pkgc" version = "0.1" requires = "pkgb/0.1" def source(self): self.output.info("Executing SOURCE!!!") """) c.save({"pkga/conanfile.py": pkga, "pkgb/conanfile.py": pkgb, "pkgc/conanfile.py": pkgc}) c.run("workspace add pkga") c.run("workspace add pkgb") c.run("workspace add pkgc") c.run("workspace source") assert "conanfile.py (pkga/0.1): Executing SOURCE!!!" in c.out assert "conanfile.py (pkgb/0.1): Executing SOURCE!!!" in c.out assert "conanfile.py (pkgc/0.1): Executing SOURCE!!!" in c.out class TestInstall: def test_install(self): c = TestClient() c.save({"conanws.yml": ""}) pkg = textwrap.dedent("""\ from conan import ConanFile class Package(ConanFile): name = "{}" version = "0.1" settings = "os", "build_type" {} generators = "CMakeToolchain" """) c.save({"pkga/conanfile.py": pkg.format("pkga", ''), "pkgb/conanfile.py": pkg.format("pkgb", 'requires="pkga/0.1"'), "pkgc/conanfile.py": pkg.format("pkgc", 'requires="pkgb/0.1"')}) c.run("workspace add pkga") c.run("workspace add pkgb") c.run("workspace add pkgc") c.run("workspace install") assert "conanfile.py (pkga/0.1): CMakeToolchain generated" in c.out assert "conanfile.py (pkgb/0.1): CMakeToolchain generated" in c.out assert "conanfile.py (pkgc/0.1): CMakeToolchain generated" in c.out def test_install_lockfile_out_error(self): # it is not possible to generate a lockfile for an orchestrated # "conan workspace install/build" command c = TestClient() c.save({"conanws.yml": ""}) c.run("workspace install --lockfile-out=conan.lock", assert_error=True) assert "error: unrecognized arguments: --lockfile-out=conan.lock" in c.out c.run("workspace create --lockfile-out=conan.lock", assert_error=True) assert "error: unrecognized arguments: --lockfile-out=conan.lock" in c.out def test_keep_core_conf(): c = TestClient() myprofile = textwrap.dedent("""\ [settings] os=FreeBSD arch=armv7 """) conanfile = textwrap.dedent("""\ from conan import ConanFile class Pkg(ConanFile): name = "pkga" version = "0.1" settings = "os", "arch" def generate(self): self.output.info(f"Generating!: {self.settings.os}-{self.settings.arch}!!!!") """) c.save_home({"profiles/myprofile": myprofile}) c.run("profile show ") c.save({"conanws.yml": "", "pkga/conanfile.py": conanfile}) c.run("workspace add pkga") # The injected -cc is still applied to every "conan install" c.run("workspace install -cc core:default_profile=myprofile") assert "conanfile.py (pkga/0.1): Generating!: FreeBSD-armv7!!!!" in c.out # also the global.conf c.save_home({"global.conf": "core:default_profile=myprofile"}) c.run("workspace install") assert "conanfile.py (pkga/0.1): Generating!: FreeBSD-armv7!!!!" in c.out def test_workspace_defining_only_paths(): c = TestClient() c.run("new workspace") conanws_with_labels = textwrap.dedent("""\ packages: - path: liba - path: libb - path: app1 """) c.save({"conanws.yml": conanws_with_labels}) # liba with user and channel too replace_in_file(ConanFileMock(), os.path.join(c.current_folder, "liba", "conanfile.py"), 'version = "0.1"', 'version = "0.1"\n user = "myuser"\n channel = "mychannel"') replace_in_file(ConanFileMock(), os.path.join(c.current_folder, "libb", "conanfile.py"), 'self.requires("liba/0.1")', 'self.requires("liba/0.1@myuser/mychannel")') c.run("workspace info") expected = textwrap.dedent("""\ packages - path: liba - path: libb - path: app1 """) assert expected in c.out c.run("workspace install") assert "conanfile.py (app1/0.1)" in c.out assert "liba/0.1@myuser/mychannel - Editable" in c.out assert "libb/0.1 - Editable" in c.out c.run("workspace super-install") assert "app1/0.1 - Editable" in c.out assert "liba/0.1@myuser/mychannel - Editable" in c.out assert "libb/0.1 - Editable" in c.out def test_workspace_defining_duplicate_references(): """ Testing duplicate references but different paths """ c = TestClient(light=True) conanws_with_labels = textwrap.dedent("""\ packages: - path: liba - path: liba1 - path: liba2 """) c.save({ "conanws.yml": conanws_with_labels, "liba/conanfile.py": GenConanfile(name="liba", version="0.1"), "liba1/conanfile.py": GenConanfile(name="liba", version="0.1"), "liba2/conanfile.py": GenConanfile(name="liba", version="0.1"), }) c.run("workspace install", assert_error=True) assert "Workspace package 'liba/0.1' already exists." in c.out def test_workspace_reference_error(): c = TestClient(light=True) conanws_with_labels = textwrap.dedent("""\ packages: - path: libx """) c.save({"conanws.yml": conanws_with_labels, "libx/conanfile.py": ""}) c.run("workspace install", assert_error=True) assert ("Workspace package reference could not be deduced by libx/conanfile.py or it is not" " correctly defined in the conanws.yml file") in c.out def test_workspace_python_error(): c = TestClient() workspace = textwrap.dedent("""\ from conan import Workspace class MyWorkspace(Workspace): def packages(self): os.listdir(self.folder) """) c.save({"conanws.yml": "", "conanws.py": "bad"}) c.run("workspace info", assert_error=True) assert "ERROR: Error loading conanws.py at" in c.out c.save({"conanws.yml": "", "conanws.py": workspace}) c.run("workspace info", assert_error=True) assert "ERROR: Workspace conanws.py file: Error in packages() method, line 5" in c.out ================================================ FILE: test/performance/.gitignore ================================================ cache/ ================================================ FILE: test/performance/__init__.py ================================================ ================================================ FILE: test/performance/test_compatibility_performance.py ================================================ import textwrap import time import pytest from conan.test.utils.tools import TestClient, GenConanfile RUN_COUNT = 2 @pytest.mark.skip(reason="Compatibility performance test") class TestCompatibilityPerformance: results = [] @pytest.fixture(scope="module", autouse=True) def timings(self): results = [] yield results import matplotlib.pyplot as plt fig = plt.figure() ax = fig.add_subplot() ax.set_title("Compatibility performance in real Artifactory server (last match, unoptimized)") #ax_tot = ax.twinx() # Plot a basic wireframe. grouping = {} x, y, y_tot_opt, y_tot_non = [], [], [], [] stderr_opt, stderr_non = [], [] for factors, run, with_optimization, duration in results: group = grouping.setdefault(factors, {"opt": [], "non": []}) if with_optimization: group["opt"].append(duration) else: group["non"].append(duration) def ssd(values, mean): return sum((v - mean) ** 2 for v in values) / (len(values) - 1) for factors, durations in sorted(grouping.items()): x.append(factors) y.append(0) mean_opt = sum(durations["opt"]) / RUN_COUNT mean_non = sum(durations["non"]) / RUN_COUNT y_tot_opt.append(mean_opt) y_tot_non.append(mean_non) # ssd_opt = ssd(durations["opt"], mean_opt) # ssd_non = ssd(durations["non"], mean_non) # stderr_opt.append(ssd_opt / (RUN_COUNT ** 0.5)) # stderr_non.append(ssd_non / (RUN_COUNT ** 0.5)) max_diff_y = max(y) min_diff_y = min(min(y), 0) # ax_tot.set_ylim(ymin=min_diff_y-0.1*min_diff_y, ymax=max_diff_y+0.1*max_diff_y) max_tot_y = max(max(y_tot_non), max(y_tot_opt)) ax.set_ylim(ymin=0, ymax=max_tot_y+0.1*max_tot_y) ax.grid(visible=True, which="major", axis="y", linestyle="--") # ax_tot.set_xticks(x) ax.set_xticks(x) ax.plot(x, y_tot_non, 'red', marker='x', label="Without optimization", linestyle=":") # ax.errorbar(x, y_tot_non, yerr=stderr_non) ax.plot(x, y_tot_opt, 'blue', marker='x', label="With optimization", linestyle=":") # ax.errorbar(x, y_tot_opt, yerr=stderr_opt) # ax_tot.plot(x, y, 'gray', marker='o', linestyle="--", label="Difference total") # ax_tot.set_ylabel("Total time diff (s)") ax.set_xlabel("Number of compatible configurations in server") ax.set_ylabel("Total time (s)") ax.legend(loc="upper left") # ax_tot.legend(loc="upper right") plt.show() @pytest.mark.parametrize("runs", range(RUN_COUNT)) @pytest.mark.parametrize("with_optimization", [True, False]) @pytest.mark.parametrize("factors", [pow(2, n) for n in range(1, 6)]) def test_list_only_compatibility(self, timings, with_optimization, factors, runs): tc = TestClient(light=True) tc.run("remote add default https://conandev.jfrog.io/artifactory/api/conan/abril-tests") tc.run("remote login default abril") tc.run("remove * -c -r=default") cppstds = [7 + i*3 for i in range(factors+1)] compiler_settings = textwrap.dedent(f""" compiler: foo: version: [1] cppstd: {cppstds}""") tc.run("version") tc.save_home({"settings_user.yml": compiler_settings}) compat = tc.load_home("extensions/plugins/compatibility/compatibility.py") compat = compat.replace("cppstd_possible_values = supported_cppstd(conanfile)", f"cppstd_possible_values = {cppstds}") tc.save_home({"extensions/plugins/compatibility/compatibility.py": compat}) threshold = -1 if with_optimization else 1 tc.save_home({"global.conf": f"user.graph:compatibility_optimization_threshold={factors + threshold}"}) compiler_args = "-s compiler=foo -s compiler.version=1" pkg_name = str(time.time()) tc.save({"conanfile.py": GenConanfile(pkg_name, "0.1").with_settings("compiler")}) if with_optimization: for i in range(factors-1): tc.run(f"create . {compiler_args} -s compiler.cppstd={7 + i*3} -nr") tc.run(f"create . {compiler_args} -s compiler.cppstd={7 + (factors-1)*3} -nr") tc.run("upload * -r=default -c") tc.run("remove *:* -c") start_time = time.time() tc.run(f"install --requires={pkg_name}/0.1 {compiler_args} -s compiler.cppstd={7 + factors*3}") end_time = time.time() if with_optimization: assert f"Found {factors} compatible configurations in remotes" in tc.out else: assert f"compatible configurations in remotes" not in tc.out duration = end_time - start_time timings.append((factors, runs, with_optimization, duration)) ================================================ FILE: test/performance/test_db_performance.py ================================================ import os import time import pytest from conan.api.model import RecipeReference from conan.internal.cache.db.cache_database import CacheDatabase from conan.test.utils.test_files import temp_folder @pytest.mark.skip(reason="This is a performance test, skip for normal runs") def test_db_performance(): f = temp_folder() # f = r"C:\conan_tests\tmp_o0846cmconans\path with spaces" print("Tempt folder: ", f) db = CacheDatabase(os.path.join(f, "mytest.sqlite")) num_refs = 1000 splits = 10 for num_split in range(10): init = time.time() for i in range(int(num_refs / splits)): index = num_split * int(num_refs / splits) + i ref = RecipeReference.loads(f"pkg/1.{index}#rev1%1") path = os.path.join(f, f"folder{index}") db.create_recipe(path, ref) creation_time = time.time() - init print(f"Creation time {num_split}:", creation_time) print(" Avg:", creation_time/num_refs) experiments = 10 texp = time.time() for experiment in range(experiments): ret = db.list_references() assert len(ret) == num_refs exp_time = time.time() - texp print("SEARCH RECIPES time:", exp_time) print(" Avg:", exp_time / experiments) texp = time.time() specific_ref = RecipeReference.loads(f"pkg/1.1#rev1%1") for experiment in range(experiments): db.get_recipe(specific_ref) exp_time = time.time() - texp print("GET RECIPE time:", exp_time) print(" Avg:", exp_time / experiments) texp = time.time() specific_ref = RecipeReference.loads(f"pkg/1.1#rev1%1") for experiment in range(experiments): db.get_latest_recipe(specific_ref) exp_time = time.time() - texp print("GET LATEST RECIPE time:", exp_time) print(" Avg:", exp_time / experiments) texp = time.time() for experiment in range(experiments): db.update_recipes_lru([specific_ref]) exp_time = time.time() - texp print("UPDATE LRU:", exp_time) print(" Avg:", exp_time / experiments) updates = 50 texp = time.time() for experiment in range(experiments): refs = [RecipeReference.loads(f"pkg/1.{index}#rev1%1") for index in range(updates)] db.update_recipes_lru(refs) exp_time = time.time() - texp print("UPDATE LRU BATCH:", exp_time) print(" Avg:", exp_time / experiments) print(" Avg:", exp_time / (experiments * updates)) ================================================ FILE: test/performance/test_large_graph.py ================================================ import cProfile import os import pstats import time from pstats import SortKey import pytest from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.skip(reason="This is a performance test, skip for normal runs") def test_large_graph(): c = TestClient(cache_folder=os.path.join(os.path.dirname(__file__), "cache")) num_test = 40 num_pkgs = 40 """for i in range(num_test): conanfile = GenConanfile(f"test{i}", "0.1") if i > 0: conanfile.with_requires(f"test{i-1}/0.1") c.save({"conanfile.py": conanfile}) c.run("create .") for i in range(num_pkgs): conanfile = GenConanfile(f"pkg{i}", "0.1").with_test_requires(f"test{num_test-1}/0.1") if i > 0: conanfile.with_requires(f"pkg{i-1}/0.1") c.save({"conanfile.py": conanfile}) c.run("create .") """ t = time.time() pr = cProfile.Profile() pr.enable() c.run(f"install --requires=pkg{num_pkgs - 1}/0.1") pr.disable() print(time.time()-t) sortby = SortKey.CUMULATIVE ps = pstats.Stats(pr).sort_stats(sortby) ps.print_stats() #graph = json.loads(c.stdout) #assert len(graph["graph"]["nodes"]) == 1 + num_pkgs + num_test * num_pkgs @pytest.mark.skip(reason="This is a performance test, skip for normal runs") def test_large_graph2(): c = TestClient(cache_folder=os.path.join(os.path.dirname(__file__), "cache")) num_test = 20 num_pkgs = 20 branches = ["a", "b", "c"] c.save({"conanfile.py": GenConanfile("testbase", "0.1")}) c.run("export .") for i in range(num_test): for branch in branches: conanfile = GenConanfile(f"test{branch}{i}", "0.1") if i > 0: conanfile.with_requires(f"test{branch}{i-1}/0.1", "testbase/0.1") else: conanfile.with_requires("testbase/0.1") c.save({"conanfile.py": conanfile}) c.run("export .") for i in range(num_pkgs): conanfile = GenConanfile(f"pkg{i}", "0.1") for branch in branches: conanfile.with_test_requires(f"test{branch}{num_test-1}/0.1") if i > 0: conanfile.with_requires(f"pkg{i-1}/0.1") c.save({"conanfile.py": conanfile}) c.run("export .") t = time.time() pr = cProfile.Profile() pr.enable() c.run(f"graph info --requires=pkg{num_pkgs - 1}/0.1") pr.disable() print(time.time()-t) sortby = SortKey.CUMULATIVE ps = pstats.Stats(pr).sort_stats(sortby) ps.print_stats() ================================================ FILE: test/unittests/__init__.py ================================================ ================================================ FILE: test/unittests/cli/__init__.py ================================================ ================================================ FILE: test/unittests/cli/common_test.py ================================================ import os from unittest.mock import MagicMock import pytest from conan.api.conan_api import ConanAPI from conan.internal.cache.home_paths import HomePaths from conan.errors import ConanException from conan.test.utils.test_files import temp_folder from conan.internal.util.files import save @pytest.fixture() def conan_api(): tmp_folder = temp_folder() home_path = HomePaths(tmp_folder) save(os.path.join(home_path.profiles_path, "default"), "") return ConanAPI(tmp_folder) @pytest.fixture() def argparse_args(): return MagicMock( profile_build=None, profile_host=None, profile_all=None, settings_build=None, settings_host=None, settings_all=None, options_build=None, options_host=None, options_all=None, conf_build=None, conf_host=None, conf_all=None, ) @pytest.mark.parametrize("conf_name", [ "core.doesnotexist:never", "core:doesnotexist" ]) def test_core_confs_not_allowed_via_cli(conan_api, argparse_args, conf_name): argparse_args.conf_build = [conf_name] argparse_args.conf_host = [conf_name] with pytest.raises(ConanException) as exc: conan_api.profiles.get_profiles_from_args(argparse_args) assert "[conf] 'core.*' configurations are not allowed in profiles" in str(exc.value) ================================================ FILE: test/unittests/cli/test_cli_ref_matching.py ================================================ import pytest from conan.api.model import ListPattern from conan.errors import ConanException @pytest.mark.parametrize("pattern, result", [("*", ("*", "latest", None, "latest")), ("zlib/1.2.11", ("zlib/1.2.11", "latest", None, "latest")), ("zlib/1.2.11#rev1", ("zlib/1.2.11", "rev1", None, "latest")), ("zlib/1.2.11:pid1", ("zlib/1.2.11", "latest", "pid1", "latest"))]) def test_cli_pattern_matching(pattern, result): pattern = ListPattern(pattern) assert result == (pattern.ref, pattern.rrev, pattern.package_id, pattern.prev) def test_list_pattern(): with pytest.raises(ConanException) as e: ListPattern("*:*", only_recipe=True) assert "Do not specify 'package_id' with 'only-recipe'" in str(e.value) ================================================ FILE: test/unittests/client/__init__.py ================================================ ================================================ FILE: test/unittests/client/build/__init__.py ================================================ ================================================ FILE: test/unittests/client/build/c_std_flags_test.py ================================================ from conan.internal.api.detect.detect_api import default_cstd from conan.internal.model.version import Version from conan.test.utils.mocks import ConanFileMock, MockSettings from conan.tools.build.flags import cstd_flag def _make_cstd_flag(compiler, compiler_version, cstd=None): conanfile = ConanFileMock() conanfile.settings = MockSettings({"compiler": compiler, "compiler.version": compiler_version, "compiler.cstd": cstd}) return cstd_flag(conanfile) def test_gcc_cstd_flags(): assert _make_cstd_flag("gcc", "4.2", "99") == "-std=c99" assert _make_cstd_flag("clang", "4.2", "99") == "-std=c99" assert _make_cstd_flag("apple-clang", "4.2", "99") == "-std=c99" assert _make_cstd_flag("msvc", "192", "11") == "/std:c11" assert _make_cstd_flag("msvc", "192", "17") == "/std:c17" assert _make_cstd_flag("msvc", "191", "11") is None assert _make_cstd_flag("msvc", "191", "17") is None def _make_cstd_default(compiler, compiler_version): return default_cstd(compiler, Version(compiler_version)) def test_gcc_cstd_defaults(): assert _make_cstd_default("gcc", "4") == "gnu99" assert _make_cstd_default("gcc", "5") == "gnu11" assert _make_cstd_default("gcc", "6") == "gnu11" assert _make_cstd_default("gcc", "6.1") == "gnu11" assert _make_cstd_default("gcc", "7.3") == "gnu11" assert _make_cstd_default("gcc", "8.1") == "gnu17" assert _make_cstd_default("gcc", "11") == "gnu17" assert _make_cstd_default("gcc", "11.1") == "gnu17" assert _make_cstd_default("gcc", "15.1") == "gnu23" def test_clang_cstd_defaults(): assert _make_cstd_default("clang", "2") == "gnu99" assert _make_cstd_default("clang", "2.1") == "gnu99" assert _make_cstd_default("clang", "3.0") == "gnu99" assert _make_cstd_default("clang", "3.1") == "gnu99" assert _make_cstd_default("clang", "3.4") == "gnu99" assert _make_cstd_default("clang", "3.5") == "gnu99" assert _make_cstd_default("clang", "5") == "gnu11" assert _make_cstd_default("clang", "5.1") == "gnu11" assert _make_cstd_default("clang", "6") == "gnu11" assert _make_cstd_default("clang", "7") == "gnu11" assert _make_cstd_default("clang", "8") == "gnu11" assert _make_cstd_default("clang", "9") == "gnu11" assert _make_cstd_default("clang", "10") == "gnu11" assert _make_cstd_default("clang", "11") == "gnu17" assert _make_cstd_default("clang", "12") == "gnu17" assert _make_cstd_default("clang", "13") == "gnu17" assert _make_cstd_default("clang", "14") == "gnu17" assert _make_cstd_default("clang", "15") == "gnu17" assert _make_cstd_default("clang", "16") == "gnu17" assert _make_cstd_default("clang", "17") == "gnu17" assert _make_cstd_default("clang", "18") == "gnu17" assert _make_cstd_default("clang", "19") == "gnu17" assert _make_cstd_default("clang", "20"), "gnu17" def test_apple_clang_cppstd_defaults(): assert _make_cstd_default("apple-clang", "9") == "gnu99" assert _make_cstd_default("apple-clang", "10") == "gnu11" assert _make_cstd_default("apple-clang", "11") == "gnu11" assert _make_cstd_default("apple-clang", "12") == "gnu17" assert _make_cstd_default("apple-clang", "13") == "gnu17" assert _make_cstd_default("apple-clang", "14") == "gnu17" assert _make_cstd_default("apple-clang", "15") == "gnu17" ================================================ FILE: test/unittests/client/build/compiler_flags_test.py ================================================ import pytest from conan.tools.build import default_cppstd, default_cstd from conan.tools.build.flags import architecture_flag, build_type_flags, threads_flags from conan.test.utils.mocks import MockSettings, ConanFileMock class TestCompilerFlags: @pytest.mark.parametrize("compiler,arch,the_os,flag", [("gcc", "x86", None, "-m32"), ("clang", "x86", None, "-m32"), ("sun-cc", "x86", None, "-m32"), ("gcc", "x86_64", None, "-m64"), ("clang", "x86_64", None, "-m64"), ("sun-cc", "x86_64", None, "-m64"), ("sun-cc", "sparc", None, "-m32"), ("sun-cc", "sparcv9", None, "-m64"), ("gcc", "armv7", None, ""), ("clang", "armv7", None, ""), ("sun-cc", "armv7", None, ""), ("gcc", "s390", None, "-m31"), ("clang", "s390", None, "-m31"), ("sun-cc", "s390", None, "-m31"), ("gcc", "s390x", None, "-m64"), ("clang", "s390x", None, "-m64"), ("sun-cc", "s390x", None, "-m64"), ("msvc", "x86", None, ""), ("msvc", "x86_64", None, ""), ("gcc", "ppc32", "AIX", "-maix32"), ("gcc", "ppc64", "AIX", "-maix64"), ]) def test_arch_flag(self, compiler, arch, the_os, flag): settings = MockSettings({"compiler": compiler, "arch": arch, "os": the_os}) conanfile = ConanFileMock() conanfile.settings = settings assert architecture_flag(conanfile) == flag @pytest.mark.parametrize("compiler,threads,flag", [("clang", None, []), ("emcc", None, []), ("emcc", "posix", ["-pthread"]), ("emcc", "wasm_workers", ["-sWASM_WORKERS=1"]), ]) def test_threads_flag(self, compiler, threads, flag): settings = MockSettings({"compiler": compiler, "compiler.threads": threads}) conanfile = ConanFileMock() conanfile.settings = settings assert threads_flags(conanfile) == flag @pytest.mark.parametrize("compiler,arch,the_os,flag", [("clang", "x86", "Windows", ""), ("clang", "x86_64", "Windows", "") ]) def test_arch_flag_clangcl(self, compiler, arch, the_os, flag): settings = MockSettings({"compiler": compiler, "arch": arch, "os": the_os}) conanfile = ConanFileMock() conanfile.conf.define("tools.build:compiler_executables", {"c": "clang-cl"}) conanfile.settings = settings assert architecture_flag(conanfile) == flag def test_catalyst(self): settings = MockSettings({"compiler": "apple-clang", "arch": "x86_64", "os": "Macos", "os.subsystem": "catalyst", "os.subsystem.ios_version": "13.1"}) conanfile = ConanFileMock() conanfile.settings = settings assert architecture_flag(conanfile) == "--target=x86_64-apple-ios13.1-macabi" settings = MockSettings({"compiler": "apple-clang", "arch": "armv8", "os": "Macos", "os.subsystem": "catalyst", "os.subsystem.ios_version": "13.1"}) conanfile = ConanFileMock() conanfile.settings = settings assert architecture_flag(conanfile) == "--target=arm64-apple-ios13.1-macabi" @pytest.mark.parametrize("os_,arch,flag", [("Linux", "x86", "-m32"), ("Linux", "x86_64", "-m64"), ("Windows", "x86", "/Qm32"), ("Windows", "x86_64", "/Qm64"), ]) def test_arch_flag_intel(self, os_, arch, flag): settings = MockSettings({"compiler": "intel-cc", "os": os_, "arch": arch}) conanfile = ConanFileMock() conanfile.settings = settings assert architecture_flag(conanfile) == flag @pytest.mark.parametrize("arch,flag", [("e2k-v2", "-march=elbrus-v2"), ("e2k-v3", "-march=elbrus-v3"), ("e2k-v4", "-march=elbrus-v4"), ("e2k-v5", "-march=elbrus-v5"), ("e2k-v6", "-march=elbrus-v6"), ("e2k-v7", "-march=elbrus-v7"), ]) def test_arch_flag_mcst_lcc(self, arch, flag): conanfile = ConanFileMock() settings = MockSettings({"compiler": "mcst-lcc", "arch": arch}) conanfile.settings = settings assert architecture_flag(conanfile) == flag @pytest.mark.parametrize("compiler,build_type,vs_toolset,flags", [("msvc", "Debug", None, "-Zi -Ob0 -Od"), ("msvc", "Release", None, "-O2 -Ob2"), ("msvc", "RelWithDebInfo", None, "-Zi -O2 -Ob1"), ("msvc", "MinSizeRel", None, "-O1 -Ob1"), ("msvc", "Debug", "v140_clang_c2", "-gline-tables-only -fno-inline -O0"), ("msvc", "Release", "v140_clang_c2", "-O2"), ("msvc", "RelWithDebInfo", "v140_clang_c2", "-gline-tables-only -O2 -fno-inline"), ("msvc", "MinSizeRel", "v140_clang_c2", ""), ("gcc", "Debug", None, "-g"), ("gcc", "Release", None, "-O3"), ("gcc", "RelWithDebInfo", None, "-O2 -g"), ("gcc", "MinSizeRel", None, "-Os"), ("clang", "Debug", None, "-g"), ("clang", "Release", None, "-O3"), ("clang", "RelWithDebInfo", None, "-O2 -g"), ("clang", "MinSizeRel", None, "-Os"), ("apple-clang", "Debug", None, "-g"), ("apple-clang", "Release", None, "-O3"), ("apple-clang", "RelWithDebInfo", None, "-O2 -g"), ("apple-clang", "MinSizeRel", None, "-Os"), ("sun-cc", "Debug", None, "-g"), ("sun-cc", "Release", None, "-xO3"), ("sun-cc", "RelWithDebInfo", None, "-xO2 -g"), ("sun-cc", "MinSizeRel", None, "-xO2 -xspace"), ]) def test_build_type_flags(self, compiler, build_type, vs_toolset, flags): settings = MockSettings({"compiler": compiler, "build_type": build_type, "compiler.toolset": vs_toolset}) conanfile = ConanFileMock() conanfile.settings = settings assert ' '.join(build_type_flags(conanfile)) == flags @pytest.mark.parametrize("compiler,build_type,flags", [("clang", "Debug", "-Zi -Ob0 -Od"), ("clang", "Release", "-O2 -Ob2"), ("clang", "RelWithDebInfo", "-Zi -O2 -Ob1"), ("clang", "MinSizeRel", "-O1 -Ob1"), ]) def test_build_type_flags_clangcl(self, compiler, build_type, flags): settings = MockSettings({"compiler": compiler, "build_type": build_type}) conanfile = ConanFileMock() conanfile.conf.define("tools.build:compiler_executables", {"c": "clang-cl"}) conanfile.settings = settings assert ' '.join(build_type_flags(conanfile)) == flags @pytest.mark.parametrize("compiler,version,flags", [("clang", "8", "gnu14"), ("gcc", "15", "gnu17") ]) def test_default_cppstd(self, compiler, version, flags): settings = MockSettings({"compiler": compiler, "compiler.version": version}) conanfile = ConanFileMock() conanfile.settings = settings assert default_cppstd(conanfile) == flags @pytest.mark.parametrize("compiler,version,flags", [("clang", "8", "gnu11"), ("gcc", "15", "gnu23") ]) def test_default_cstd(self, compiler, version, flags): settings = MockSettings({"compiler": compiler, "compiler.version": version}) conanfile = ConanFileMock() conanfile.settings = settings assert default_cstd(conanfile) == flags ================================================ FILE: test/unittests/client/build/cpp_std_flags_test.py ================================================ from conan.internal.api.detect.detect_api import default_cppstd from conan.tools.build import cppstd_flag from conan.internal.model.version import Version from conan.test.utils.mocks import MockSettings, ConanFileMock def _make_cppstd_flag(compiler, compiler_version, cppstd=None): conanfile = ConanFileMock() conanfile.settings = MockSettings({"compiler": compiler, "compiler.version": compiler_version, "compiler.cppstd": cppstd}) return cppstd_flag(conanfile) def _make_cppstd_default(compiler, compiler_version): return default_cppstd(compiler, Version(compiler_version)) class TestCompilerFlags: def test_gcc_cppstd_flags(self): assert _make_cppstd_flag("gcc", "4.2", "98") == "-std=c++98" assert _make_cppstd_flag("gcc", "4.2", "gnu98") == "-std=gnu++98" assert _make_cppstd_flag("gcc", "4.2", "11") is None assert _make_cppstd_flag("gcc", "4.2", "14") is None assert _make_cppstd_flag("gcc", "4.3", "98") == "-std=c++98" assert _make_cppstd_flag("gcc", "4.3", "gnu98") == "-std=gnu++98" assert _make_cppstd_flag("gcc", "4.3", "11") == "-std=c++0x" assert _make_cppstd_flag("gcc", "4.3", "14") is None assert _make_cppstd_flag("gcc", "4.6", "11") == '-std=c++0x' assert _make_cppstd_flag("gcc", "4.6", "14") is None assert _make_cppstd_flag("gcc", "4.7", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "4.7", "14") is None assert _make_cppstd_flag("gcc", "4.8", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "4.8", "14") == '-std=c++1y' assert _make_cppstd_flag("gcc", "4.8", "17") is None assert _make_cppstd_flag("gcc", "4.9", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "4.9", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "4.9", "17") is None assert _make_cppstd_flag("gcc", "5", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "5", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "5", "gnu14") == '-std=gnu++14' assert _make_cppstd_flag("gcc", "5", "17") == '-std=c++1z' assert _make_cppstd_flag("gcc", "5", "gnu17") == '-std=gnu++1z' assert _make_cppstd_flag("gcc", "5.1", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "5.1", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "5.1", "17") == '-std=c++1z' assert _make_cppstd_flag("gcc", "7", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "7", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "7", "17") == '-std=c++17' assert _make_cppstd_flag("gcc", "8", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "8", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "8", "17") == '-std=c++17' assert _make_cppstd_flag("gcc", "8", "20") == '-std=c++2a' assert _make_cppstd_flag("gcc", "8", "23") is None assert _make_cppstd_flag("gcc", "11", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "11", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "11", "17") == '-std=c++17' assert _make_cppstd_flag("gcc", "11", "20") == '-std=c++20' assert _make_cppstd_flag("gcc", "11", "23") == '-std=c++23' assert _make_cppstd_flag("gcc", "11", "26") is None assert _make_cppstd_flag("gcc", "14", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "14", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "14", "17") == '-std=c++17' assert _make_cppstd_flag("gcc", "14", "20") == '-std=c++20' assert _make_cppstd_flag("gcc", "14", "23") == '-std=c++23' assert _make_cppstd_flag("gcc", "14", "26") == '-std=c++26' assert _make_cppstd_flag("gcc", "15", "11") == '-std=c++11' assert _make_cppstd_flag("gcc", "15", "14") == '-std=c++14' assert _make_cppstd_flag("gcc", "15", "17") == '-std=c++17' assert _make_cppstd_flag("gcc", "15", "20") == '-std=c++20' assert _make_cppstd_flag("gcc", "15", "23") == '-std=c++23' assert _make_cppstd_flag("gcc", "15", "26") == '-std=c++26' def test_gcc_cppstd_defaults(self): assert _make_cppstd_default("gcc", "4") == "gnu98" assert _make_cppstd_default("gcc", "5") == "gnu98" assert _make_cppstd_default("gcc", "6") == "gnu14" assert _make_cppstd_default("gcc", "6.1") == "gnu14" assert _make_cppstd_default("gcc", "7.3") == "gnu14" assert _make_cppstd_default("gcc", "8.1") == "gnu14" assert _make_cppstd_default("gcc", "11") == "gnu17" assert _make_cppstd_default("gcc", "11.1") == "gnu17" assert _make_cppstd_default("gcc", "15.1") == "gnu17" def test_clang_cppstd_flags(self): assert _make_cppstd_flag("clang", "2.0", "98") is None assert _make_cppstd_flag("clang", "2.0", "gnu98") is None assert _make_cppstd_flag("clang", "2.0", "11") is None assert _make_cppstd_flag("clang", "2.0", "14") is None assert _make_cppstd_flag("clang", "2.1", "98") == "-std=c++98" assert _make_cppstd_flag("clang", "2.1", "gnu98") == "-std=gnu++98" assert _make_cppstd_flag("clang", "2.1", "11") == "-std=c++0x" assert _make_cppstd_flag("clang", "2.1", "14") is None assert _make_cppstd_flag("clang", "3.0", "11") == '-std=c++0x' assert _make_cppstd_flag("clang", "3.0", "14") is None assert _make_cppstd_flag("clang", "3.1", "11") == '-std=c++11' assert _make_cppstd_flag("clang", "3.1", "14") is None assert _make_cppstd_flag("clang", "3.4", "11") == '-std=c++11' assert _make_cppstd_flag("clang", "3.4", "14") == '-std=c++1y' assert _make_cppstd_flag("clang", "3.4", "17") is None assert _make_cppstd_flag("clang", "3.5", "11") == '-std=c++11' assert _make_cppstd_flag("clang", "3.5", "14") == '-std=c++14' assert _make_cppstd_flag("clang", "3.5", "17") == '-std=c++1z' assert _make_cppstd_flag("clang", "5", "11") == '-std=c++11' assert _make_cppstd_flag("clang", "5", "14") == '-std=c++14' assert _make_cppstd_flag("clang", "5", "gnu14") == '-std=gnu++14' assert _make_cppstd_flag("clang", "5", "17") == '-std=c++17' assert _make_cppstd_flag("clang", "5.1", "11") == '-std=c++11' assert _make_cppstd_flag("clang", "5.1", "14") == '-std=c++14' assert _make_cppstd_flag("clang", "5.1", "17") == '-std=c++17' for version in ["6", "7", "8", "9", "10", "11"]: assert _make_cppstd_flag("clang", version, "11") == '-std=c++11' assert _make_cppstd_flag("clang", version, "14") == '-std=c++14' assert _make_cppstd_flag("clang", version, "17") == '-std=c++17' assert _make_cppstd_flag("clang", version, "20") == '-std=c++2a' assert _make_cppstd_flag("clang", version, "23") is None assert _make_cppstd_flag("clang", "12", "11") == '-std=c++11' assert _make_cppstd_flag("clang", "12", "14") == '-std=c++14' assert _make_cppstd_flag("clang", "12", "17") == '-std=c++17' assert _make_cppstd_flag("clang", "12", "20") == '-std=c++20' assert _make_cppstd_flag("clang", "12", "23") == '-std=c++2b' assert _make_cppstd_flag("clang", "12", "26") is None assert _make_cppstd_flag("clang", "17", "11") == '-std=c++11' assert _make_cppstd_flag("clang", "17", "14") == '-std=c++14' assert _make_cppstd_flag("clang", "17", "17") == '-std=c++17' assert _make_cppstd_flag("clang", "17", "20") == '-std=c++20' assert _make_cppstd_flag("clang", "17", "23") == '-std=c++23' assert _make_cppstd_flag("clang", "17", "26") == '-std=c++26' def test_clang_cppstd_defaults(self): assert _make_cppstd_default("clang", "2") == "gnu98" assert _make_cppstd_default("clang", "2.1") == "gnu98" assert _make_cppstd_default("clang", "3.0") == "gnu98" assert _make_cppstd_default("clang", "3.1") == "gnu98" assert _make_cppstd_default("clang", "3.4") == "gnu98" assert _make_cppstd_default("clang", "3.5") == "gnu98" assert _make_cppstd_default("clang", "5") == "gnu98" assert _make_cppstd_default("clang", "5.1") == "gnu98" assert _make_cppstd_default("clang", "6") == "gnu14" assert _make_cppstd_default("clang", "7") == "gnu14" assert _make_cppstd_default("clang", "8") == "gnu14" assert _make_cppstd_default("clang", "9") == "gnu14" assert _make_cppstd_default("clang", "10") == "gnu14" assert _make_cppstd_default("clang", "11") == "gnu14" assert _make_cppstd_default("clang", "12") == "gnu14" assert _make_cppstd_default("clang", "13") == "gnu14" assert _make_cppstd_default("clang", "14") == "gnu14" assert _make_cppstd_default("clang", "15") == "gnu14" assert _make_cppstd_default("clang", "16") == "gnu17" def test_apple_clang_cppstd_flags(self): assert _make_cppstd_flag("apple-clang", "3.9", "98") is None assert _make_cppstd_flag("apple-clang", "3.9", "gnu98") is None assert _make_cppstd_flag("apple-clang", "3.9", "11") is None assert _make_cppstd_flag("apple-clang", "3.9", "14") is None assert _make_cppstd_flag("apple-clang", "4.0", "98") == "-std=c++98" assert _make_cppstd_flag("apple-clang", "4.0", "gnu98") == "-std=gnu++98" assert _make_cppstd_flag("apple-clang", "4.0", "11") == "-std=c++11" assert _make_cppstd_flag("apple-clang", "4.0", "14") is None assert _make_cppstd_flag("apple-clang", "5.0", "98") == "-std=c++98" assert _make_cppstd_flag("apple-clang", "5.0", "gnu98") == "-std=gnu++98" assert _make_cppstd_flag("apple-clang", "5.0", "11") == "-std=c++11" assert _make_cppstd_flag("apple-clang", "5.0", "14") is None assert _make_cppstd_flag("apple-clang", "5.1", "98") == "-std=c++98" assert _make_cppstd_flag("apple-clang", "5.1", "gnu98") == "-std=gnu++98" assert _make_cppstd_flag("apple-clang", "5.1", "11") == "-std=c++11" assert _make_cppstd_flag("apple-clang", "5.1", "14") == "-std=c++1y" assert _make_cppstd_flag("apple-clang", "6.1", "11") == '-std=c++11' assert _make_cppstd_flag("apple-clang", "6.1", "14") == '-std=c++14' assert _make_cppstd_flag("apple-clang", "6.1", "17") == "-std=c++1z" assert _make_cppstd_flag("apple-clang", "7", "11") == '-std=c++11' assert _make_cppstd_flag("apple-clang", "7", "14") == '-std=c++14' assert _make_cppstd_flag("apple-clang", "7", "17") == "-std=c++1z" assert _make_cppstd_flag("apple-clang", "8", "11") == '-std=c++11' assert _make_cppstd_flag("apple-clang", "8", "14") == '-std=c++14' assert _make_cppstd_flag("apple-clang", "8", "17") == "-std=c++1z" assert _make_cppstd_flag("apple-clang", "9", "11") == '-std=c++11' assert _make_cppstd_flag("apple-clang", "9", "14") == '-std=c++14' assert _make_cppstd_flag("apple-clang", "9", "17") == "-std=c++1z" assert _make_cppstd_flag("apple-clang", "9.1", "11") == '-std=c++11' assert _make_cppstd_flag("apple-clang", "9.1", "14") == '-std=c++14' assert _make_cppstd_flag("apple-clang", "9.1", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "9.1", "20") is None assert _make_cppstd_flag("apple-clang", "10.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "10.0", "20") == "-std=c++2a" assert _make_cppstd_flag("apple-clang", "11.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "11.0", "20") == "-std=c++2a" assert _make_cppstd_flag("apple-clang", "12.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "12.0", "20") == "-std=c++2a" assert _make_cppstd_flag("apple-clang", "12.0", "23") is None assert _make_cppstd_flag("apple-clang", "13.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "13.0", "gnu17") == "-std=gnu++17" assert _make_cppstd_flag("apple-clang", "13.0", "20") == "-std=c++20" assert _make_cppstd_flag("apple-clang", "13.0", "gnu20") == "-std=gnu++20" assert _make_cppstd_flag("apple-clang", "13.0", "23") == "-std=c++2b" assert _make_cppstd_flag("apple-clang", "13.0", "gnu23") == "-std=gnu++2b" assert _make_cppstd_flag("apple-clang", "14.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "14.0", "gnu17") == "-std=gnu++17" assert _make_cppstd_flag("apple-clang", "14.0", "20") == "-std=c++20" assert _make_cppstd_flag("apple-clang", "14.0", "gnu20") == "-std=gnu++20" assert _make_cppstd_flag("apple-clang", "14.0", "23") == "-std=c++2b" assert _make_cppstd_flag("apple-clang", "14.0", "gnu23") == "-std=gnu++2b" assert _make_cppstd_flag("apple-clang", "15.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "15.0", "gnu17") == "-std=gnu++17" assert _make_cppstd_flag("apple-clang", "15.0", "20") == "-std=c++20" assert _make_cppstd_flag("apple-clang", "15.0", "gnu20") == "-std=gnu++20" assert _make_cppstd_flag("apple-clang", "15.0", "23") == "-std=c++2b" assert _make_cppstd_flag("apple-clang", "15.0", "gnu23") == "-std=gnu++2b" assert _make_cppstd_flag("apple-clang", "15.0", "26") is None assert _make_cppstd_flag("apple-clang", "16.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "16.0", "gnu17") == "-std=gnu++17" assert _make_cppstd_flag("apple-clang", "16.0", "20") == "-std=c++20" assert _make_cppstd_flag("apple-clang", "16.0", "gnu20") == "-std=gnu++20" assert _make_cppstd_flag("apple-clang", "16.0", "23") == "-std=c++23" assert _make_cppstd_flag("apple-clang", "16.0", "gnu23") == "-std=gnu++23" assert _make_cppstd_flag("apple-clang", "16.0", "26") == "-std=c++26" assert _make_cppstd_flag("apple-clang", "16.0", "gnu26") == "-std=gnu++26" assert _make_cppstd_flag("apple-clang", "17.0", "17") == "-std=c++17" assert _make_cppstd_flag("apple-clang", "17.0", "gnu17") == "-std=gnu++17" assert _make_cppstd_flag("apple-clang", "17.0", "20") == "-std=c++20" assert _make_cppstd_flag("apple-clang", "17.0", "gnu20") == "-std=gnu++20" assert _make_cppstd_flag("apple-clang", "17.0", "23") == "-std=c++23" assert _make_cppstd_flag("apple-clang", "17.0", "gnu23") == "-std=gnu++23" assert _make_cppstd_flag("apple-clang", "17.0", "26") == "-std=c++26" assert _make_cppstd_flag("apple-clang", "17.0", "gnu26") == "-std=gnu++26" def test_apple_clang_cppstd_defaults(self): assert _make_cppstd_default("apple-clang", "2") == "gnu98" assert _make_cppstd_default("apple-clang", "3") == "gnu98" assert _make_cppstd_default("apple-clang", "4") == "gnu98" assert _make_cppstd_default("apple-clang", "5") == "gnu98" assert _make_cppstd_default("apple-clang", "6") == "gnu98" assert _make_cppstd_default("apple-clang", "7") == "gnu98" assert _make_cppstd_default("apple-clang", "8") == "gnu98" assert _make_cppstd_default("apple-clang", "9") == "gnu98" assert _make_cppstd_default("apple-clang", "10") == "gnu98" assert _make_cppstd_default("apple-clang", "11") == "gnu98" assert _make_cppstd_default("apple-clang", "12") == "gnu98" assert _make_cppstd_default("apple-clang", "13") == "gnu98" assert _make_cppstd_default("apple-clang", "14") == "gnu98" assert _make_cppstd_default("apple-clang", "15") == "gnu98" assert _make_cppstd_default("apple-clang", "16") == "gnu98" assert _make_cppstd_default("apple-clang", "17") == "gnu14" def test_visual_cppstd_flags(self): assert _make_cppstd_flag("msvc", "170", "11") is None assert _make_cppstd_flag("msvc", "170", "14") is None assert _make_cppstd_flag("msvc", "170", "17") is None assert _make_cppstd_flag("msvc", "180", "11") is None assert _make_cppstd_flag("msvc", "190", "14") == '/std:c++14' assert _make_cppstd_flag("msvc", "190", "17") == '/std:c++latest' assert _make_cppstd_flag("msvc", "191", "11") is None assert _make_cppstd_flag("msvc", "191", "14") == '/std:c++14' assert _make_cppstd_flag("msvc", "191", "17") == '/std:c++17' assert _make_cppstd_flag("msvc", "191", "20") == '/std:c++latest' assert _make_cppstd_flag("msvc", "192", "17") == '/std:c++17' assert _make_cppstd_flag("msvc", "192", "20") == '/std:c++20' assert _make_cppstd_flag("msvc", "193", "20") == '/std:c++20' assert _make_cppstd_flag("msvc", "193", "23") == '/std:c++latest' def test_visual_cppstd_defaults(self): assert _make_cppstd_default("msvc", "170") is None assert _make_cppstd_default("msvc", "180") is None assert _make_cppstd_default("msvc", "190") == "14" assert _make_cppstd_default("msvc", "191") == "14" assert _make_cppstd_default("msvc", "192") == "14" assert _make_cppstd_default("msvc", "193") == "14" def test_intel_cppstd_flag(self): assert _make_cppstd_flag("intel-cc", "2024.1", "gnu98") == '-std=gnu++98' assert _make_cppstd_flag("intel-cc", "2024.1", "11") == '-std=c++11' assert _make_cppstd_flag("intel-cc", "2024.1", "14") == '-std=c++14' assert _make_cppstd_flag("intel-cc", "2024.1", "17") == '-std=c++17' assert _make_cppstd_flag("intel-cc", "2024.1", "20") == '-std=c++20' assert _make_cppstd_flag("intel-cc", "2025.1", "17") == '-std=c++17' assert _make_cppstd_flag("intel-cc", "2025.1", "20") == '-std=c++20' assert _make_cppstd_flag("intel-cc", "2025.1", "23") == '-std=c++2b' def test_mcst_lcc_cppstd_defaults(self): assert _make_cppstd_default("mcst-lcc", "1.19") == "gnu98" assert _make_cppstd_default("mcst-lcc", "1.20") == "gnu98" assert _make_cppstd_default("mcst-lcc", "1.21") == "gnu98" assert _make_cppstd_default("mcst-lcc", "1.22") == "gnu98" assert _make_cppstd_default("mcst-lcc", "1.23") == "gnu98" assert _make_cppstd_default("mcst-lcc", "1.24") == "gnu14" assert _make_cppstd_default("mcst-lcc", "1.25") == "gnu14" def test_mcst_lcc_cppstd_flag(self): assert _make_cppstd_flag("mcst-lcc", "1.19", "98") == "-std=c++98" assert _make_cppstd_flag("mcst-lcc", "1.19", "11") is None assert _make_cppstd_flag("mcst-lcc", "1.19", "14") is None assert _make_cppstd_flag("mcst-lcc", "1.19", "17") is None assert _make_cppstd_flag("mcst-lcc", "1.19", "20") is None assert _make_cppstd_flag("mcst-lcc", "1.20", "98") == "-std=c++98" assert _make_cppstd_flag("mcst-lcc", "1.20", "11") is None assert _make_cppstd_flag("mcst-lcc", "1.20", "14") is None assert _make_cppstd_flag("mcst-lcc", "1.20", "17") is None assert _make_cppstd_flag("mcst-lcc", "1.20", "20") is None assert _make_cppstd_flag("mcst-lcc", "1.21", "98") == "-std=c++98" assert _make_cppstd_flag("mcst-lcc", "1.21", "11") == "-std=c++11" assert _make_cppstd_flag("mcst-lcc", "1.21", "14") == "-std=c++14" assert _make_cppstd_flag("mcst-lcc", "1.21", "17") is None assert _make_cppstd_flag("mcst-lcc", "1.21", "20") is None assert _make_cppstd_flag("mcst-lcc", "1.22", "98") == "-std=c++98" assert _make_cppstd_flag("mcst-lcc", "1.22", "11") == "-std=c++11" assert _make_cppstd_flag("mcst-lcc", "1.22", "14") == "-std=c++14" assert _make_cppstd_flag("mcst-lcc", "1.22", "17") is None assert _make_cppstd_flag("mcst-lcc", "1.22", "20") is None assert _make_cppstd_flag("mcst-lcc", "1.23", "98") == "-std=c++98" assert _make_cppstd_flag("mcst-lcc", "1.23", "11") == "-std=c++11" assert _make_cppstd_flag("mcst-lcc", "1.23", "14") == "-std=c++14" assert _make_cppstd_flag("mcst-lcc", "1.23", "17") is None assert _make_cppstd_flag("mcst-lcc", "1.23", "20") is None assert _make_cppstd_flag("mcst-lcc", "1.24", "98") == "-std=c++98" assert _make_cppstd_flag("mcst-lcc", "1.24", "11") == "-std=c++11" assert _make_cppstd_flag("mcst-lcc", "1.24", "14") == "-std=c++14" assert _make_cppstd_flag("mcst-lcc", "1.24", "17") == "-std=c++17" assert _make_cppstd_flag("mcst-lcc", "1.24", "20") is None assert _make_cppstd_flag("mcst-lcc", "1.25", "98") == "-std=c++98" assert _make_cppstd_flag("mcst-lcc", "1.25", "11") == "-std=c++11" assert _make_cppstd_flag("mcst-lcc", "1.25", "14") == "-std=c++14" assert _make_cppstd_flag("mcst-lcc", "1.25", "17") == "-std=c++17" assert _make_cppstd_flag("mcst-lcc", "1.25", "20") == "-std=c++2a" ================================================ FILE: test/unittests/client/command/__init__.py ================================================ ================================================ FILE: test/unittests/client/command/parse_arguments_test.py ================================================ import argparse import pytest from conan.cli.args import add_profiles_args @pytest.mark.parametrize("argument", [ ["options", "-o", "--options"], ["profile", "-pr", "--profile"], ["settings", "-s", "--settings"], ["conf", "-c", "--conf"] ]) class TestArgsParseProfile: """ Check argparse for profile arguments """ @pytest.fixture(autouse=True) def setup(self, argument): self.argument = argument self.item, self.short_arg, self.long_arg = self.argument self.args_dest_build = '{}_build'.format(self.item) self.args_dest_host = '{}_host'.format(self.item) def _run_parse(self, *args): parser = argparse.ArgumentParser() add_profiles_args(parser) parsed_args = parser.parse_args(*args) build = getattr(parsed_args, self.args_dest_build) host = getattr(parsed_args, self.args_dest_host) return build, host def test_default(self): """ The old '--settings', '--profile',... refers to the build machine """ build, host = self._run_parse([self.long_arg, "it1"]) assert host == ["it1"] assert build is None build, host = self._run_parse([self.long_arg, "it1", self.short_arg, "it2"]) assert host == ["it1", "it2"] assert build is None def test_build_machine(self): """ If provided with build suffix (':b', ':build'), those correspond to the build machine """ long_arg = "{}:build".format(self.long_arg) short_arg = "{}:b".format(self.short_arg) build, host = self._run_parse([long_arg, "it1"]) assert build == ["it1"] assert host is None build, host = self._run_parse([long_arg, "it1", short_arg, "it2"]) assert build == ["it1", "it2"] assert host is None def test_mix_old_and_host_machine(self): """ Old arguments and new ':host' ones are composable """ new_long_arg = "{}:host".format(self.long_arg) new_short_arg = "{}:h".format(self.short_arg) build, host = self._run_parse([new_long_arg, "it1", self.long_arg, "it2", new_short_arg, "it3", self.short_arg, "it4"]) assert host == ["it1", "it2", "it3", "it4"] assert build is None def test_host_machine(self): """ If provided with host suffix (':h', ':host'), those correspond to the host machine """ long_arg = "{}:host".format(self.long_arg) short_arg = "{}:h".format(self.short_arg) build, host = self._run_parse([long_arg, "it1"]) assert host == ["it1"] assert build is None build, host = self._run_parse([long_arg, "it1", short_arg, "it2"]) assert host == ["it1", "it2"] assert build is None def test_build_and_host(self): """ Of course, we can provide build and host in the same command line """ build, host = self._run_parse(["{}:build".format(self.long_arg), "b1", "{}:host".format(self.long_arg), "h1"]) assert build == ["b1"] assert host == ["h1"] ================================================ FILE: test/unittests/client/conan_output_test.py ================================================ import sys from unittest import mock import pytest from conan.api.output import ConanOutput, init_colorama from conan.test.utils.mocks import RedirectedTestOutput from conan.test.utils.tools import redirect_output class TestConanOutput: @pytest.mark.parametrize("isatty, env", [(True, {"NO_COLOR": "1"}), (True, {"NO_COLOR": "0"})]) def test_output_color_prevent_strip(self, isatty, env): with mock.patch("colorama.init") as init: with mock.patch("sys.stderr.isatty", return_value=isatty), \ mock.patch.dict("os.environ", env, clear=True): init_colorama(sys.stderr) out = ConanOutput() assert out.color is False init.assert_not_called() @pytest.mark.parametrize("force", ["1", "0", "foo"]) def test_output_forced(force): env = {"CLICOLOR_FORCE": force} forced = force != "0" with mock.patch("colorama.init") as init: with mock.patch("sys.stderr.isatty", return_value=False), \ mock.patch.dict("os.environ", env, clear=True): init_colorama(sys.stderr) out = ConanOutput() assert out.color is forced if not forced: init.assert_not_called() def test_output_chainable(): stderr = RedirectedTestOutput() with redirect_output(stderr): ConanOutput(scope="My package")\ .title("My title")\ .highlight("Worked")\ .info("But there was more that needed to be said") assert "My package" in stderr.getvalue() assert "My title" in stderr.getvalue() assert "Worked" in stderr.getvalue() assert "But there was more that needed to be said" in stderr.getvalue() ================================================ FILE: test/unittests/client/conanfile_loader_test.py ================================================ import os import sys import textwrap import pytest from conan.internal.loader import ConanFileLoader, ConanFileTextLoader, load_python_file from conan.errors import ConanException from conan.test.utils.test_files import temp_folder from conan.internal.util.files import save, chdir class TestConanLoaderTxt: def test_conanfile_txt_errors(self): # Invalid content file_content = '''[requires} OpenCV/2.4.10@phil/stable # My requirement for CV ''' with pytest.raises(ConanException, match="Bad syntax"): ConanFileTextLoader(file_content) file_content = '{hello}' with pytest.raises(ConanException, match="Unexpected line"): ConanFileTextLoader(file_content) def test_plain_text_parser(self): # Valid content file_content = '''[requires] OpenCV/2.4.10@phil/stable # My requirement for CV OpenCV2/2.4.10@phil/stable # OpenCV3/2.4.10@phil/stable [generators] one # My generator for this two [options] OpenCV:use_python=True # Some option OpenCV:other_option=False OpenCV2:use_python2=1 OpenCV2:other_option=Cosa # ''' parser = ConanFileTextLoader(file_content) exp = ['OpenCV/2.4.10@phil/stable', 'OpenCV2/2.4.10@phil/stable', 'OpenCV3/2.4.10@phil/stable'] assert parser.requirements == exp def test_revision_parsing(self): # Valid content file_content = '''[requires] OpenCV/2.4.10@user/stable#RREV1 # My requirement for CV ''' parser = ConanFileTextLoader(file_content) exp = ['OpenCV/2.4.10@user/stable#RREV1'] assert parser.requirements == exp def test_load_conan_txt(self): file_content = '''[requires] OpenCV/2.4.10@phil/stable OpenCV2/2.4.10@phil/stable [tool_requires] Mypkg/1.0.0@phil/stable [generators] one two [options] OpenCV/*:use_python=True OpenCV/*:other_option=False OpenCV2/*:use_python2=1 OpenCV2/*:other_option=Cosa ''' tmp_dir = temp_folder() file_path = os.path.join(tmp_dir, "file.txt") save(file_path, file_content) loader = ConanFileLoader(None, None) ret = loader.load_conanfile_txt(file_path) assert len(ret.requires.values()) == 3 assert ret.generators == ["one", "two"] assert ret.options.dumps() == 'OpenCV/*:other_option=False\n' \ 'OpenCV/*:use_python=True\n' \ 'OpenCV2/*:other_option=Cosa\n' \ 'OpenCV2/*:use_python2=1' def test_load_options_error(self): conanfile_txt = textwrap.dedent(""" [options] myoption: myvalue """) tmp_dir = temp_folder() file_path = os.path.join(tmp_dir, "file.txt") save(file_path, conanfile_txt) loader = ConanFileLoader(None, None) with pytest.raises(ConanException, match=r"Error while parsing \[options\] in conanfile.txt\n" \ r"Options should be specified as 'pkg/\*:option=value'"): loader.load_conanfile_txt(file_path) def test_layout_not_predefined(self): txt = textwrap.dedent(""" [layout] missing """) tmp_dir = temp_folder() file_path = os.path.join(tmp_dir, "conanfile.txt") save(file_path, txt) with pytest.raises(ConanException) as exc: loader = ConanFileLoader(None, None) loader.load_conanfile_txt(file_path) assert "Unknown predefined layout 'missing'" in str(exc.value) def test_layout_multiple(self): txt = textwrap.dedent(""" [layout] cmake_layout vs_layout """) tmp_dir = temp_folder() file_path = os.path.join(tmp_dir, "conanfile.txt") save(file_path, txt) with pytest.raises(ConanException) as exc: loader = ConanFileLoader(None, None) loader.load_conanfile_txt(file_path) assert "Only one layout can be declared in the [layout] section of the conanfile.txt" \ in str(exc.value) class TestImportModuleLoader: @staticmethod def _create_and_load(myfunc, value, subdir_name, add_subdir_init): subdir_content = textwrap.dedent(""" def get_value(): return {value} def {myfunc}(): return "{myfunc}" """) side_content = textwrap.dedent(""" def get_side_value(): return {value} def side_{myfunc}(): return "{myfunc}" """) conanfile = textwrap.dedent(""" import pickle from {subdir}.api import get_value, {myfunc} from file import get_side_value, side_{myfunc} from fractions import Fraction def conanfile_func(): return get_value(), {myfunc}(), get_side_value(), side_{myfunc}(), str(Fraction(1,1)) """) expected_return = (value, myfunc, value, myfunc, "1") tmp = temp_folder() with chdir(tmp): save("conanfile.py", conanfile.format(value=value, myfunc=myfunc, subdir=subdir_name)) save("file.py", side_content.format(value=value, myfunc=myfunc)) save("{}/api.py".format(subdir_name), subdir_content.format(value=value, myfunc=myfunc)) if add_subdir_init: save("__init__.py", "") save("{}/__init__.py".format(subdir_name), "") loaded, module_id = load_python_file(os.path.join(tmp, "conanfile.py")) return loaded, module_id, expected_return @pytest.mark.parametrize("sub1,sub2", [(True, False), (False, True), (False, False)]) def test_py3_recipe_colliding_init_filenames(self, sub1, sub2): myfunc1, value1 = "recipe1", 42 myfunc2, value2 = "recipe2", 23 loaded1, module_id1, exp_ret1 = self._create_and_load(myfunc1, value1, "subdir", sub1) loaded2, module_id2, exp_ret2 = self._create_and_load(myfunc2, value2, "subdir", sub2) assert module_id1 != module_id2 assert loaded1.conanfile_func() == exp_ret1 assert loaded2.conanfile_func() == exp_ret2 def test_recipe_colliding_filenames(self): myfunc1, value1 = "recipe1", 42 myfunc2, value2 = "recipe2", 23 loaded1, module_id1, exp_ret1 = self._create_and_load(myfunc1, value1, "subdir", True) loaded2, module_id2, exp_ret2 = self._create_and_load(myfunc2, value2, "subdir", True) assert module_id1 != module_id2 assert loaded1.conanfile_func() == exp_ret1 assert loaded2.conanfile_func() == exp_ret2 @pytest.mark.parametrize("add_subdir_init", [(True, ), (False, )]) def test_wrong_imports(self, add_subdir_init): myfunc1, value1 = "recipe1", 42 # Item imported does not exist, but file exists # We use some existing and imported Python stdlib import with pytest.raises(ConanException, match="Unable to load conanfile in"): self._create_and_load(myfunc1, value1, "textwrap", add_subdir_init) # File does not exists in already existing module with pytest.raises(ConanException, match="Unable to load conanfile in"): self._create_and_load(myfunc1, value1, "conans", add_subdir_init) def test_helpers_python_library(self): mylogger = """ value = "" def append(data): global value value += data """ temp = temp_folder() save(os.path.join(temp, "myconanlogger.py"), mylogger) conanfile = "import myconanlogger" temp1 = temp_folder() save(os.path.join(temp1, "conanfile.py"), conanfile) temp2 = temp_folder() save(os.path.join(temp2, "conanfile.py"), conanfile) try: sys.path.append(temp) loaded1, _ = load_python_file(os.path.join(temp1, "conanfile.py")) loaded2, _ = load_python_file(os.path.join(temp2, "conanfile.py")) assert loaded1.myconanlogger == loaded2.myconanlogger assert loaded1.myconanlogger.value == loaded2.myconanlogger.value finally: sys.path.remove(temp) ================================================ FILE: test/unittests/client/conf/__init__.py ================================================ ================================================ FILE: test/unittests/client/conf/config_installer/__init__.py ================================================ ================================================ FILE: test/unittests/client/conf/config_installer/test_install_folder.py ================================================ import os from conan.internal.api.config.config_installer import tmp_config_install_folder from conan.test.utils.test_files import temp_folder class TestInstallFolder: def test_unique_install_folder(self): """ Validate if tmp_config_install_folder is removing old folder before creating a new one tmp_config_install_folder must create the same folder, but all items must be exclude when a new folder is created. """ cache_folder = temp_folder() with tmp_config_install_folder(cache_folder) as tmp_folder_first: temp_file = os.path.join(tmp_folder_first, "foobar.txt") open(temp_file, "w+") with tmp_config_install_folder(cache_folder) as tmp_folder_second: assert tmp_folder_first == tmp_folder_second assert not os.path.exists(temp_file) ================================================ FILE: test/unittests/client/conf/detect/__init__.py ================================================ ================================================ FILE: test/unittests/client/conf/detect/test_gcc_compiler.py ================================================ from unittest import mock import pytest from conan.internal.api.detect.detect_api import detect_gcc_compiler @pytest.mark.parametrize("version", ["10", "4.2", '7', "8.12"]) def test_detect_gcc_10(version): with mock.patch("platform.system", return_value="Linux"): with mock.patch("conan.internal.api.detect.detect_api.detect_runner", return_value=(0, version)): compiler, installed_version, compiler_exe = detect_gcc_compiler() assert compiler == 'gcc' assert installed_version == version assert compiler_exe == 'gcc' ================================================ FILE: test/unittests/client/file_copier/__init__.py ================================================ ================================================ FILE: test/unittests/client/file_copier/test_report_copied_files.py ================================================ from conan.api.output import ConanOutput from conan.internal.model.manifest import FileTreeManifest from conan.test.utils.mocks import RedirectedTestOutput from conan.test.utils.tools import redirect_output class TestReportCopiedFiles: def test_output_string(self): manifest = FileTreeManifest(0, file_sums={'/abs/path/to/file.pdf': "", '../rel/path/to/file2.pdf': "", '../rel/path/to/file3.pdf': "", '../rel/path/to/file4.pdf': "", '../rel/path/to/file5.pdf': "", '../rel/path/to/file6.pdf': "", '../rel/path/to/file7.pdf': "", '/without/ext/no_ext1': "", 'no_ext2': "", 'a/other.txt': ""}) output = RedirectedTestOutput() with redirect_output(output): manifest.report_summary(ConanOutput()) lines = sorted(str(output).splitlines()) assert "Copied 7 '.pdf' files" == lines[2] assert "Copied 2 files: no_ext1, no_ext2" == lines[1] assert "Copied 1 '.txt' file: other.txt" == lines[0] ================================================ FILE: test/unittests/client/graph/__init__.py ================================================ ================================================ FILE: test/unittests/client/graph/build_mode_test.py ================================================ import pytest from conan.internal.graph.build_mode import BuildMode from conan.errors import ConanException from conan.api.model import RecipeReference from conan.test.utils.mocks import ConanFileMock, RedirectedTestOutput from conan.test.utils.tools import redirect_output @pytest.fixture def conanfile(): return ConanFileMock(None) def test_skip_package(conanfile): build_mode = BuildMode(["!zlib/*", "other*"]) assert not build_mode.forced(conanfile, RecipeReference.loads("zlib/1.2.11#23423423")) assert build_mode.forced(conanfile, RecipeReference.loads("other/1.2")) build_mode = BuildMode(["!zlib/*", "*"]) assert not build_mode.forced(conanfile, RecipeReference.loads("zlib/1.2.11#23423423")) assert build_mode.forced(conanfile, RecipeReference.loads("other/1.2")) build_mode = BuildMode(["!zlib/*"]) assert not build_mode.forced(conanfile, RecipeReference.loads("zlib/1.2.11#23423423")) assert not build_mode.forced(conanfile, RecipeReference.loads("other/1.2")) def test_invalid_configuration(): for mode in ["missing", "cascade"]: with pytest.raises(ConanException, match=r"--build=never not compatible " r"with other options"): BuildMode([mode, "never"]) def test_common_build_force(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("hello/0.1@user/testing") build_mode = BuildMode(["hello/*"]) assert build_mode.forced(conanfile, reference) is True def test_no_user_channel(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("hello/0.1@") build_mode = BuildMode(["hello/0.1@"]) assert build_mode.forced(conanfile, reference) is True def test_revision_included(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("hello/0.1@user/channel#rrev1") build_mode = BuildMode(["hello/0.1@user/channel#rrev1"]) assert build_mode.forced(conanfile, reference) is True def test_no_user_channel_revision_included(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("hello/0.1@#rrev1") build_mode = BuildMode(["hello/0.1@#rrev1"]) assert build_mode.forced(conanfile, reference) is True def test_non_matching_build_force(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("Bar/0.1@user/testing") build_mode = BuildMode(["hello"]) assert build_mode.forced(conanfile, reference) is False def test_full_reference_build_force(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("bar/0.1@user/testing") build_mode = BuildMode(["bar/0.1@user/testing"]) assert build_mode.forced(conanfile, reference) is True def test_non_matching_full_reference_build_force(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("bar/0.1@user/stable") build_mode = BuildMode(["bar/0.1@user/testing"]) assert build_mode.forced(conanfile, reference) is False def test_multiple_builds(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("bar/0.1@user/stable") build_mode = BuildMode(["bar/*", "Foo/*"]) assert build_mode.forced(conanfile, reference) is True def test_allowed(conanfile): build_mode = BuildMode(["missing"]) assert build_mode.allowed(conanfile) is True build_mode = BuildMode(None) assert build_mode.allowed(conanfile) is False def test_casing(conanfile): output = RedirectedTestOutput() with redirect_output(output): reference = RecipeReference.loads("boost/1.69.0@user/stable") build_mode = BuildMode(["boost*"]) assert build_mode.forced(conanfile, reference) is True build_mode = BuildMode(["bo*"]) assert build_mode.forced(conanfile, reference) is True output.clear() build_mode = BuildMode(["Boost*"]) assert build_mode.forced(conanfile, reference) is False build_mode = BuildMode(["Bo*"]) assert build_mode.forced(conanfile, reference) is False def test_pattern_matching(conanfile): output = RedirectedTestOutput() with redirect_output(output): build_mode = BuildMode(["boost*"]) reference = RecipeReference.loads("boost/1.69.0@user/stable") assert (build_mode.forced(conanfile, reference)) is True reference = RecipeReference.loads("boost_addons/1.0.0@user/stable") assert (build_mode.forced(conanfile, reference)) is True reference = RecipeReference.loads("myboost/1.0@user/stable") assert (build_mode.forced(conanfile, reference)) is False reference = RecipeReference.loads("foo/boost@user/stable") assert (build_mode.forced(conanfile, reference)) is False reference = RecipeReference.loads("foo/1.0@boost/stable") assert (build_mode.forced(conanfile, reference)) is False reference = RecipeReference.loads("foo/1.0@user/boost") assert build_mode.forced(conanfile, reference) is False build_mode = BuildMode(["foo/*@user/stable"]) reference = RecipeReference.loads("foo/1.0.0@user/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("foo/1.0@user/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("foo/1.0.0-abcdefg@user/stable") assert build_mode.forced(conanfile, reference) is True build_mode = BuildMode(["*@user/stable"]) reference = RecipeReference.loads("foo/1.0.0@user/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("bar/1.0@user/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("foo/1.0.0-abcdefg@user/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("foo/1.0.0@NewUser/stable") assert build_mode.forced(conanfile, reference) is False build_mode = BuildMode(["*tool*"]) reference = RecipeReference.loads("tool/0.1@lasote/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("pythontool/0.1@lasote/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("sometool/1.2@user/channel") assert build_mode.forced(conanfile, reference) is True build_mode = BuildMode(["tool/*"]) reference = RecipeReference.loads("tool/0.1@lasote/stable") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("tool/1.1@user/testing") assert build_mode.forced(conanfile, reference) is True reference = RecipeReference.loads("pythontool/0.1@lasote/stable") assert build_mode.forced(conanfile, reference) is False ================================================ FILE: test/unittests/client/graph/deps_graph_test.py ================================================ from unittest.mock import Mock from conan.internal.graph.graph import CONTEXT_HOST from conan.internal.graph.graph_builder import DepsGraph, Node from conan.internal.model.conan_file import ConanFile from conan.api.model import RecipeReference class TestDepsGraph: def test_node(self): """ nodes are different even if contain same values, so they can be repeated if necessary in the graph (common static libraries) """ ref1 = RecipeReference.loads("hello/0.1@user/stable") ref2 = RecipeReference.loads("hello/0.1@user/stable") conanfile1 = ConanFile(None) conanfile2 = ConanFile(None) n1 = Node(ref1, conanfile1, context=CONTEXT_HOST) n2 = Node(ref2, conanfile2, context=CONTEXT_HOST) assert n1 != n2 def test_basic_levels(self): ref1 = RecipeReference.loads("hello/1.0@user/stable") ref2 = RecipeReference.loads("hello/2.0@user/stable") ref3 = RecipeReference.loads("hello/3.0@user/stable") deps = DepsGraph() n1 = Node(ref1, Mock(), context=CONTEXT_HOST) n2 = Node(ref2, Mock(), context=CONTEXT_HOST) n3 = Node(ref3, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n2) deps.add_node(n3) deps.add_edge(n1, n2, None) deps.add_edge(n2, n3, None) assert [[n3], [n2], [n1]] == deps.by_levels() def test_multi_levels(self): ref1 = RecipeReference.loads("hello/1.0@user/stable") ref2 = RecipeReference.loads("hello/2.0@user/stable") ref31 = RecipeReference.loads("hello/31.0@user/stable") ref32 = RecipeReference.loads("hello/32.0@user/stable") deps = DepsGraph() n1 = Node(ref1, Mock(), context=CONTEXT_HOST) n2 = Node(ref2, Mock(), context=CONTEXT_HOST) n31 = Node(ref31, Mock(), context=CONTEXT_HOST) n32 = Node(ref32, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n2) deps.add_node(n32) deps.add_node(n31) deps.add_edge(n1, n2, None) deps.add_edge(n2, n31, None) deps.add_edge(n2, n32, None) assert [[n31, n32], [n2], [n1]] == deps.by_levels() def test_multi_levels_2(self): ref1 = RecipeReference.loads("hello/1.0@user/stable") ref2 = RecipeReference.loads("hello/2.0@user/stable") ref5 = RecipeReference.loads("hello/5.0@user/stable") ref31 = RecipeReference.loads("hello/31.0@user/stable") ref32 = RecipeReference.loads("hello/32.0@user/stable") deps = DepsGraph() n1 = Node(ref1, Mock(), context=CONTEXT_HOST) n2 = Node(ref2, Mock(), context=CONTEXT_HOST) n5 = Node(ref5, Mock(), context=CONTEXT_HOST) n31 = Node(ref31, Mock(), context=CONTEXT_HOST) n32 = Node(ref32, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n5) deps.add_node(n2) deps.add_node(n32) deps.add_node(n31) deps.add_edge(n1, n2, None) deps.add_edge(n1, n5, None) deps.add_edge(n2, n31, None) deps.add_edge(n2, n32, None) assert [[n31, n32, n5], [n2], [n1]] == deps.by_levels() def test_multi_levels_3(self): ref1 = RecipeReference.loads("hello/1.0@user/stable") ref2 = RecipeReference.loads("hello/2.0@user/stable") ref5 = RecipeReference.loads("hello/5.0@user/stable") ref31 = RecipeReference.loads("hello/31.0@user/stable") ref32 = RecipeReference.loads("hello/32.0@user/stable") deps = DepsGraph() n1 = Node(ref1, Mock(), context=CONTEXT_HOST) n2 = Node(ref2, Mock(), context=CONTEXT_HOST) n5 = Node(ref5, Mock(), context=CONTEXT_HOST) n31 = Node(ref31, Mock(), context=CONTEXT_HOST) n32 = Node(ref32, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n5) deps.add_node(n2) deps.add_node(n32) deps.add_node(n31) deps.add_edge(n1, n2, None) deps.add_edge(n1, n5, None) deps.add_edge(n2, n31, None) deps.add_edge(n2, n32, None) deps.add_edge(n32, n5, None) assert [[n31, n5], [n32], [n2], [n1]] == deps.by_levels() ================================================ FILE: test/unittests/client/migrations/__init__.py ================================================ ================================================ FILE: test/unittests/client/migrations/test_migrator.py ================================================ import os import platform import pytest from conan.errors import ConanMigrationError from conans.migrations import Migrator from conan.test.utils.test_files import temp_folder class FakeMigrator(Migrator): def __init__(self, cache_folder, current_version): self.cache_folder = cache_folder super(FakeMigrator, self).__init__(cache_folder, current_version) class TestMigratorPermissionTest: @pytest.mark.skipif(platform.system() == "Windows", reason="Can't apply chmod on Windows") def test_invalid_permission(self): conf_path = temp_folder(False) os.chmod(conf_path, 0o444) conf_path = os.path.join(conf_path, "foo") migrator = FakeMigrator(conf_path, "latest") with pytest.raises(ConanMigrationError) as error: migrator.migrate() assert f"Can't write version file in '{conf_path}/version.txt'" in str(error.value) ================================================ FILE: test/unittests/client/optimize_conanfile_load_test.py ================================================ from conan.test.utils.tools import TestClient class TestOptimizeConanFileLoad: def test_multiple_load(self): """ when a conanfile is used more than once in a dependency graph, the python file should be read and interpreted just once, then instance 2 different ConanFile objects. The module global value "mycounter" is global to all instances, this should be discouraged to use as if it was an instance value. In this test there are 2 nodes "build/0.1" as it is a build-requires of both the conanfile.py and the test_package/conanfile.py """ client = TestClient() conanfile = """from conan import ConanFile mycounter = 0 class Pkg(ConanFile): mycounter2 = 0 def configure(self): global mycounter mycounter += 1 self.mycounter2 += 1 self.output.info("MyCounter1 %s, MyCounter2 %s" % (mycounter, self.mycounter2)) """ client.save({"conanfile.py": conanfile}) client.run("create . --name=build --version=0.1 --user=user --channel=testing") test_conanfile = conanfile + """ def requirements(self): self.requires(self.tested_reference_str) def test(self): pass """ client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile, "myprofile": "[tool_requires]\nbuild/0.1@user/testing"}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -pr=myprofile") assert "build/0.1@user/testing: MyCounter1 2, MyCounter2 1" in client.out ================================================ FILE: test/unittests/client/pkg_sign_test.py ================================================ import json import os import pytest from conan.test.utils.tools import temp_folder, save_files, load from conan.internal.rest.pkg_sign import _save_manifest, _save_signatures, _verify_files_checksums from conan.errors import ConanException @pytest.fixture def pkg_sign_tools(): main_folder = temp_folder() artifacts_folder = os.path.join(main_folder, "af") os.mkdir(artifacts_folder) signature_folder = os.path.join(main_folder, "sf") os.mkdir(signature_folder) save_files(artifacts_folder, {"conan_package.tgz": "", "conanmanifest.txt": ""}) return artifacts_folder, signature_folder def test_save_manifest_content_with_empty_files(pkg_sign_tools): """Test that _create_manifest_content correctly creates manifest for empty files.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) content = json.loads(load(os.path.join(signature_folder, "pkgsign-manifest.json"))) # Verify structure assert "files" in content assert isinstance(content["files"], list) assert len(content["files"]) == 2 # Files should be sorted alphabetically files = content["files"] assert files[0]["file"] == "conan_package.tgz" assert files[1]["file"] == "conanmanifest.txt" # Empty file SHA256 empty_file_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" assert files[0]["sha256"] == empty_file_sha256 assert files[1]["sha256"] == empty_file_sha256 def test_create_manifest_content_ignores_directories(pkg_sign_tools): """Test that _save_manifest creates a json file that only includes files, not directories.""" artifacts_folder, signature_folder = pkg_sign_tools # Create a subdirectory subdir = os.path.join(artifacts_folder, "subdir") os.mkdir(subdir) _save_manifest(artifacts_folder, signature_folder) content = json.loads(load(os.path.join(signature_folder, "pkgsign-manifest.json"))) filenames = [f["file"] for f in content["files"]] # Should not include the directory assert "subdir" not in filenames assert len(content["files"]) == 2 # Only the two original files def test_save_load_manifest(pkg_sign_tools): """Test that saving and loading manifest preserves all data correctly.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) # Verify file exists manifest_path = os.path.join(signature_folder, "pkgsign-manifest.json") assert os.path.isfile(manifest_path) # Load and verify content manifest = json.loads(load(manifest_path)) assert "files" in manifest assert isinstance(manifest["files"], list) assert len(manifest["files"]) == 2 # Verify files are sorted and contain expected data filenames = [f["file"] for f in manifest["files"]] assert filenames == ["conan_package.tgz", "conanmanifest.txt"] # Verify each file entry has required fields for file_entry in manifest["files"]: assert "file" in file_entry assert "sha256" in file_entry assert len(file_entry["sha256"]) == 64 def test_save_load_signatures(pkg_sign_tools): """Test that saving and loading signatures preserves all data correctly.""" artifacts_folder, signature_folder = pkg_sign_tools # Manifest must exist before saving signatures _save_manifest(artifacts_folder, signature_folder) signatures = [{ "method": "openssl-dgst", "provider": "my-organization", "sign_artifacts": { "manifest": "pkgsign-manifest.json", "conan_package.tgz": "conan_package.tgz.sig", "conanmanifest.txt": "conanmanifest.txt.sig" } }] _save_signatures(signature_folder, signatures) # Verify file exists signatures_path = os.path.join(signature_folder, "pkgsign-signatures.json") assert os.path.isfile(signatures_path) # Load and verify content loaded = json.loads(load(signatures_path)) assert len(loaded["signatures"]) == 1 signature = loaded["signatures"][0] assert signature["method"] == "openssl-dgst" assert signature["provider"] == "my-organization" assert signature["sign_artifacts"]["manifest"] == "pkgsign-manifest.json" assert signature["sign_artifacts"]["conan_package.tgz"] == "conan_package.tgz.sig" assert signature["sign_artifacts"]["conanmanifest.txt"] == "conanmanifest.txt.sig" def test_save_signatures_with_multiple_signatures(pkg_sign_tools): """Test that _save_signatures can handle multiple signature entries.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) signatures = [ { "method": "gpg", "provider": "my-organization", "sign_artifacts": {"signature": "pkgsign-manifest.json.gpg"} }, { "method": "cosign", "provider": "my-organization", "sign_artifacts": {"signature": "pkgsign-manifest.json.sig"} } ] _save_signatures(signature_folder, signatures) signatures_path = os.path.join(signature_folder, "pkgsign-signatures.json") loaded = json.loads(load(signatures_path)) assert len(loaded["signatures"]) == 2 assert loaded["signatures"][0]["method"] == "gpg" assert loaded["signatures"][0]["sign_artifacts"]["signature"] == "pkgsign-manifest.json.gpg" assert loaded["signatures"][1]["method"] == "cosign" assert loaded["signatures"][1]["sign_artifacts"]["signature"] == "pkgsign-manifest.json.sig" def test_save_signatures_validates_required_fields(pkg_sign_tools): """Test that _save_signatures validates required signature fields.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) # Missing method with pytest.raises(ConanException, match="'method' is missing in signature data"): _save_signatures(signature_folder, [{"provider": "my-organization","sign_artifacts": {}}]) # Missing provider with pytest.raises(ConanException, match="'provider' is missing in signature data"): _save_signatures(signature_folder, [{"method": "gpg", "sign_artifacts": {}}]) # Missing sign_artifacts with pytest.raises(ConanException, match="'sign_artifacts' is missing in signature data"): _save_signatures(signature_folder, [{"method": "gpg", "provider": "my-organization"}]) # sign_artifacts not a dict with pytest.raises(ConanException, match="'sign_artifacts' must be a dict"): _save_signatures(signature_folder, [{"method": "gpg", "provider": "my-organization", "sign_artifacts": "not a dict"}]) def test_verify_files_checksums_success(pkg_sign_tools): """Test that verify_files_checksums succeeds when all checksums match.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) files = { "conan_package.tgz": os.path.join(artifacts_folder, "conan_package.tgz"), "conanmanifest.txt": os.path.join(artifacts_folder, "conanmanifest.txt") } # Should not raise an exception _verify_files_checksums(signature_folder, files) def test_verify_files_checksums_partial_files(pkg_sign_tools): """Test that verify_files_checksums works with a subset of files. This is to test in case that conan_sources.tgz is not present.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) # Verify only one file files = { "conanmanifest.txt": os.path.join(artifacts_folder, "conanmanifest.txt") } # Should not raise an exception _verify_files_checksums(signature_folder, files) def test_verify_files_checksums_mismatch(pkg_sign_tools): """Test that verify_files_checksums raises exception when checksums don't match.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) # Modify file content to cause checksum mismatch modified_file = os.path.join(artifacts_folder, "conan_package.tgz") with open(modified_file, "w") as f: f.write("modified content") files = { "conan_package.tgz": modified_file, "conanmanifest.txt": os.path.join(artifacts_folder, "conanmanifest.txt") } with pytest.raises(ConanException, match="Checksum mismatch for file conan_package.tgz"): _verify_files_checksums(signature_folder, files) def test_verify_files_checksums_missing_file_in_manifest(pkg_sign_tools): """Test that verify_files_checksums handles files not in manifest.""" artifacts_folder, signature_folder = pkg_sign_tools _save_manifest(artifacts_folder, signature_folder) # Try to verify a file that doesn't exist in manifest new_file = os.path.join(artifacts_folder, "new_file.txt") with open(new_file, "w") as f: f.write("content") files = {"new_file.txt": new_file} # Should raise exception because file is not in manifest (expected_checksum is None) with pytest.raises(ConanException, match="Checksum mismatch"): _verify_files_checksums(signature_folder, files) ================================================ FILE: test/unittests/client/profile_loader/__init__.py ================================================ ================================================ FILE: test/unittests/client/profile_loader/compiler_cppstd_test.py ================================================ import os import textwrap import yaml import pytest from jinja2 import Template from conan.internal.cache.cache import PkgCache from conan.internal.cache.home_paths import HomePaths from conan.internal.api.profile.profile_loader import ProfileLoader from conan.internal.default_settings import default_settings_yml from conan.errors import ConanException from conan.internal.model.conf import ConfDefinition from conan.internal.model.settings import Settings from conan.test.utils.test_files import temp_folder from conan.internal.util.files import save class TestSettingsCppStd: @pytest.fixture(autouse=True) def setup(self): self.cache_folder = temp_folder() self.cache = PkgCache(self.cache_folder, ConfDefinition()) self.home_paths = HomePaths(self.cache_folder) save(self.home_paths.profile_plugin_path, "") def _save_profile(self, compiler_cppstd=None, filename="default"): fullpath = os.path.join(self.home_paths.profiles_path, filename) t = Template(textwrap.dedent(""" [settings] os=Macos arch=x86_64 compiler=apple-clang {% if compiler_cppstd %}compiler.cppstd={{ compiler_cppstd }}{% endif %} compiler.libcxx=libc++ compiler.version=10.0 """)) save(fullpath, t.render(compiler_cppstd=compiler_cppstd)) return filename def test_no_compiler_cppstd(self): # https://github.com/conan-io/conan/issues/5128 fullpath = os.path.join(self.home_paths.profiles_path, "default") t = textwrap.dedent(""" [settings] os=Macos arch=x86_64 compiler=apple-clang compiler.libcxx=libc++ compiler.version=10.0 compiler.cppstd = 14 """) save(fullpath, t) profile_loader = ProfileLoader(self.cache_folder) with pytest.raises(ConanException, match="'settings.compiler.cppstd' doesn't exist for 'apple-clang'"): profile = profile_loader.from_cli_args(["default"], None, None, None, None) settings = Settings(yaml.safe_load(default_settings_yml.replace("cppstd", "foobar"))) profile.process_settings(settings) def test_no_value(self): self._save_profile() profile_loader = ProfileLoader(self.cache_folder) r = profile_loader.from_cli_args(["default"], None, None, None, None) assert "compiler.cppstd" not in r.settings def test_value_none(self): self._save_profile(compiler_cppstd="None") profile_loader = ProfileLoader(self.cache_folder) # It is incorrect to assign compiler.cppstd=None in the profile with pytest.raises(ConanException, match="Invalid setting"): r = profile_loader.from_cli_args(["default"], None, None, None, None) settings = Settings(yaml.safe_load(default_settings_yml)) r.process_settings(settings) def test_value_valid(self): self._save_profile(compiler_cppstd="11") profile_loader = ProfileLoader(self.cache_folder) r = profile_loader.from_cli_args(["default"], None, None, None, None) assert r.settings["compiler.cppstd"] == "11" assert "cppstd" not in r.settings def test_value_invalid(self): self._save_profile(compiler_cppstd="13") profile_loader = ProfileLoader(self.cache_folder) with pytest.raises(ConanException, match="Invalid setting '13' is not a valid " "'settings.compiler.cppstd' value"): r = profile_loader.from_cli_args(["default"], None, None, None, None) settings = Settings(yaml.safe_load(default_settings_yml)) r.process_settings(settings) ================================================ FILE: test/unittests/client/profile_loader/profile_loader_test.py ================================================ import os import textwrap import pytest from conan.internal.api.profile.profile_loader import _ProfileParser, ProfileLoader from conan.errors import ConanException from conan.api.model import RecipeReference from conan.test.utils.mocks import ConanFileMock, MockSettings from conan.test.utils.test_files import temp_folder from conan.internal.util.files import save def test_profile_parser(): txt = textwrap.dedent(r""" include(a/path/to\profile.txt) include(other/path/to/file.txt) [settings] os=2 """) a = _ProfileParser(txt) assert a.includes == [r"a/path/to\profile.txt", "other/path/to/file.txt"] assert a.profile_text == textwrap.dedent("""[settings] os=2""") txt = "" a = _ProfileParser(txt) assert a.includes == [] assert a.profile_text == "" txt = textwrap.dedent(r""" includes(a/path/to\profile.txt) """) with pytest.raises(ConanException): try: _ProfileParser(txt) except Exception as error: assert "Error while parsing line 1" in error.args[0] raise def test_profiles_includes(): tmp = temp_folder() def save_profile(txt, name): abs_profile_path = os.path.join(tmp, name) save(abs_profile_path, txt) os.mkdir(os.path.join(tmp, "subdir")) profile0 = textwrap.dedent(""" {% set ROOTVAR=0 %} [tool_requires] one/1.{{ROOTVAR}}@lasote/stable two/1.2@lasote/stable """) save_profile(profile0, "subdir/profile0.txt") profile1 = textwrap.dedent(""" # Include in subdir, curdir include(./profile0.txt) [settings] os=Windows [options] zlib*:aoption=1 zlib*:otheroption=1 """) save_profile(profile1, "subdir/profile1.txt") profile2 = textwrap.dedent(""" # Include in subdir {% set MYVAR=1 %} include(./subdir/profile1.txt) [settings] os={{MYVAR}} """) save_profile(profile2, "profile2.txt") profile3 = textwrap.dedent(""" [tool_requires] one/1.5@lasote/stable """) save_profile(profile3, "profile3.txt") profile4 = textwrap.dedent(""" include(./profile2.txt) include(./profile3.txt) [options] zlib*:otheroption=12 """) save_profile(profile4, "profile4.txt") profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile = profile_loader.load_profile("./profile4.txt", tmp) assert profile.settings == {"os": "1"} assert profile.options["zlib*"].aoption == 1 assert profile.options["zlib*"].otheroption == 12 assert profile.tool_requires == {"*": [RecipeReference.loads("one/1.5@lasote/stable"), RecipeReference.loads("two/1.2@lasote/stable")]} def test_profile_compose_platform_tool_requires(): tmp = temp_folder() save(os.path.join(tmp, "profile0"), "[platform_tool_requires]\ntool1/1.0") save(os.path.join(tmp, "profile1"), "[platform_tool_requires]\ntool2/2.0") save(os.path.join(tmp, "profile2"), "include(./profile0)\n[platform_tool_requires]\ntool3/3.0") save(os.path.join(tmp, "profile3"), "include(./profile0)\n[platform_tool_requires]\ntool1/1.1") profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile2 = profile_loader.load_profile("./profile2", tmp) assert profile2.platform_tool_requires == [RecipeReference.loads("tool1/1.0"), RecipeReference.loads("tool3/3.0")] profile3 = profile_loader.load_profile("./profile3", tmp) assert profile3.platform_tool_requires == [RecipeReference.loads("tool1/1.1")] profile0 = profile_loader.load_profile("./profile0", tmp) profile1 = profile_loader.load_profile("./profile1", tmp) profile0.compose_profile(profile1) assert profile0.platform_tool_requires == [RecipeReference.loads("tool1/1.0"), RecipeReference.loads("tool2/2.0")] def test_profile_compose_tool_requires(): tmp = temp_folder() save(os.path.join(tmp, "profile0"), "[tool_requires]\ntool1/1.0") save(os.path.join(tmp, "profile1"), "[tool_requires]\ntool2/2.0") save(os.path.join(tmp, "profile2"), "include(./profile0)\n[tool_requires]\ntool3/3.0") save(os.path.join(tmp, "profile3"), "include(./profile0)\n[tool_requires]\ntool1/1.1") profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile2 = profile_loader.load_profile("./profile2", tmp) assert profile2.tool_requires == {"*": [RecipeReference.loads("tool1/1.0"), RecipeReference.loads("tool3/3.0")]} profile3 = profile_loader.load_profile("./profile3", tmp) assert profile3.tool_requires == {"*": [RecipeReference.loads("tool1/1.1")]} profile0 = profile_loader.load_profile("./profile0", tmp) profile1 = profile_loader.load_profile("./profile1", tmp) profile0.compose_profile(profile1) assert profile0.tool_requires == {"*": [RecipeReference.loads("tool1/1.0"), RecipeReference.loads("tool2/2.0")]} def test_profile_include_order(): tmp = temp_folder() save(os.path.join(tmp, "profile1.txt"), '{% set MYVAR="fromProfile1" %}') profile2 = textwrap.dedent(""" include(./profile1.txt) {% set MYVAR="fromProfile2" %} [settings] os={{MYVAR}} """) save(os.path.join(tmp, "profile2.txt"), profile2) profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile = profile_loader.load_profile("./profile2.txt", tmp) assert profile.settings["os"] == "fromProfile2" def test_profile_load_absolute_path(): """ When passing absolute path as profile file, it MUST be used. read_profile(/abs/path/profile, /abs, /.conan/profiles) /abs/path/profile MUST be consumed as target profile """ current_profile_folder = temp_folder() current_profile_path = os.path.join(current_profile_folder, "default") save(current_profile_path, "[settings]\nos=Windows") profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile = profile_loader.load_profile(current_profile_path) assert "Windows" == profile.settings["os"] def test_profile_load_relative_path_dot(): """ When passing relative ./path as profile file, it MUST be used read_profile(./profiles/profile, /tmp, /.conan/profiles) /tmp/profiles/profile MUST be consumed as target profile """ current_profile_folder = temp_folder() current_profile_path = os.path.join(current_profile_folder, "profiles", "default") save(current_profile_path, "[settings]\nos=Windows") profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile = profile_loader.load_profile("./profiles/default", current_profile_folder) assert "Windows" == profile.settings["os"] def test_profile_load_relative_path_pardir(): """ When passing relative ../path as profile file, it MUST be used read_profile(../profiles/profile, /tmp/current, /.conan/profiles) /tmp/profiles/profile MUST be consumed as target profile """ tmp = temp_folder() current_profile_path = os.path.join(tmp, "profiles", "default") cwd = os.path.join(tmp, "user_folder") save(current_profile_path, "[settings]\nos=Windows") profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile = profile_loader.load_profile("../profiles/default", cwd) assert "Windows" == profile.settings["os"] def test_profile_buildenv(): tmp = temp_folder() txt = textwrap.dedent(""" [buildenv] MyVar1=My Value; 11 MyVar1+=MyValue12 MyPath1=(path)/some/path11 MyPath1+=(path)/other path/path12 """) current_profile_path = os.path.join(tmp, "default") save(current_profile_path, txt) profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile = profile_loader.load_profile(current_profile_path) buildenv = profile.buildenv env = buildenv.get_profile_env(None) conanfile = ConanFileMock() conanfile.settings_build = MockSettings({"os": "Linux", "arch": "x86_64"}) env_vars = env.vars(conanfile) assert env_vars.get("MyVar1") == "My Value; 11 MyValue12" assert env_vars.get("MyPath1") == "/some/path11:/other path/path12" conanfile.settings_build = MockSettings({"os": "Windows", "arch": "x86_64"}) env_vars = env.vars(conanfile) assert env_vars.get("MyPath1") == "/some/path11;/other path/path12" @pytest.mark.parametrize("conf_name", [ "core.gzip:compresslevel=5", "core.gzip:compresslevel" ]) def test_profile_core_confs_error(conf_name): tmp = temp_folder() current_profile_path = os.path.join(tmp, "default") save(current_profile_path, "") profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error with pytest.raises(ConanException) as exc: profile_loader.from_cli_args([], [], [], [conf_name], None) assert "[conf] 'core.*' configurations are not allowed in profiles" in str(exc.value) def test_profile_compose_numbers(): tmp = temp_folder() txt = textwrap.dedent(""" [conf] user.version:value=8.1 pkg/*:user.version:value=10 """) current_profile_path = os.path.join(tmp, "default") save(current_profile_path, txt) profile_loader = ProfileLoader(cache_folder=temp_folder()) # If not used cache, will not error profile = profile_loader.load_profile(current_profile_path) assert profile.conf.get("user.version:value") == 8.1 assert profile.conf.get("pkg/*:user.version:value") == 10 ================================================ FILE: test/unittests/client/remote_manager_test.py ================================================ import os import sys import pytest from conan.internal.api.uploader import compress_files from conan.internal.rest.remote_manager import uncompress_file from conan.test.utils.test_files import temp_folder from conan.internal.util.files import save class TestRemoteManager: def test_compress_files_tgz(self): folder = temp_folder() save(os.path.join(folder, "one_file.txt"), "The contents") save(os.path.join(folder, "Two_file.txt"), "Two contents") files = { "one_file.txt": os.path.join(folder, "one_file.txt"), "Two_file.txt": os.path.join(folder, "Two_file.txt"), } path = compress_files(files, "conan_package.tgz", dest_dir=folder) assert os.path.exists(path) expected_path = os.path.join(folder, "conan_package.tgz") assert path == expected_path def test_compress_and_uncompress_xz_files(self): folder = temp_folder() save(os.path.join(folder, "one_file.txt"), "The contents") save(os.path.join(folder, "Two_file.txt"), "Two contents") files = { "one_file.txt": os.path.join(folder, "one_file.txt"), "Two_file.txt": os.path.join(folder, "Two_file.txt"), } path = compress_files(files, "conan_package.txz", dest_dir=folder) assert os.path.exists(path) expected_path = os.path.join(folder, "conan_package.txz") assert path == expected_path extract_dir = os.path.join(folder, "extracted") uncompress_file(path, extract_dir) extract_files = list(sorted(os.listdir(extract_dir))) expected_files = sorted(files.keys()) assert extract_files == expected_files for name, path in files.items(): extract_path = os.path.join(extract_dir, name) with open(path, "r") as f1, open(extract_path, "r") as f2: assert f1.read() == f2.read() @pytest.mark.skipif(sys.version_info.minor < 14, reason="zstd needs Python >= 3.14") def test_compress_and_uncompress_zst_files(self): folder = temp_folder() save(os.path.join(folder, "one_file.txt"), "The contents") save(os.path.join(folder, "Two_file.txt"), "Two contents") files = { "one_file.txt": os.path.join(folder, "one_file.txt"), "Two_file.txt": os.path.join(folder, "Two_file.txt"), } path = compress_files(files, "conan_package.tzst", dest_dir=folder) assert os.path.exists(path) expected_path = os.path.join(folder, "conan_package.tzst") assert path == expected_path extract_dir = os.path.join(folder, "extracted") uncompress_file(path, extract_dir) extract_files = list(sorted(os.listdir(extract_dir))) expected_files = sorted(files.keys()) assert extract_files == expected_files for name, path in sorted(files.items()): extract_path = os.path.join(extract_dir, name) with open(path, "r") as f1, open(extract_path, "r") as f2: assert f1.read() == f2.read() ================================================ FILE: test/unittests/client/rest/__init__.py ================================================ ================================================ FILE: test/unittests/client/rest/downloader_test.py ================================================ import os import re import tempfile import pytest from conan.internal.rest.file_downloader import FileDownloader from conan.errors import ConanException class MockResponse: def __init__(self, data, headers, status_code=200): self.data = data self.ok = True self.status_code = status_code self.headers = headers.copy() self.headers.update({key.lower(): value for key, value in headers.items()}) def iter_content(self, size): for i in range(0, len(self.data), size): yield self.data[i:i + size] def close(self): pass class MockRequester: def __init__(self, data, chunk_size=None, accept_ranges=True, echo_header=None): self._data = data self._chunk_size = chunk_size if chunk_size is not None else len(data) self._accept_ranges = accept_ranges self._echo_header = echo_header.copy() if echo_header else {} def get(self, *_args, **kwargs): start = 0 headers = kwargs.get("headers") or {} transfer_range = headers.get("range", "") match = re.match(r"bytes=([0-9]+)-", transfer_range) status = 200 headers = {"Content-Length": len(self._data), "Accept-Ranges": "bytes"} if match and self._accept_ranges: start = int(match.groups()[0]) assert start < len(self._data) status = 206 headers.update({"Content-Length": str(len(self._data) - start), "Content-Range": "bytes {}-{}/{}".format(start, len(self._data) - 1, len(self._data))}) else: headers.update(self._echo_header) response = MockResponse(self._data[start:start + self._chunk_size], status_code=status, headers=headers) return response class TestDownloaderUnit: @pytest.fixture(autouse=True) def setup(self): d = tempfile.mkdtemp() self.target = os.path.join(d, "target") def test_succeed_download_to_file_if_not_interrupted(self): expected_content = b"some data" requester = MockRequester(expected_content) downloader = FileDownloader(requester=requester) downloader.download("fake_url", file_path=self.target) actual_content = open(self.target, "rb").read() assert expected_content == actual_content def test_resume_download_to_file_if_interrupted(self): expected_content = b"some data" requester = MockRequester(expected_content, chunk_size=4) downloader = FileDownloader(requester=requester) downloader.download("fake_url", file_path=self.target, verify_ssl=None, retry=0, retry_wait=0) actual_content = open(self.target, "rb").read() assert expected_content == actual_content def test_fail_interrupted_download_to_file_if_no_progress(self): expected_content = b"some data" requester = MockRequester(expected_content, chunk_size=0) downloader = FileDownloader(requester=requester) with pytest.raises(ConanException, match=r"Download failed"): downloader.download("fake_url", file_path=self.target) def test_fail_interrupted_download_if_server_not_accepting_ranges(self): expected_content = b"some data" requester = MockRequester(expected_content, chunk_size=4, accept_ranges=False) downloader = FileDownloader(requester=requester) with pytest.raises(ConanException, match=r"Incorrect Content-Range header"): downloader.download("fake_url", file_path=self.target) def test_download_with_compressed_content_and_bigger_content_length(self): expected_content = b"some data" echo_header = {"Content-Encoding": "gzip", "Content-Length": len(expected_content) + 1} requester = MockRequester(expected_content, echo_header=echo_header) downloader = FileDownloader(requester=requester) downloader.download("fake_url", file_path=self.target) actual_content = open(self.target, "rb").read() assert expected_content == actual_content def test_download_with_compressed_content_and_smaller_content_length(self): expected_content = b"some data" echo_header = {"Content-Encoding": "gzip", "Content-Length": len(expected_content) - 1} requester = MockRequester(expected_content, echo_header=echo_header) downloader = FileDownloader(requester=requester) downloader.download("fake_url", file_path=self.target) actual_content = open(self.target, "rb").read() assert expected_content == actual_content ================================================ FILE: test/unittests/client/rest/requester_test.py ================================================ import os from conan.api.conan_api import ConanAPI from conan.internal.cache.home_paths import HomePaths from conan.internal.util.files import save from conan.test.utils.test_files import temp_folder def test_requester_reinit(): """ Ensure the reinitialization of the requester gets the new global configuration """ tmp_folder = temp_folder() home_path = HomePaths(tmp_folder) save(os.path.join(home_path.profiles_path, "default"), "") conan_api = ConanAPI(tmp_folder) save(os.path.join(home_path.global_conf_path), "core.net.http:timeout=(10, 20)") conan_api.reinit() assert conan_api._api_helpers.requester._timeout == (10, 20) ================================================ FILE: test/unittests/client/rest/response_test.py ================================================ from collections import namedtuple from conan.internal import rest class TestRestString: def _get_html_response(self): headers = {"content-type": "text/html;charset=utf-8"} return namedtuple("Response", "status_code headers reason content")( 404, headers, "Not Found", b'\n\n