Installer: Add Windows uninstaller program

pull/3696/head
Stenzek 5 months ago
parent 22c8d91109
commit 4756b17899
No known key found for this signature in database

@ -59,6 +59,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "util-tests", "src\util-test
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "installer", "src\installer\installer.vcxproj", "{CD6D0C84-042E-4C9A-AAB5-D3BDC80273DA}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "uninstaller", "src\uninstaller\uninstaller.vcxproj", "{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@ -1004,6 +1006,31 @@ Global
{CD6D0C84-042E-4C9A-AAB5-D3BDC80273DA}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64
{CD6D0C84-042E-4C9A-AAB5-D3BDC80273DA}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64
{CD6D0C84-042E-4C9A-AAB5-D3BDC80273DA}.ReleaseLTCG-Clang-SSE2|x64.Build.0 = ReleaseLTCG-Clang-SSE2|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Debug|ARM64.ActiveCfg = Debug-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Debug|x64.ActiveCfg = Debug|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Debug-Clang|ARM64.ActiveCfg = Debug-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Debug-Clang|x64.ActiveCfg = Debug-Clang|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Debug-Clang-SSE2|ARM64.ActiveCfg = Debug-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Debug-Clang-SSE2|x64.ActiveCfg = Debug-Clang-SSE2|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.DebugFast|ARM64.ActiveCfg = DebugFast-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.DebugFast|x64.ActiveCfg = DebugFast|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.DebugFast-Clang|ARM64.ActiveCfg = DebugFast-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.DebugFast-Clang|x64.ActiveCfg = DebugFast-Clang|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Devel-Clang|ARM64.ActiveCfg = Devel-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Devel-Clang|x64.ActiveCfg = Devel-Clang|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Release|ARM64.ActiveCfg = Release-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Release|x64.ActiveCfg = Release|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Release-Clang|ARM64.ActiveCfg = Release-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.Release-Clang|x64.ActiveCfg = Release-Clang|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG-Clang|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG-Clang|ARM64.Build.0 = ReleaseLTCG-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG-Clang|x64.ActiveCfg = ReleaseLTCG-Clang|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG-Clang|x64.Build.0 = ReleaseLTCG-Clang|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64
{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}.ReleaseLTCG-Clang-SSE2|x64.Build.0 = ReleaseLTCG-Clang-SSE2|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

@ -9,6 +9,7 @@ endif()
if(WIN32)
add_subdirectory(installer)
add_subdirectory(uninstaller)
endif()
if(BUILD_QT_FRONTEND)

@ -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,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\dep\msvc\vsprops\Configurations.props" />
<ItemGroup>
<ClCompile Include="main.cpp" />
</ItemGroup>
<ItemGroup>
<Manifest Include="uninstaller.manifest" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\common\common.vcxproj">
<Project>{ee054e08-3799-4a59-a422-18259c105ffd}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="uninstaller.rc" />
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{91C19063-A8D7-421C-A0E3-A1A3ECFC5EE5}</ProjectGuid>
</PropertyGroup>
<Import Project="..\..\dep\msvc\vsprops\Win32Application.props" />
<Import Project="..\common\common.props" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)dep\minizip\include;$(SolutionDir)src</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>%(AdditionalDependencies);Comctl32.lib;zlibstatic.lib</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<Import Project="..\..\dep\msvc\vsprops\Targets.props" />
</Project>

@ -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…
Cancel
Save