vcpkg/toolsrc/src/vcpkg/commands.integrate.cpp

604 lines
23 KiB
C++

#include <vcpkg/base/checks.h>
#include <vcpkg/base/expected.h>
#include <vcpkg/base/files.h>
#include <vcpkg/base/system.print.h>
#include <vcpkg/base/system.process.h>
#include <vcpkg/base/util.h>
#include <vcpkg/commands.integrate.h>
#include <vcpkg/metrics.h>
#include <vcpkg/tools.h>
#include <vcpkg/userconfig.h>
#include <vcpkg/vcpkgcmdarguments.h>
#include <vcpkg/vcpkgpaths.h>
namespace vcpkg::Commands::Integrate
{
#if defined(_WIN32)
static std::string create_appdata_shortcut(const std::string& target_path) noexcept
{
return Strings::format(R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Condition="Exists('%s') and '$(VCPkgLocalAppDataDisabled)' == ''" Project="%s" />
</Project>
)###",
target_path,
target_path);
}
#endif
#if defined(_WIN32)
static std::string create_system_targets_shortcut() noexcept
{
return R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- version 1 -->
<PropertyGroup>
<VCLibPackagePath Condition="'$(VCLibPackagePath)' == ''">$(LOCALAPPDATA)\vcpkg\vcpkg.user</VCLibPackagePath>
</PropertyGroup>
<Import Condition="'$(VCLibPackagePath)' != '' and Exists('$(VCLibPackagePath).props')" Project="$(VCLibPackagePath).props" />
<Import Condition="'$(VCLibPackagePath)' != '' and Exists('$(VCLibPackagePath).targets')" Project="$(VCLibPackagePath).targets" />
</Project>
)###";
}
#endif
#if defined(_WIN32)
static std::string create_nuget_targets_file_contents(const fs::path& msbuild_vcpkg_targets_file) noexcept
{
const std::string as_string = msbuild_vcpkg_targets_file.string();
return Strings::format(R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="%s" Condition="Exists('%s')" />
<Target Name="CheckValidPlatform" BeforeTargets="Build">
<Error Text="Unsupported architecture combination. Remove the 'vcpkg' nuget package." Condition="'$(VCPkgEnabled)' != 'true' and '$(VCPkgDisableError)' == ''"/>
</Target>
</Project>
)###",
as_string,
as_string);
}
#endif
#if defined(_WIN32)
static std::string create_nuget_props_file_contents() noexcept
{
return R"###(
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VCPkgLocalAppDataDisabled>true</VCPkgLocalAppDataDisabled>
</PropertyGroup>
</Project>
)###";
}
#endif
#if defined(_WIN32)
static std::string get_nuget_id(const fs::path& vcpkg_root_dir)
{
std::string dir_id = fs::generic_u8string(vcpkg_root_dir);
std::replace(dir_id.begin(), dir_id.end(), '/', '.');
dir_id.erase(1, 1); // Erasing the ":"
// NuGet id cannot have invalid characters. We will only use alphanumeric and dot.
Util::erase_remove_if(dir_id, [](char c) { return !isalnum(static_cast<unsigned char>(c)) && (c != '.'); });
const std::string nuget_id = "vcpkg." + dir_id;
return nuget_id;
}
#endif
#if defined(_WIN32)
static std::string create_nuspec_file_contents(const fs::path& vcpkg_root_dir,
const std::string& nuget_id,
const std::string& nupkg_version)
{
static constexpr auto CONTENT_TEMPLATE = R"(
<package>
<metadata>
<id>@NUGET_ID@</id>
<version>@VERSION@</version>
<authors>vcpkg</authors>
<description>
This package imports all libraries currently installed in @VCPKG_DIR@. This package does not contain any libraries and instead refers to the folder directly (like a symlink).
</description>
</metadata>
<files>
<file src="vcpkg.nuget.props" target="build\native\@NUGET_ID@.props" />
<file src="vcpkg.nuget.targets" target="build\native\@NUGET_ID@.targets" />
</files>
</package>
)";
std::string content = Strings::replace_all(CONTENT_TEMPLATE, "@NUGET_ID@", nuget_id);
content = Strings::replace_all(std::move(content), "@VCPKG_DIR@", vcpkg_root_dir.string());
content = Strings::replace_all(std::move(content), "@VERSION@", nupkg_version);
return content;
}
#endif
#if defined(_WIN32)
enum class ElevationPromptChoice
{
YES,
NO
};
static ElevationPromptChoice elevated_cmd_execute(const std::string& param)
{
SHELLEXECUTEINFOW sh_ex_info{};
sh_ex_info.cbSize = sizeof(sh_ex_info);
sh_ex_info.fMask = SEE_MASK_NOCLOSEPROCESS;
sh_ex_info.hwnd = nullptr;
sh_ex_info.lpVerb = L"runas";
sh_ex_info.lpFile = L"cmd"; // Application to start
auto wparam = Strings::to_utf16(param);
sh_ex_info.lpParameters = wparam.c_str(); // Additional parameters
sh_ex_info.lpDirectory = nullptr;
sh_ex_info.nShow = SW_HIDE;
sh_ex_info.hInstApp = nullptr;
if (!ShellExecuteExW(&sh_ex_info))
{
return ElevationPromptChoice::NO;
}
if (sh_ex_info.hProcess == nullptr)
{
return ElevationPromptChoice::NO;
}
WaitForSingleObject(sh_ex_info.hProcess, INFINITE);
CloseHandle(sh_ex_info.hProcess);
return ElevationPromptChoice::YES;
}
#endif
#if defined(_WIN32)
static fs::path get_appdata_targets_path()
{
return System::get_appdata_local().value_or_exit(VCPKG_LINE_INFO) / fs::u8path("vcpkg/vcpkg.user.targets");
}
#endif
#if defined(_WIN32)
static fs::path get_appdata_props_path()
{
return System::get_appdata_local().value_or_exit(VCPKG_LINE_INFO) / fs::u8path("vcpkg/vcpkg.user.props");
}
#endif
static fs::path get_path_txt_path() { return get_user_dir() / "vcpkg.path.txt"; }
#if defined(_WIN32)
static void integrate_install_msbuild14(Files::Filesystem& fs, const fs::path& tmp_dir)
{
static const std::array<fs::path, 2> OLD_SYSTEM_TARGET_FILES = {
System::get_program_files_32_bit().value_or_exit(VCPKG_LINE_INFO) /
"MSBuild/14.0/Microsoft.Common.Targets/ImportBefore/vcpkg.nuget.targets",
System::get_program_files_32_bit().value_or_exit(VCPKG_LINE_INFO) /
"MSBuild/14.0/Microsoft.Common.Targets/ImportBefore/vcpkg.system.targets"};
static const fs::path SYSTEM_WIDE_TARGETS_FILE =
System::get_program_files_32_bit().value_or_exit(VCPKG_LINE_INFO) /
"MSBuild/Microsoft.Cpp/v4.0/V140/ImportBefore/Default/vcpkg.system.props";
// TODO: This block of code should eventually be removed
for (auto&& old_system_wide_targets_file : OLD_SYSTEM_TARGET_FILES)
{
if (fs.exists(old_system_wide_targets_file))
{
const std::string param =
Strings::format(R"(/c "DEL "%s" /Q > nul")", old_system_wide_targets_file.string());
const ElevationPromptChoice user_choice = elevated_cmd_execute(param);
switch (user_choice)
{
case ElevationPromptChoice::YES: break;
case ElevationPromptChoice::NO:
System::print2(System::Color::warning, "Warning: Previous integration file was not removed\n");
Checks::exit_fail(VCPKG_LINE_INFO);
default: Checks::unreachable(VCPKG_LINE_INFO);
}
}
}
bool should_install_system = true;
const Expected<std::string> system_wide_file_contents = fs.read_contents(SYSTEM_WIDE_TARGETS_FILE);
static const std::regex RE(R"###(<!-- version (\d+) -->)###");
if (const auto contents_data = system_wide_file_contents.get())
{
std::match_results<std::string::const_iterator> match;
const auto found = std::regex_search(*contents_data, match, RE);
if (found)
{
const int ver = atoi(match[1].str().c_str());
if (ver >= 1) should_install_system = false;
}
}
if (should_install_system)
{
const fs::path sys_src_path = tmp_dir / "vcpkg.system.targets";
fs.write_contents(sys_src_path, create_system_targets_shortcut(), VCPKG_LINE_INFO);
const std::string param = Strings::format(R"(/c "mkdir "%s" & copy "%s" "%s" /Y > nul")",
SYSTEM_WIDE_TARGETS_FILE.parent_path().string(),
sys_src_path.string(),
SYSTEM_WIDE_TARGETS_FILE.string());
const ElevationPromptChoice user_choice = elevated_cmd_execute(param);
switch (user_choice)
{
case ElevationPromptChoice::YES: break;
case ElevationPromptChoice::NO:
System::print2(System::Color::warning, "Warning: integration was not applied\n");
Checks::exit_fail(VCPKG_LINE_INFO);
default: Checks::unreachable(VCPKG_LINE_INFO);
}
Checks::check_exit(VCPKG_LINE_INFO,
fs.exists(SYSTEM_WIDE_TARGETS_FILE),
"Error: failed to copy targets file to %s",
SYSTEM_WIDE_TARGETS_FILE.string());
}
}
#endif
static void integrate_install(const VcpkgPaths& paths)
{
auto& fs = paths.get_filesystem();
#if defined(_WIN32)
{
std::error_code ec;
const fs::path tmp_dir = paths.buildsystems / "tmp";
fs.create_directory(paths.buildsystems, ec);
fs.create_directory(tmp_dir, ec);
integrate_install_msbuild14(fs, tmp_dir);
const fs::path appdata_src_path = tmp_dir / "vcpkg.user.targets";
fs.write_contents(appdata_src_path,
create_appdata_shortcut(fs::u8string(paths.buildsystems_msbuild_targets)),
VCPKG_LINE_INFO);
auto appdata_dst_path = get_appdata_targets_path();
const auto rc = fs.copy_file(appdata_src_path, appdata_dst_path, fs::copy_options::overwrite_existing, ec);
if (!rc || ec)
{
System::print2(System::Color::error,
"Error: Failed to copy file: ",
fs::u8string(appdata_src_path),
" -> ",
fs::u8string(appdata_dst_path),
"\n");
Checks::exit_fail(VCPKG_LINE_INFO);
}
const fs::path appdata_src_path2 = tmp_dir / "vcpkg.user.props";
fs.write_contents(appdata_src_path2,
create_appdata_shortcut(fs::u8string(paths.buildsystems_msbuild_props)),
VCPKG_LINE_INFO);
auto appdata_dst_path2 = get_appdata_props_path();
const auto rc2 =
fs.copy_file(appdata_src_path2, appdata_dst_path2, fs::copy_options::overwrite_existing, ec);
if (!rc2 || ec)
{
System::print2(System::Color::error,
"Error: Failed to copy file: ",
fs::u8string(appdata_src_path2),
" -> ",
fs::u8string(appdata_dst_path2),
"\n");
Checks::exit_fail(VCPKG_LINE_INFO);
}
}
#endif
const auto pathtxt = get_path_txt_path();
std::error_code ec;
fs.write_contents(pathtxt, fs::generic_u8string(paths.root), VCPKG_LINE_INFO);
System::print2(System::Color::success, "Applied user-wide integration for this vcpkg root.\n");
const fs::path cmake_toolchain = paths.buildsystems / "vcpkg.cmake";
#if defined(_WIN32)
System::printf(
R"(
All MSBuild C++ projects can now #include any installed libraries.
Linking will be handled automatically.
Installing new libraries will make them instantly available.
CMake projects should use: "-DCMAKE_TOOLCHAIN_FILE=%s"
)",
fs::generic_u8string(cmake_toolchain));
#else
System::printf(
R"(
CMake projects should use: "-DCMAKE_TOOLCHAIN_FILE=%s"
)",
fs::generic_u8string(cmake_toolchain));
#endif
Checks::exit_success(VCPKG_LINE_INFO);
}
static void integrate_remove(Files::Filesystem& fs)
{
std::error_code ec;
bool was_deleted = false;
#if defined(_WIN32)
const fs::path path = get_appdata_targets_path();
was_deleted |= fs.remove(path, ec);
Checks::check_exit(VCPKG_LINE_INFO, !ec, "Error: Unable to remove user-wide integration: %s", ec.message());
const fs::path path2 = get_appdata_props_path();
was_deleted |= fs.remove(path2, ec);
Checks::check_exit(VCPKG_LINE_INFO, !ec, "Error: Unable to remove user-wide integration: %s", ec.message());
#endif
was_deleted |= fs.remove(get_path_txt_path(), ec);
Checks::check_exit(VCPKG_LINE_INFO, !ec, "Error: Unable to remove user-wide integration: %s", ec.message());
if (was_deleted)
{
System::print2(System::Color::success, "User-wide integration was removed\n");
}
else
{
System::print2(System::Color::success, "User-wide integration is not installed\n");
}
Checks::exit_success(VCPKG_LINE_INFO);
}
#if defined(WIN32)
static void integrate_project(const VcpkgPaths& paths)
{
auto& fs = paths.get_filesystem();
const fs::path& nuget_exe = paths.get_tool_exe(Tools::NUGET);
const fs::path& buildsystems_dir = paths.buildsystems;
const fs::path tmp_dir = buildsystems_dir / "tmp";
std::error_code ec;
fs.create_directory(buildsystems_dir, ec);
fs.create_directory(tmp_dir, ec);
const fs::path targets_file_path = tmp_dir / "vcpkg.nuget.targets";
const fs::path props_file_path = tmp_dir / "vcpkg.nuget.props";
const fs::path nuspec_file_path = tmp_dir / "vcpkg.nuget.nuspec";
const std::string nuget_id = get_nuget_id(paths.root);
const std::string nupkg_version = "1.0.0";
fs.write_contents(
targets_file_path, create_nuget_targets_file_contents(paths.buildsystems_msbuild_targets), VCPKG_LINE_INFO);
fs.write_contents(props_file_path, create_nuget_props_file_contents(), VCPKG_LINE_INFO);
fs.write_contents(
nuspec_file_path, create_nuspec_file_contents(paths.root, nuget_id, nupkg_version), VCPKG_LINE_INFO);
// Using all forward slashes for the command line
const std::string cmd_line = Strings::format(R"("%s" pack -OutputDirectory "%s" "%s")",
fs::u8string(nuget_exe),
fs::u8string(buildsystems_dir),
fs::u8string(nuspec_file_path));
const int exit_code =
System::cmd_execute_and_capture_output(cmd_line, System::get_clean_environment()).exit_code;
const fs::path nuget_package = buildsystems_dir / Strings::format("%s.%s.nupkg", nuget_id, nupkg_version);
Checks::check_exit(
VCPKG_LINE_INFO, exit_code == 0 && fs.exists(nuget_package), "Error: NuGet package creation failed");
System::print2(System::Color::success, "Created nupkg: ", fs::u8string(nuget_package), '\n');
auto source_path = fs::u8string(buildsystems_dir);
source_path = Strings::replace_all(std::move(source_path), "`", "``");
System::printf(R"(
With a project open, go to Tools->NuGet Package Manager->Package Manager Console and paste:
Install-Package %s -Source "%s"
)",
nuget_id,
source_path);
Checks::exit_success(VCPKG_LINE_INFO);
}
#endif
#if defined(_WIN32)
static void integrate_powershell(const VcpkgPaths& paths)
{
static constexpr StringLiteral TITLE = "PowerShell Tab-Completion";
const fs::path script_path = paths.scripts / "addPoshVcpkgToPowershellProfile.ps1";
const auto& ps = paths.get_tool_exe("powershell-core");
const std::string cmd = Strings::format(R"("%s" -NoProfile -ExecutionPolicy Bypass -Command "& {& '%s' }")",
fs::u8string(ps),
fs::u8string(script_path));
const int rc = System::cmd_execute(cmd);
if (rc)
{
System::printf(System::Color::error,
"%s\n"
"Could not run:\n"
" '%s'\n",
TITLE,
fs::generic_u8string(script_path));
{
auto locked_metrics = Metrics::g_metrics.lock();
locked_metrics->track_property("error", "powershell script failed");
locked_metrics->track_property("title", TITLE);
}
}
Checks::exit_with_code(VCPKG_LINE_INFO, rc);
}
#else
static void integrate_bash(const VcpkgPaths& paths)
{
const auto home_path = System::get_environment_variable("HOME").value_or_exit(VCPKG_LINE_INFO);
const fs::path bashrc_path = fs::path{home_path} / ".bashrc";
auto& fs = paths.get_filesystem();
const fs::path completion_script_path = paths.scripts / "vcpkg_completion.bash";
Expected<std::vector<std::string>> maybe_bashrc_content = fs.read_lines(bashrc_path);
Checks::check_exit(
VCPKG_LINE_INFO, maybe_bashrc_content.has_value(), "Unable to read %s", fs::u8string(bashrc_path));
std::vector<std::string> bashrc_content = maybe_bashrc_content.value_or_exit(VCPKG_LINE_INFO);
std::vector<std::string> matches;
for (auto&& line : bashrc_content)
{
std::smatch match;
if (std::regex_match(line, match, std::regex{R"###(^source.*scripts/vcpkg_completion.bash$)###"}))
{
matches.push_back(line);
}
}
if (!matches.empty())
{
System::printf("vcpkg bash completion is already imported to your %s file.\n"
"The following entries were found:\n"
" %s\n"
"Please make sure you have started a new bash shell for the changes to take effect.\n",
fs::u8string(bashrc_path),
Strings::join("\n ", matches));
Checks::exit_success(VCPKG_LINE_INFO);
}
System::printf("Adding vcpkg completion entry to %s\n", fs::u8string(bashrc_path));
bashrc_content.push_back(Strings::format("source %s", fs::u8string(completion_script_path)));
fs.write_contents(bashrc_path, Strings::join("\n", bashrc_content) + '\n', VCPKG_LINE_INFO);
Checks::exit_success(VCPKG_LINE_INFO);
}
static void integrate_fish(const VcpkgPaths& paths)
{
fs::path fish_completions_path;
const auto config_path = System::get_environment_variable("XDG_CONFIG_HOME");
if (config_path.has_value())
{
fish_completions_path = fs::path{config_path.value_or_exit(VCPKG_LINE_INFO)};
}
else
{
const auto home_path = System::get_environment_variable("HOME").value_or_exit(VCPKG_LINE_INFO);
fish_completions_path = fs::path{home_path} / ".config";
}
fish_completions_path = fish_completions_path / "fish" / "completions" / "vcpkg.fish";
if (fs::stdfs::exists(fish_completions_path))
{
System::printf("vcpkg fish completion is already added at %s.\n", fs::u8string(fish_completions_path));
Checks::exit_success(VCPKG_LINE_INFO);
}
const fs::path completion_script_path = paths.scripts / "vcpkg_completion.fish";
System::printf("Adding vcpkg completion entry at %s.\n", fs::u8string(fish_completions_path));
fs::stdfs::create_symlink(completion_script_path, fish_completions_path);
Checks::exit_success(VCPKG_LINE_INFO);
}
#endif
void append_helpstring(HelpTableFormatter& table)
{
#if defined(_WIN32)
table.format("vcpkg integrate install",
"Make installed packages available user-wide. Requires admin privileges on first use");
table.format("vcpkg integrate remove", "Remove user-wide integration");
table.format("vcpkg integrate project", "Generate a referencing nuget package for individual VS project use");
table.format("vcpkg integrate powershell", "Enable PowerShell tab-completion");
#else // ^^^ defined(_WIN32) // !defined(_WIN32) vvv
table.format("vcpkg integrate install", "Make installed packages available user-wide");
table.format("vcpkg integrate remove", "Remove user-wide integration");
table.format("vcpkg integrate bash", "Enable bash tab-completion");
table.format("vcpkg integrate fish", "Enable fish tab-completion");
#endif // ^^^ !defined(_WIN32)
}
std::string get_helpstring()
{
HelpTableFormatter table;
append_helpstring(table);
return std::move(table.m_str);
}
namespace Subcommand
{
static const std::string INSTALL = "install";
static const std::string REMOVE = "remove";
static const std::string PROJECT = "project";
static const std::string POWERSHELL = "powershell";
static const std::string BASH = "bash";
static const std::string FISH = "x-fish";
}
static std::vector<std::string> valid_arguments(const VcpkgPaths&)
{
return
{
Subcommand::INSTALL, Subcommand::REMOVE,
#if defined(_WIN32)
Subcommand::PROJECT, Subcommand::POWERSHELL,
#else
Subcommand::BASH, Subcommand::FISH,
#endif
};
}
const CommandStructure COMMAND_STRUCTURE = {
"Commands:\n" + get_helpstring(),
1,
1,
{},
&valid_arguments,
};
void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths)
{
(void)args.parse_arguments(COMMAND_STRUCTURE);
if (args.command_arguments[0] == Subcommand::INSTALL)
{
return integrate_install(paths);
}
if (args.command_arguments[0] == Subcommand::REMOVE)
{
return integrate_remove(paths.get_filesystem());
}
#if defined(_WIN32)
if (args.command_arguments[0] == Subcommand::PROJECT)
{
return integrate_project(paths);
}
if (args.command_arguments[0] == Subcommand::POWERSHELL)
{
return integrate_powershell(paths);
}
#else
if (args.command_arguments[0] == Subcommand::BASH)
{
return integrate_bash(paths);
}
if (args.command_arguments[0] == Subcommand::FISH)
{
return integrate_fish(paths);
}
#endif
Checks::exit_with_message(VCPKG_LINE_INFO, "Unknown parameter %s for integrate", args.command_arguments[0]);
}
void IntegrateCommand::perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths) const
{
Integrate::perform_and_exit(args, paths);
}
}