winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 1b0c45fbe9c822808db85fa1d6b43564db9a11c6
parent e6c9359e7ea53a85c005b9e597ca1bdaa668f0fb
Author: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com>
Date:   Thu, 16 Mar 2023 00:15:01 -0700

Fix PATH behavior of non-symlink installations for Portables/Zip (#3002)


Diffstat:
Msrc/AppInstallerCLICore/PortableInstaller.cpp | 63+++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/AppInstallerCLICore/PortableInstaller.h | 9++-------
Msrc/AppInstallerCLICore/Workflows/PortableFlow.cpp | 7++-----
Msrc/AppInstallerCLITests/InstallFlow.cpp | 12+++++++-----
Msrc/AppInstallerCLITests/UpdateFlow.cpp | 10++++++----
5 files changed, 58 insertions(+), 43 deletions(-)

diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -147,9 +147,9 @@ namespace AppInstaller::CLI::Portable } else { - // Symlink creation should only fail if the user executes without admin rights or developer mode. - // Resort to adding install directory to PATH directly. - AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH."); + // If symlink creation fails, resort to adding the package directory to PATH. + AICLI_LOG(Core, Info, << "Failed to create symlink at: " << filePath); + AddToPathVariable(std::filesystem::path(entry.SymlinkTarget).parent_path()); CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); } } @@ -167,6 +167,19 @@ namespace AppInstaller::CLI::Portable AICLI_LOG(CLI, Info, << "Deleting portable exe at: " << filePath); std::filesystem::remove(filePath); } + else if (fileType == PortableFileType::Symlink) + { + if (Filesystem::SymlinkExists(filePath)) + { + AICLI_LOG(CLI, Info, << "Deleting portable symlink at: " << filePath); + std::filesystem::remove(filePath); + } + else if (InstallDirectoryAddedToPath) + { + // If symlink doesn't exist, check if install directory was added to PATH directly and remove. + RemoveFromPathVariable(std::filesystem::path(entry.SymlinkTarget).parent_path()); + } + } else if (fileType == PortableFileType::Symlink && Filesystem::SymlinkExists(filePath)) { AICLI_LOG(CLI, Info, << "Deleting portable symlink at: " << filePath); @@ -188,7 +201,7 @@ namespace AppInstaller::CLI::Portable bool deleteIndex = false; { PortableIndex existingIndex = PortableIndex::Open(existingIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - + for (auto expectedEntry : m_expectedEntries) { RemoveFile(expectedEntry); @@ -262,7 +275,10 @@ namespace AppInstaller::CLI::Portable ApplyDesiredState(); - AddToPathVariable(); + if (!InstallDirectoryAddedToPath) + { + AddToPathVariable(GetPortableLinksLocation(GetScope())); + } } void PortableInstaller::Uninstall() @@ -271,7 +287,10 @@ namespace AppInstaller::CLI::Portable RemoveInstallDirectory(); - RemoveFromPathVariable(); + if (!InstallDirectoryAddedToPath) + { + RemoveFromPathVariable(GetPortableLinksLocation(GetScope())); + } m_portableARPEntry.Delete(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); @@ -314,36 +333,34 @@ namespace AppInstaller::CLI::Portable } } - void PortableInstaller::AddToPathVariable() + void PortableInstaller::AddToPathVariable(const std::filesystem::path& value) { - const std::filesystem::path& pathValue = InstallDirectoryAddedToPath ? TargetInstallLocation : GetPortableLinksLocation(GetScope()); - if (PathVariable(GetScope()).Append(pathValue)) + if (PathVariable(GetScope()).Append(value)) { - AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); + AICLI_LOG(Core, Info, << "Appending portable target directory to PATH registry: " << value); m_stream << Resource::String::ModifiedPathRequiresShellRestart << std::endl; } else { - AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); + AICLI_LOG(CLI, Info, << "Portable target directory already exists in PATH registry: " << value); } } - void PortableInstaller::RemoveFromPathVariable() + void PortableInstaller::RemoveFromPathVariable(const std::filesystem::path& value) { - const std::filesystem::path& pathValue = InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); - if (std::filesystem::exists(pathValue) && !std::filesystem::is_empty(pathValue)) + if (std::filesystem::exists(value) && !std::filesystem::is_empty(value)) { - AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue); + AICLI_LOG(Core, Info, << "Install directory is not empty: " << value); } else { - if (PathVariable(GetScope()).Remove(pathValue)) + if (PathVariable(GetScope()).Remove(value)) { - AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); + AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << value); } else { - AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); + AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << value); } } } @@ -391,14 +408,16 @@ namespace AppInstaller::CLI::Portable std::filesystem::path targetFullPath = PortableTargetFullPath; std::filesystem::path symlinkFullPath = PortableSymlinkFullPath; - if (!symlinkFullPath.empty()) + // Order matters here so that file entries are removed before symlink entries during uninstall from registry. + // This is to ensure that the directory is fully uninstalled before attempting to remove from PATH registry. + if (!targetFullPath.empty()) { - m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); + m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256))); } - if (!targetFullPath.empty()) + if (!symlinkFullPath.empty()) { - m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256))); + m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); } } } diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h @@ -67,11 +67,6 @@ namespace AppInstaller::CLI::Portable m_portableARPEntry.SetValue(valueName, value); } - std::filesystem::path GetInstallDirectoryForPathVariable() - { - return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); - } - std::filesystem::path GetPortableIndexFileName() { return Utility::ConvertToUTF16(GetProductCode() + ".db"); @@ -114,7 +109,7 @@ namespace AppInstaller::CLI::Portable void CreateTargetInstallDirectory(); void RemoveInstallDirectory(); - void AddToPathVariable(); - void RemoveFromPathVariable(); + void AddToPathVariable(const std::filesystem::path& value); + void RemoveFromPathVariable(const std::filesystem::path& value); }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -172,6 +172,8 @@ namespace AppInstaller::CLI::Workflow // InstallerPath will point to a directory if it is extracted from an archive. if (std::filesystem::is_directory(installerPath)) { + portableInstaller.RecordToIndex = true; + for (const auto& entry : std::filesystem::directory_iterator(installerPath)) { std::filesystem::path entryPath = entry.path(); @@ -189,11 +191,6 @@ namespace AppInstaller::CLI::Workflow } } - if (entries.size() > 1) - { - portableInstaller.RecordToIndex = true; - } - const std::vector<Manifest::NestedInstallerFile>& nestedInstallerFiles = context.Get<Execution::Data::Installer>()->NestedInstallerFiles; for (const auto& nestedInstallerFile : nestedInstallerFiles) diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp @@ -636,23 +636,24 @@ TEST_CASE("InstallFlow_Portable", "[InstallFlow][workflow]") TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]") { + TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); std::ostringstream installOutput; TestContext installContext{ installOutput, std::cin }; auto PreviousThreadGlobals = installContext.SetForCurrentThread(); OverridePortableInstaller(installContext); TestHook::SetCreateSymlinkResult_Override createSymlinkResultOverride(false); + const auto& targetDirectory = tempDirectory.GetPath(); 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()); - // 'DefaultSource' is expected because we are installing from a local manifest. - const auto& portableUserRoot = AppInstaller::Runtime::GetPathTo(AppInstaller::Runtime::PathName::PortablePackageUserRoot); - const auto& portableTargetDirectory = portableUserRoot / "AppInstallerCliTest.TestPortableInstaller__DefaultSource"; - const auto& portableTargetPath = portableTargetDirectory / "AppInstallerTestExeInstaller.exe"; + const auto& portableTargetPath = targetDirectory / "AppInstallerTestExeInstaller.exe"; REQUIRE(std::filesystem::exists(portableTargetPath)); - REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableTargetDirectory)); + REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory)); // Perform uninstall std::ostringstream uninstallOutput; @@ -664,6 +665,7 @@ TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]") 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/UpdateFlow.cpp b/src/AppInstallerCLITests/UpdateFlow.cpp @@ -180,6 +180,7 @@ TEST_CASE("UpdateFlow_UpdatePortable", "[UpdateFlow][workflow]") TEST_CASE("UpdateFlow_Portable_SymlinkCreationFail", "[UpdateFlow][workflow]") { // Update portable with symlink creation failure verify that it succeeds. + TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); std::ostringstream updateOutput; TestContext context{ updateOutput, std::cin }; auto PreviousThreadGlobals = context.SetForCurrentThread(); @@ -187,15 +188,17 @@ TEST_CASE("UpdateFlow_Portable_SymlinkCreationFail", "[UpdateFlow][workflow]") AppInstaller::Filesystem::TestHook_SetCreateSymlinkResult_Override(&overrideCreateSymlinkStatus); 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); INFO(updateOutput.str()); - const auto& portableTargetDirectory = AppInstaller::Runtime::GetPathTo(AppInstaller::Runtime::PathName::PortablePackageUserRoot) / "AppInstallerCliTest.TestPortableInstaller__TestSource"; - const auto& portableTargetPath = portableTargetDirectory / "AppInstallerTestExeInstaller.exe"; + const auto& portableTargetPath = targetDirectory / "AppInstallerTestExeInstaller.exe"; REQUIRE(std::filesystem::exists(portableTargetPath)); - REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableTargetDirectory)); + REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory)); // Perform uninstall std::ostringstream uninstallOutput; @@ -209,7 +212,6 @@ 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(portableTargetDirectory)); } TEST_CASE("UpdateFlow_UpdateExeWithUnsupportedArgs", "[UpdateFlow][workflow]")