diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index b8aed9dfaa..d5a657f19b 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -219,12 +219,6 @@ jobs:
isPackaged: true
filter: "TestCategory!=InProcess&TestCategory!=OutOfProcess"
- - template: templates/e2e-test.template.yml
- parameters:
- title: "COM API E2E Tests (In-process)"
- isPackaged: false
- filter: "TestCategory=InProcess"
-
- task: PowerShell@2
displayName: 'Set program files directory'
inputs:
@@ -236,6 +230,20 @@ jobs:
Write-Host "##vso[task.setvariable variable=platformProgramFiles;]${env:ProgramFiles}"
}
+ # Resolves resource strings utilized by InProc E2E tests.
+ - task: CopyFiles@2
+ displayName: 'Copy resources.pri to dotnet directory'
+ inputs:
+ SourceFolder: '$(buildOutDir)\AppInstallerCLI'
+ TargetFolder: '$(platformProgramFiles)\dotnet'
+ Contents: resources.pri
+
+ - template: templates/e2e-test.template.yml
+ parameters:
+ title: "COM API E2E Tests (In-process)"
+ isPackaged: false
+ filter: "TestCategory=InProcess"
+
# Winmd accessed by test runner process (dotnet.exe)
- task: CopyFiles@2
displayName: 'Copy winmd to dotnet directory'
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
index 4cc8f5d9da..a78da26639 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
@@ -269,6 +269,7 @@
+
@@ -327,6 +328,7 @@
Create
+
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
index 4904eab23d..5ddf0b00df 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
@@ -185,6 +185,9 @@
Workflows
+
+ Header Files
+
@@ -337,6 +340,9 @@
Workflows
+
+ Source Files
+
diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h
index becf945155..9ab78e1873 100644
--- a/src/AppInstallerCLICore/ExecutionContextData.h
+++ b/src/AppInstallerCLICore/ExecutionContextData.h
@@ -4,10 +4,9 @@
#include
#include
#include
-#include
-#include
#include "CompletionData.h"
#include "PackageCollection.h"
+#include "PortableInstaller.h"
#include "Workflows/WorkflowBase.h"
#include
@@ -54,8 +53,8 @@ namespace AppInstaller::CLI::Execution
Dependencies,
DependencySource,
AllowedArchitectures,
- PortableEntry,
AllowUnknownScope,
+ PortableInstaller,
Max
};
@@ -220,15 +219,15 @@ namespace AppInstaller::CLI::Execution
};
template <>
- struct DataMapping
+ struct DataMapping
{
- using value_t = Portable::PortableEntry;
+ using value_t = bool;
};
template <>
- struct DataMapping
+ struct DataMapping
{
- using value_t = bool;
+ using value_t = CLI::Portable::PortableInstaller;
};
}
}
diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp
new file mode 100644
index 0000000000..4d50bf0045
--- /dev/null
+++ b/src/AppInstallerCLICore/PortableInstaller.cpp
@@ -0,0 +1,461 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "ExecutionContext.h"
+#include "PortableInstaller.h"
+#include "winget/Manifest.h"
+#include "winget/ManifestCommon.h"
+#include "winget/Filesystem.h"
+#include "winget/PathVariable.h"
+#include "Microsoft/PortableIndex.h"
+#include "Microsoft/Schema/IPortableIndex.h"
+#include
+
+using namespace AppInstaller::Utility;
+using namespace AppInstaller::Registry;
+using namespace AppInstaller::Registry::Portable;
+using namespace AppInstaller::Registry::Environment;
+using namespace AppInstaller::Repository;
+using namespace AppInstaller::Repository::SQLite;
+using namespace AppInstaller::Repository::Microsoft;
+using namespace AppInstaller::Repository::Microsoft::Schema;
+
+namespace AppInstaller::CLI::Portable
+{
+ std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope)
+ {
+ if (scope == Manifest::ScopeEnum::Machine)
+ {
+ return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation);
+ }
+ else
+ {
+ return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation);
+ }
+ }
+
+ std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch)
+ {
+ if (scope == Manifest::ScopeEnum::Machine)
+ {
+ if (arch == Utility::Architecture::X86)
+ {
+ return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86);
+ }
+ else
+ {
+ return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64);
+ }
+ }
+ else
+ {
+ return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot);
+ }
+ }
+
+ bool VerifyPortableFile(AppInstaller::Portable::PortableFileEntry& entry)
+ {
+ std::filesystem::path filePath = entry.GetFilePath();
+ PortableFileType fileType = entry.FileType;
+
+ if (fileType == PortableFileType::File)
+ {
+ if (std::filesystem::exists(filePath) && !SHA256::AreEqual(SHA256::ComputeHashFromFile(filePath), SHA256::ConvertToBytes(entry.SHA256)))
+ {
+ return false;
+ }
+ }
+ else if (fileType == PortableFileType::Symlink)
+ {
+ if (Filesystem::SymlinkExists(filePath) && !Filesystem::VerifySymlink(filePath, entry.SymlinkTarget))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ void PortableInstaller::InstallFile(AppInstaller::Portable::PortableFileEntry& entry)
+ {
+ PortableFileType fileType = entry.FileType;
+ std::filesystem::path filePath = entry.GetFilePath();
+
+ if (entry.FileType == PortableFileType::File)
+ {
+ if (std::filesystem::exists(filePath))
+ {
+ AICLI_LOG(Core, Info, << "Removing existing portable file at: " << filePath);
+ std::filesystem::remove(filePath);
+ }
+
+ AICLI_LOG(Core, Info, << "Moving portable exe to: " << filePath);
+
+ if (!RecordToIndex)
+ {
+ CommitToARPEntry(PortableValueName::PortableTargetFullPath, filePath);
+ CommitToARPEntry(PortableValueName::SHA256, entry.SHA256);
+ }
+
+ Filesystem::RenameFile(entry.CurrentPath, filePath);
+ }
+ else if (fileType == PortableFileType::Directory)
+ {
+ AICLI_LOG(Core, Info, << "Moving directory to: " << filePath);
+ Filesystem::RenameFile(entry.CurrentPath, filePath);
+ }
+ else if (entry.FileType == PortableFileType::Symlink && !InstallDirectoryAddedToPath)
+ {
+ std::filesystem::file_status status = std::filesystem::status(filePath);
+ if (std::filesystem::is_directory(status))
+ {
+ AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << filePath << "points to an existing directory.");
+ THROW_HR(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY);
+ }
+
+ if (!RecordToIndex)
+ {
+ CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, filePath);
+ }
+
+ if (std::filesystem::remove(filePath))
+ {
+ AICLI_LOG(CLI, Info, << "Removed existing file at " << filePath);
+ m_stream << Resource::String::OverwritingExistingFileAtMessage << ' ' << filePath << std::endl;
+ }
+
+ if (Filesystem::CreateSymlink(entry.SymlinkTarget, filePath))
+ {
+ AICLI_LOG(Core, Info, << "Symlink created at: " << filePath);
+ }
+ 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.");
+ CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true);
+ }
+ }
+ }
+
+ void PortableInstaller::RemoveFile(AppInstaller::Portable::PortableFileEntry& entry)
+ {
+ const auto& filePath = entry.GetFilePath();
+ PortableFileType fileType = entry.FileType;
+
+ if (fileType == PortableFileType::File && std::filesystem::exists(filePath))
+ {
+ AICLI_LOG(CLI, Info, << "Deleting portable exe at: " << filePath);
+ std::filesystem::remove(filePath);
+ }
+ 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);
+ std::filesystem::remove_all(filePath);
+ }
+ }
+
+ // TODO: Optimize by applying the difference between expected and desired state.
+ void PortableInstaller::ApplyDesiredState()
+ {
+ std::filesystem::path existingIndexPath = InstallLocation / GetPortableIndexFileName();
+ if (std::filesystem::exists(existingIndexPath))
+ {
+ bool deleteIndex = false;
+ {
+ PortableIndex existingIndex = PortableIndex::Open(existingIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite);
+
+ for (auto expectedEntry : m_expectedEntries)
+ {
+ RemoveFile(expectedEntry);
+ existingIndex.RemovePortableFile(expectedEntry);
+ }
+
+ deleteIndex = existingIndex.IsEmpty();
+ }
+
+ if (deleteIndex)
+ {
+ std::filesystem::remove(existingIndexPath);
+ AICLI_LOG(CLI, Info, << "Portable index deleted: " << existingIndexPath);
+ }
+ }
+ else
+ {
+ for (auto expectedEntry : m_expectedEntries)
+ {
+ RemoveFile(expectedEntry);
+ }
+ }
+
+ // Check if existing install location differs from the target install location for proper cleanup.
+ if (!TargetInstallLocation.empty() && TargetInstallLocation != InstallLocation)
+ {
+ RemoveInstallDirectory();
+ }
+
+ if (RecordToIndex)
+ {
+ std::filesystem::path targetIndexPath = TargetInstallLocation / GetPortableIndexFileName();
+ PortableIndex targetIndex = std::filesystem::exists(targetIndexPath) ?
+ PortableIndex::Open(targetIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) :
+ PortableIndex::CreateNew(targetIndexPath.u8string());
+
+ for (auto desiredEntry : m_desiredEntries)
+ {
+ targetIndex.AddOrUpdatePortableFile(desiredEntry);
+ InstallFile(desiredEntry);
+ }
+ }
+ else
+ {
+ for (auto desiredEntry : m_desiredEntries)
+ {
+ InstallFile(desiredEntry);
+ }
+ }
+ }
+
+ bool PortableInstaller::VerifyExpectedState()
+ {
+ for (auto entry : m_expectedEntries)
+ {
+ if (!VerifyPortableFile(entry))
+ {
+ AICLI_LOG(CLI, Info, << "Portable file has been modified: " << entry.GetFilePath());
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ void PortableInstaller::Install()
+ {
+ RegisterARPEntry();
+
+ CreateTargetInstallDirectory();
+
+ ApplyDesiredState();
+
+ AddToPathVariable();
+ }
+
+ void PortableInstaller::Uninstall()
+ {
+ ApplyDesiredState();
+
+ RemoveInstallDirectory();
+
+ RemoveFromPathVariable();
+
+ m_portableARPEntry.Delete();
+ AICLI_LOG(CLI, Info, << "PortableARPEntry deleted.");
+ }
+
+ void PortableInstaller::CreateTargetInstallDirectory()
+ {
+ if (std::filesystem::create_directories(TargetInstallLocation))
+ {
+ AICLI_LOG(Core, Info, << "Created target install directory: " << TargetInstallLocation);
+ CommitToARPEntry(PortableValueName::InstallDirectoryCreated, true);
+ }
+
+ CommitToARPEntry(PortableValueName::InstallLocation, TargetInstallLocation);
+ }
+
+ void PortableInstaller::RemoveInstallDirectory()
+ {
+ if (std::filesystem::exists(InstallLocation) && InstallDirectoryCreated)
+ {
+ if (Purge)
+ {
+ m_stream << Resource::String::PurgeInstallDirectory << std::endl;
+ const auto& removedFilesCount = std::filesystem::remove_all(InstallLocation);
+ AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories");
+ }
+ else
+ {
+ if (std::filesystem::is_empty(InstallLocation))
+ {
+ AICLI_LOG(CLI, Info, << "Removing empty install directory: " << InstallLocation);
+ std::filesystem::remove(InstallLocation);
+ }
+ else
+ {
+ AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << InstallLocation);
+ m_stream << Resource::String::FilesRemainInInstallDirectory << InstallLocation << std::endl;
+ }
+ }
+ }
+ }
+
+ void PortableInstaller::AddToPathVariable()
+ {
+ const std::filesystem::path& pathValue = InstallDirectoryAddedToPath ? TargetInstallLocation : GetPortableLinksLocation(GetScope());
+ if (PathVariable(GetScope()).Append(pathValue))
+ {
+ AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue);
+ m_stream << Resource::String::ModifiedPathRequiresShellRestart << std::endl;
+ }
+ else
+ {
+ AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue);
+ }
+ }
+
+ void PortableInstaller::RemoveFromPathVariable()
+ {
+ const std::filesystem::path& pathValue = InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope());
+ if (std::filesystem::exists(pathValue) && !std::filesystem::is_empty(pathValue))
+ {
+ AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue);
+ }
+ else
+ {
+ if (PathVariable(GetScope()).Remove(pathValue))
+ {
+ AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue);
+ }
+ else
+ {
+ AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue);
+ }
+ }
+ }
+
+ void PortableInstaller::SetAppsAndFeaturesMetadata(const Manifest::Manifest& manifest, const std::vector& entries)
+ {
+ AppInstaller::Manifest::AppsAndFeaturesEntry entry;
+ if (!entries.empty())
+ {
+ entry = entries[0];
+ }
+
+ if (entry.DisplayName.empty())
+ {
+ entry.DisplayName = manifest.CurrentLocalization.Get();
+ }
+ if (entry.DisplayVersion.empty())
+ {
+ entry.DisplayVersion = manifest.Version;
+ }
+ if (entry.Publisher.empty())
+ {
+ entry.Publisher = manifest.CurrentLocalization.Get();
+ }
+
+ DisplayName = entry.DisplayName;
+ DisplayVersion = entry.DisplayVersion;
+ Publisher = entry.Publisher;
+ InstallDate = Utility::GetCurrentDateForARP();
+ URLInfoAbout = manifest.CurrentLocalization.Get();
+ HelpLink = manifest.CurrentLocalization.Get();
+ }
+
+ void PortableInstaller::SetExpectedState()
+ {
+ const auto& indexPath = InstallLocation / GetPortableIndexFileName();
+
+ if (std::filesystem::exists(indexPath))
+ {
+ PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite);
+ m_expectedEntries = portableIndex.GetAllPortableFiles();
+ }
+ else
+ {
+ std::filesystem::path targetFullPath = PortableTargetFullPath;
+ std::filesystem::path symlinkFullPath = PortableSymlinkFullPath;
+
+ if (!symlinkFullPath.empty())
+ {
+ m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath)));
+ }
+
+ if (!targetFullPath.empty())
+ {
+ m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256)));
+ }
+ }
+ }
+
+ PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) :
+ m_portableARPEntry(PortableARPEntry(scope, arch, productCode))
+ {
+ if (ARPEntryExists())
+ {
+ DisplayName = GetStringValue(PortableValueName::DisplayName);
+ DisplayVersion = GetStringValue(PortableValueName::DisplayVersion);
+ HelpLink = GetStringValue(PortableValueName::HelpLink);
+ InstallDate = GetStringValue(PortableValueName::InstallDate);
+ Publisher = GetStringValue(PortableValueName::Publisher);
+ SHA256 = GetStringValue(PortableValueName::SHA256);
+ URLInfoAbout = GetStringValue(PortableValueName::URLInfoAbout);
+ UninstallString = GetStringValue(PortableValueName::UninstallString);
+ WinGetInstallerType = GetStringValue(PortableValueName::WinGetInstallerType);
+ WinGetPackageIdentifier = GetStringValue(PortableValueName::WinGetPackageIdentifier);
+ WinGetSourceIdentifier = GetStringValue(PortableValueName::WinGetSourceIdentifier);
+ InstallLocation = GetPathValue(PortableValueName::InstallLocation);
+ PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath);
+ PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath);
+ InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath);
+ InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated);
+ }
+
+ SetExpectedState();
+ }
+
+ void PortableInstaller::RegisterARPEntry()
+ {
+ CommitToARPEntry(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier);
+ CommitToARPEntry(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier);
+ CommitToARPEntry(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode());
+ CommitToARPEntry(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable));
+ CommitToARPEntry(PortableValueName::DisplayName, DisplayName);
+ CommitToARPEntry(PortableValueName::DisplayVersion, DisplayVersion);
+ CommitToARPEntry(PortableValueName::Publisher, Publisher);
+ CommitToARPEntry(PortableValueName::InstallDate, InstallDate);
+ CommitToARPEntry(PortableValueName::URLInfoAbout, URLInfoAbout);
+ CommitToARPEntry(PortableValueName::HelpLink, HelpLink);
+ }
+
+ std::string PortableInstaller::GetStringValue(PortableValueName valueName)
+ {
+ if (m_portableARPEntry[valueName].has_value())
+ {
+ return m_portableARPEntry[valueName]->GetValue();
+ }
+ else
+ {
+ return {};
+ }
+ }
+
+ std::filesystem::path PortableInstaller::GetPathValue(PortableValueName valueName)
+ {
+ if (m_portableARPEntry[valueName].has_value())
+ {
+ return m_portableARPEntry[valueName]->GetValue();
+ }
+ {
+ return {};
+ }
+ }
+
+ bool PortableInstaller::GetBoolValue(PortableValueName valueName)
+ {
+ if (m_portableARPEntry[valueName].has_value())
+ {
+ return m_portableARPEntry[valueName]->GetValue();
+ }
+ else
+ {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h
new file mode 100644
index 0000000000..664720db38
--- /dev/null
+++ b/src/AppInstallerCLICore/PortableInstaller.h
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "winget/PortableARPEntry.h"
+#include "winget/PortableFileEntry.h"
+#include
+
+using namespace AppInstaller::Registry::Portable;
+
+namespace AppInstaller::CLI::Portable
+{
+ std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope);
+
+ std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch);
+
+ // Object representation of the metadata and functionality required for installing a Portable package.
+ struct PortableInstaller
+ {
+ // These values are initialized based on the values from the entry in ARP
+ std::string DisplayName;
+ std::string DisplayVersion;
+ std::string HelpLink;
+ std::string InstallDate;
+ std::filesystem::path InstallLocation;
+ std::filesystem::path PortableSymlinkFullPath;
+ std::filesystem::path PortableTargetFullPath;
+ std::string Publisher;
+ std::string SHA256;
+ std::string URLInfoAbout;
+ std::string UninstallString;
+ std::string WinGetInstallerType;
+ std::string WinGetPackageIdentifier;
+ std::string WinGetSourceIdentifier;
+ bool InstallDirectoryCreated = false;
+ // If we fail to create a symlink, add install directory to PATH variable
+ bool InstallDirectoryAddedToPath = false;
+
+ bool IsUpdate = false;
+ bool Purge = false;
+ bool RecordToIndex = false;
+
+ // This is the incoming target install location determined from the context args.
+ std::filesystem::path TargetInstallLocation;
+
+ PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode);
+
+ bool VerifyExpectedState();
+
+ void SetDesiredState(const std::vector& desiredEntries)
+ {
+ m_desiredEntries = desiredEntries;
+ };
+
+ void PrepareForCleanUp()
+ {
+ m_expectedEntries = m_desiredEntries;
+ m_desiredEntries = {};
+ }
+
+ void Install();
+
+ void Uninstall();
+
+ template
+ void CommitToARPEntry(PortableValueName valueName, T value)
+ {
+ m_portableARPEntry.SetValue(valueName, value);
+ }
+
+ std::filesystem::path GetInstallDirectoryForPathVariable()
+ {
+ return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope());
+ }
+
+ std::filesystem::path GetPortableIndexFileName()
+ {
+ return Utility::ConvertToUTF16(GetProductCode() + ".db");
+ }
+
+ Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); };
+
+ Utility::Architecture GetArch() { return m_portableARPEntry.GetArchitecture(); };
+
+ std::string GetProductCode() { return m_portableARPEntry.GetProductCode(); };
+
+ bool ARPEntryExists() { return m_portableARPEntry.Exists(); };
+
+ std::string GetOutputMessage()
+ {
+ return m_stream.str();
+ }
+
+ void SetAppsAndFeaturesMetadata(
+ const Manifest::Manifest& manifest,
+ const std::vector& entries);
+
+ private:
+ PortableARPEntry m_portableARPEntry;
+ std::vector m_desiredEntries;
+ std::vector m_expectedEntries;
+ std::stringstream m_stream;
+
+ std::string GetStringValue(PortableValueName valueName);
+ std::filesystem::path GetPathValue(PortableValueName valueName);
+ bool GetBoolValue(PortableValueName valueName);
+
+ void SetExpectedState();
+ void RegisterARPEntry();
+
+ void ApplyDesiredState();
+ void InstallFile(AppInstaller::Portable::PortableFileEntry& desiredState);
+ void RemoveFile(AppInstaller::Portable::PortableFileEntry& desiredState);
+
+ void CreateTargetInstallDirectory();
+ void RemoveInstallDirectory();
+
+ void AddToPathVariable();
+ void RemoveFromPathVariable();
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h
index 5c8843712b..e40ae0bb7e 100644
--- a/src/AppInstallerCLICore/Resources.h
+++ b/src/AppInstallerCLICore/Resources.h
@@ -71,6 +71,8 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ExternalDependencies);
WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveFailed);
+ WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveSucceeded);
+ WINGET_DEFINE_RESOURCE_STRINGID(ExtractingArchive);
WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError);
WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledByAdminSettingMessage);
WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage);
@@ -231,7 +233,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden);
WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverrideRequired);
WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFailed);
- WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFromArchiveNotSupported);
+ WINGET_DEFINE_RESOURCE_STRINGID(PortablePackageAlreadyExists);
WINGET_DEFINE_RESOURCE_STRINGID(PortableRegistryCollisionOverridden);
WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PreserveArgumentDescription);
@@ -363,7 +365,6 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(SourceUpdateCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceUpdateOne);
WINGET_DEFINE_RESOURCE_STRINGID(SystemArchitecture);
- WINGET_DEFINE_RESOURCE_STRINGID(SymlinkModified);
WINGET_DEFINE_RESOURCE_STRINGID(TagArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ThankYou);
WINGET_DEFINE_RESOURCE_STRINGID(ThirdPartSoftwareNotices);
diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp
index 3f11a0430d..6b54656e5b 100644
--- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp
@@ -4,27 +4,35 @@
#include "ArchiveFlow.h"
#include "winget/Archive.h"
#include "winget/Filesystem.h"
+#include "PortableFlow.h"
using namespace AppInstaller::Manifest;
namespace AppInstaller::CLI::Workflow
{
+ namespace
+ {
+ constexpr std::wstring_view s_Extracted = L"extracted";
+ }
+
void ExtractFilesFromArchive(Execution::Context& context)
{
const auto& installerPath = context.Get();
- const auto& installerParentPath = installerPath.parent_path();
+ std::filesystem::path destinationFolder = installerPath.parent_path() / s_Extracted;
+ std::filesystem::create_directory(destinationFolder);
- // TODO: For portables, extract portables to final install location and log to local database.
- HRESULT hr = AppInstaller::Archive::TryExtractArchive(installerPath, installerParentPath);
- AICLI_LOG(CLI, Info, << "Extracting archive to: " << installerParentPath);
+ AICLI_LOG(CLI, Info, << "Extracting archive to: " << destinationFolder);
+ context.Reporter.Info() << Resource::String::ExtractingArchive << std::endl;
+ HRESULT result = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder);
- if (SUCCEEDED(hr))
+ if (SUCCEEDED(result))
{
AICLI_LOG(CLI, Info, << "Successfully extracted archive");
+ context.Reporter.Info() << Resource::String::ExtractArchiveSucceeded << std::endl;
}
else
{
- AICLI_LOG(CLI, Info, << "Failed to extract archive with code " << hr);
+ AICLI_LOG(CLI, Info, << "Failed to extract archive with code " << result);
context.Reporter.Error() << Resource::String::ExtractArchiveFailed << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED);
}
@@ -40,29 +48,33 @@ namespace AppInstaller::CLI::Workflow
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST);
}
- const auto& installerPath = context.Get();
- const auto& installerParentPath = installerPath.parent_path();
- const auto& relativeFilePath = ConvertToUTF16(installer.NestedInstallerFiles[0].RelativeFilePath);
-
- const std::filesystem::path& nestedInstallerPath = installerParentPath / relativeFilePath;
+ std::filesystem::path targetInstallerPath = context.Get().parent_path() / s_Extracted;
- if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, installerParentPath))
+ for (const auto& nestedInstallerFile : installer.NestedInstallerFiles)
{
- AICLI_LOG(CLI, Error, << "Path points to a location outside of the install directory: " << nestedInstallerPath);
- context.Reporter.Error() << Resource::String::InvalidPathToNestedInstaller << std::endl;
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH);
- }
- else if (!std::filesystem::exists(nestedInstallerPath))
- {
- AICLI_LOG(CLI, Error, << "Unable to locate nested installer at: " << nestedInstallerPath);
- context.Reporter.Error() << Resource::String::NestedInstallerNotFound << ' ' << nestedInstallerPath << std::endl;
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND);
- }
- else
- {
- AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath);
- context.Add(nestedInstallerPath);
+ const std::filesystem::path& nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath);
+
+ if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, targetInstallerPath))
+ {
+ AICLI_LOG(CLI, Error, << "Path points to a location outside of the install directory: " << nestedInstallerPath);
+ context.Reporter.Error() << Resource::String::InvalidPathToNestedInstaller << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH);
+ }
+ else if (!std::filesystem::exists(nestedInstallerPath))
+ {
+ AICLI_LOG(CLI, Error, << "Unable to locate nested installer at: " << nestedInstallerPath);
+ context.Reporter.Error() << Resource::String::NestedInstallerNotFound << ' ' << nestedInstallerPath << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND);
+ }
+ else if (!IsPortableType(installer.NestedInstallerType))
+ {
+ // Update the installerPath to the extracted non-portable installer.
+ AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath);
+ targetInstallerPath = nestedInstallerPath;
+ }
}
+
+ context.Add(targetInstallerPath);
}
void EnsureValidNestedInstallerMetadataForArchiveInstall(Execution::Context& context)
diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
index 0422eb5e6c..632a3f2d43 100644
--- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
@@ -336,6 +336,8 @@ namespace AppInstaller::CLI::Workflow
void PortableInstall(Execution::Context& context)
{
context <<
+ InitializePortableInstaller <<
+ VerifyPackageAndSourceMatch <<
PortableInstallImpl <<
ReportInstallerResult("Portable"sv, APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED, true);
}
@@ -474,7 +476,6 @@ namespace AppInstaller::CLI::Workflow
context <<
Workflow::EnsureFeatureEnabledForArchiveInstall <<
Workflow::EnsureSupportForPortableInstall <<
- Workflow::EnsureNonPortableTypeForArchiveInstall <<
Workflow::EnsureValidNestedInstallerMetadataForArchiveInstall;
}
diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp
index 85ae72b71a..21640d8042 100644
--- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp
@@ -2,21 +2,18 @@
// Licensed under the MIT License.
#include "pch.h"
#include "PortableFlow.h"
+#include "PortableInstaller.h"
#include "WorkflowBase.h"
#include "winget/Filesystem.h"
-#include "winget/PortableARPEntry.h"
-#include "winget/PortableEntry.h"
-#include "winget/PathVariable.h"
-#include "AppInstallerStrings.h"
+#include "winget/PortableFileEntry.h"
+#include
using namespace AppInstaller::Manifest;
-using namespace AppInstaller::Portable;
-using namespace AppInstaller::Utility;
-using namespace AppInstaller::Registry;
-using namespace AppInstaller::Registry::Portable;
-using namespace AppInstaller::Registry::Environment;
using namespace AppInstaller::Repository;
-using namespace std::filesystem;
+using namespace AppInstaller::Utility;
+using namespace AppInstaller::CLI::Portable;
+using namespace AppInstaller::Portable;
+using namespace AppInstaller::Repository::Microsoft;
namespace AppInstaller::CLI::Workflow
{
@@ -24,14 +21,6 @@ namespace AppInstaller::CLI::Workflow
{
constexpr std::string_view s_DefaultSource = "*DefaultSource"sv;
- void AppendExeExtension(std::filesystem::path& value)
- {
- if (value.extension() != ".exe")
- {
- value += ".exe";
- }
- }
-
std::string GetPortableProductCode(Execution::Context& context)
{
const std::string& packageId = context.Get().Id;
@@ -49,484 +38,295 @@ namespace AppInstaller::CLI::Workflow
return MakeSuitablePathPart(packageId + "_" + source);
}
- std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch)
- {
- if (scope == Manifest::ScopeEnum::Machine)
- {
- if (arch == Utility::Architecture::X86)
- {
- return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86);
- }
- else
- {
- return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64);
- }
- }
- else
- {
- return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot);
- }
- }
-
- std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope)
- {
- if (scope == Manifest::ScopeEnum::Machine)
- {
- return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation);
- }
- else
- {
- return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation);
- }
- }
-
- std::filesystem::path GetPortableTargetDirectory(Execution::Context& context)
+ void EnsureValidArgsForPortableInstall(Execution::Context& context)
{
- Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
- Utility::Architecture arch = context.Get()->Arch;
- std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation);
- std::filesystem::path targetInstallDirectory;
+ std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename);
- if (!locationArg.empty())
+ try
{
- targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) };
+ if (MakeSuitablePathPart(renameArg) != renameArg)
+ {
+ context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS);
+ }
}
- else
+ catch (...)
{
- const std::string& productCode = GetPortableProductCode(context);
- targetInstallDirectory = GetPortableInstallRoot(scope, arch);
- targetInstallDirectory /= ConvertToUTF16(productCode);
+ context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS);
}
-
- return targetInstallDirectory;
}
- std::filesystem::path GetPortableTargetFullPath(Execution::Context& context)
+ void EnsureVolumeSupportsReparsePoints(Execution::Context& context)
{
- const std::filesystem::path& installerPath = context.Get();
- const std::filesystem::path& targetInstallDirectory = GetPortableTargetDirectory(context);
- std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename);
-
- std::filesystem::path fileName;
- if (!renameArg.empty())
- {
- fileName = ConvertToUTF16(renameArg);
+ Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
+ const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(scope);
+
+ if (!AppInstaller::Filesystem::SupportsReparsePoints(symlinkDirectory))
+ {
+ context.Reporter.Error() << Resource::String::ReparsePointsNotSupportedError << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED);
}
- else
+ }
+
+ void EnsureRunningAsAdminForMachineScopeInstall(Execution::Context& context)
+ {
+ // Admin is required for machine scope install or else creating a symlink in the %PROGRAMFILES% link location will fail.
+ Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
+ if (scope == Manifest::ScopeEnum::Machine)
{
- fileName = installerPath.filename();
+ context << Workflow::EnsureRunningAsAdmin;
}
-
- AppendExeExtension(fileName);
- return targetInstallDirectory / fileName;
}
+ }
- std::filesystem::path GetPortableSymlinkFullPath(Execution::Context& context)
- {
- const std::filesystem::path& installerPath = context.Get();
- const std::vector& commands = context.Get()->Commands;
- Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
- std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename);
+ void VerifyPackageAndSourceMatch(Execution::Context& context)
+ {
+ const std::string& packageIdentifier = context.Get().Id;
- std::filesystem::path commandAlias;
- if (!renameArg.empty())
- {
- commandAlias = ConvertToUTF16(renameArg);
- }
- else
+ std::string sourceIdentifier;
+ if (context.Contains(Execution::Data::PackageVersion))
+ {
+ sourceIdentifier = context.Get()->GetSource().GetIdentifier();
+ }
+ else
+ {
+ sourceIdentifier = s_DefaultSource;
+ }
+
+ PortableInstaller& portableInstaller = context.Get();
+ if (portableInstaller.ARPEntryExists())
+ {
+ if (packageIdentifier != portableInstaller.WinGetPackageIdentifier || sourceIdentifier != portableInstaller.WinGetSourceIdentifier)
{
- if (!commands.empty())
+ // TODO: Replace HashOverride with --Force when argument behavior gets updated.
+ if (!context.Args.Contains(Execution::Args::Type::HashOverride))
{
- commandAlias = ConvertToUTF16(commands[0]);
+ AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry");
+ context.Reporter.Error() << Resource::String::PortablePackageAlreadyExists << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS);
}
else
{
- commandAlias = installerPath.filename();
+ AICLI_LOG(CLI, Info, << "Overriding registry match check...");
+ context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl;
}
- }
-
- AppendExeExtension(commandAlias);
- return GetPortableLinksLocation(scope) / commandAlias;
+ }
}
- Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall(
- const std::vector& appsAndFeaturesEntries,
- const AppInstaller::Manifest::Manifest& manifest)
+ portableInstaller.WinGetPackageIdentifier = packageIdentifier;
+ portableInstaller.WinGetSourceIdentifier = sourceIdentifier;
+ }
+
+ void InitializePortableInstaller(Execution::Context& context)
+ {
+ Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown;
+ bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate);
+ if (isUpdate)
{
- AppInstaller::Manifest::AppsAndFeaturesEntry appsAndFeaturesEntry;
- if (!appsAndFeaturesEntries.empty())
- {
- appsAndFeaturesEntry = appsAndFeaturesEntries[0];
- }
-
- if (appsAndFeaturesEntry.DisplayName.empty())
- {
- appsAndFeaturesEntry.DisplayName = manifest.CurrentLocalization.Get();
- }
- if (appsAndFeaturesEntry.DisplayVersion.empty())
- {
- appsAndFeaturesEntry.DisplayVersion = manifest.Version;
- }
- if (appsAndFeaturesEntry.Publisher.empty())
+ IPackageVersion::Metadata installationMetadata = context.Get()->GetMetadata();
+ auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope);
+ if (installerScopeItr != installationMetadata.end())
{
- appsAndFeaturesEntry.Publisher = manifest.CurrentLocalization.Get();
+ scope = Manifest::ConvertToScopeEnum(installerScopeItr->second);
}
-
- return appsAndFeaturesEntry;
+ }
+ else
+ {
+ scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
}
- void InitializePortableARPEntry(Execution::Context& context)
- {
- const std::string& packageIdentifier = context.Get().Id;
+ Utility::Architecture arch = context.Get()->Arch;
+ const std::string& productCode = GetPortableProductCode(context);
- std::string sourceIdentifier;
- if (context.Contains(Execution::Data::PackageVersion))
- {
- sourceIdentifier = context.Get()->GetSource().GetIdentifier();
- }
- else
- {
- sourceIdentifier = s_DefaultSource;
- }
-
- Portable::PortableEntry& portableEntry = context.Get();
-
- if (portableEntry.Exists())
- {
- if (packageIdentifier != portableEntry.WinGetPackageIdentifier || sourceIdentifier != portableEntry.WinGetSourceIdentifier)
- {
- // TODO: Replace HashOverride with --Force when argument behavior gets updated.
- if (!context.Args.Contains(Execution::Args::Type::HashOverride))
- {
- AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry");
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS);
- }
- else
- {
- AICLI_LOG(CLI, Info, << "Overriding registry match check...");
- context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl;
- }
- }
- }
+ PortableInstaller portableInstaller = PortableInstaller(scope, arch, productCode);
+ portableInstaller.IsUpdate = isUpdate;
+
+ // Set target install directory
+ std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation);
+ std::filesystem::path targetInstallDirectory;
- portableEntry.Commit(PortableValueName::WinGetPackageIdentifier, portableEntry.WinGetPackageIdentifier = packageIdentifier);
- portableEntry.Commit(PortableValueName::WinGetSourceIdentifier, portableEntry.WinGetSourceIdentifier = sourceIdentifier);
- portableEntry.Commit(PortableValueName::UninstallString, portableEntry.UninstallString = "winget uninstall --product-code " + GetPortableProductCode(context));
- portableEntry.Commit(PortableValueName::WinGetInstallerType, portableEntry.WinGetInstallerType = InstallerTypeToString(InstallerTypeEnum::Portable));
+ if (!locationArg.empty())
+ {
+ targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) };
}
-
- void MovePortableExe(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- const std::filesystem::path& installerPath = context.Get();
- portableEntry.Commit(PortableValueName::PortableTargetFullPath, portableEntry.PortableTargetFullPath = GetPortableTargetFullPath(context));
- portableEntry.Commit(PortableValueName::InstallLocation, portableEntry.InstallLocation = GetPortableTargetDirectory(context));
- portableEntry.Commit(PortableValueName::SHA256, portableEntry.SHA256 = SHA256::ConvertToString(context.Get().second));
- portableEntry.MovePortableExe(installerPath);
+ else
+ {
+ targetInstallDirectory = GetPortableInstallRoot(scope, arch);
+ targetInstallDirectory /= ConvertToUTF16(productCode);
}
- void RemovePortableExe(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- const auto& targetPath = portableEntry.PortableTargetFullPath;
+ portableInstaller.TargetInstallLocation = targetInstallDirectory;
+ portableInstaller.SetAppsAndFeaturesMetadata(context.Get(), context.Get()->AppsAndFeaturesEntries);
+ context.Add(std::move(portableInstaller));
+ }
- if (std::filesystem::exists(targetPath))
- {
- if (!portableEntry.VerifyPortableExeHash())
- {
- bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride);
- if (overrideHashMismatch)
- {
- context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl;
- }
- else
- {
- context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl;
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED);
- }
- }
+ std::vector GetDesiredStateForPortableInstall(Execution::Context& context)
+ {
+ std::filesystem::path& installerPath = context.Get();
+ PortableInstaller& portableInstaller = context.Get();
+ std::vector entries;
- std::filesystem::remove(targetPath);
- AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPath);
- }
- else
+ const std::filesystem::path& targetInstallDirectory = portableInstaller.TargetInstallLocation;
+ const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(portableInstaller.GetScope());
+
+ // InstallerPath will point to a directory if it is extracted from an archive.
+ if (std::filesystem::is_directory(installerPath))
+ {
+ for (const auto& entry : std::filesystem::directory_iterator(installerPath))
{
- AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << targetPath);
- }
- }
-
- void RemoveInstallDirectory(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- const auto& installDirectory = portableEntry.InstallLocation;
-
- if (std::filesystem::exists(installDirectory))
- {
- const auto& isCreated = portableEntry.InstallDirectoryCreated;
- bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate);
-
- if (context.Args.Contains(Execution::Args::Type::Purge) ||
- (!isUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)))
- {
- if (isCreated)
- {
- context.Reporter.Warn() << Resource::String::PurgeInstallDirectory << std::endl;
- const auto& removedFilesCount = std::filesystem::remove_all(installDirectory);
- AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories");
- }
- else
- {
- context.Reporter.Warn() << Resource::String::UnableToPurgeInstallDirectory << std::endl;
- }
-
- }
- else if (std::filesystem::is_empty(installDirectory))
+ std::filesystem::path entryPath = entry.path();
+ PortableFileEntry portableFile;
+ std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path());
+ std::filesystem::path targetPath = targetInstallDirectory / relativePath;
+
+ if (std::filesystem::is_directory(entryPath))
{
- if (isCreated)
- {
- std::filesystem::remove(installDirectory);
- AICLI_LOG(CLI, Info, << "Install directory deleted: " << installDirectory);
- }
+ entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath, targetPath)));
}
else
{
- context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectory << std::endl;
- }
- }
- else
- {
- AICLI_LOG(CLI, Info, << "Install directory does not exist: " << installDirectory);
- }
- }
-
- void CreatePortableSymlink(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- if (portableEntry.InstallDirectoryAddedToPath)
- {
- AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation.");
- return;
+ entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, targetPath, {})));
+ }
}
- const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context);
- portableEntry.Commit(PortableValueName::PortableSymlinkFullPath, portableEntry.PortableSymlinkFullPath = symlinkFullPath);
-
- std::filesystem::file_status status = std::filesystem::status(symlinkFullPath);
- if (std::filesystem::is_directory(status))
+ if (entries.size() > 1)
{
- AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkFullPath << "points to an existing directory.");
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY);
+ portableInstaller.RecordToIndex = true;
}
- if (std::filesystem::remove(symlinkFullPath))
- {
- AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkFullPath);
- context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkFullPath.u8string() << std::endl;
- }
+ const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles;
- portableEntry.CreatePortableSymlink();
- }
-
- void AddToPathVariable(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- const std::filesystem::path& pathValue = portableEntry.GetPathValue();
- if (portableEntry.AddToPathVariable())
+ for (const auto& nestedInstallerFile : nestedInstallerFiles)
{
- AICLI_LOG(CLI, Info, << "Appended target directory to PATH registry: " << pathValue);
- context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl;
+ const std::filesystem::path& targetPath = targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath);
+
+ std::filesystem::path commandAlias;
+ if (nestedInstallerFile.PortableCommandAlias.empty())
+ {
+ commandAlias = targetPath.filename();
+ }
+ else
+ {
+ commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias);
+ }
+
+ Filesystem::AppendExtension(commandAlias, ".exe");
+ entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetPath)));
}
- else
- {
- AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue);
- }
- }
-
- void RemoveFromPathVariable(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- const std::filesystem::path& pathValue = portableEntry.GetPathValue();
- if (portableEntry.RemoveFromPathVariable())
+ }
+ else
+ {
+ std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename);
+ const std::vector& commands = context.Get()->Commands;
+ std::filesystem::path fileName;
+ std::filesystem::path commandAlias;
+
+ if (!renameArg.empty())
{
- AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue);
+ fileName = commandAlias = ConvertToUTF16(renameArg);
}
else
{
- AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue);
- }
- }
-
- void RemovePortableSymlink(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- const auto& symlinkPath = portableEntry.PortableSymlinkFullPath;
-
- if (!std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath)))
- {
- AICLI_LOG(Core, Info, << "The registry value for [PortableSymlinkFullPath] does not point to a valid symlink file.");
- return;
- }
-
- if (portableEntry.VerifySymlinkTarget())
- {
- if (!std::filesystem::remove(symlinkPath))
+ if (!commands.empty())
{
- AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPath);
+ commandAlias = ConvertToUTF16(commands[0]);
}
+ else
+ {
+ commandAlias = installerPath.filename();
+ }
+
+ fileName = installerPath.filename();
}
- else
- {
- context.Reporter.Warn() << Resource::String::SymlinkModified << std::endl;
- }
- }
-
- void RemovePortableARPEntry(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- portableEntry.RemoveARPEntry();
- AICLI_LOG(CLI, Info, << "PortableARPEntry deleted.");
- }
- void CommitPortableMetadataToRegistry(Execution::Context& context)
- {
- Portable::PortableEntry& portableEntry = context.Get();
- const AppInstaller::Manifest::Manifest& manifest = context.Get();
- const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get()->AppsAndFeaturesEntries, manifest);
+ AppInstaller::Filesystem::AppendExtension(fileName, ".exe");
+ AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe");
- portableEntry.Commit(PortableValueName::DisplayName, portableEntry.DisplayName = entry.DisplayName);
- portableEntry.Commit(PortableValueName::DisplayVersion, portableEntry.DisplayVersion = entry.DisplayVersion);
- portableEntry.Commit(PortableValueName::Publisher, portableEntry.Publisher = entry.Publisher);
- portableEntry.Commit(PortableValueName::InstallDate, portableEntry.InstallDate = Utility::GetCurrentDateForARP());
- portableEntry.Commit(PortableValueName::URLInfoAbout, portableEntry.URLInfoAbout = manifest.CurrentLocalization.Get());
- portableEntry.Commit(PortableValueName::HelpLink, portableEntry.HelpLink = manifest.CurrentLocalization.Get < Manifest::Localization::PublisherSupportUrl>());
+ const std::filesystem::path& targetFullPath = targetInstallDirectory / fileName;
+ entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, {})));
+ entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetFullPath)));
}
- void EnsureValidArgsForPortableInstall(Execution::Context& context)
- {
- std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename);
-
- try
- {
- if (MakeSuitablePathPart(renameArg) != renameArg)
- {
- context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl;
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS);
- }
- }
- catch (...)
- {
- context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl;
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS);
- }
- }
-
- void EnsureVolumeSupportsReparsePoints(Execution::Context& context)
- {
- Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
- const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(scope);
-
- if (!AppInstaller::Filesystem::SupportsReparsePoints(symlinkDirectory))
- {
- context.Reporter.Error() << Resource::String::ReparsePointsNotSupportedError << std::endl;
- AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED);
- }
- }
-
- void EnsureRunningAsAdminForMachineScopeInstall(Execution::Context& context)
- {
- // Admin is required for machine scope install or else creating a symlink in the %PROGRAMFILES% link location will fail.
- Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
- if (scope == Manifest::ScopeEnum::Machine)
- {
- context << Workflow::EnsureRunningAsAdmin;
- }
- }
+ return entries;
}
-
+
void PortableInstallImpl(Execution::Context& context)
{
- Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown;
- bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate);
- if (isUpdate)
- {
- IPackageVersion::Metadata installationMetadata = context.Get()->GetMetadata();
- auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope);
- if (installerScopeItr != installationMetadata.end())
- {
- scope = Manifest::ConvertToScopeEnum(installerScopeItr->second);
- }
- }
- else
- {
- scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
- }
-
- PortableARPEntry uninstallEntry = PortableARPEntry(
- scope,
- context.Get()->Arch,
- GetPortableProductCode(context));
-
- PortableEntry portableEntry = PortableEntry(uninstallEntry);
- context.Add(std::move(portableEntry));
+ PortableInstaller& portableInstaller = context.Get();
try
{
context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl;
- context <<
- InitializePortableARPEntry <<
- MovePortableExe <<
- CreatePortableSymlink <<
- AddToPathVariable <<
- CommitPortableMetadataToRegistry;
+ std::vector desiredState = GetDesiredStateForPortableInstall(context);
+
+ portableInstaller.SetDesiredState(desiredState);
+
+ if (!portableInstaller.VerifyExpectedState())
+ {
+ if (context.Args.Contains(Execution::Args::Type::HashOverride))
+ {
+ context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl;
+ }
+ else
+ {
+ context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED);
+ }
+ }
- context.Add(context.GetTerminationHR());
+ portableInstaller.Install();
+ context.Add(ERROR_SUCCESS);
+ context.Reporter.Warn() << portableInstaller.GetOutputMessage();
}
catch (...)
{
context.Add(Workflow::HandleException(context, std::current_exception()));
- }
-
- // Reset termination to allow for ReportInstallResult to process return code.
- context.ResetTermination();
-
- // Perform cleanup only if the install fails and is not an update.
- const auto& installReturnCode = context.Get();
-
- if (installReturnCode != 0 && installReturnCode != APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS && !isUpdate)
- {
- context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl;
- auto uninstallPortableContextPtr = context.CreateSubContext();
- Execution::Context& uninstallPortableContext = *uninstallPortableContextPtr;
- auto previousThreadGlobals = uninstallPortableContext.SetForCurrentThread();
- uninstallPortableContext.Add(context.Get());
- uninstallPortableContext << PortableUninstallImpl;
+ if (!portableInstaller.IsUpdate)
+ {
+ context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl;
+ portableInstaller.PrepareForCleanUp();
+; portableInstaller.Uninstall();
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED);
+ }
}
}
void PortableUninstallImpl(Execution::Context& context)
{
+ PortableInstaller& portableInstaller = context.Get();
+
try
{
- context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl;
+ context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl;
+
+ if (!portableInstaller.VerifyExpectedState())
+ {
+ // TODO: replace with appropriate --force argument when available.
+ if (context.Args.Contains(Execution::Args::Type::HashOverride))
+ {
+ context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl;
+ }
+ else
+ {
+ context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED);
+ }
+ }
- context <<
- RemovePortableExe <<
- RemoveInstallDirectory <<
- RemovePortableSymlink <<
- RemoveFromPathVariable <<
- RemovePortableARPEntry;
+ portableInstaller.Purge = context.Args.Contains(Execution::Args::Type::Purge) ||
+ (!portableInstaller.IsUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve));
- context.Add(context.GetTerminationHR());
+ portableInstaller.Uninstall();
+ context.Add(ERROR_SUCCESS);
+ context.Reporter.Warn() << portableInstaller.GetOutputMessage();
}
catch (...)
{
context.Add(Workflow::HandleException(context, std::current_exception()));
}
-
- // Reset termination to allow for ReportUninstallResult to process return code.
- context.ResetTermination();
}
void EnsureSupportForPortableInstall(Execution::Context& context)
@@ -555,16 +355,4 @@ namespace AppInstaller::CLI::Workflow
}
}
}
-
- // TODO: remove this check once support for portable in archive has been implemented
- void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context)
- {
- auto nestedInstallerType = context.Get().value().NestedInstallerType;
-
- if (nestedInstallerType == InstallerTypeEnum::Portable)
- {
- context.Reporter.Error() << Resource::String::PortableInstallFromArchiveNotSupported << std::endl;
- AICLI_TERMINATE_CONTEXT(ERROR_NOT_SUPPORTED);
- }
- }
}
\ No newline at end of file
diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h
index 1c2b9d8105..4b1c1032ca 100644
--- a/src/AppInstallerCLICore/Workflows/PortableFlow.h
+++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h
@@ -17,9 +17,27 @@ namespace AppInstaller::CLI::Workflow
// Outputs: None
void PortableUninstallImpl(Execution::Context& context);
+ // Verifies that the portable install operation is supported.
+ // Required Args: None
+ // Inputs: Scope, Rename
+ // Outputs: None
void EnsureSupportForPortableInstall(Execution::Context& context);
+ // Verifies that the portable uninstall operation is supported.
+ // Required Args: None
+ // Inputs: Scope
+ // Outputs: None
void EnsureSupportForPortableUninstall(Execution::Context& context);
- void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context);
+ // Initializes the portable installer.
+ // Required Args: None
+ // Inputs: Scope, Architecture, Manifest, Installer
+ // Outputs: None
+ void InitializePortableInstaller(Execution::Context& context);
+
+ // Verifies that the package identifier and the source identifier match the ARP entry.
+ // Required Args: None
+ // Inputs: Manifest, PackageVersion, PortableInstaller
+ // Outputs: None
+ void VerifyPackageAndSourceMatch(Execution::Context& context);
}
\ No newline at end of file
diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp
index 26aa633ed5..ae79875266 100644
--- a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp
@@ -7,8 +7,6 @@
#include "ShellExecuteInstallerHandler.h"
#include "AppInstallerMsixInfo.h"
#include "PortableFlow.h"
-#include "winget/PortableARPEntry.h"
-
#include
using namespace AppInstaller::CLI::Execution;
@@ -16,6 +14,7 @@ using namespace AppInstaller::Manifest;
using namespace AppInstaller::Msix;
using namespace AppInstaller::Repository;
using namespace AppInstaller::Registry;
+using namespace AppInstaller::CLI::Portable;
namespace AppInstaller::CLI::Workflow
{
@@ -139,15 +138,14 @@ namespace AppInstaller::CLI::Workflow
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND);
}
- const std::string installedScope = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope];
+ const std::string installedScope = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope];
const std::string installedArch = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture];
- Registry::Portable::PortableARPEntry uninstallEntry = Registry::Portable::PortableARPEntry(
- ConvertToScopeEnum(installedScope),
- Utility::ConvertToArchitectureEnum(installedArch),
+
+ PortableInstaller portableInstaller = PortableInstaller(
+ Manifest::ConvertToScopeEnum(installedScope),
+ Utility::ConvertToArchitectureEnum(installedArch),
productCodes[0]);
- Portable::PortableEntry portableEntry = Portable::PortableEntry(uninstallEntry);
-
- context.Add(portableEntry);
+ context.Add(std::move(portableInstaller));
break;
}
default:
diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs
index 503276b9eb..56655dfdba 100644
--- a/src/AppInstallerCLIE2ETests/InstallCommand.cs
+++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs
@@ -161,7 +161,7 @@ public void InstallPortableExe()
packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
commandAlias = fileName = "AppInstallerTestExeInstaller.exe";
- var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe");
+ var result = TestCommon.RunAICLICommand("install", $"{packageId}");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully installed"));
// If no location specified, default behavior is to create a package directory with the name "{packageId}_{sourceId}"
@@ -174,7 +174,7 @@ public void InstallPortableExeWithCommand()
var installDir = TestCommon.GetRandomTestDir();
string packageId, commandAlias, fileName, productCode;
packageId = "AppInstallerTest.TestPortableExeWithCommand";
- productCode = packageId + "_" + Constants.TestSourceIdentifier;
+ productCode = packageId + "_" + Constants.TestSourceIdentifier;
fileName = "AppInstallerTestExeInstaller.exe";
commandAlias = "testCommand.exe";
@@ -237,7 +237,7 @@ public void InstallPortableToExistingDirectory()
productCode = packageId + "_" + Constants.TestSourceIdentifier;
commandAlias = fileName = "AppInstallerTestExeInstaller.exe";
- var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestPortableExe -l {existingDir}");
+ var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {existingDir}");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully installed"));
TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true);
@@ -246,26 +246,23 @@ public void InstallPortableToExistingDirectory()
[Test]
public void InstallPortableFailsWithCleanup()
{
- string installDir = TestCommon.GetPortablePackagesDirectory();
- string winGetDir = Directory.GetParent(installDir).FullName;
- string packageId, commandAlias, fileName, packageDirName, productCode;
+ string packageId, commandAlias;
packageId = "AppInstallerTest.TestPortableExe";
- packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
- commandAlias = fileName = "AppInstallerTestExeInstaller.exe";
+ commandAlias = "AppInstallerTestExeInstaller.exe";
// Create a directory with the same name as the symlink in order to cause install to fail.
string symlinkDirectory = TestCommon.GetPortableSymlinkDirectory(TestCommon.Scope.User);
string conflictDirectory = Path.Combine(symlinkDirectory, commandAlias);
+
Directory.CreateDirectory(conflictDirectory);
- var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe");
+ var result = TestCommon.RunAICLICommand("install", $"{packageId}");
// Remove directory prior to assertions as this will impact other tests if assertions fail.
Directory.Delete(conflictDirectory, true);
Assert.AreNotEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Unable to create symlink, path points to a directory."));
- TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false);
}
[Test]
@@ -277,7 +274,7 @@ public void ReinstallPortable()
packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
commandAlias = fileName = "AppInstallerTestExeInstaller.exe";
- var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe");
+ var result = TestCommon.RunAICLICommand("install", $"{packageId}");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
string symlinkDirectory = TestCommon.GetPortableSymlinkDirectory(TestCommon.Scope.User);
@@ -288,10 +285,9 @@ public void ReinstallPortable()
Assert.False(result.StdOut.Contains($"Overwriting existing file: {symlinkPath}"));
// Perform second install and verify that file overwrite message is displayed.
- var result2 = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe");
+ var result2 = TestCommon.RunAICLICommand("install", $"{packageId}");
Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode);
Assert.True(result2.StdOut.Contains("Successfully installed"));
- Assert.True(result2.StdOut.Contains($"Overwriting existing file: {symlinkPath}"));
TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true);
}
@@ -307,7 +303,7 @@ public void InstallPortable_UserScope()
packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
commandAlias = fileName = "AppInstallerTestExeInstaller.exe";
- var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe --scope user");
+ var result = TestCommon.RunAICLICommand("install", $"{packageId} --scope user");
ConfigureInstallBehavior(Constants.PortablePackageUserRoot, string.Empty);
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully installed"));
@@ -325,7 +321,7 @@ public void InstallPortable_MachineScope()
packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
commandAlias = fileName = "AppInstallerTestExeInstaller.exe";
- var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe --scope machine");
+ var result = TestCommon.RunAICLICommand("install", $"{packageId} --scope machine");
ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, string.Empty);
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully installed"));
@@ -333,7 +329,7 @@ public void InstallPortable_MachineScope()
}
[Test]
- public void InstallZipWithExe()
+ public void InstallZip_Exe()
{
var installDir = TestCommon.GetRandomTestDir();
var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithExe --silent -l {installDir}");
@@ -342,6 +338,22 @@ public void InstallZipWithExe()
Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom"));
}
+ [Test]
+ public void InstallZip_Portable()
+ {
+ string installDir = TestCommon.GetPortablePackagesDirectory();
+ string packageId, commandAlias, fileName, packageDirName, productCode;
+ packageId = "AppInstallerTest.TestZipInstallerWithPortable";
+ packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
+ commandAlias = "TestPortable.exe";
+ fileName = "AppInstallerTestExeInstaller.exe";
+
+ var result = TestCommon.RunAICLICommand("install", $"{packageId}");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Successfully installed"));
+ TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.User);
+ }
+
[Test]
public void InstallZipWithInvalidRelativeFilePath()
{
diff --git a/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs
index 486118aa95..9ae0deba8d 100644
--- a/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs
+++ b/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs
@@ -198,7 +198,7 @@ public async Task UninstallPortableModifiedSymlink()
// Uninstall
var uninstallResult = await packageManager.UninstallPackageAsync(searchResult.CatalogPackage, TestFactory.CreateUninstallOptions());
- Assert.AreEqual(UninstallResultStatus.Ok, uninstallResult.Status);
+ Assert.AreEqual(UninstallResultStatus.UninstallError, uninstallResult.Status);
Assert.True(modifiedSymlinkInfo.Exists, "Modified symlink should still exist");
// Remove modified symlink as to not interfere with other tests
diff --git a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs
index ee22d312f0..8b7beab395 100644
--- a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs
+++ b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs
@@ -108,7 +108,7 @@ public async Task UpgradePortableARPMismatch()
// Upgrade
var upgradeResult = await packageManager.UpgradePackageAsync(searchResult.CatalogPackage, upgradeOptions);
Assert.AreEqual(InstallResultStatus.InstallError, upgradeResult.Status);
- Assert.AreEqual(Constants.ErrorCode.ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS, (int)upgradeResult.InstallerErrorCode);
+ Assert.AreEqual(Constants.ErrorCode.ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS, upgradeResult.ExtendedErrorCode.HResult);
// Find package again, it should have not been upgraded
searchResult = FindOnePackage(compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId);
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.2.0.0.0.yaml
new file mode 100644
index 0000000000..5acafad386
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.2.0.0.0.yaml
@@ -0,0 +1,18 @@
+PackageIdentifier: AppInstallerTest.TestZipInstallerWithPortable
+PackageVersion: 2.0.0.0
+PackageName: TestZipInstallerWithPortable
+PackageLocale: en-US
+Publisher: AppInstallerTest
+License: Test
+ShortDescription: E2E test for installing a zip with portable.
+Installers:
+ - Architecture: x64
+ InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip
+ InstallerType: zip
+ InstallerSha256:
+ NestedInstallerType: portable
+ NestedInstallerFiles:
+ - RelativeFilePath: AppInstallerTestExeInstaller.exe
+ PortableCommandAlias: TestPortable
+ManifestType: singleton
+ManifestVersion: 1.4.0
\ No newline at end of file
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.yaml
new file mode 100644
index 0000000000..8f7b0dae36
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.yaml
@@ -0,0 +1,18 @@
+PackageIdentifier: AppInstallerTest.TestZipInstallerWithPortable
+PackageVersion: 1.0.0.0
+PackageName: TestZipInstallerWithPortable
+PackageLocale: en-US
+Publisher: AppInstallerTest
+License: Test
+ShortDescription: E2E test for installing a zip with portable.
+Installers:
+ - Architecture: x64
+ InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip
+ InstallerType: zip
+ InstallerSha256:
+ NestedInstallerType: portable
+ NestedInstallerFiles:
+ - RelativeFilePath: AppInstallerTestExeInstaller.exe
+ PortableCommandAlias: TestPortable
+ManifestType: singleton
+ManifestVersion: 1.4.0
\ No newline at end of file
diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs
index 9be0cb8943..61a15dacbb 100644
--- a/src/AppInstallerCLIE2ETests/UninstallCommand.cs
+++ b/src/AppInstallerCLIE2ETests/UninstallCommand.cs
@@ -4,8 +4,8 @@
namespace AppInstallerCLIE2ETests
{
using NUnit.Framework;
- using System.IO;
-
+ using System.IO;
+
public class UninstallCommand : BaseCommand
{
// Custom product code for overriding the default in the test exe
@@ -81,7 +81,7 @@ public void UninstallPortable()
public void UninstallPortableWithProductCode()
{
// Uninstall a Portable with ProductCode
- string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages");
+ string installDir = TestCommon.GetPortablePackagesDirectory();
string packageId, commandAlias, fileName, packageDirName, productCode;
packageId = "AppInstallerTest.TestPortableExe";
packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
@@ -97,30 +97,53 @@ public void UninstallPortableWithProductCode()
[Test]
public void UninstallPortableModifiedSymlink()
{
- string packageId, commandAlias;
+ string installDir = TestCommon.GetPortablePackagesDirectory();
+ string packageId, commandAlias, fileName, packageDirName, productCode;
packageId = "AppInstallerTest.TestPortableExe";
- commandAlias = "AppInstallerTestExeInstaller.exe";
+ packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
+ commandAlias = fileName = "AppInstallerTestExeInstaller.exe";
TestCommon.RunAICLICommand("install", $"{packageId}");
- string symlinkDirectory = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Links");
+ string symlinkDirectory = TestCommon.GetPortableSymlinkDirectory(TestCommon.Scope.User);
string symlinkPath = Path.Combine(symlinkDirectory, commandAlias);
// Replace symlink with modified symlink
File.Delete(symlinkPath);
FileSystemInfo modifiedSymlinkInfo = File.CreateSymbolicLink(symlinkPath, "fakeTargetExe");
+
var result = TestCommon.RunAICLICommand("uninstall", $"{packageId}");
+ Assert.AreEqual(Constants.ErrorCode.ERROR_PORTABLE_UNINSTALL_FAILED, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Unable to remove Portable package as it has been modified; to override this check use --force"));
+ Assert.True(modifiedSymlinkInfo.Exists, "Modified symlink should still exist");
- // Remove modified symlink as to not interfere with other tests
- bool modifiedSymlinkExists = modifiedSymlinkInfo.Exists;
- modifiedSymlinkInfo.Delete();
+ // Try again with --force
+ var result2 = TestCommon.RunAICLICommand("uninstall", $"{packageId} --force");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode);
+ Assert.True(result2.StdOut.Contains("Portable package has been modified; proceeding due to --force"));
+ Assert.True(result2.StdOut.Contains("Successfully uninstalled"));
+
+ TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false);
+ }
+
+ [Test]
+ public void UninstallZip_Portable()
+ {
+ string installDir = TestCommon.GetPortablePackagesDirectory();
+ string packageId, commandAlias, fileName, packageDirName, productCode;
+ packageId = "AppInstallerTest.TestZipInstallerWithPortable";
+ packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
+ commandAlias = "TestPortable.exe";
+ fileName = "AppInstallerTestExeInstaller.exe";
+ var testreuslt = TestCommon.RunAICLICommand("install", $"{packageId}");
+ var result = TestCommon.RunAICLICommand("uninstall", $"{packageId}");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully uninstalled"));
- Assert.True(result.StdOut.Contains("Portable symlink not deleted as it was modified and points to a different target exe"));
- Assert.True(modifiedSymlinkExists, "Modified symlink should still exist");
+ TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false);
}
+
[Test]
public void UninstallNotIndexed()
{
diff --git a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs
index 460d060f1c..293fdc18b0 100644
--- a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs
+++ b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs
@@ -113,5 +113,25 @@ public void UpgradePortableMachineScope()
Assert.True(result2.StdOut.Contains("Successfully installed"));
TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.Machine);
}
+
+ [Test]
+ public void UpgradeZip_Portable()
+ {
+ string installDir = TestCommon.GetPortablePackagesDirectory();
+ string packageId, commandAlias, fileName, packageDirName, productCode;
+ packageId = "AppInstallerTest.TestZipInstallerWithPortable";
+ packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
+ commandAlias = "TestPortable.exe";
+ fileName = "AppInstallerTestExeInstaller.exe";
+
+ var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithPortable -v 1.0.0.0");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Successfully installed"));
+
+ var result2 = TestCommon.RunAICLICommand("upgrade", $"{packageId} -v 2.0.0.0");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode);
+ Assert.True(result2.StdOut.Contains("Successfully installed"));
+ TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.User);
+ }
}
}
diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
index 18c07fd368..ff41cfe009 100644
--- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
+++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
@@ -1344,11 +1344,11 @@ Please specify one of them using the `--source` option to proceed.
Both `purge` and `preserve` arguments are provided
- Portable exe has been modified; proceeding due to --force
+ Portable package has been modified; proceeding due to --force
{Locked="--force"}
- Unable to remove Portable exe as it has been modified; to override this check use --force
+ Unable to remove Portable package as it has been modified; to override this check use --force
{Locked="--force"}
@@ -1372,15 +1372,9 @@ Please specify one of them using the `--source` option to proceed.
Installation Notes:
-
- Portable symlink not deleted as it was modified and points to a different target exe
-
A provided argument is not supported for this package
-
- Installing a portable package from an archive is not yet supported
-
Failed to extract the contents of the archive
@@ -1435,4 +1429,13 @@ Please specify one of them using the `--source` option to proceed.
Disable interactive prompts
Description for a command line argument, shown next to it in the help
+
+ Portable package from a different source already exists
+
+
+ Successfully extracted archive
+
+
+ Extracting archive...
+
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index 50ee1604ff..fbf1d266d4 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -213,7 +213,7 @@
-
+
@@ -298,7 +298,7 @@
true
-
+
true
@@ -439,6 +439,12 @@
true
+
+ true
+
+
+ true
+
true
@@ -634,7 +640,7 @@
true
-
+
true
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index 64d36f600b..ece41c6e45 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -227,7 +227,7 @@
Source Files\Common
-
+
Source Files\Common
@@ -318,6 +318,12 @@
TestData
+
+ TestData
+
+
+ TestData
+
TestData
@@ -537,7 +543,7 @@
TestData
-
+
TestData
@@ -588,7 +594,7 @@
TestData
-
+
TestData
diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp
index 304f27965b..7354c3ed9f 100644
--- a/src/AppInstallerCLITests/Archive.cpp
+++ b/src/AppInstallerCLITests/Archive.cpp
@@ -19,6 +19,7 @@ TEST_CASE("Extract_ZipArchive", "[archive]")
HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath);
+ std::filesystem::path expectedPath = tempDirectoryPath / "test.txt";
REQUIRE(SUCCEEDED(hr));
- REQUIRE(std::filesystem::exists(tempDirectoryPath / "test.txt"));
+ REQUIRE(std::filesystem::exists(expectedPath));
}
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/Filesystem.cpp b/src/AppInstallerCLITests/Filesystem.cpp
index 88af44fb43..2e851cac06 100644
--- a/src/AppInstallerCLITests/Filesystem.cpp
+++ b/src/AppInstallerCLITests/Filesystem.cpp
@@ -28,4 +28,32 @@ TEST_CASE("PathEscapesDirectory", "[filesystem]")
REQUIRE(PathEscapesBaseDirectory(badPath2, basePath));
REQUIRE_FALSE(PathEscapesBaseDirectory(goodPath, basePath));
REQUIRE_FALSE(PathEscapesBaseDirectory(goodPath2, basePath));
+}
+
+TEST_CASE("VerifySymlink", "[filesystem]")
+{
+ TestCommon::TempDirectory tempDirectory("TempDirectory");
+ const std::filesystem::path& basePath = tempDirectory.GetPath();
+
+ std::filesystem::path testFilePath = basePath / "testFile.txt";
+ std::filesystem::path symlinkPath = basePath / "symlink.exe";
+
+ TestCommon::TempFile testFile(testFilePath);
+ std::ofstream file2(testFile, std::ofstream::out);
+ file2.close();
+
+ std::filesystem::create_symlink(testFile.GetPath(), symlinkPath);
+
+ REQUIRE(SymlinkExists(symlinkPath));
+ REQUIRE(VerifySymlink(symlinkPath, testFilePath));
+ REQUIRE_FALSE(VerifySymlink(symlinkPath, "badPath"));
+
+ std::filesystem::remove(testFilePath);
+
+ // Ensure that symlink existence does not check the target
+ REQUIRE(SymlinkExists(symlinkPath));
+
+ std::filesystem::remove(symlinkPath);
+
+ REQUIRE_FALSE(SymlinkExists(symlinkPath));
}
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp
deleted file mode 100644
index df18a69bab..0000000000
--- a/src/AppInstallerCLITests/PortableEntry.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-#include "pch.h"
-#include "TestCommon.h"
-#include
-#include
-#include
-#include
-
-using namespace AppInstaller::Portable;
-using namespace AppInstaller::Utility;
-using namespace TestCommon;
-
-TEST_CASE("VerifyPortableMove", "[PortableEntry]")
-{
- PortableARPEntry testARPEntry = PortableARPEntry(
- AppInstaller::Manifest::ScopeEnum::User,
- Architecture::X64,
- "testProductCode");
-
- PortableEntry testEntry = PortableEntry(testARPEntry);
- TestCommon::TempDirectory tempDirectory("TempDirectory", false);
- testEntry.InstallLocation = tempDirectory.GetPath();
- testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "output.txt";
-
- TestCommon::TempFile testFile("input.txt");
- std::ofstream file(testFile.GetPath(), std::ofstream::out);
- file.close();
-
- testEntry.MovePortableExe(testFile.GetPath());
- REQUIRE(std::filesystem::exists(testEntry.PortableTargetFullPath));
- REQUIRE(testEntry.InstallDirectoryCreated);
-
- // Create a second PortableEntry instance to emulate installing for a second time. (ARP entry should already exist)
- PortableARPEntry testARPEntry2 = PortableARPEntry(
- AppInstaller::Manifest::ScopeEnum::User,
- Architecture::X64,
- "testProductCode");
-
- PortableEntry testEntry2 = PortableEntry(testARPEntry2);
- REQUIRE(testEntry2.InstallDirectoryCreated); // InstallDirectoryCreated should already be initialized as true.
-
- testEntry2.InstallLocation = tempDirectory.GetPath();
- testEntry2.PortableTargetFullPath = tempDirectory.GetPath() / "output2.txt";
-
- TestCommon::TempFile testFile2("input2.txt");
- std::ofstream file2(testFile2, std::ofstream::out);
- file2.close();
-
- testEntry2.MovePortableExe(testFile2.GetPath());
- REQUIRE(std::filesystem::exists(testEntry2.PortableTargetFullPath));
- // InstallDirectoryCreated value should be preserved even though the directory was not created;
- REQUIRE(testEntry2.InstallDirectoryCreated);
- testEntry2.RemoveARPEntry();
-}
-
-TEST_CASE("VerifySymlinkCheck", "[PortableEntry]")
-{
- PortableARPEntry testARPEntry = PortableARPEntry(
- AppInstaller::Manifest::ScopeEnum::User,
- Architecture::X64,
- "testProductCode");
-
- PortableEntry testEntry = PortableEntry(testARPEntry);
-
- TestCommon::TempFile testFile("target.txt");
- std::ofstream file(testFile.GetPath(), std::ofstream::out);
- file.close();
-
- TestCommon::TempDirectory tempDirectory("TempDirectory", true);
- testEntry.PortableTargetFullPath = testFile.GetPath();
- testEntry.PortableSymlinkFullPath = tempDirectory.GetPath() / "symlink.exe";
-
- testEntry.CreatePortableSymlink();
-
- REQUIRE(testEntry.VerifySymlinkTarget());
-
- // Modify with incorrect target full path.
- testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "invalidTarget.txt";
- REQUIRE_FALSE(testEntry.VerifySymlinkTarget());
- testEntry.RemoveARPEntry();
-}
-
-TEST_CASE("VerifyPathVariableModified", "[PortableEntry]")
-{
- PortableARPEntry testARPEntry = PortableARPEntry(
- AppInstaller::Manifest::ScopeEnum::User,
- Architecture::X64,
- "testProductCode");
-
- PortableEntry testEntry = PortableEntry(testARPEntry);
- testEntry.InstallDirectoryAddedToPath = true;
- TestCommon::TempDirectory tempDirectory("TempDirectory", false);
- const std::filesystem::path& pathValue = tempDirectory.GetPath();
- testEntry.InstallLocation = pathValue;
- testEntry.AddToPathVariable();
-
- AppInstaller::Registry::Environment::PathVariable pathVariable(AppInstaller::Manifest::ScopeEnum::User);
- REQUIRE(pathVariable.Contains(pathValue));
-
- testEntry.RemoveFromPathVariable();
- REQUIRE_FALSE(pathVariable.Contains(pathValue));
- testEntry.RemoveARPEntry();
-}
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/PortableIndex.cpp b/src/AppInstallerCLITests/PortableIndex.cpp
index aadc1bf5b9..06410e8cc0 100644
--- a/src/AppInstallerCLITests/PortableIndex.cpp
+++ b/src/AppInstallerCLITests/PortableIndex.cpp
@@ -7,17 +7,19 @@
#include
#include
#include
+#include
using namespace std::string_literals;
using namespace TestCommon;
+using namespace AppInstaller::Portable;
using namespace AppInstaller::Repository::Microsoft;
using namespace AppInstaller::Repository::SQLite;
using namespace AppInstaller::Repository::Microsoft::Schema;
-void CreateFakePortableFile(IPortableIndex::PortableFile& file)
+void CreateFakePortableFile(PortableFileEntry& file)
{
file.SetFilePath("testPortableFile.exe");
- file.FileType = IPortableIndex::PortableFileType::File;
+ file.FileType = PortableFileType::File;
file.SHA256 = "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b";
file.SymlinkTarget = "testSymlinkTarget.exe";
}
@@ -65,7 +67,7 @@ TEST_CASE("PortableIndexAddEntryToTable", "[portableIndex]")
TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
INFO("Using temporary file named: " << tempFile.GetPath());
- IPortableIndex::PortableFile portableFile;
+ PortableFileEntry portableFile;
CreateFakePortableFile(portableFile);
{
@@ -96,7 +98,7 @@ TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]")
TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
INFO("Using temporary file named: " << tempFile.GetPath());
- IPortableIndex::PortableFile portableFile;
+ PortableFileEntry portableFile;
CreateFakePortableFile(portableFile);
PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
@@ -104,7 +106,7 @@ TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]")
// Apply changes to portable file
std::string updatedHash = "2db8ae7657c6622b04700137740002c51c36588e566651c9f67b4b096c8ad18b";
- portableFile.FileType = IPortableIndex::PortableFileType::Symlink;
+ portableFile.FileType = PortableFileType::Symlink;
portableFile.SHA256 = updatedHash;
portableFile.SymlinkTarget = "fakeSymlinkTarget.exe";
@@ -114,8 +116,8 @@ TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]")
Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly);
auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1);
REQUIRE(fileFromIndex.has_value());
- REQUIRE(fileFromIndex->GetFilePath() == "testPortableFile.exe");
- REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink);
+ REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath());
+ REQUIRE(fileFromIndex->FileType == PortableFileType::Symlink);
REQUIRE(fileFromIndex->SHA256 == updatedHash);
REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe");
}
@@ -137,7 +139,7 @@ TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]")
TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
INFO("Using temporary file named: " << tempFile.GetPath());
- IPortableIndex::PortableFile portableFile;
+ PortableFileEntry portableFile;
CreateFakePortableFile(portableFile);
PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
@@ -147,7 +149,7 @@ TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]")
// Change file path to all upper case should still successfully update.
portableFile.SetFilePath("TESTPORTABLEFILE.exe");
std::string updatedHash = "2db8ae7657c6622b04700137740002c51c36588e566651c9f67b4b096c8ad18b";
- portableFile.FileType = IPortableIndex::PortableFileType::Symlink;
+ portableFile.FileType = PortableFileType::Symlink;
portableFile.SHA256 = updatedHash;
portableFile.SymlinkTarget = "fakeSymlinkTarget.exe";
@@ -157,8 +159,8 @@ TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]")
Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly);
auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1);
REQUIRE(fileFromIndex.has_value());
- REQUIRE(fileFromIndex->GetFilePath() == "TESTPORTABLEFILE.exe");
- REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink);
+ REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath());
+ REQUIRE(fileFromIndex->FileType == PortableFileType::Symlink);
REQUIRE(fileFromIndex->SHA256 == updatedHash);
REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe");
}
@@ -169,7 +171,7 @@ TEST_CASE("PortableIndex_AddDuplicateFile", "[portableIndex]")
TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
INFO("Using temporary file named: " << tempFile.GetPath());
- IPortableIndex::PortableFile portableFile;
+ PortableFileEntry portableFile;
CreateFakePortableFile(portableFile);
PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
@@ -185,7 +187,7 @@ TEST_CASE("PortableIndex_RemoveWithId", "[portableIndex]")
TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
INFO("Using temporary file named: " << tempFile.GetPath());
- IPortableIndex::PortableFile portableFile;
+ PortableFileEntry portableFile;
CreateFakePortableFile(portableFile);
PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp
new file mode 100644
index 0000000000..a9953daae9
--- /dev/null
+++ b/src/AppInstallerCLITests/PortableInstaller.cpp
@@ -0,0 +1,174 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "TestCommon.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace std::string_literals;
+using namespace AppInstaller::CLI::Portable;
+using namespace AppInstaller::Filesystem;
+using namespace AppInstaller::Manifest;
+using namespace AppInstaller::Registry::Environment;
+using namespace AppInstaller::Repository::Microsoft;
+using namespace AppInstaller::Repository::SQLite;
+using namespace AppInstaller::Repository::Microsoft::Schema;
+using namespace AppInstaller::Utility;
+using namespace TestCommon;
+
+TEST_CASE("PortableInstaller_InstallToRegistry", "[PortableInstaller]")
+{
+ TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false);
+
+ std::vector desiredTestState;
+
+ TestCommon::TempFile testPortable("testPortable.txt");
+ std::ofstream file(testPortable, std::ofstream::out);
+ file.close();
+
+ std::filesystem::path targetPath = tempDirectory.GetPath() / "testPortable.txt";
+ std::filesystem::path symlinkPath = tempDirectory.GetPath() / "testSymlink.exe";
+
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable.GetPath(), targetPath, {})));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath, targetPath)));
+
+ PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode");
+ portableInstaller.TargetInstallLocation = tempDirectory.GetPath();
+ portableInstaller.SetDesiredState(desiredTestState);
+ REQUIRE(portableInstaller.VerifyExpectedState());
+
+ portableInstaller.Install();
+
+ PortableInstaller portableInstaller2 = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode");
+ REQUIRE(portableInstaller2.ARPEntryExists());
+ REQUIRE(std::filesystem::exists(portableInstaller2.PortableTargetFullPath));
+ REQUIRE(AppInstaller::Filesystem::SymlinkExists(portableInstaller2.PortableSymlinkFullPath));
+
+ portableInstaller2.Uninstall();
+ REQUIRE_FALSE(std::filesystem::exists(portableInstaller2.PortableTargetFullPath));
+ REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(portableInstaller2.PortableSymlinkFullPath));
+ REQUIRE_FALSE(std::filesystem::exists(portableInstaller2.InstallLocation));
+}
+
+TEST_CASE("PortableInstaller_InstallToIndex_CreateInstallRoot", "[PortableInstaller]")
+{
+ TempDirectory installRootDirectory = TestCommon::TempDirectory("PortableInstallRoot", false);
+
+ std::vector desiredTestState;
+
+ TestCommon::TempFile testPortable("testPortable.txt");
+ std::ofstream file1(testPortable, std::ofstream::out);
+ file1.close();
+
+ TestCommon::TempFile testPortable2("testPortable2.txt");
+ std::ofstream file2(testPortable2, std::ofstream::out);
+ file2.close();
+
+ TestCommon::TempDirectory testDirectoryFolder("testDirectory", true);
+
+ std::filesystem::path installRootPath = installRootDirectory.GetPath();
+ std::filesystem::path targetPath = installRootPath / "testPortable.txt";
+ std::filesystem::path targetPath2 = installRootPath / "testPortable2.txt";
+ std::filesystem::path symlinkPath = installRootPath / "testSymlink.exe";
+ std::filesystem::path symlinkPath2 = installRootPath / "testSymlink2.exe";
+ std::filesystem::path directoryPath = installRootPath / "testDirectory";
+
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable.GetPath(), targetPath, {})));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable2.GetPath(), targetPath2, {})));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath, targetPath)));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath2, targetPath2)));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(testDirectoryFolder.GetPath(), directoryPath)));
+
+ PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode");
+ portableInstaller.TargetInstallLocation = installRootDirectory.GetPath();
+ portableInstaller.RecordToIndex = true;
+ portableInstaller.SetDesiredState(desiredTestState);
+ REQUIRE(portableInstaller.VerifyExpectedState());
+
+ portableInstaller.Install();
+
+ REQUIRE(std::filesystem::exists(installRootPath / portableInstaller.GetPortableIndexFileName()));
+ REQUIRE(std::filesystem::exists(targetPath));
+ REQUIRE(std::filesystem::exists(targetPath2));
+ REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath));
+ REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2));
+ REQUIRE(std::filesystem::exists(directoryPath));
+
+ PortableInstaller portableInstaller2 = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode");
+ REQUIRE(portableInstaller2.ARPEntryExists());
+
+ portableInstaller2.Uninstall();
+
+ // Install root directory should be removed since it was created.
+ REQUIRE_FALSE(std::filesystem::exists(installRootPath));
+ REQUIRE_FALSE(std::filesystem::exists(targetPath));
+ REQUIRE_FALSE(std::filesystem::exists(targetPath2));
+ REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath));
+ REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2));
+ REQUIRE_FALSE(std::filesystem::exists(directoryPath));
+}
+
+TEST_CASE("PortableInstaller_InstallToIndex_ExistingInstallRoot", "[PortableInstaller]")
+{
+ TempDirectory installRootDirectory = TestCommon::TempDirectory("PortableInstallRoot", true);
+
+ std::vector desiredTestState;
+
+ TestCommon::TempFile testPortable("testPortable.txt");
+ std::ofstream file1(testPortable, std::ofstream::out);
+ file1.close();
+
+ TestCommon::TempFile testPortable2("testPortable2.txt");
+ std::ofstream file2(testPortable2, std::ofstream::out);
+ file2.close();
+
+ TestCommon::TempDirectory testDirectoryFolder("testDirectory", true);
+
+ std::filesystem::path installRootPath = installRootDirectory.GetPath();
+ std::filesystem::path targetPath = installRootPath / "testPortable.txt";
+ std::filesystem::path targetPath2 = installRootPath / "testPortable2.txt";
+ std::filesystem::path symlinkPath = installRootPath / "testSymlink.exe";
+ std::filesystem::path symlinkPath2 = installRootPath / "testSymlink2.exe";
+ std::filesystem::path directoryPath = installRootPath / "testDirectory";
+
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable.GetPath(), targetPath, {})));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable2.GetPath(), targetPath2, {})));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath, targetPath)));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath2, targetPath2)));
+ desiredTestState.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(testDirectoryFolder.GetPath(), directoryPath)));
+
+ PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode");
+ portableInstaller.TargetInstallLocation = installRootDirectory.GetPath();
+ portableInstaller.RecordToIndex = true;
+ portableInstaller.SetDesiredState(desiredTestState);
+ REQUIRE(portableInstaller.VerifyExpectedState());
+
+ portableInstaller.Install();
+
+ REQUIRE(std::filesystem::exists(installRootPath / portableInstaller.GetPortableIndexFileName()));
+ REQUIRE(std::filesystem::exists(targetPath));
+ REQUIRE(std::filesystem::exists(targetPath2));
+ REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath));
+ REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2));
+ REQUIRE(std::filesystem::exists(directoryPath));
+
+ PortableInstaller portableInstaller2 = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode");
+ REQUIRE(portableInstaller2.ARPEntryExists());
+
+ portableInstaller2.Uninstall();
+
+ // Install root directory should still exist since it was created previously.
+ REQUIRE(std::filesystem::exists(installRootPath));
+ REQUIRE_FALSE(std::filesystem::exists(targetPath));
+ REQUIRE_FALSE(std::filesystem::exists(targetPath2));
+ REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath));
+ REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2));
+ REQUIRE_FALSE(std::filesystem::exists(directoryPath));
+}
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_ZipWithExe.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Zip_Exe.yaml
similarity index 100%
rename from src/AppInstallerCLITests/TestData/InstallFlowTest_ZipWithExe.yaml
rename to src/AppInstallerCLITests/TestData/InstallFlowTest_Zip_Exe.yaml
diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml
new file mode 100644
index 0000000000..71f230b868
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml
@@ -0,0 +1,22 @@
+# Bad manifest. Installer type zip should not have any duplicate PortableCommandAlias values.
+PackageIdentifier: microsoft.msixsdk
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Installer
+Publisher: Microsoft Corporation
+Moniker: AICLITestExe
+License: Test
+ShortDescription: Test installer for zip without nestedInstallers specified
+Scope: User
+Installers:
+ - Architecture: x64
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: zip
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ NestedInstallerFiles:
+ - RelativeFilePath: relativeFilePath1
+ PortableCommandAlias: DUPLICATEALIAS
+ - RelativeFilePath: relativeFilePath2
+ PortableCommandAlias: duplicateAlias
+ManifestType: singleton
+ManifestVersion: 1.4.0
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml
new file mode 100644
index 0000000000..81286b78f8
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml
@@ -0,0 +1,22 @@
+# Bad manifest. Installer type zip should not have any duplicate PortableCommandAlias values.
+PackageIdentifier: microsoft.msixsdk
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Installer
+Publisher: Microsoft Corporation
+Moniker: AICLITestExe
+License: Test
+ShortDescription: Test installer for zip without nestedInstallers specified
+Scope: User
+Installers:
+ - Architecture: x64
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: zip
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ NestedInstallerFiles:
+ - RelativeFilePath: RELATIVEFILEPATH
+ PortableCommandAlias: alias1
+ - RelativeFilePath: relativefilepath
+ PortableCommandAlias: alias2
+ManifestType: singleton
+ManifestVersion: 1.4.0
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_ZipWithExe.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Zip_Exe.yaml
similarity index 100%
rename from src/AppInstallerCLITests/TestData/UpdateFlowTest_ZipWithExe.yaml
rename to src/AppInstallerCLITests/TestData/UpdateFlowTest_Zip_Exe.yaml
diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp
index 365aa73be5..b8981e86cc 100644
--- a/src/AppInstallerCLITests/WorkFlow.cpp
+++ b/src/AppInstallerCLITests/WorkFlow.cpp
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -55,6 +56,7 @@ using namespace AppInstaller::Repository;
using namespace AppInstaller::Settings;
using namespace AppInstaller::Utility;
using namespace AppInstaller::Settings;
+using namespace AppInstaller::CLI::Portable;
#define REQUIRE_TERMINATED_WITH(_context_,_hr_) \
@@ -225,8 +227,8 @@ namespace
if (input.empty() || input == "AppInstallerCliTest.TestZipInstaller")
{
- auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_ZipWithExe.yaml"));
- auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_ZipWithExe.yaml"));
+ auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml"));
+ auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Zip_Exe.yaml"));
result.Matches.emplace_back(
ResultMatch(
TestPackage::Make(
@@ -1040,7 +1042,7 @@ TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow][workflow]")
REQUIRE(!std::filesystem::exists(installResultPath.GetPath()));
}
-TEST_CASE("InstallFlow_ZipWithExe", "[InstallFlow][workflow]")
+TEST_CASE("InstallFlow_Zip_Exe", "[InstallFlow][workflow]")
{
TestCommon::TempFile installResultPath("TestExeInstalled.txt");
TestCommon::TestUserSettings testSettings;
@@ -1052,7 +1054,7 @@ TEST_CASE("InstallFlow_ZipWithExe", "[InstallFlow][workflow]")
OverrideForShellExecute(context);
OverrideForExtractInstallerFromArchive(context);
OverrideForVerifyAndSetNestedInstaller(context);
- context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string());
+ context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string());
InstallCommand install({});
install.Execute(context);
@@ -1079,7 +1081,7 @@ TEST_CASE("InstallFlow_Zip_BadRelativePath", "[InstallFlow][workflow]")
auto previousThreadGlobals = context.SetForCurrentThread();
OverrideForShellExecute(context);
OverrideForExtractInstallerFromArchive(context);
- context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string());
+ context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string());
InstallCommand install({});
install.Execute(context);
@@ -1163,7 +1165,7 @@ TEST_CASE("ExtractInstallerFromArchive_InvalidZip", "[InstallFlow][workflow]")
std::ostringstream installOutput;
TestContext context{ installOutput, std::cin };
auto previousThreadGlobals = context.SetForCurrentThread();
- auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_ZipWithExe.yaml"));
+ auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml"));
context.Add(manifest);
context.Add(manifest.Installers.at(0));
// Provide an invalid zip file which should be handled appropriately.
@@ -1716,7 +1718,7 @@ TEST_CASE("ShowFlow_NestedInstallerType", "[ShowFlow][workflow]")
std::ostringstream showOutput;
TestContext context{ showOutput, std::cin };
auto previousThreadGlobals = context.SetForCurrentThread();
- context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string());
+ context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string());
ShowCommand show({});
show.Execute(context);
@@ -1930,7 +1932,7 @@ TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow][workflow]")
REQUIRE(updateResultStr.find("/ver3.0.0.0") != std::string::npos);
}
-TEST_CASE("UpdateFlow_UpdateZipWithExe", "[UpdateFlow][workflow]")
+TEST_CASE("UpdateFlow_UpdateZip_Exe", "[UpdateFlow][workflow]")
{
TestCommon::TempFile updateResultPath("TestExeInstalled.txt");
diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp
index f80e60fd2e..d4edac4537 100644
--- a/src/AppInstallerCLITests/YamlManifest.cpp
+++ b/src/AppInstallerCLITests/YamlManifest.cpp
@@ -275,6 +275,8 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]")
{ "Manifest-Bad-InstallerTypePortable-InvalidAppsAndFeatures.yaml", "Only zero or one entry for Apps and Features may be specified for InstallerType portable." },
{ "Manifest-Bad-InstallerTypePortable-InvalidCommands.yaml", "Only zero or one value for Commands may be specified for InstallerType portable." },
{ "Manifest-Bad-InstallerTypePortable-InvalidScope.yaml", "Scope is not supported for InstallerType portable." },
+ { "Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml", "Duplicate portable command alias found." },
+ { "Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml", "Duplicate relative file path found." },
{ "Manifest-Bad-InstallerTypeZip-InvalidRelativeFilePath.yaml", "Relative file path must not point to a location outside of archive directory" },
{ "Manifest-Bad-InstallerTypeZip-MissingRelativeFilePath.yaml", "Required field missing. [RelativeFilePath]" },
{ "Manifest-Bad-InstallerTypeZip-MultipleNestedInstallers.yaml", "Only one entry for NestedInstallerFiles can be specified for non-portable InstallerTypes." },
diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
index 4b95471821..953e49792f 100644
--- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
+++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
@@ -330,7 +330,7 @@
-
+
@@ -407,7 +407,6 @@
-
diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
index e8a005b75b..574833856a 100644
--- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
+++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
@@ -192,7 +192,7 @@
Public\winget
-
+
Public\winget
@@ -365,9 +365,6 @@
Source Files
-
- Source Files
-
Source Files
diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp
index 41cbde0a12..a699e12fea 100644
--- a/src/AppInstallerCommonCore/Archive.cpp
+++ b/src/AppInstallerCommonCore/Archive.cpp
@@ -32,7 +32,7 @@ namespace AppInstaller::Archive
wil::com_ptr pShellItemFrom;
STRRET strFolderName;
WCHAR szFolderName[MAX_PATH];
- RETURN_IF_FAILED(pArchiveShellFolder->GetDisplayNameOf(pidlChild.get(), SHGDN_INFOLDER, &strFolderName));
+ RETURN_IF_FAILED(pArchiveShellFolder->GetDisplayNameOf(pidlChild.get(), SHGDN_INFOLDER | SHGDN_FORPARSING, &strFolderName));
RETURN_IF_FAILED(StrRetToBuf(&strFolderName, pidlChild.get(), szFolderName, MAX_PATH));
RETURN_IF_FAILED(SHCreateItemWithParent(pidlFull.get(), pArchiveShellFolder.get(), pidlChild.get(), IID_PPV_ARGS(&pShellItemFrom)));
RETURN_IF_FAILED(pFileOperation->CopyItem(pShellItemFrom.get(), pShellItemTo.get(), NULL, NULL));
diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp
index 346d5936a4..008ff9966d 100644
--- a/src/AppInstallerCommonCore/Errors.cpp
+++ b/src/AppInstallerCommonCore/Errors.cpp
@@ -180,8 +180,6 @@ namespace AppInstaller
return "Failed to install portable package";
case APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED:
return "Volume does not support reparse points.";
- case APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS:
- return "Portable package from a different source already exists.";
case APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY:
return "Unable to create symlink, path points to a directory.";
case APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION:
diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerCommonCore/Filesystem.cpp
index 652402aa5a..25d9bc2629 100644
--- a/src/AppInstallerCommonCore/Filesystem.cpp
+++ b/src/AppInstallerCommonCore/Filesystem.cpp
@@ -142,7 +142,7 @@ namespace AppInstaller::Filesystem
}
#endif
- bool CreateSymlink(const std::filesystem::path& to, const std::filesystem::path& target)
+ bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link)
{
#ifndef AICLI_DISABLE_TEST_HOOKS
if (s_CreateSymlinkResult_TestHook_Override)
@@ -152,7 +152,7 @@ namespace AppInstaller::Filesystem
#endif
try
{
- std::filesystem::create_symlink(to, target);
+ std::filesystem::create_symlink(target, link);
return true;
}
catch (std::filesystem::filesystem_error& error)
@@ -168,6 +168,25 @@ namespace AppInstaller::Filesystem
}
}
+ bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target)
+ {
+ const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(symlink);
+ return symlinkTargetPath == target;
+ }
+
+ void AppendExtension(std::filesystem::path& target, const std::string& value)
+ {
+ if (target.extension() != value)
+ {
+ target += value;
+ }
+ }
+
+ bool SymlinkExists(const std::filesystem::path& symlinkPath)
+ {
+ return std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath));
+ }
+
std::filesystem::path GetExpandedPath(const std::string& path)
{
std::string trimPath = path;
diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp
index a1e3f3943c..d761c3d7fc 100644
--- a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp
+++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp
@@ -497,6 +497,11 @@ namespace AppInstaller::Manifest
return (installerType == InstallerTypeEnum::Zip);
}
+ bool IsPortableType(InstallerTypeEnum installerType)
+ {
+ return (installerType == InstallerTypeEnum::Portable);
+ }
+
bool IsNestedInstallerTypeSupported(InstallerTypeEnum nestedInstallerType)
{
return (
diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
index c8b6b1c8aa..f27088b566 100644
--- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
+++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
@@ -32,6 +32,8 @@ namespace AppInstaller::Manifest
{ AppInstaller::Manifest::ManifestError::InstallerTypeDoesNotWriteAppsAndFeaturesEntry, "The specified installer type does not write to Apps and Features entry."sv },
{ AppInstaller::Manifest::ManifestError::IncompleteMultiFileManifest, "The multi file manifest is incomplete.A multi file manifest must contain at least version, installer and defaultLocale manifest."sv },
{ AppInstaller::Manifest::ManifestError::InconsistentMultiFileManifestFieldValue, "The multi file manifest has inconsistent field values."sv },
+ { AppInstaller::Manifest::ManifestError::DuplicatePortableCommandAlias, "Duplicate portable command alias found."sv },
+ { AppInstaller::Manifest::ManifestError::DuplicateRelativeFilePath, "Duplicate relative file path found."sv },
{ AppInstaller::Manifest::ManifestError::DuplicateMultiFileManifestType, "The multi file manifest should contain only one file with the particular ManifestType."sv },
{ AppInstaller::Manifest::ManifestError::DuplicateMultiFileManifestLocale, "The multi file manifest contains duplicate PackageLocale."sv },
{ AppInstaller::Manifest::ManifestError::UnsupportedMultiFileManifestType, "The multi file manifest should not contain file with the particular ManifestType."sv },
@@ -260,20 +262,37 @@ namespace AppInstaller::Manifest
resultErrors.emplace_back(ManifestError::ExceededNestedInstallerFilesLimit, "NestedInstallerFiles");
}
+ std::set commandAliasSet;
+ std::set relativeFilePathSet;
+
for (const auto& nestedInstallerFile : installer.NestedInstallerFiles)
{
if (nestedInstallerFile.RelativeFilePath.empty())
{
resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "RelativeFilePath");
+ break;
}
- else
+
+ // Check that the relative file path does not escape base directory.
+ const std::filesystem::path& basePath = std::filesystem::current_path();
+ const std::filesystem::path& fullPath = basePath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath);
+ if (AppInstaller::Filesystem::PathEscapesBaseDirectory(fullPath, basePath))
{
- const std::filesystem::path& basePath = std::filesystem::current_path();
- const std::filesystem::path& fullPath = basePath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath);
- if (AppInstaller::Filesystem::PathEscapesBaseDirectory(fullPath, basePath))
- {
- resultErrors.emplace_back(ManifestError::RelativeFilePathEscapesDirectory, "RelativeFilePath");
- }
+ resultErrors.emplace_back(ManifestError::RelativeFilePathEscapesDirectory, "RelativeFilePath");
+ }
+
+ // Check for duplicate relative filepath values.
+ if (!relativeFilePathSet.insert(Utility::ToLower(nestedInstallerFile.RelativeFilePath)).second)
+ {
+ resultErrors.emplace_back(ManifestError::DuplicateRelativeFilePath, "RelativeFilePath");
+ }
+
+ // Check for duplicate portable command alias values.
+ const auto& alias = Utility::ToLower(nestedInstallerFile.PortableCommandAlias);
+ if (!alias.empty() && !commandAliasSet.insert(alias).second)
+ {
+ resultErrors.emplace_back(ManifestError::DuplicatePortableCommandAlias, "PortableCommandAlias");
+ break;
}
}
}
diff --git a/src/AppInstallerCommonCore/PortableARPEntry.cpp b/src/AppInstallerCommonCore/PortableARPEntry.cpp
index 73a39f214c..084341067a 100644
--- a/src/AppInstallerCommonCore/PortableARPEntry.cpp
+++ b/src/AppInstallerCommonCore/PortableARPEntry.cpp
@@ -36,6 +36,7 @@ namespace AppInstaller::Registry::Portable
{
m_scope = scope;
m_arch = arch;
+ m_productCode = productCode;
if (m_scope == Manifest::ScopeEnum::Machine)
{
@@ -59,7 +60,7 @@ namespace AppInstaller::Registry::Portable
m_samDesired = KEY_WOW64_64KEY;
}
- m_subKey += L"\\" + ConvertToUTF16(productCode);
+ m_subKey += L"\\" + ConvertToUTF16(m_productCode);
m_key = Key::OpenIfExists(m_root, m_subKey, 0, KEY_ALL_ACCESS);
if (m_key != NULL)
{
@@ -96,27 +97,6 @@ namespace AppInstaller::Registry::Portable
}
}
- bool PortableARPEntry::IsSamePortablePackageEntry(const std::string& packageId, const std::string& sourceId)
- {
- auto existingWinGetPackageId = m_key[std::wstring{ s_WinGetPackageIdentifier }];
- auto existingWinGetSourceId = m_key[std::wstring{ s_WinGetSourceIdentifier }];
-
- bool isSamePackageId = false;
- bool isSamePackageSource = false;
-
- if (existingWinGetPackageId.has_value())
- {
- isSamePackageId = existingWinGetPackageId.value().GetValue() == packageId;
- }
-
- if (existingWinGetSourceId.has_value())
- {
- isSamePackageSource = existingWinGetSourceId.value().GetValue() == sourceId;
- }
-
- return isSamePackageId && isSamePackageSource;
- }
-
std::optional PortableARPEntry::operator[](PortableValueName valueName) const
{
return m_key[std::wstring{ ToString(valueName) }];
diff --git a/src/AppInstallerCommonCore/PortableEntry.cpp b/src/AppInstallerCommonCore/PortableEntry.cpp
deleted file mode 100644
index 32d18d42aa..0000000000
--- a/src/AppInstallerCommonCore/PortableEntry.cpp
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-#include "pch.h"
-#include "winget/PortableEntry.h"
-#include "winget/PortableARPEntry.h"
-#include "winget/Manifest.h"
-#include "winget/Filesystem.h"
-#include "winget/PathVariable.h"
-#include "Public/AppInstallerLogging.h"
-
-using namespace AppInstaller::Registry;
-using namespace AppInstaller::Registry::Portable;
-using namespace AppInstaller::Registry::Environment;
-
-namespace AppInstaller::Portable
-{
- PortableEntry::PortableEntry(PortableARPEntry& portableARPEntry) :
- m_portableARPEntry(portableARPEntry)
- {
- // Initialize all values if present
- if (Exists())
- {
- DisplayName = GetStringValue(PortableValueName::DisplayName);
- DisplayVersion = GetStringValue(PortableValueName::DisplayVersion);
- HelpLink = GetStringValue(PortableValueName::HelpLink);
- InstallDate = GetStringValue(PortableValueName::InstallDate);
- Publisher = GetStringValue(PortableValueName::Publisher);
- SHA256 = GetStringValue(PortableValueName::SHA256);
- URLInfoAbout = GetStringValue(PortableValueName::URLInfoAbout);
- UninstallString = GetStringValue(PortableValueName::UninstallString);
- WinGetInstallerType = GetStringValue(PortableValueName::WinGetInstallerType);
- WinGetPackageIdentifier = GetStringValue(PortableValueName::WinGetPackageIdentifier);
- WinGetSourceIdentifier = GetStringValue(PortableValueName::WinGetSourceIdentifier);
-
- InstallLocation = GetPathValue(PortableValueName::InstallLocation);
- PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath);
- PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath);
- InstallLocation = GetPathValue(PortableValueName::InstallLocation);
-
- InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated);
- InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath);
- }
- }
-
- void PortableEntry::MovePortableExe(const std::filesystem::path& installerPath)
- {
- bool isDirectoryCreated = false;
- if (std::filesystem::create_directories(InstallLocation))
- {
- AICLI_LOG(Core, Info, << "Created target install directory: " << InstallLocation);
- isDirectoryCreated = true;
- }
-
- if (std::filesystem::exists(PortableTargetFullPath))
- {
- std::filesystem::remove(PortableTargetFullPath);
- AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << PortableTargetFullPath);
- }
-
- Filesystem::RenameFile(installerPath, PortableTargetFullPath);
- AICLI_LOG(Core, Info, << "Portable exe moved to: " << PortableTargetFullPath);
-
- // Only assign this value if this is a new portable install or the install directory was actually created.
- // Otherwise, we want to preserve the existing value from the prior install.
- if (!Exists() || isDirectoryCreated)
- {
- Commit(PortableValueName::InstallDirectoryCreated, InstallDirectoryCreated = isDirectoryCreated);
- }
- }
-
- bool PortableEntry::VerifyPortableExeHash()
- {
- std::ifstream inStream{ PortableTargetFullPath, std::ifstream::binary };
- const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream);
- inStream.close();
-
- return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(SHA256), targetFileHash);
- }
-
- void PortableEntry::CreatePortableSymlink()
- {
- if (Filesystem::CreateSymlink(PortableTargetFullPath, PortableSymlinkFullPath))
- {
- AICLI_LOG(Core, Info, << "Symlink created at: " << PortableSymlinkFullPath);
- }
- else
- {
- // Symlink creation should only fail if the user executes in user mode and non-admin.
- // Resort to adding install directory to PATH directly.
- AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH.");
- Commit(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true);
- }
- }
-
- bool PortableEntry::VerifySymlinkTarget()
- {
- AICLI_LOG(Core, Info, << "Expected portable target path: " << PortableTargetFullPath);
- const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(PortableSymlinkFullPath);
-
- if (symlinkTargetPath == PortableTargetFullPath)
- {
- AICLI_LOG(Core, Info, << "Portable symlink target matches portable target path: " << symlinkTargetPath);
- return true;
- }
- else
- {
- AICLI_LOG(Core, Info, << "Portable symlink does not match portable target path: " << symlinkTargetPath);
- return false;
- }
- }
-
- bool PortableEntry::AddToPathVariable()
- {
- return PathVariable(GetScope()).Append(GetPathValue());
- }
-
- bool PortableEntry::RemoveFromPathVariable()
- {
- bool removeFromPath = true;
- std::filesystem::path pathValue = GetPathValue();
- if (!InstallDirectoryAddedToPath)
- {
- // Default links directory must be empty before removing from PATH.
- if (!std::filesystem::is_empty(pathValue))
- {
- AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue);
- removeFromPath = false;
- }
- }
-
- if (removeFromPath)
- {
- return PathVariable(GetScope()).Remove(pathValue);
- }
- else
- {
- return false;
- }
- }
-
- void PortableEntry::RemoveARPEntry()
- {
- m_portableARPEntry.Delete();
- }
-
- std::string PortableEntry::GetStringValue(PortableValueName valueName)
- {
- if (m_portableARPEntry[valueName].has_value())
- {
- return m_portableARPEntry[valueName]->GetValue();
- }
- else
- {
- return {};
- }
- }
-
- std::filesystem::path PortableEntry::GetPathValue(PortableValueName valueName)
- {
- if (m_portableARPEntry[valueName].has_value())
- {
- return m_portableARPEntry[valueName]->GetValue();
- }
- {
- return {};
- }
- }
-
- bool PortableEntry::GetBoolValue(PortableValueName valueName)
- {
- if (m_portableARPEntry[valueName].has_value())
- {
- return m_portableARPEntry[valueName]->GetValue();
- }
- else
- {
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h b/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h
index 0f6cd4dae3..959231abe2 100644
--- a/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h
+++ b/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h
@@ -53,6 +53,9 @@ namespace AppInstaller::Utility {
// Computes the hash from a given stream.
static HashBuffer ComputeHash(std::istream& in);
+ // Computes the hash from a given file path.
+ static HashBuffer ComputeHashFromFile(const std::filesystem::path& path);
+
static std::string ConvertToString(const HashBuffer& hashBuffer);
static std::wstring ConvertToWideString(const HashBuffer& hashBuffer);
diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h
index dbde4c31db..e4e859891b 100644
--- a/src/AppInstallerCommonCore/Public/winget/Filesystem.h
+++ b/src/AppInstallerCommonCore/Public/winget/Filesystem.h
@@ -21,6 +21,16 @@ namespace AppInstaller::Filesystem
void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to);
// Creates a symlink that points to the target path.
+ bool CreateSymlink(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);
+
+ // Appends the .exe extension to the path if not present.
+ void AppendExtension(std::filesystem::path& value, const std::string& extension);
+
+ // 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.
diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h
index addb1d457b..80401672b5 100644
--- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h
+++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h
@@ -325,6 +325,9 @@ namespace AppInstaller::Manifest
// Gets a value indicating whether the given installer type is an archive.
bool IsArchiveType(InstallerTypeEnum installerType);
+ // Gets a value indicating whether the given installer type is a portable.
+ bool IsPortableType(InstallerTypeEnum installerType);
+
// Gets a value indicating whether the given nested installer type is supported.
bool IsNestedInstallerTypeSupported(InstallerTypeEnum nestedInstallerType);
diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h
index ddc01d5425..52aa910680 100644
--- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h
+++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h
@@ -23,6 +23,8 @@ namespace AppInstaller::Manifest
WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionOverlapWithIndex);
WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionValidationInternalError);
WINGET_DEFINE_RESOURCE_STRINGID(BothAllowedAndExcludedMarketsDefined);
+ WINGET_DEFINE_RESOURCE_STRINGID(DuplicatePortableCommandAlias);
+ WINGET_DEFINE_RESOURCE_STRINGID(DuplicateRelativeFilePath);
WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestLocale);
WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestType);
WINGET_DEFINE_RESOURCE_STRINGID(DuplicateInstallerEntry);
diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h
index caf9bce8ff..f4a22f191f 100644
--- a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h
+++ b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h
@@ -34,8 +34,6 @@ namespace AppInstaller::Registry::Portable
std::optional operator[](PortableValueName valueName) const;
- bool IsSamePortablePackageEntry(const std::string& packageId, const std::string& sourceId);
-
bool Exists() { return m_exists; }
void SetValue(PortableValueName valueName, const std::wstring& value);
@@ -47,9 +45,11 @@ namespace AppInstaller::Registry::Portable
Registry::Key GetKey() { return m_key; };
Manifest::ScopeEnum GetScope() { return m_scope; };
Utility::Architecture GetArchitecture() { return m_arch; };
+ std::string GetProductCode() { return m_productCode; };
private:
bool m_exists = false;
+ std::string m_productCode;
Key m_key;
HKEY m_root;
std::wstring m_subKey;
diff --git a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableEntry.h
deleted file mode 100644
index 67fc4c1b33..0000000000
--- a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-#pragma once
-#include
-#include
-#include "winget/PortableARPEntry.h"
-#include
-#include
-
-using namespace AppInstaller::Registry;
-using namespace AppInstaller::Registry::Portable;
-
-namespace AppInstaller::Portable
-{
- struct PortableEntry
- {
- std::string DisplayName;
- std::string DisplayVersion;
- std::string HelpLink;
- std::string InstallDate;
- bool InstallDirectoryCreated = false;
- std::filesystem::path InstallLocation;
- std::filesystem::path PortableSymlinkFullPath;
- std::filesystem::path PortableTargetFullPath;
- std::string Publisher;
- std::string SHA256;
- std::string URLInfoAbout;
- std::string UninstallString;
- std::string WinGetInstallerType;
- std::string WinGetPackageIdentifier;
- std::string WinGetSourceIdentifier;
- bool InstallDirectoryAddedToPath = false;
-
- template
- void Commit(PortableValueName valueName, T value)
- {
- m_portableARPEntry.SetValue(valueName, value);
- }
-
- Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); };
-
- bool Exists() { return m_portableARPEntry.Exists(); };
-
- PortableEntry(PortableARPEntry& portableARPEntry);
-
- std::filesystem::path GetPathValue() const
- {
- return InstallDirectoryAddedToPath ? InstallLocation : PortableSymlinkFullPath.parent_path();
- }
-
- bool VerifyPortableExeHash();
-
- bool VerifySymlinkTarget();
-
- void MovePortableExe(const std::filesystem::path& installerPath);
-
- void CreatePortableSymlink();
-
- bool AddToPathVariable();
-
- bool RemoveFromPathVariable();
-
- void RemoveARPEntry();
-
- private:
- PortableARPEntry m_portableARPEntry;
- std::string GetStringValue(PortableValueName valueName);
- std::filesystem::path GetPathValue(PortableValueName valueName);
- bool GetBoolValue(PortableValueName valueName);
- };
-}
\ No newline at end of file
diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h
new file mode 100644
index 0000000000..6c7976d2db
--- /dev/null
+++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "AppInstallerSHA256.h"
+#include
+#include
+
+namespace AppInstaller::Portable
+{
+ // File type enum of the portable file
+ enum class PortableFileType
+ {
+ Unknown,
+ File,
+ Directory,
+ Symlink
+ };
+
+ // Metadata representation of a portable file placed down during installation
+ struct PortableFileEntry
+ {
+ // Version 1.0
+ PortableFileType FileType = PortableFileType::Unknown;
+ std::string SHA256;
+ std::string SymlinkTarget;
+ std::filesystem::path CurrentPath;
+
+ void SetFilePath(const std::filesystem::path& path)
+ {
+ if (FileType != PortableFileType::Symlink)
+ {
+ m_filePath = std::filesystem::weakly_canonical(path);
+ }
+ else
+ {
+ m_filePath = path;
+ }
+ };
+
+ std::filesystem::path GetFilePath() const { return m_filePath; };
+
+ static PortableFileEntry CreateFileEntry(const std::filesystem::path& currentPath, const std::filesystem::path& targetPath, const std::string& sha256)
+ {
+ PortableFileEntry fileEntry;
+ fileEntry.FileType = PortableFileType::File;
+ fileEntry.CurrentPath = currentPath;
+ fileEntry.SetFilePath(targetPath);
+
+ if (sha256.empty())
+ {
+ fileEntry.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(currentPath));
+ }
+ else
+ {
+ fileEntry.SHA256 = sha256;
+ }
+ return fileEntry;
+ }
+
+ static PortableFileEntry CreateSymlinkEntry(const std::filesystem::path& symlinkPath, const std::filesystem::path& targetPath)
+ {
+ PortableFileEntry symlinkEntry;
+ symlinkEntry.FileType = PortableFileType::Symlink;
+ symlinkEntry.SetFilePath(symlinkPath);
+ symlinkEntry.SymlinkTarget = targetPath.u8string();
+ return symlinkEntry;
+ }
+
+ static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& currentPath, const std::filesystem::path& directoryPath)
+ {
+ PortableFileEntry directoryEntry;
+ directoryEntry.FileType = PortableFileType::Directory;
+ directoryEntry.CurrentPath = currentPath;
+ directoryEntry.SetFilePath(directoryPath);
+ return directoryEntry;
+ }
+
+ private:
+ std::filesystem::path m_filePath;
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerCommonCore/SHA256.cpp b/src/AppInstallerCommonCore/SHA256.cpp
index 9aa0d68eda..adcf1441c1 100644
--- a/src/AppInstallerCommonCore/SHA256.cpp
+++ b/src/AppInstallerCommonCore/SHA256.cpp
@@ -148,6 +148,15 @@ namespace AppInstaller::Utility {
}
}
+
+ SHA256::HashBuffer SHA256::ComputeHashFromFile(const std::filesystem::path& path)
+ {
+ std::ifstream inStream{ path, std::ifstream::binary };
+ const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream);
+ inStream.close();
+ return targetFileHash;
+ }
+
void SHA256::SHA256ContextDeleter::operator()(SHA256Context* context)
{
delete context;
diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp
index 3b09ee00a0..f3bb074715 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp
+++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp
@@ -4,6 +4,7 @@
#include "PortableIndex.h"
#include "SQLiteStorageBase.h"
#include "Schema/Portable_1_0/PortableIndexInterface.h"
+#include "winget/Filesystem.h"
namespace AppInstaller::Repository::Microsoft
{
@@ -19,6 +20,9 @@ namespace AppInstaller::Repository::Microsoft
result.m_interface->CreateTable(result.m_dbconn);
+ const auto& filePathUTF16 = Utility::ConvertToUTF16(filePath);
+ SetFileAttributes(filePathUTF16.c_str(), GetFileAttributes(filePathUTF16.c_str()) | FILE_ATTRIBUTE_HIDDEN);
+
result.SetLastWriteTime();
savepoint.Commit();
@@ -26,7 +30,7 @@ namespace AppInstaller::Repository::Microsoft
return result;
}
- PortableIndex::IdType PortableIndex::AddPortableFile(const Schema::IPortableIndex::PortableFile& file)
+ PortableIndex::IdType PortableIndex::AddPortableFile(const Portable::PortableFileEntry& file)
{
std::lock_guard lockInterface{ *m_interfaceLock };
AICLI_LOG(Repo, Verbose, << "Adding portable file for [" << file.GetFilePath() << "]");
@@ -42,7 +46,7 @@ namespace AppInstaller::Repository::Microsoft
return result;
}
- void PortableIndex::RemovePortableFile(const Schema::IPortableIndex::PortableFile& file)
+ void PortableIndex::RemovePortableFile(const Portable::PortableFileEntry& file)
{
AICLI_LOG(Repo, Verbose, << "Removing portable file [" << file.GetFilePath() << "]");
std::lock_guard lockInterface{ *m_interfaceLock };
@@ -56,7 +60,7 @@ namespace AppInstaller::Repository::Microsoft
savepoint.Commit();
}
- bool PortableIndex::UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file)
+ bool PortableIndex::UpdatePortableFile(const Portable::PortableFileEntry& file)
{
AICLI_LOG(Repo, Verbose, << "Updating portable file [" << file.GetFilePath() << "]");
std::lock_guard lockInterface{ *m_interfaceLock };
@@ -74,6 +78,34 @@ namespace AppInstaller::Repository::Microsoft
return result;
}
+ bool PortableIndex::Exists(const Portable::PortableFileEntry& file)
+ {
+ AICLI_LOG(Repo, Verbose, << "Checking if portable file exists [" << file.GetFilePath() << "]");
+ return m_interface->Exists(m_dbconn, file);
+ }
+
+ bool PortableIndex::IsEmpty()
+ {
+ return m_interface->IsEmpty(m_dbconn);
+ }
+
+ void PortableIndex::AddOrUpdatePortableFile(const Portable::PortableFileEntry& file)
+ {
+ if (Exists(file))
+ {
+ UpdatePortableFile(file);
+ }
+ else
+ {
+ AddPortableFile(file);
+ }
+ }
+
+ std::vector PortableIndex::GetAllPortableFiles()
+ {
+ return m_interface->GetAllPortableFiles(m_dbconn);
+ }
+
std::unique_ptr PortableIndex::CreateIPortableIndex() const
{
if (m_version == Schema::Version{ 1, 0 } ||
diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h
index 2becefd82c..8319451d81 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h
+++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h
@@ -5,8 +5,11 @@
#include "Microsoft/Schema/IPortableIndex.h"
#include "Microsoft/Schema/Portable_1_0/PortableTable.h"
#include "Microsoft/SQLiteStorageBase.h"
+#include "winget/PortableFileEntry.h"
#include
+using namespace AppInstaller::Portable;
+
namespace AppInstaller::Repository::Microsoft
{
struct PortableIndex : SQLiteStorageBase
@@ -29,11 +32,19 @@ namespace AppInstaller::Repository::Microsoft
return { filePath, disposition, std::move(indexFile) };
}
- IdType AddPortableFile(const Schema::IPortableIndex::PortableFile& file);
+ IdType AddPortableFile(const Portable::PortableFileEntry& file);
+
+ void RemovePortableFile(const Portable::PortableFileEntry& file);
+
+ bool UpdatePortableFile(const Portable::PortableFileEntry& file);
+
+ void AddOrUpdatePortableFile(const Portable::PortableFileEntry& file);
+
+ std::vector GetAllPortableFiles();
- void RemovePortableFile(const Schema::IPortableIndex::PortableFile& file);
+ bool Exists(const Portable::PortableFileEntry& file);
- bool UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file);
+ bool IsEmpty();
private:
// Constructor used to open an existing index.
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h
index 2ac19f9543..b14f278e56 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h
@@ -3,37 +3,13 @@
#pragma once
#include "SQLiteWrapper.h"
#include "Microsoft/Schema/Version.h"
+#include "winget/PortableFileEntry.h"
#include
namespace AppInstaller::Repository::Microsoft::Schema
{
struct IPortableIndex
{
- // File type enum of the portable file
- enum class PortableFileType
- {
- Unknown,
- File,
- Directory,
- Symlink
- };
-
- // Metadata representation of a portable file placed down during installation
- struct PortableFile
- {
- // Version 1.0
- PortableFileType FileType = PortableFileType::Unknown;
- std::string SHA256;
- std::string SymlinkTarget;
-
- void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::weakly_canonical(path); };
-
- std::filesystem::path GetFilePath() const { return m_filePath; };
-
- private:
- std::filesystem::path m_filePath;
- };
-
virtual ~IPortableIndex() = default;
// Gets the schema version that this index interface is built for.
@@ -44,13 +20,22 @@ namespace AppInstaller::Repository::Microsoft::Schema
// Version 1.0
// Adds a portable file to the index.
- virtual SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) = 0;
+ virtual SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0;
// Removes a portable file from the index.
- virtual SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0;
+ virtual SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0;
// Updates the file with matching FilePath in the index.
// The return value indicates whether the index was modified by the function.
- virtual std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0;
+ virtual std::pair UpdatePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0;
+
+ // Returns a bool value indicating whether the PortableFile already exists in the index.
+ virtual bool Exists(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0;
+
+ // Returns a bool value indicating whether the index is empty.
+ virtual bool IsEmpty(SQLite::Connection& connection) = 0;
+
+ // Returns a vector including all the portable files recorded in the index.
+ virtual std::vector GetAllPortableFiles(SQLite::Connection& connection) = 0;
};
}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h
index b87f07d5a6..ce97b2538f 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h
@@ -13,8 +13,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
void CreateTable(SQLite::Connection& connection) override;
private:
- SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) override;
- SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) override;
- std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) override;
+ SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override;
+ SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override;
+ std::pair UpdatePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override;
+ bool Exists(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override;
+ bool IsEmpty(SQLite::Connection& connection) override;
+ std::vector GetAllPortableFiles(SQLite::Connection& connection) override;
};
}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp
index d7e49588ac..a5ffac47db 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp
@@ -8,7 +8,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
{
namespace
{
- std::optional GetExistingPortableFileId(const SQLite::Connection& connection, const IPortableIndex::PortableFile& file)
+ std::optional GetExistingPortableFileId(const SQLite::Connection& connection, const Portable::PortableFileEntry& file)
{
auto result = PortableTable::SelectByFilePath(connection, file.GetFilePath());
@@ -33,7 +33,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
savepoint.Commit();
}
- SQLite::rowid_t PortableIndexInterface::AddPortableFile(SQLite::Connection& connection, const PortableFile& file)
+ SQLite::rowid_t PortableIndexInterface::AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file)
{
auto portableEntryResult = GetExistingPortableFileId(connection, file);
@@ -46,7 +46,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
return portableFileId;
}
- SQLite::rowid_t PortableIndexInterface::RemovePortableFile(SQLite::Connection& connection, const PortableFile& file)
+ SQLite::rowid_t PortableIndexInterface::RemovePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file)
{
auto portableEntryResult = GetExistingPortableFileId(connection, file);
@@ -60,7 +60,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
return portableEntryResult.value();
}
- std::pair PortableIndexInterface::UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file)
+ std::pair PortableIndexInterface::UpdatePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file)
{
auto portableEntryResult = GetExistingPortableFileId(connection, file);
@@ -73,4 +73,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
savepoint.Commit();
return { status, portableEntryResult.value() };
}
+
+ bool PortableIndexInterface::Exists(SQLite::Connection& connection, const Portable::PortableFileEntry& file)
+ {
+ return GetExistingPortableFileId(connection, file).has_value();
+ }
+
+ bool PortableIndexInterface::IsEmpty(SQLite::Connection& connection)
+ {
+ return PortableTable::IsEmpty(connection);
+ }
+
+ std::vector PortableIndexInterface::GetAllPortableFiles(SQLite::Connection& connection)
+ {
+ return PortableTable::GetAllPortableFiles(connection);
+ }
}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
index b6c0c830d1..2efca323fd 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
@@ -64,7 +64,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
builder.Execute(connection);
}
- SQLite::rowid_t PortableTable::AddPortableFile(SQLite::Connection& connection, const IPortableIndex::PortableFile& file)
+ SQLite::rowid_t PortableTable::AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file)
{
SQLite::Builder::StatementBuilder builder;
builder.InsertInto(s_PortableTable_Table_Name)
@@ -78,7 +78,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
return connection.GetLastInsertRowID();
}
- bool PortableTable::UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const IPortableIndex::PortableFile& file)
+ bool PortableTable::UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const Portable::PortableFileEntry& file)
{
SQLite::Builder::StatementBuilder builder;
builder.Update(s_PortableTable_Table_Name).Set()
@@ -92,7 +92,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
return connection.GetChanges() != 0;
}
- std::optional PortableTable::GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id)
+ std::optional PortableTable::GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id)
{
SQLite::Builder::StatementBuilder builder;
builder.Select({ s_PortableTable_FilePath_Column,
@@ -103,12 +103,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
SQLite::Statement select = builder.Prepare(connection);
- IPortableIndex::PortableFile portableFile;
+ Portable::PortableFileEntry portableFile;
if (select.Step())
{
- auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow();
- portableFile.SetFilePath(std::move(filePath));
+ auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow();
portableFile.FileType = fileType;
+ portableFile.SetFilePath(std::move(filePath));
portableFile.SHA256 = std::move(sha256);
portableFile.SymlinkTarget = std::move(symlinkTarget);
return portableFile;
@@ -150,4 +150,29 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
return (countStatement.GetColumn(0) == 0);
}
+
+ std::vector PortableTable::GetAllPortableFiles(SQLite::Connection& connection)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.Select({ s_PortableTable_FilePath_Column,
+ s_PortableTable_FileType_Column,
+ s_PortableTable_SHA256_Column,
+ s_PortableTable_SymlinkTarget_Column })
+ .From(s_PortableTable_Table_Name);
+
+ SQLite::Statement select = builder.Prepare(connection);
+ std::vector result;
+ while (select.Step())
+ {
+ Portable::PortableFileEntry portableFile;
+ auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow();
+ portableFile.FileType = fileType;
+ portableFile.SetFilePath(std::move(filePath));
+ portableFile.SHA256 = std::move(sha256);
+ portableFile.SymlinkTarget = std::move(symlinkTarget);
+ result.emplace_back(std::move(portableFile));
+ }
+
+ return result;
+ }
}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h
index 513e100010..3bd5c6a23c 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h
@@ -30,15 +30,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
static std::optional SelectByFilePath(const SQLite::Connection& connection, const std::filesystem::path& path);
// Selects the portable file by rowid from the table, returning the portable file object if it exists.
- static std::optional GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id);
+ static std::optional GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id);
// Adds the portable file into the table.
- static SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const IPortableIndex::PortableFile& file);
+ static SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file);
// Removes the portable file from the table by id.
static void RemovePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id);
// Updates the portable file in the table by id.
- static bool UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const IPortableIndex::PortableFile& file);
+ static bool UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const Portable::PortableFileEntry& file);
+
+ // Gets all portable files recorded in the index.
+ static std::vector GetAllPortableFiles(SQLite::Connection& connection);
};
}
\ No newline at end of file