From 0add6dcbe7a14cb3d44ebb26876eb144a8abd478 Mon Sep 17 00:00:00 2001 From: Rich Purnell Date: Sun, 7 Jun 2026 02:39:09 +0800 Subject: [PATCH 1/4] Remove invalid CreateSymlink overload declaration --- src/AppInstallerSharedLib/Public/winget/Filesystem.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index f450444234..a41706d24f 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -38,7 +38,6 @@ namespace AppInstaller::Filesystem // Checks if the path is a symlink and exists. bool SymlinkExists(const std::filesystem::path& symlinkPath); - bool CreateSymlink(const std::filesystem::path& path, const std::filesystem::path& target); // Get expanded file system path. std::filesystem::path GetExpandedPath(const std::string& path); From b8d7dfac869565353be20fbb07e179affbb3f291 Mon Sep 17 00:00:00 2001 From: Rich Purnell Date: Mon, 8 Jun 2026 19:29:57 +0800 Subject: [PATCH 2/4] Remove invalid if branches --- src/AppInstallerCLICore/PortableInstaller.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 1e3498e55a..a87fd3fbe3 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -194,11 +194,6 @@ namespace AppInstaller::CLI::Portable RemoveFromPathVariable(std::filesystem::path(Utility::ConvertToUTF16(entry.SymlinkTarget)).parent_path()); } } - else if (fileType == PortableFileType::Symlink && Filesystem::SymlinkExists(filePath)) - { - AICLI_LOG(CLI, Info, << "Deleting portable symlink at: " << filePath); - std::filesystem::remove(filePath); - } else if (fileType == PortableFileType::Directory && std::filesystem::exists(filePath)) { AICLI_LOG(CLI, Info, << "Removing directory at " << filePath); From f36771072fa3fa5ad64c9a0c6caf1ef06855ffb3 Mon Sep 17 00:00:00 2001 From: Rich Purnell Date: Fri, 12 Jun 2026 21:34:09 +0800 Subject: [PATCH 3/4] Improve symlink creation and PATH variable removal logic Remove InstallDirectoryAddedToPath check during symlink creation in installation. Symlink targets may reside in different directories, and the previous directory-empty check would always fail due to leftover *.db files, preventing proper environment variable cleanup. Add optional checkIfEmpty parameter (default: true) to RemoveFromPathVariable to control whether the target directory must exist and be empty before removing it from PATH. During uninstallation, when InstallDirectoryAddedToPath is true, skip the emptiness check and remove the environment variable unconditionally. --- src/AppInstallerCLICore/PortableInstaller.cpp | 11 +++++------ src/AppInstallerCLICore/PortableInstaller.h | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index a87fd3fbe3..5ba55f79e8 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -126,7 +126,7 @@ namespace AppInstaller::CLI::Portable { std::filesystem::path symlinkTargetPath{ Utility::ConvertToUTF16(entry.SymlinkTarget) }; - if (BinariesDependOnPath && !InstallDirectoryAddedToPath) + if (BinariesDependOnPath) { // Scenario indicated by 'ArchiveBinariesDependOnPath' manifest entry. // Skip symlink creation for portables dependent on binaries that require the install directory to be added to PATH. @@ -135,7 +135,7 @@ namespace AppInstaller::CLI::Portable AICLI_LOG(Core, Info, << "Install directory added to PATH: " << installDirectory); CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); } - else if (!InstallDirectoryAddedToPath) + else { std::filesystem::file_status status = std::filesystem::status(filePath); if (std::filesystem::is_directory(status)) @@ -191,7 +191,7 @@ namespace AppInstaller::CLI::Portable else if (InstallDirectoryAddedToPath) { // If symlink doesn't exist, check if install directory was added to PATH directly and remove. - RemoveFromPathVariable(std::filesystem::path(Utility::ConvertToUTF16(entry.SymlinkTarget)).parent_path()); + RemoveFromPathVariable(std::filesystem::path(Utility::ConvertToUTF16(entry.SymlinkTarget)).parent_path(), false); } } else if (fileType == PortableFileType::Directory && std::filesystem::exists(filePath)) @@ -370,9 +370,9 @@ namespace AppInstaller::CLI::Portable } } - void PortableInstaller::RemoveFromPathVariable(std::filesystem::path value) + void PortableInstaller::RemoveFromPathVariable(std::filesystem::path value, bool checkIfEmpty /*= true*/) { - if (std::filesystem::exists(value) && !std::filesystem::is_empty(value)) + if (checkIfEmpty && std::filesystem::exists(value) && !std::filesystem::is_empty(value)) { AICLI_LOG(Core, Info, << "Install directory is not empty: " << value); } @@ -382,7 +382,6 @@ namespace AppInstaller::CLI::Portable // Necessary for handling old path values associated with winget-cli#5033 if (PathVariable(GetScope()).Remove(value) || PathVariable(GetScope()).Remove(value.make_preferred())) { - InstallDirectoryAddedToPath = false; AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << value); } else diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index 9f282efcd0..162cebd15e 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -114,6 +114,6 @@ namespace AppInstaller::CLI::Portable void RemoveInstallDirectory(); void AddToPathVariable(std::filesystem::path value); - void RemoveFromPathVariable(std::filesystem::path value); + void RemoveFromPathVariable(std::filesystem::path value, bool checkIfEmpty = true); }; } From b6930d72627fd328efba9abfeda2ee548d98d3b1 Mon Sep 17 00:00:00 2001 From: Rich Purnell Date: Fri, 12 Jun 2026 22:29:29 +0800 Subject: [PATCH 4/4] Resolve alias creation failure with symlink/hardlink fallback Implement a graceful fallback strategy for creating and removing aliases to handle permission issues and cross-device link failures. When creating a symlink: Attempt to create a symlink first; exit on success. If it fails due to insufficient permissions, proceed to step 2; otherwise, throw an exception. Fall back to creating a hard link; exit on success, otherwise proceed to step 3. Enter InstallDirectoryAddedToPath mode and add symlinkTargetPath.parent_path() to the PATH environment variable. When removing a symlink: If the target is identified as a symlink, delete it; otherwise, proceed to step 2. If the file exists and is a regular file (treated as a hard link), delete it; otherwise, proceed to step 3. Enter InstallDirectoryAddedToPath mode and remove symlinkTargetPath.parent_path() from the PATH environment variable. --- src/AppInstallerCLICore/PortableInstaller.cpp | 73 +++++++++++-------- src/AppInstallerCLICore/PortableInstaller.h | 4 + src/AppInstallerCLITests/InstallFlow.cpp | 58 +++++++++++++-- src/AppInstallerCLITests/TestHooks.h | 17 +++++ src/AppInstallerCLITests/UpdateFlow.cpp | 53 ++++++++++++-- src/AppInstallerSharedLib/Filesystem.cpp | 28 +++++++ .../Public/winget/Filesystem.h | 3 + 7 files changed, 192 insertions(+), 44 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 5ba55f79e8..e11d96483a 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -157,8 +157,14 @@ namespace AppInstaller::CLI::Portable if (Filesystem::CreateSymlink(symlinkTargetPath, filePath)) { + m_hasCreatedSymlink = true; AICLI_LOG(Core, Info, << "Symlink created at: " << filePath << " with target path: " << symlinkTargetPath); } + else if (Filesystem::CreateFileHardLink(symlinkTargetPath, filePath)) + { + m_hasCreatedSymlink = true; + AICLI_LOG(Core, Info, << "Hardlink created at: " << filePath << " with target path: " << symlinkTargetPath); + } else { // If symlink creation fails, resort to adding the package directory to PATH. @@ -187,8 +193,15 @@ namespace AppInstaller::CLI::Portable { AICLI_LOG(CLI, Info, << "Deleting portable symlink at: " << filePath); std::filesystem::remove(filePath); + m_hasRemovedSymlink = true; + } + else if (std::filesystem::exists(filePath) && std::filesystem::is_regular_file(std::filesystem::status(filePath))) + { + AICLI_LOG(CLI, Info, << "Deleting portable hard link at: " << filePath); + std::filesystem::remove(filePath); + m_hasRemovedSymlink = true; } - else if (InstallDirectoryAddedToPath) + else { // If symlink doesn't exist, check if install directory was added to PATH directly and remove. RemoveFromPathVariable(std::filesystem::path(Utility::ConvertToUTF16(entry.SymlinkTarget)).parent_path(), false); @@ -289,7 +302,7 @@ namespace AppInstaller::CLI::Portable ApplyDesiredState(); - if (!InstallDirectoryAddedToPath) + if (m_hasCreatedSymlink) { AddToPathVariable(GetPortableLinksLocation(GetScope())); } @@ -302,20 +315,20 @@ namespace AppInstaller::CLI::Portable } } - void PortableInstaller::Uninstall() - { - ApplyDesiredState(); + void PortableInstaller::Uninstall() + { + ApplyDesiredState(); - RemoveInstallDirectory(); + RemoveInstallDirectory(); - if (!InstallDirectoryAddedToPath) - { - RemoveFromPathVariable(GetPortableLinksLocation(GetScope())); - } + if (m_hasRemovedSymlink) + { + RemoveFromPathVariable(GetPortableLinksLocation(GetScope())); + } - m_portableARPEntry.Delete(); - AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); - } + m_portableARPEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + } void PortableInstaller::CreateTargetInstallDirectory() { @@ -371,25 +384,25 @@ namespace AppInstaller::CLI::Portable } void PortableInstaller::RemoveFromPathVariable(std::filesystem::path value, bool checkIfEmpty /*= true*/) - { + { if (checkIfEmpty && std::filesystem::exists(value) && !std::filesystem::is_empty(value)) - { - AICLI_LOG(Core, Info, << "Install directory is not empty: " << value); - } - else - { + { + AICLI_LOG(Core, Info, << "Install directory is not empty: " << value); + } + else + { // Attempt to remove both the original and the preferred format to ensure removal - // Necessary for handling old path values associated with winget-cli#5033 - if (PathVariable(GetScope()).Remove(value) || PathVariable(GetScope()).Remove(value.make_preferred())) - { - AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << value); - } - else - { - AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << value); - } - } - } + // Necessary for handling old path values associated with winget-cli#5033 + if (PathVariable(GetScope()).Remove(value) || PathVariable(GetScope()).Remove(value.make_preferred())) + { + AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << value); + } + else + { + AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << value); + } + } + } void PortableInstaller::SetAppsAndFeaturesMetadata(const Manifest::Manifest& manifest, const std::vector& entries) { diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index 162cebd15e..c66767b91d 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -35,6 +35,7 @@ namespace AppInstaller::CLI::Portable bool InstallDirectoryCreated = false; bool BinariesDependOnPath = false; // If we fail to create a symlink, add install directory to PATH variable + // TODO: Variable is redundant, remove it. bool InstallDirectoryAddedToPath = false; bool IsUpdate = false; @@ -94,6 +95,9 @@ namespace AppInstaller::CLI::Portable AppInstaller::Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntry(); private: + // True if any symlink creation succeeds. No persistence required. + bool m_hasCreatedSymlink = false; + bool m_hasRemovedSymlink = false; PortableARPEntry m_portableARPEntry; std::vector m_desiredEntries; std::vector m_expectedEntries; diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index 64fb767bbc..21fa91ecb8 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -731,7 +731,7 @@ TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]") TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); std::ostringstream installOutput; TestContext installContext{ installOutput, std::cin }; - auto PreviousThreadGlobals = installContext.SetForCurrentThread(); + installContext.SetForCurrentThread(); OverridePortableInstaller(installContext); TestHook::SetCreateSymlinkResult_Override createSymlinkResultOverride(false); const auto& targetDirectory = tempDirectory.GetPath(); @@ -743,18 +743,22 @@ TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]") InstallCommand install({}); install.Execute(installContext); - { - INFO(installOutput.str()); + AppInstaller::CLI::Portable::PortableInstaller& portableInstaller = installContext.Get(); + std::filesystem::path portableLinksLocation = AppInstaller::CLI::Portable::GetPortableLinksLocation(portableInstaller.GetScope()); + std::filesystem::path symlinkFile = portableLinksLocation / "AppInstallerTestExeInstaller.exe"; - // Use CHECK to allow the uninstall to still occur - CHECK(std::filesystem::exists(portableTargetPath)); - CHECK(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory)); - } + INFO(installOutput.str()); + + // Use CHECK to allow the uninstall to still occur + CHECK(std::filesystem::exists(portableTargetPath)); + + CHECK(std::filesystem::exists(symlinkFile)); + CHECK(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableLinksLocation)); // Perform uninstall std::ostringstream uninstallOutput; TestContext uninstallContext{ uninstallOutput, std::cin }; - auto previousThreadGlobals = uninstallContext.SetForCurrentThread(); + uninstallContext.SetForCurrentThread(); uninstallContext.Args.AddArg(Execution::Args::Type::Name, "AppInstaller Test Portable Exe"sv); uninstallContext.Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); @@ -762,6 +766,44 @@ TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]") uninstall.Execute(uninstallContext); INFO(uninstallOutput.str()); REQUIRE_FALSE(std::filesystem::exists(portableTargetPath)); + REQUIRE_FALSE(std::filesystem::exists(symlinkFile)); +} + +TEST_CASE("InstallFlow_Portable_HardLinkCreationFail", "[InstallFlow][workflow]") +{ + TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); + std::ostringstream installOutput; + TestContext installContext{ installOutput, std::cin }; + installContext.SetForCurrentThread(); + OverridePortableInstaller(installContext); + TestHook::SetCreateSymlinkResult_Override createSymlinkResultOverride(false); + TestHook::SetCreateHardLinkResult_Override createHardLinkResultOverride(false); + const auto& targetDirectory = tempDirectory.GetPath(); + const auto& portableTargetPath = targetDirectory / "AppInstallerTestExeInstaller.exe"; + installContext.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Portable.yaml").GetPath().u8string()); + installContext.Args.AddArg(Execution::Args::Type::InstallLocation, targetDirectory.u8string()); + installContext.Args.AddArg(Execution::Args::Type::InstallScope, "user"sv); + + InstallCommand install({}); + install.Execute(installContext); + + INFO(installOutput.str()); + + // Use CHECK to allow the uninstall to still occur + CHECK(std::filesystem::exists(portableTargetPath)); + CHECK(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory)); + + // Perform uninstall + std::ostringstream uninstallOutput; + TestContext uninstallContext{ uninstallOutput, std::cin }; + uninstallContext.SetForCurrentThread(); + uninstallContext.Args.AddArg(Execution::Args::Type::Name, "AppInstaller Test Portable Exe"sv); + uninstallContext.Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); + + UninstallCommand uninstall({}); + uninstall.Execute(uninstallContext); + INFO(uninstallOutput.str()); + REQUIRE_FALSE(std::filesystem::exists(portableTargetPath)); } TEST_CASE("PortableInstallFlow_UserScope", "[InstallFlow][workflow]") diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index 8c976a1381..fde2c7ba60 100644 --- a/src/AppInstallerCLITests/TestHooks.h +++ b/src/AppInstallerCLITests/TestHooks.h @@ -68,6 +68,7 @@ namespace AppInstaller namespace Filesystem { void TestHook_SetCreateSymlinkResult_Override(bool* status); + void TestHook_SetCreateHardLinkResult_Override(bool* status); } namespace Archive @@ -138,6 +139,22 @@ namespace TestHook bool m_status; }; + struct SetCreateHardLinkResult_Override + { + SetCreateHardLinkResult_Override(bool status) : m_status(status) + { + AppInstaller::Filesystem::TestHook_SetCreateHardLinkResult_Override(&m_status); + } + + ~SetCreateHardLinkResult_Override() + { + AppInstaller::Filesystem::TestHook_SetCreateHardLinkResult_Override(nullptr); + } + + private: + bool m_status; + }; + struct SetScanArchiveResult_Override { SetScanArchiveResult_Override(bool status) : m_status(status) diff --git a/src/AppInstallerCLITests/UpdateFlow.cpp b/src/AppInstallerCLITests/UpdateFlow.cpp index 9d70283c78..c2ee003050 100644 --- a/src/AppInstallerCLITests/UpdateFlow.cpp +++ b/src/AppInstallerCLITests/UpdateFlow.cpp @@ -184,9 +184,49 @@ TEST_CASE("UpdateFlow_Portable_SymlinkCreationFail", "[UpdateFlow][workflow]") TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); std::ostringstream updateOutput; TestContext context{ updateOutput, std::cin }; - auto PreviousThreadGlobals = context.SetForCurrentThread(); - bool overrideCreateSymlinkStatus = false; - AppInstaller::Filesystem::TestHook_SetCreateSymlinkResult_Override(&overrideCreateSymlinkStatus); + context.SetForCurrentThread(); + TestHook::SetCreateSymlinkResult_Override createSymlinkResultOverride(false); + OverridePortableInstaller(context); + OverrideForCompositeInstalledSource(context, CreateTestSource({ TSR::TestInstaller_Portable })); + const auto& targetDirectory = tempDirectory.GetPath(); + context.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Portable.Query); + context.Args.AddArg(Execution::Args::Type::InstallLocation, targetDirectory.u8string()); + context.Args.AddArg(Execution::Args::Type::InstallScope, "user"sv); + + UpgradeCommand update({}); + update.Execute(context); + + // Get the PortableInstaller to access the scope information + AppInstaller::CLI::Portable::PortableInstaller& portableInstaller = context.Get(); + std::filesystem::path portableLinksLocation = AppInstaller::CLI::Portable::GetPortableLinksLocation(portableInstaller.GetScope()); + std::filesystem::path symlinkFile = portableLinksLocation / "AppInstallerTestExeInstaller.exe"; + + INFO(updateOutput.str()); + CHECK(std::filesystem::exists(symlinkFile)); + CHECK(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableLinksLocation)); + + // Perform uninstall + std::ostringstream uninstallOutput; + TestContext uninstallContext{ uninstallOutput, std::cin }; + uninstallContext.SetForCurrentThread(); + OverrideForCompositeInstalledSource(uninstallContext, CreateTestSource({ TSR::TestInstaller_Portable })); + uninstallContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Portable.Query); + + UninstallCommand uninstall({}); + uninstall.Execute(uninstallContext); + INFO(uninstallOutput.str()); + + REQUIRE_FALSE(std::filesystem::exists(symlinkFile)); +} + +TEST_CASE("UpdateFlow_Portable_HardLinkCreationFail", "[UpdateFlow][workflow]") +{ + TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + context.SetForCurrentThread(); + TestHook::SetCreateSymlinkResult_Override createSymlinkResultOverride(false); + TestHook::SetCreateHardLinkResult_Override createHardLinkResultOverride(false); OverridePortableInstaller(context); OverrideForCompositeInstalledSource(context, CreateTestSource({ TSR::TestInstaller_Portable })); const auto& targetDirectory = tempDirectory.GetPath(); @@ -198,13 +238,13 @@ TEST_CASE("UpdateFlow_Portable_SymlinkCreationFail", "[UpdateFlow][workflow]") update.Execute(context); INFO(updateOutput.str()); const auto& portableTargetPath = targetDirectory / "AppInstallerTestExeInstaller.exe"; - REQUIRE(std::filesystem::exists(portableTargetPath)); - REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory)); + CHECK(std::filesystem::exists(portableTargetPath)); + CHECK(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory)); // Perform uninstall std::ostringstream uninstallOutput; TestContext uninstallContext{ uninstallOutput, std::cin }; - auto previousThreadGlobals = uninstallContext.SetForCurrentThread(); + uninstallContext.SetForCurrentThread(); OverrideForCompositeInstalledSource(uninstallContext, CreateTestSource({ TSR::TestInstaller_Portable })); uninstallContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Portable.Query); @@ -213,6 +253,7 @@ TEST_CASE("UpdateFlow_Portable_SymlinkCreationFail", "[UpdateFlow][workflow]") INFO(uninstallOutput.str()); REQUIRE_FALSE(std::filesystem::exists(portableTargetPath)); + REQUIRE_FALSE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory)); } TEST_CASE("UpdateFlow_UpdateExeWithUnsupportedArgs", "[UpdateFlow][workflow]") diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp index 85acbf09a3..5cdb5c0106 100644 --- a/src/AppInstallerSharedLib/Filesystem.cpp +++ b/src/AppInstallerSharedLib/Filesystem.cpp @@ -474,6 +474,34 @@ namespace AppInstaller::Filesystem } } +#ifndef AICLI_DISABLE_TEST_HOOKS + static bool* s_CreateHardLinkResult_TestHook_Override = nullptr; + + void TestHook_SetCreateHardLinkResult_Override(bool* status) + { + s_CreateHardLinkResult_TestHook_Override = status; + } +#endif + + bool CreateFileHardLink(const std::filesystem::path& target, const std::filesystem::path& link) + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_CreateHardLinkResult_TestHook_Override) + { + return *s_CreateHardLinkResult_TestHook_Override; + } +#endif + try + { + std::filesystem::create_hard_link(target, link); + return true; + } + catch (...) + { + return false; + } + } + bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target) { // Use read_symlink to get the symlink's recorded target without traversing the filesystem diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index a41706d24f..01dc7ecf2e 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -30,6 +30,9 @@ namespace AppInstaller::Filesystem // Creates a symlink that points to the target path. bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link); + // Creates a hard link that points to the target path. + bool CreateFileHardLink(const std::filesystem::path& target, const std::filesystem::path& link); + // Verifies that a symlink points to the target path. bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target);