mirror of https://github.com/stenzek/duckstation
Installer: Add Windows uninstaller program
parent
22c8d91109
commit
4756b17899
@ -0,0 +1,11 @@
|
||||
add_executable(uninstaller
|
||||
main.cpp
|
||||
resource.h
|
||||
uninstaller.manifest
|
||||
uninstaller.rc
|
||||
)
|
||||
|
||||
target_include_directories(uninstaller PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||
target_link_libraries(uninstaller PRIVATE common fmt)
|
||||
target_link_libraries(uninstaller PRIVATE "Comctl32.lib")
|
||||
set_target_properties(uninstaller PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
@ -0,0 +1,357 @@
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "common/error.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/path.h"
|
||||
#include "common/scoped_guard.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/windows_headers.h"
|
||||
|
||||
#include "installer/installer_params.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <KnownFolders.h>
|
||||
#include <ShlObj.h>
|
||||
#include <Shobjidl.h>
|
||||
#include <combaseapi.h>
|
||||
#include <shellapi.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
static constexpr const wchar_t* WINDOW_TITLE = L"DuckStation Uninstaller";
|
||||
|
||||
template<typename... T>
|
||||
static int FormatMessageBox(HWND hwnd, UINT type, fmt::format_string<T...> fmt, T&&... args)
|
||||
{
|
||||
const std::string message = fmt::format(fmt, std::forward<T>(args)...);
|
||||
return MessageBoxW(hwnd, StringUtil::UTF8StringToWideString(message).c_str(), WINDOW_TITLE, type);
|
||||
}
|
||||
|
||||
static std::string GetTempDirectory()
|
||||
{
|
||||
wchar_t temp_path[MAX_PATH];
|
||||
if (GetTempPathW(MAX_PATH, temp_path) == 0)
|
||||
return {};
|
||||
|
||||
return StringUtil::WideStringToUTF8String(temp_path);
|
||||
}
|
||||
|
||||
static bool RecursiveDeleteDirectory(const std::string& path)
|
||||
{
|
||||
if (!FileSystem::DirectoryExists(path.c_str()))
|
||||
return true;
|
||||
|
||||
Microsoft::WRL::ComPtr<IFileOperation> fo;
|
||||
HRESULT hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(fo.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "CoCreateInstance() for IFileOperation failed:\n{}",
|
||||
Error::CreateHResult(hr).GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IShellItem> item;
|
||||
hr = SHCreateItemFromParsingName(StringUtil::UTF8StringToWideString(path).c_str(), NULL,
|
||||
IID_PPV_ARGS(item.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "SHCreateItemFromParsingName() failed:\n{}",
|
||||
Error::CreateHResult(hr).GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = fo->SetOperationFlags(FOF_NOCONFIRMATION | FOF_SILENT);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONWARNING | MB_OK, "IFileOperation::SetOperationFlags() failed: {}",
|
||||
Error::CreateHResult(hr).GetDescription());
|
||||
}
|
||||
|
||||
hr = fo->DeleteItem(item.Get(), nullptr);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "IFileOperation::DeleteItem() failed:\n{}",
|
||||
Error::CreateHResult(hr).GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
item.Reset();
|
||||
hr = fo->PerformOperations();
|
||||
if (FAILED(hr))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "IFileOperation::PerformOperations() failed:\n{}",
|
||||
Error::CreateHResult(hr).GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool RemoveDesktopShortcut()
|
||||
{
|
||||
PWSTR desktop_folder = nullptr;
|
||||
HRESULT hr = SHGetKnownFolderPath(FOLDERID_Desktop, 0, nullptr, &desktop_folder);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "SHGetKnownFolderPath(FOLDERID_Desktop) failed: {}",
|
||||
Error::CreateHResult(hr).GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string shortcut_path =
|
||||
Path::Combine(StringUtil::WideStringToUTF8String(desktop_folder), INSTALLER_SHORTCUT_FILENAME);
|
||||
CoTaskMemFree(desktop_folder);
|
||||
|
||||
if (!FileSystem::FileExists(shortcut_path.c_str()))
|
||||
return true;
|
||||
|
||||
Error error;
|
||||
if (!FileSystem::DeleteFile(shortcut_path.c_str(), &error))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to delete desktop shortcut: {}", error.GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool RemoveStartMenuShortcut()
|
||||
{
|
||||
PWSTR programs_folder = nullptr;
|
||||
HRESULT hr = SHGetKnownFolderPath(FOLDERID_Programs, 0, nullptr, &programs_folder);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "SHGetKnownFolderPath(FOLDERID_Programs) failed: {}",
|
||||
Error::CreateHResult(hr).GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string shortcut_path =
|
||||
Path::Combine(StringUtil::WideStringToUTF8String(programs_folder), INSTALLER_SHORTCUT_FILENAME);
|
||||
CoTaskMemFree(programs_folder);
|
||||
|
||||
if (!FileSystem::FileExists(shortcut_path.c_str()))
|
||||
return true;
|
||||
|
||||
Error error;
|
||||
if (!FileSystem::DeleteFile(shortcut_path.c_str(), &error))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to delete Start Menu shortcut: ", error.GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool RemoveUninstallerEntry()
|
||||
{
|
||||
const LSTATUS status = RegDeleteTreeW(HKEY_CURRENT_USER, INSTALLER_UNINSTALL_REG_KEY);
|
||||
if (status != ERROR_SUCCESS && status != ERROR_FILE_NOT_FOUND)
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to delete uninstaller registry key: {}",
|
||||
Error::CreateWin32(status).GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string GetUserDataDirectory()
|
||||
{
|
||||
// Check LocalAppData\DuckStation (new location)
|
||||
PWSTR appdata_directory = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &appdata_directory)))
|
||||
{
|
||||
if (std::wcslen(appdata_directory) > 0)
|
||||
{
|
||||
std::string path = Path::Combine(StringUtil::WideStringToUTF8String(appdata_directory), "DuckStation");
|
||||
CoTaskMemFree(appdata_directory);
|
||||
if (FileSystem::DirectoryExists(path.c_str()))
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
CoTaskMemFree(appdata_directory);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool PerformUninstall(const std::string& install_directory)
|
||||
{
|
||||
RemoveDesktopShortcut();
|
||||
RemoveStartMenuShortcut();
|
||||
RemoveUninstallerEntry();
|
||||
|
||||
// Remove the installation directory
|
||||
if (!RecursiveDeleteDirectory(install_directory))
|
||||
return false;
|
||||
|
||||
// Check if user wants to remove user data
|
||||
const std::string user_data_dir = GetUserDataDirectory();
|
||||
if (!user_data_dir.empty() && FileSystem::DirectoryExists(user_data_dir.c_str()))
|
||||
{
|
||||
if (FormatMessageBox(nullptr, MB_ICONWARNING | MB_YESNO,
|
||||
"Do you want to remove user data (save games, memory cards, settings)?\n\n"
|
||||
"Location: {}\n\n"
|
||||
"WARNING: This data cannot be recovered once deleted!",
|
||||
user_data_dir) == IDYES)
|
||||
{
|
||||
RecursiveDeleteDirectory(user_data_dir);
|
||||
}
|
||||
}
|
||||
|
||||
MessageBoxW(nullptr, L"DuckStation has been uninstalled.", WINDOW_TITLE, MB_ICONINFORMATION | MB_OK);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool CopyToTempAndRelaunch(const std::string& current_exe_path, const std::string_view& install_directory)
|
||||
{
|
||||
const std::string temp_dir = GetTempDirectory();
|
||||
if (temp_dir.empty())
|
||||
{
|
||||
MessageBoxW(nullptr, L"Failed to get temporary directory.", WINDOW_TITLE, MB_ICONERROR | MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string temp_exe_path = Path::Combine(temp_dir, "duckstation_uninstall.exe");
|
||||
|
||||
Error error;
|
||||
if (!FileSystem::CopyFilePath(current_exe_path.c_str(), temp_exe_path.c_str(), true, &error))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to copy uninstaller to temporary location:\n{}",
|
||||
error.GetDescription());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quote the install directory in case it contains spaces
|
||||
const std::wstring args_w = StringUtil::UTF8StringToWideString(fmt::format("\"{}\"", install_directory));
|
||||
const std::wstring temp_exe_path_w = StringUtil::UTF8StringToWideString(temp_exe_path);
|
||||
|
||||
SHELLEXECUTEINFOW sei = {};
|
||||
sei.cbSize = sizeof(sei);
|
||||
sei.fMask = SEE_MASK_DEFAULT;
|
||||
sei.lpFile = temp_exe_path_w.c_str();
|
||||
sei.lpParameters = args_w.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
|
||||
if (!ShellExecuteExW(&sei))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to launch uninstaller from temporary location:\n{}",
|
||||
Error::CreateWin32(GetLastError()).GetDescription());
|
||||
FileSystem::DeleteFile(temp_exe_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int RunFromInstallDirectory()
|
||||
{
|
||||
Error error;
|
||||
const std::string current_exe_path = FileSystem::GetProgramPath(&error);
|
||||
if (current_exe_path.empty())
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to get current executable path:\n{}",
|
||||
error.GetDescription());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const std::string_view install_directory = Path::GetDirectory(current_exe_path);
|
||||
|
||||
// Verify this is a DuckStation installation by checking for the main executable
|
||||
const std::string main_exe_path = Path::Combine(install_directory, INSTALLER_PROGRAM_FILENAME);
|
||||
if (!FileSystem::FileExists(main_exe_path.c_str()))
|
||||
{
|
||||
MessageBoxW(nullptr,
|
||||
L"This does not appear to be a valid DuckStation installation.\n\n"
|
||||
L"The main executable was not found in the same directory as the uninstaller.",
|
||||
WINDOW_TITLE, MB_ICONERROR | MB_OK);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Confirm uninstallation with user
|
||||
if (FormatMessageBox(nullptr, MB_ICONQUESTION | MB_YESNO,
|
||||
"This will uninstall DuckStation from:\n\n{}\n\n"
|
||||
"Do you want to continue?",
|
||||
install_directory) != IDYES)
|
||||
{
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Copy to temp and relaunch
|
||||
if (!CopyToTempAndRelaunch(current_exe_path, install_directory))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
static int RunFromTemp(const std::string& install_directory)
|
||||
{
|
||||
const bool success = PerformUninstall(install_directory);
|
||||
|
||||
#if 0
|
||||
// Schedule ourselves for deletion on reboot
|
||||
const std::string current_exe_path = FileSystem::GetProgramPath(nullptr);
|
||||
if (!current_exe_path.empty())
|
||||
{
|
||||
const std::wstring current_exe_path_w = FileSystem::GetWin32Path(current_exe_path);
|
||||
if (!current_exe_path_w.empty())
|
||||
{
|
||||
// TODO: This requires admin rights. Find a better way.
|
||||
if (!MoveFileExW(current_exe_path_w.c_str(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT))
|
||||
{
|
||||
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to schedule temp uninstaller for deletion: {}",
|
||||
Error::CreateWin32(GetLastError()).GetDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
|
||||
{
|
||||
// IFileOperation requires single-threaded apartment mode
|
||||
const bool com_initialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED));
|
||||
const ScopedGuard com_guard = [com_initialized]() {
|
||||
if (com_initialized)
|
||||
CoUninitialize();
|
||||
};
|
||||
|
||||
// Parse command line - we only accept zero or one argument
|
||||
int argc = 0;
|
||||
LPWSTR* argv = nullptr;
|
||||
if (lpCmdLine && std::wcslen(lpCmdLine) > 0 && !(argv = CommandLineToArgvW(lpCmdLine, &argc)))
|
||||
{
|
||||
MessageBoxW(nullptr, L"Failed to parse command line.", WINDOW_TITLE, MB_ICONERROR | MB_OK);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const ScopedGuard argv_guard = [argv]() {
|
||||
if (argv)
|
||||
LocalFree(argv);
|
||||
};
|
||||
|
||||
// If no arguments, we're running from the install directory - copy to temp and relaunch
|
||||
if (argc < 1 || (argc == 1 && std::wcslen(argv[0]) == 0))
|
||||
return RunFromInstallDirectory();
|
||||
|
||||
// App uninstaller sets argv[0] to the program name.
|
||||
if (argc == 1)
|
||||
{
|
||||
const std::string install_directory = StringUtil::WideStringToUTF8String(argv[0]);
|
||||
if (FileSystem::DirectoryExists(install_directory.c_str()))
|
||||
return RunFromTemp(install_directory);
|
||||
else
|
||||
return RunFromInstallDirectory();
|
||||
}
|
||||
|
||||
// Too many arguments
|
||||
MessageBoxW(nullptr, L"Invalid command line arguments.\n\nUsage: uninstaller.exe [install_directory]", WINDOW_TITLE,
|
||||
MB_ICONERROR | MB_OK);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by duckstation-qt.rc
|
||||
//
|
||||
#define IDI_ICON1 102
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 103
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="com.github.stenzek.duckstation.uninstaller"
|
||||
type="win32"
|
||||
/>
|
||||
<description>DuckStation Uninstaller</description>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
@ -0,0 +1,110 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "winres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (Australia) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS
|
||||
#pragma code_page(1252)
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,0,0
|
||||
PRODUCTVERSION 1,0,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x1L
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "0c0904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "https://github.com/stenzek/duckstation"
|
||||
VALUE "FileDescription", "DuckStation Uninstaller"
|
||||
VALUE "FileVersion", "1.0.0.0"
|
||||
VALUE "InternalName", "uninstaller.exe"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2020-2026 Stenzek"
|
||||
VALUE "OriginalFilename", "uninstaller.exe"
|
||||
VALUE "ProductName", "DuckStation"
|
||||
VALUE "ProductVersion", "1.0.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0xc09, 1200
|
||||
END
|
||||
END
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_ICON1 ICON "..\\duckstation-qt\\duckstation-qt.ico"
|
||||
|
||||
#endif // English (Australia) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="uninstaller.manifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="uninstaller.rc" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Loading…
Reference in New Issue