commit 5d63c3870e195c9b0ee1618902f6fa7aa43165d1 parent 312ad6dc130638946a16b36fbddaf343634ff34e Author: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com> Date: Thu, 28 Apr 2022 13:56:32 -0700 Implementation for Portable install flow (#2078) Diffstat:
49 files changed, 1374 insertions(+), 148 deletions(-)
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -162,6 +162,7 @@ EXTRADEBUG EXTRAFLAGS FAILIFTHERE fakeswitch +fallthrough FATALEXIT FEBAB FIELDTAG diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -217,6 +217,7 @@ llvm localhost localizationpriority LPBYTE +LPDWORD LPWSTR LSTATUS LTDA @@ -259,6 +260,7 @@ netlify Newtonsoft NOEXPAND nonetwork +nonterminated normer NOSEPARATOR NOTAPROPERTY @@ -272,6 +274,7 @@ objbase objidl ofile Outptr +OSVERSION Packagedx packageinuse parametermap @@ -316,6 +319,7 @@ Redist REFIID regexes REGSAM +reparse restsource rgex rhs @@ -359,6 +363,7 @@ SUSE swervy SYD SYG +symlink sysrefcomp Tagit TCpp @@ -393,6 +398,7 @@ uninstalls unknwn unparsable UNSCOPED +unvirtualized UParse UPSERT uris @@ -404,8 +410,10 @@ USHORT utils uuid UWP +VALUENAMECASE VERSI VERSIE +virtualization vns vscode vstest diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -102,12 +102,12 @@ "type": "boolean", "default": false }, - "PortableAppUserRoot": { + "PortablePackageUserRoot": { "description": "The default root directory where packages are installed to under User scope. Applies to the portable installer type.", "type": "string", "default": "%LOCALAPPDATA%/Microsoft/WinGet/Packages/" }, - "PortableAppMachineRoot": { + "PortablePackageMachineRoot": { "description": "The default root directory where packages are installed to under Machine scope. Applies to the portable installer type.", "type": "string", "default": "%PROGRAMFILES%/WinGet/Packages/" diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -281,6 +281,7 @@ <ClInclude Include="Workflows\ImportExportFlow.h" /> <ClInclude Include="Workflows\MsiInstallFlow.h" /> <ClInclude Include="Workflows\MSStoreInstallerHandler.h" /> + <ClInclude Include="Workflows\PortableInstallFlow.h" /> <ClInclude Include="Workflows\SettingsFlow.h" /> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> <ClInclude Include="Workflows\InstallFlow.h" /> @@ -332,6 +333,7 @@ <ClCompile Include="Workflows\ImportExportFlow.cpp" /> <ClCompile Include="Workflows\MsiInstallFlow.cpp" /> <ClCompile Include="Workflows\MSStoreInstallerHandler.cpp" /> + <ClCompile Include="Workflows\PortableInstallFlow.cpp" /> <ClCompile Include="Workflows\SettingsFlow.cpp" /> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> <ClCompile Include="Workflows\InstallFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -176,6 +176,9 @@ <ClInclude Include="Public\COMContext.h"> <Filter>Public</Filter> </ClInclude> + <ClInclude Include="Workflows\PortableInstallFlow.h"> + <Filter>Workflows</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -319,6 +322,9 @@ <ClCompile Include="Workflows\DownloadFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> + <ClCompile Include="Workflows\PortableInstallFlow.cpp"> + <Filter>Workflows</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -88,13 +88,15 @@ namespace AppInstaller::CLI case Args::Type::ExperimentalArg: return Argument{ "arg", NoAlias, Args::Type::ExperimentalArg, Resource::String::ExperimentalArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::ExperimentalArg }; case Args::Type::Rename: - return Argument{ "rename", NoAlias, Args::Type::Rename, Resource::String::RenameArgumentDescription, ArgumentType::Positional, false }; + return Argument{ "rename", 'r', Args::Type::Rename, Resource::String::RenameArgumentDescription, ArgumentType::Standard, false }; case Args::Type::Purge: return Argument{ "purge", NoAlias, Args::Type::Purge, Resource::String::PurgeArgumentDescription, ArgumentType::Flag, false }; case Args::Type::Preserve: return Argument{ "preserve", NoAlias, Args::Type::Preserve, Resource::String::PreserveArgumentDescription, ArgumentType::Flag, false }; case Args::Type::Wait: return Argument{ "wait", NoAlias, Args::Type::Wait, Resource::String::WaitArgumentDescription, ArgumentType::Flag, false }; + case Args::Type::ProductCode: + return Argument{ "product-code", NoAlias, Args::Type::ProductCode, Resource::String::ProductCodeArgumentDescription, ArgumentType::Standard, false }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -45,6 +45,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::AcceptPackageAgreements), Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), + Argument::ForType(Args::Type::Rename), }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -46,6 +46,7 @@ namespace AppInstaller::CLI::Execution // Uninstall behavior Purge, // Removes all files and directories related to a package during an uninstall. Only applies to the portable installerType. Preserve, // Retains any files and directories created by the portable exe. + ProductCode, // Uninstalls using the product code as the identifier. //Source Command SourceName, diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -95,6 +95,9 @@ namespace AppInstaller::CLI::Execution // Returns a value indicating whether the context is terminated. bool IsTerminated() const { return m_isTerminated; } + // Resets the context to a nonterminated state. + void ResetTermination() { m_terminationHR = S_OK; m_isTerminated = false; } + // Gets the HRESULT reason for the termination. HRESULT GetTerminationHR() const { return m_terminationHR; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -169,6 +169,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ManifestValidationSuccess); WINGET_DEFINE_RESOURCE_STRINGID(ManifestValidationWarning); WINGET_DEFINE_RESOURCE_STRINGID(MissingArgumentError); + WINGET_DEFINE_RESOURCE_STRINGID(ModifiedPathRequiresShellRestart); WINGET_DEFINE_RESOURCE_STRINGID(MonikerArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); @@ -197,6 +198,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Options); WINGET_DEFINE_RESOURCE_STRINGID(OutputFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(OverwritingExistingFileAtMessage); WINGET_DEFINE_RESOURCE_STRINGID(Package); WINGET_DEFINE_RESOURCE_STRINGID(PackageAgreementsNotAgreedTo); WINGET_DEFINE_RESOURCE_STRINGID(PackageAgreementsPrompt); @@ -206,17 +208,21 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PoliciesEnabled); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesState); + WINGET_DEFINE_RESOURCE_STRINGID(PortableRegistryCollisionOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PreserveArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PrivacyStatement); + WINGET_DEFINE_RESOURCE_STRINGID(ProductCodeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionNo); WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionYes); WINGET_DEFINE_RESOURCE_STRINGID(PurgeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(QueryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(RainbowArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(RenameArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ReparsePointsNotSupportedError); WINGET_DEFINE_RESOURCE_STRINGID(ReportIdentityFound); WINGET_DEFINE_RESOURCE_STRINGID(RequiredArgError); + WINGET_DEFINE_RESOURCE_STRINGID(ReservedFilenameError); WINGET_DEFINE_RESOURCE_STRINGID(RetroArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SearchCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SearchCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "DownloadFlow.h" +#include "winget/Filesystem.h" #include <AppInstallerMsixInfo.h> @@ -38,6 +39,7 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Exe: case InstallerTypeEnum::Inno: case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Portable: return L".exe"sv; case InstallerTypeEnum::Msi: case InstallerTypeEnum::Wix: @@ -122,68 +124,6 @@ namespace AppInstaller::CLI::Workflow return false; } - - // Complicated rename algorithm due to somewhat arbitrary failures. - // 1. First, try to rename. - // 2. Then, create an empty file for the target, and attempt to rename. - // 3. Then, try repeatedly for 500ms in case it is a timing thing. - // 4. Attempt to use a hard link if available. - // 5. Copy the file if nothing else has worked so far. - void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to) - { - // 1. First, try to rename. - try - { - // std::filesystem::rename() handles motw correctly if applicable. - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - - // 2. Then, create an empty file for the target, and attempt to rename. - // This seems to fix things in certain cases, so we do it. - try - { - { - std::ofstream targetFile{ to }; - } - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - - // 3. Then, try repeatedly for 500ms in case it is a timing thing. - for (int i = 0; i < 5; ++i) - { - try - { - std::this_thread::sleep_for(100ms); - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - } - - // 4. Attempt to use a hard link if available. - if (Runtime::SupportsHardLinks(from)) - { - try - { - // Create a hard link to the file; the installer will be left in the temp directory afterward - // but it is better to succeed the operation and leave a file around than to fail. - // First we have to remove the target file as the function will not overwrite. - std::filesystem::remove(to); - std::filesystem::create_hard_link(from, to); - return; - } - CATCH_LOG(); - } - - // 5. Copy the file if nothing else has worked so far. - // Create a copy of the file; the installer will be left in the temp directory afterward - // but it is better to succeed the operation and leave a file around than to fail. - std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing); - } } void DownloadInstaller(Execution::Context& context) @@ -210,6 +150,7 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Inno: case InstallerTypeEnum::Msi: case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Portable: case InstallerTypeEnum::Wix: context << DownloadInstallerFile; break; @@ -513,7 +454,7 @@ namespace AppInstaller::CLI::Workflow return; } - RenameFile(installerPath, renamedDownloadedInstaller); + Filesystem::RenameFile(installerPath, renamedDownloadedInstaller); installerPath.assign(renamedDownloadedInstaller); AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << installerPath); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -9,6 +9,7 @@ #include "ShellExecuteInstallerHandler.h" #include "MSStoreInstallerHandler.h" #include "MsiInstallFlow.h" +#include "PortableInstallFlow.h" #include "WorkflowBase.h" #include "Workflows/DependenciesFlow.h" #include <AppInstallerDeployment.h> @@ -116,6 +117,8 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Error() << Resource::String::NoApplicableInstallers << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); } + + context << EnsureSupportForInstall; } void ShowInstallationDisclaimer(Execution::Context& context) @@ -257,6 +260,9 @@ namespace AppInstaller::CLI::Workflow EnsureStorePolicySatisfied << (isUpdate ? MSStoreUpdate : MSStoreInstall); break; + case InstallerTypeEnum::Portable: + context << PortableInstall; + break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -278,6 +284,13 @@ namespace AppInstaller::CLI::Workflow ReportInstallerResult("MsiInstallProduct"sv, APPINSTALLER_CLI_ERROR_MSI_INSTALL_FAILED); } + void PortableInstall(Execution::Context& context) + { + context << + PortableInstallImpl << + ReportInstallerResult("Portable"sv, APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED, true); + } + void MsixInstall(Execution::Context& context) { std::string uri; @@ -398,6 +411,12 @@ namespace AppInstaller::CLI::Workflow Workflow::InstallPackageInstaller; } + void EnsureSupportForInstall(Execution::Context& context) + { + context << + Workflow::EnsureSupportForPortableInstall; + } + void InstallMultiplePackages::operator()(Execution::Context& context) const { if (m_ensurePackageAgreements) diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -84,6 +84,18 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void MsixInstall(Execution::Context& context); + // Runs the flow for installing a Portable package. + // Required Args: None + // Inputs: Installer, InstallerPath + // Outputs: None + void PortableInstall(Execution::Context& context); + + // Verifies parameters for install to ensure success. + // Required Args: None + // Inputs: + // Outputs: None + void EnsureSupportForInstall(Execution::Context& context); + // Reports the return code returned by the installer. // Required Args: None // Inputs: Manifest, Installer, InstallerResult diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -22,6 +22,29 @@ namespace AppInstaller::CLI::Workflow { namespace { + struct PortableInstallFilter : public details::FilterField + { + PortableInstallFilter() : details::FilterField("Portable Install") {} + + InapplicabilityFlags IsApplicable(const Manifest::ManifestInstaller& installer) override + { + // Unvirtualized resources restricted capability is only supported for >= 10.0.18362 + // TODO: Add support for OS versions that don't support virtualization. + if (installer.InstallerType == InstallerTypeEnum::Portable && !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version("10.0.18362"))) + { + return InapplicabilityFlags::OSVersion; + } + + return InapplicabilityFlags::None; + } + + std::string ExplainInapplicable(const Manifest::ManifestInstaller&) override + { + std::string result = "Current OS is lower than supported MinOSVersion (10.0.18362) for Portable install"; + return result; + } + }; + struct OSVersionFilter : public details::FilterField { OSVersionFilter() : details::FilterField("OS Version") {} @@ -566,6 +589,7 @@ namespace AppInstaller::CLI::Workflow ManifestComparator::ManifestComparator(const Execution::Context& context, const Repository::IPackageVersion::Metadata& installationMetadata) { AddFilter(std::make_unique<OSVersionFilter>()); + AddFilter(std::make_unique<PortableInstallFilter>()); AddFilter(InstalledScopeFilter::Create(installationMetadata)); AddFilter(MarketFilter::Create()); diff --git a/src/AppInstallerCLICore/Workflows/PortableInstallFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableInstallFlow.cpp @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PortableInstallFlow.h" +#include "winget/Filesystem.h" +#include "winget/PortableARPEntry.h" +#include "AppInstallerStrings.h" + +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Registry; +using namespace AppInstaller::Registry::Portable; +using namespace std::filesystem; + +namespace AppInstaller::CLI::Workflow +{ + namespace + { + constexpr std::wstring_view s_PathName = L"Path"; + constexpr std::wstring_view s_PathSubkey_User = L"Environment"; + constexpr std::wstring_view s_PathSubkey_Machine = L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; + constexpr std::string_view s_LocalSource = "*Local"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<Execution::Data::Manifest>().Id; + + std::string source; + if (context.Contains(Execution::Data::Source)) + { + source = context.Get<Execution::Data::Source>().GetIdentifier(); + } + else + { + source = s_LocalSource; + } + + 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) + { + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + Utility::Architecture arch = context.Get<Execution::Data::Installer>()->Arch; + std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); + std::filesystem::path targetInstallDirectory; + + if (!locationArg.empty()) + { + targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; + } + else + { + const std::string& productCode = GetPortableProductCode(context); + targetInstallDirectory = GetPortableInstallRoot(scope, arch); + targetInstallDirectory /= ConvertToUTF16(productCode); + } + + return targetInstallDirectory; + } + + std::filesystem::path GetPortableTargetFullPath(Execution::Context& context) + { + const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); + 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); + } + else + { + fileName = installerPath.filename(); + } + + AppendExeExtension(fileName); + return targetInstallDirectory / fileName; + } + + std::filesystem::path GetPortableSymlinkFullPath(Execution::Context& context) + { + const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); + const std::vector<string_t>& commands = context.Get<Execution::Data::Installer>()->Commands; + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + + std::filesystem::path commandAlias; + if (!renameArg.empty()) + { + commandAlias = ConvertToUTF16(renameArg); + } + else + { + if (!commands.empty()) + { + commandAlias = ConvertToUTF16(commands[0]); + } + else + { + commandAlias = installerPath.filename(); + } + } + + AppendExeExtension(commandAlias); + return GetPortableLinksLocation(scope) / commandAlias; + } + + Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall(const std::vector<AppInstaller::Manifest::AppsAndFeaturesEntry>& appsAndFeaturesEntries, const AppInstaller::Manifest::Manifest& manifest) + { + AppInstaller::Manifest::AppsAndFeaturesEntry appsAndFeaturesEntry; + if (!appsAndFeaturesEntries.empty()) + { + appsAndFeaturesEntry = appsAndFeaturesEntries[0]; + } + + if (appsAndFeaturesEntry.DisplayName.empty()) + { + appsAndFeaturesEntry.DisplayName = manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>(); + } + if (appsAndFeaturesEntry.DisplayVersion.empty()) + { + appsAndFeaturesEntry.DisplayVersion = manifest.Version; + } + if (appsAndFeaturesEntry.Publisher.empty()) + { + appsAndFeaturesEntry.Publisher = manifest.DefaultLocalization.Get<Manifest::Localization::Publisher>(); + } + + return appsAndFeaturesEntry; + } + + bool AddToPathRegistry(Execution::Context& context) + { + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + const std::filesystem::path& linksDirectory = GetPortableLinksLocation(scope); + + Key key; + if (scope == Manifest::ScopeEnum::Machine) + { + key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); + } + else + { + key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); + } + + std::wstring pathName = std::wstring{ s_PathName }; + std::string portableLinksDir = Normalize(linksDirectory.u8string()); + std::string pathValue = Normalize(key[pathName]->GetValue<Value::Type::String>()); + + if (pathValue.find(portableLinksDir) == std::string::npos) + { + if (pathValue.back() != ';') + { + pathValue += ";"; + } + + pathValue += portableLinksDir + ";"; + AICLI_LOG(CLI, Info, << "Adding to Path environment variable: " << portableLinksDir); + key.SetValue(pathName, ConvertToUTF16(pathValue), REG_EXPAND_SZ); + return true; + } + else + { + AICLI_LOG(CLI, Verbose, << "Path already existed in environment variable. Skipping..."); + return false; + } + } + + void WritePortableEntryToUninstallRegistry(Execution::Context& context) + { + const AppInstaller::Manifest::Manifest& manifest = context.Get<Execution::Data::Manifest>(); + const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get<Execution::Data::Installer>()->AppsAndFeaturesEntries, manifest); + const std::string& packageIdentifier = manifest.Id; + + std::string sourceIdentifier; + if (context.Contains(Execution::Data::Source)) + { + sourceIdentifier = context.Get<Execution::Data::Source>().GetIdentifier(); + } + else + { + sourceIdentifier = s_LocalSource; + } + + const std::wstring& productCode = ConvertToUTF16(GetPortableProductCode(context)); + + Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( + ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)), + context.Get<Execution::Data::Installer>()->Arch, + productCode); + + if(uninstallEntry.Exists()) + { + if (uninstallEntry.IsSamePortablePackageEntry(packageIdentifier, sourceIdentifier)) + { + // 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; + } + } + } + + AICLI_LOG(CLI, Info, << "Begin writing to Uninstall registry."); + uninstallEntry.SetValue(PortableValueName::DisplayName, entry.DisplayName); + uninstallEntry.SetValue(PortableValueName::DisplayVersion, entry.DisplayVersion); + uninstallEntry.SetValue(PortableValueName::Publisher, entry.Publisher); + uninstallEntry.SetValue(PortableValueName::InstallDate, Utility::GetCurrentDateForARP()); + uninstallEntry.SetValue(PortableValueName::URLInfoAbout, manifest.DefaultLocalization.Get<Manifest::Localization::PackageUrl>()); + uninstallEntry.SetValue(PortableValueName::HelpLink, manifest.DefaultLocalization.Get<Manifest::Localization::PublisherSupportUrl>()); + uninstallEntry.SetValue(PortableValueName::UninstallString, L"winget uninstall --product-code " + productCode); + uninstallEntry.SetValue(PortableValueName::WinGetInstallerType, ConvertToUTF16(InstallerTypeToString(InstallerTypeEnum::Portable))); + uninstallEntry.SetValue(PortableValueName::WinGetPackageIdentifier, manifest.Id); + uninstallEntry.SetValue(PortableValueName::WinGetSourceIdentifier, sourceIdentifier); + uninstallEntry.SetValue(PortableValueName::PortableTargetFullPath, GetPortableTargetFullPath(context).wstring()); + uninstallEntry.SetValue(PortableValueName::PortableSymlinkFullPath, GetPortableSymlinkFullPath(context).wstring()); + uninstallEntry.SetValue(PortableValueName::SHA256, Utility::SHA256::ConvertToWideString(context.Get<Execution::Data::HashPair>().second)); + uninstallEntry.SetValue(PortableValueName::InstallLocation, GetPortableTargetDirectory(context).wstring()); + AICLI_LOG(CLI, Info, << "Writing to Uninstall registry complete."); + } + + void MovePortableExeAndCreateSymlink(Execution::Context& context) + { + const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); + const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); + const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); + const std::filesystem::path& targetDirectory = GetPortableTargetDirectory(context); + + bool isDirectoryCreated = false; + if (std::filesystem::create_directories(targetDirectory)) + { + AICLI_LOG(CLI, Info, << "Created target install directory: " << targetDirectory); + isDirectoryCreated = true; + } + + Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( + ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)), + context.Get<Execution::Data::Installer>()->Arch, + ConvertToUTF16(GetPortableProductCode(context))); + uninstallEntry.SetValue(PortableValueName::InstallDirectoryCreated, isDirectoryCreated); + + Filesystem::RenameFile(installerPath, targetFullPath); + AICLI_LOG(CLI, Info, << "Portable exe moved to: " << targetFullPath); + + std::filesystem::file_status status = std::filesystem::status(symlinkFullPath); + if (std::filesystem::is_directory(status)) + { + 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); + } + else + { + context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << symlinkFullPath.u8string() << std::endl; + std::filesystem::remove(symlinkFullPath); + } + + std::filesystem::create_symlink(targetFullPath, symlinkFullPath); + AICLI_LOG(CLI, Info, << "Symlink created at: " << symlinkFullPath); + + if (AddToPathRegistry(context)) + { + context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; + } + } + + 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 PortableInstallImpl(Execution::Context& context) + { + try + { + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; + + context << + WritePortableEntryToUninstallRegistry << + MovePortableExeAndCreateSymlink; + + context.Add<Execution::Data::OperationReturnCode>(context.GetTerminationHR()); + } + catch (...) + { + context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); + } + + // Reset termination to allow for ReportInstallResult to process return code. + context.ResetTermination(); + + // TODO: create subcontext for uninstall + } + + void EnsureSupportForPortableInstall(Execution::Context& context) + { + auto installerType = context.Get<Execution::Data::Installer>().value().InstallerType; + + if (installerType == InstallerTypeEnum::Portable) + { + context << + Workflow::EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::PortableInstall) << + EnsureValidArgsForPortableInstall << + EnsureVolumeSupportsReparsePoints; + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PortableInstallFlow.h b/src/AppInstallerCLICore/Workflows/PortableInstallFlow.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Installs the portable package. + // Required Args: None + // Inputs: Manifest, Scope, Rename, Location + // Outputs: None + void PortableInstallImpl(Execution::Context& context); + + void EnsureSupportForPortableInstall(Execution::Context& context); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.h b/src/AppInstallerCLICore/Workflows/UninstallFlow.h @@ -30,6 +30,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void MsixUninstall(Execution::Context& context); + // Removes the Portable package. + // Required Args: None + // Inputs: ProductCode + // Outputs: None + void PortableUninstall(Execution::Context& context); + // Records the uninstall to the tracking catalog. // Required Args: None // Inputs: Package diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -49,6 +49,7 @@ namespace AppInstallerCLIE2ETests ConfigureFeature("experimentalCmd", status); ConfigureFeature("dependencies", status); ConfigureFeature("directMSI", status); + ConfigureFeature("portableInstall", status); } } } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -3,6 +3,7 @@ namespace AppInstallerCLIE2ETests { + using Microsoft.Win32; using NUnit.Framework; using System.IO; @@ -11,6 +12,13 @@ namespace AppInstallerCLIE2ETests private const string InstallTestMsiInstalledFile = @"AppInstallerTestExeInstaller.exe"; private const string InstallTestMsiProductId = @"{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; private const string InstallTestMsixName = @"6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; + private const string TestSourceIdentifier = @"WingetE2E.Tests_8wekyb3d8bbwe"; + + [OneTimeSetUp] + public void OneTimeSetup() + { + ConfigureFeature("portableInstall", true); + } [Test] public void InstallAppDoesNotExist() @@ -155,6 +163,97 @@ namespace AppInstallerCLIE2ETests } } + [Test] + public void InstallPortableExe() + { + string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe"); + 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}" + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); + } + + [Test] + public void InstallPortableExeWithCommand() + { + var installDir = TestCommon.GetRandomTestDir(); + string packageId, commandAlias, fileName, productCode; + packageId = "AppInstallerTest.TestPortableExeWithCommand"; + productCode = packageId + "_" + TestSourceIdentifier; + fileName = "AppInstallerTestExeInstaller.exe"; + commandAlias = "testCommand.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(installDir, commandAlias, fileName, productCode, true); + } + + [Test] + public void InstallPortableExeWithRename() + { + var installDir = TestCommon.GetRandomTestDir(); + string packageId, productCode, renameArgValue; + packageId = "AppInstallerTest.TestPortableExeWithCommand"; + productCode = packageId + "_" + TestSourceIdentifier; + renameArgValue = "testRename.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir} --rename {renameArgValue}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(installDir, renameArgValue, renameArgValue, productCode, true); + } + + [Test] + public void InstallPortableInvalidRename() + { + var installDir = TestCommon.GetRandomTestDir(); + string packageId, renameArgValue; + packageId = "AppInstallerTest.TestPortableExeWithCommand"; + renameArgValue = "test?"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir} --rename {renameArgValue}"); + Assert.AreNotEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("The specified filename is not a valid filename")); + } + + [Test] + public void InstallPortableReservedNames() + { + var installDir = TestCommon.GetRandomTestDir(); + string packageId, renameArgValue; + packageId = "AppInstallerTest.TestPortableExeWithCommand"; + renameArgValue = "CON"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir} --rename {renameArgValue}"); + Assert.AreNotEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("The specified filename is not a valid filename")); + } + + [Test] + public void InstallPortableToExistingDirectory() + { + var installDir = TestCommon.GetRandomTestDir(); + var existingDir = Path.Combine(installDir, "testDirectory"); + Directory.CreateDirectory(existingDir); + + string packageId, commandAlias, fileName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + productCode = packageId + "_" + TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestPortableExe -l {existingDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true); + } + private bool VerifyTestExeInstalled(string installDir, string expectedContent = null) { if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -3,11 +3,12 @@ namespace AppInstallerCLIE2ETests { + using Microsoft.Win32; using NUnit.Framework; using System; using System.Diagnostics; using System.IO; - using System.Text; + using System.Linq; using System.Threading; public class TestCommon @@ -277,6 +278,48 @@ namespace AppInstallerCLIE2ETests return RunCommand("powershell", $"Get-AppxPackage \"{name}\" | Remove-AppxPackage"); } + public static void VerifyPortablePackage( + string installDir, + string commandAlias, + string filename, + string productCode, + bool shouldExist) + { + string exePath = Path.Combine(installDir, filename); + FileInfo exeFile = new FileInfo(exePath); + Assert.AreEqual(shouldExist, exeFile.Exists, $"Expected portable exe path: {exePath}"); + + string symlinkDirectory = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Links"); + string symlinkPath = Path.Combine(symlinkDirectory, commandAlias); + FileInfo symlinkFile = new FileInfo(symlinkPath); + Assert.AreEqual(shouldExist, symlinkFile.Exists, $"Expected portable symlink path: {symlinkPath}"); + + string subKey = @$"Software\Microsoft\Windows\CurrentVersion\Uninstall"; + using (RegistryKey uninstallRegistryKey = Registry.CurrentUser.OpenSubKey(subKey, true)) + { + RegistryKey portableEntry = uninstallRegistryKey.OpenSubKey(productCode, true); + Assert.AreEqual(shouldExist, portableEntry != null, $"Expected {productCode} subkey in path: {subKey}"); + // TODO: Remove delete once uninstall is implemented. + uninstallRegistryKey.DeleteSubKey(productCode); + } + + using (RegistryKey environmentRegistryKey = Registry.CurrentUser.OpenSubKey(@"Environment", true)) + { + string pathName = "Path"; + var currentPathValue = (string)environmentRegistryKey.GetValue(pathName); + var portablePathValue = symlinkDirectory + ';'; + bool isAddedToPath = currentPathValue.Contains(portablePathValue); + if (isAddedToPath) + { + string initialPathValue = currentPathValue.Replace(portablePathValue, ""); + environmentRegistryKey.SetValue(pathName, initialPathValue); + } + Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {portablePathValue}"); + } + + // TODO: Call uninstall command for cleanup when implemented + } + /// <summary> /// Copies log files to the path %TEMP%\E2ETestLogs /// </summary> diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.yaml @@ -0,0 +1,14 @@ +PackageIdentifier: AppInstallerTest.TestPortableExe +PackageVersion: 1.0.0.0 +PackageName: TestPortableExe +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for portable install. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: portable + InstallerSha256: <EXEHASH> +ManifestType: singleton +ManifestVersion: 1.2.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller_WithCommand.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller_WithCommand.yaml @@ -0,0 +1,16 @@ +PackageIdentifier: AppInstallerTest.TestPortableExeWithCommand +PackageVersion: 1.0.0.0 +PackageName: TestPortableExeWithCommand +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for portable install with command value. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: portable + InstallerSha256: <EXEHASH> + Commands: + - testCommand +ManifestType: singleton +ManifestVersion: 1.2.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -6,12 +6,15 @@ xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" + xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" IgnorableNamespaces="uap uap3 uap5 rescap"> <Identity Name="WinGetDevCLI" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" Version="0.0.2.0" /> <Properties> <DisplayName>WinGet Dev CLI</DisplayName> <PublisherDisplayName>Microsoft Corporation</PublisherDisplayName> <Logo>Images\StoreLogo.png</Logo> + <desktop6:FileSystemWriteVirtualization>disabled</desktop6:FileSystemWriteVirtualization> + <desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization> </Properties> <Dependencies> <!-- Minimum supported version is 1809 (October 2018 Update, aka RS5) --> @@ -58,5 +61,6 @@ <Capabilities> <rescap:Capability Name="runFullTrust" /> <rescap:Capability Name="packageManagement" /> + <rescap:Capability Name="unvirtualizedResources" /> </Capabilities> </Package> \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1287,6 +1287,24 @@ Please specify one of them using the `--source` option to proceed.</value> <data name="WaitArgumentDescription" xml:space="preserve"> <value>Prompts the user to press any key before exiting</value> </data> + <data name="ModifiedPathRequiresShellRestart" xml:space="preserve"> + <value>Path environment variable modified; restart your shell to use the new value.</value> + </data> + <data name="ProductCodeArgumentDescription" xml:space="preserve"> + <value>Filters using the product code</value> + </data> + <data name="PortableRegistryCollisionOverridden" xml:space="preserve"> + <value>A portable package with the same name but from a different source already exists; proceeding due to --force</value> + </data> + <data name="ReparsePointsNotSupportedError" xml:space="preserve"> + <value>The volume does not support reparse points</value> + </data> + <data name="ReservedFilenameError" xml:space="preserve"> + <value>The specified filename is not a valid filename</value> + </data> + <data name="OverwritingExistingFileAtMessage" xml:space="preserve"> + <value>Overwriting existing file:</value> + </data> <data name="NoPackageSelectionArgumentProvided" xml:space="preserve"> <value>No package selection argument was provided; see the help for details about finding a package.</value> </data> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -267,6 +267,12 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_ExpectedReturnCodes.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Portable.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Portable_WithCommand.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Bad-Invalid.json"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -380,10 +386,10 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-InstallerTypePortable-InvalidScope.yaml"> <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-InstallerTypePortable-InvalidAppsAndFeatures.yaml"> <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-InstallerTypePortable-InvalidCommands.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -263,10 +263,10 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-InstallerTypePortable-InvalidScope.yaml"> <Filter>TestData</Filter> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-InstallerTypePortable-InvalidAppsAndFeatures.yaml"> <Filter>TestData</Filter> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-InstallerTypePortable-InvalidCommands.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -438,6 +438,12 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_ExpectedReturnCodes.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Portable.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Portable_WithCommand.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallerArgTest_Msi_WithSwitches.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/Registry.cpp b/src/AppInstallerCLITests/Registry.cpp @@ -29,6 +29,32 @@ TEST_CASE("OpenIfExists_NotFound", "[registry]") REQUIRE(!key); } +TEST_CASE("CreateKeyAndDelete", "[registry]") +{ + std::wstring subkey = L"Foo\\Bar"; + wil::unique_hkey root = RegCreateVolatileTestRoot(); + Key key = Key::Create(root.get(), subkey, REG_OPTION_VOLATILE); + REQUIRE(key); + Key::Delete(root.get(), subkey, KEY_WOW64_64KEY); + Key secondKey = Key::OpenIfExists(root.get(), subkey); + REQUIRE(!secondKey); +} + +TEST_CASE("SetKeyValue", "[registry]") +{ + std::wstring valueName = L"TestValueName"; + std::wstring valueValue = L"TestValueValue"; + std::wstring subkey = L"FooBar"; + + wil::unique_hkey root = RegCreateVolatileTestRoot(); + Key key = Key::Create(root.get(), subkey, REG_OPTION_VOLATILE); + key.SetValue(valueName, valueValue, REG_SZ); + auto value = key[valueName]; + REQUIRE(value); + REQUIRE(value->GetType() == Value::Type::String); + REQUIRE(value->GetValue<Value::Type::String>() == ConvertToUTF8(valueValue)); +} + TEST_CASE("EnumerateKeys", "[registry]") { wil::unique_hkey root = RegCreateVolatileTestRoot(); diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp @@ -193,4 +193,4 @@ TEST_CASE("GetFileNameFromURI", "[strings]") REQUIRE(GetFileNameFromURI("https://github.com/microsoft/winget-cli/pull/1722").u8string() == "1722"); REQUIRE(GetFileNameFromURI("https://github.com/microsoft/winget-cli/README.md").u8string() == "README.md"); REQUIRE(GetFileNameFromURI("https://microsoft.com/").u8string() == ""); -} +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml @@ -0,0 +1,15 @@ +PackageIdentifier: AppInstallerCliTest.TestPortable +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Portable Exe +Publisher: Microsoft Corporation +AppMoniker: AICLITestPortable +License: Test +ProductCode: AppInstallerCliTest.TestExeInstaller +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: portable + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestType: singleton +ManifestVersion: 1.2.0 diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable_WithCommand.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable_WithCommand.yaml @@ -0,0 +1,16 @@ +PackageIdentifier: AppInstallerCliTest.TestPortable +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Portable Exe +Publisher: Microsoft Corporation +AppMoniker: AICLITestPortable +License: Test +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: portable + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + Commands: + - portableCommand +ManifestType: singleton +ManifestVersion: 1.2.0 diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -14,6 +14,7 @@ #include <Workflows/DownloadFlow.h> #include <Workflows/InstallFlow.h> #include <Workflows/MsiInstallFlow.h> +#include <Workflows/PortableInstallFlow.h> #include <Workflows/UninstallFlow.h> #include <Workflows/UpdateFlow.h> #include <Workflows/DependenciesFlow.h> @@ -525,6 +526,30 @@ void OverrideForShellExecute(TestContext& context, std::vector<Dependency>& inst OverrideForUpdateInstallerMotw(context); } +void OverrideForPortableInstall(TestContext& context) +{ + context.Override({ DownloadInstallerFile, [](TestContext& context) + { + context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); + } }); + + context.Override({ RenameDownloadedInstaller, [](TestContext&) + { + } }); + + OverrideForUpdateInstallerMotw(context); + + context.Override({ PortableInstall, [](TestContext&) + { + // Write out the install command + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestPortableInstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + file.close(); + } }); +} + void OverrideForDirectMsi(TestContext& context) { OverrideForCheckExistingInstaller(context); @@ -872,6 +897,28 @@ TEST_CASE("MsiInstallFlow_DirectMsi", "[InstallFlow][workflow]") REQUIRE(installResultStr.find("/quiet") != std::string::npos); } +TEST_CASE("PortableInstallFlow", "[InstallFlow][workflow]") +{ + TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); + TestCommon::TempFile portableInstallResultPath("TestPortableInstalled.txt"); + + TestCommon::TestUserSettings testSettings; + testSettings.Set<Setting::EFPortableInstall>(true); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForPortableInstall(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Portable.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::InstallLocation, tempDirectory); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(std::filesystem::exists(portableInstallResultPath.GetPath())); +} + TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow][workflow]") { { diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -317,8 +317,10 @@ <ClInclude Include="Public\winget\ManifestYamlPopulator.h" /> <ClInclude Include="Public\winget\MsiExecArguments.h" /> <ClInclude Include="Public\winget\NameNormalization.h" /> + <ClInclude Include="Public\winget\Filesystem.h" /> <ClInclude Include="Public\winget\Regex.h" /> <ClInclude Include="Public\winget\Registry.h" /> + <ClInclude Include="Public\winget\PortableARPEntry.h" /> <ClInclude Include="Public\winget\ManifestSchemaValidation.h" /> <ClInclude Include="Public\winget\Resources.h" /> <ClInclude Include="Public\winget\Settings.h" /> @@ -336,6 +338,7 @@ <ClCompile Include="Debugging.cpp" /> <ClCompile Include="DependenciesGraph.cpp" /> <ClCompile Include="DODownloader.cpp" /> + <ClCompile Include="Filesystem.cpp" /> <ClCompile Include="GroupPolicy.cpp"> <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> </ClCompile> @@ -384,6 +387,7 @@ <ClCompile Include="AppInstallerTelemetry.cpp" /> <ClCompile Include="Settings.cpp" /> <ClCompile Include="SHA256.cpp" /> + <ClCompile Include="PortableARPEntry.cpp" /> <ClCompile Include="Synchronization.cpp" /> <ClCompile Include="Telemetry\TraceLogging.cpp" /> <ClCompile Include="Architecture.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -189,6 +189,12 @@ <ClInclude Include="Public\winget\Debugging.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\Filesystem.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\PortableARPEntry.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -326,6 +332,12 @@ <ClCompile Include="Debugging.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Filesystem.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="PortableARPEntry.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/DateTime.cpp b/src/AppInstallerCommonCore/DateTime.cpp @@ -49,6 +49,19 @@ namespace AppInstaller::Utility return result; } + std::string GetCurrentDateForARP() + { + auto now = std::chrono::system_clock::now(); + std::time_t tt = std::chrono::system_clock::to_time_t(now); + + struct tm newTime; + localtime_s(&newTime, &tt); + + std::stringstream ss; + ss << std::put_time(&newTime, "%Y%m%d"); + return ss.str(); + } + int64_t GetCurrentUnixEpoch() { static_assert(std::is_same_v<int64_t, decltype(time(nullptr))>, "time returns a 64-bit integer"); diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp @@ -10,10 +10,12 @@ #include "Public/AppInstallerLogging.h" #include "Public/AppInstallerTelemetry.h" #include "Public/winget/UserSettings.h" +#include "Public/winget/Filesystem.h" #include "DODownloader.h" using namespace AppInstaller::Runtime; using namespace AppInstaller::Settings; +using namespace AppInstaller::Filesystem; namespace AppInstaller::Utility { diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -174,12 +174,20 @@ namespace AppInstaller return "Upgrade version is unknown and override is not specified"; case APPINSTALLER_CLI_ERROR_ICU_CONVERSION_ERROR: return "ICU conversion error"; + case APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED: + 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_INSTALL_PACKAGE_IN_USE: return "Application is currently running.Exit the application then try again."; case APPINSTALLER_CLI_ERROR_INSTALL_INSTALL_IN_PROGRESS: return "Another installation is already in progress.Try again later."; case APPINSTALLER_CLI_ERROR_INSTALL_FILE_IN_USE: - return "One or more file is being used.Exit the application then try again."; + return "One or more file is being used. Exit the application then try again."; case APPINSTALLER_CLI_ERROR_INSTALL_MISSING_DEPENDENCY: return "This package has a dependency missing from your system."; case APPINSTALLER_CLI_ERROR_INSTALL_DISK_FULL: diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerCommonCore/Filesystem.cpp @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/AppInstallerStrings.h" + +namespace AppInstaller::Filesystem +{ + using namespace std::chrono_literals; + using namespace std::string_view_literals; + + DWORD GetVolumeInformationFlagsByHandle(HANDLE anyFileHandle) + { + DWORD flags = 0; + wchar_t fileSystemName[MAX_PATH]; + THROW_LAST_ERROR_IF(!GetVolumeInformationByHandleW( + anyFileHandle, /*hFile*/ + NULL, /*lpVolumeNameBuffer*/ + 0, /*nVolumeNameSize*/ + NULL, /*lpVolumeSerialNumber*/ + NULL, /*lpMaximumComponentLength*/ + &flags, /*lpFileSystemFlags*/ + fileSystemName, /*lpFileSystemNameBuffer*/ + MAX_PATH /*nFileSystemNameSize*/)); + + // Vista and older does not report all flags, fix them up here + if (!(flags & FILE_SUPPORTS_HARD_LINKS) && !_wcsicmp(fileSystemName, L"NTFS")) + { + flags |= FILE_SUPPORTS_HARD_LINKS | FILE_SUPPORTS_EXTENDED_ATTRIBUTES | FILE_SUPPORTS_OPEN_BY_FILE_ID | FILE_SUPPORTS_USN_JOURNAL; + } + + return flags; + } + + DWORD GetVolumeInformationFlags(const std::filesystem::path& anyPath) + { + wil::unique_hfile fileHandle{ CreateFileW( + anyPath.c_str(), /*lpFileName*/ + 0, /*dwDesiredAccess*/ + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, /*dwShareMode*/ + NULL, /*lpSecurityAttributes*/ + OPEN_EXISTING, /*dwCreationDisposition*/ + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, /*dwFlagsAndAttributes*/ + NULL /*hTemplateFile*/) }; + + THROW_LAST_ERROR_IF(fileHandle.get() == INVALID_HANDLE_VALUE); + + return GetVolumeInformationFlagsByHandle(fileHandle.get()); + } + + bool SupportsNamedStreams(const std::filesystem::path& path) + { + return (GetVolumeInformationFlags(path) & FILE_NAMED_STREAMS) != 0; + } + + bool SupportsHardLinks(const std::filesystem::path& path) + { + return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_HARD_LINKS) != 0; + } + + bool SupportsReparsePoints(const std::filesystem::path& path) + { + return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_REPARSE_POINTS) != 0; + } + + // Complicated rename algorithm due to somewhat arbitrary failures. + // 1. First, try to rename. + // 2. Then, create an empty file for the target, and attempt to rename. + // 3. Then, try repeatedly for 500ms in case it is a timing thing. + // 4. Attempt to use a hard link if available. + // 5. Copy the file if nothing else has worked so far. + void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to) + { + // 1. First, try to rename. + try + { + // std::filesystem::rename() handles motw correctly if applicable. + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + + // 2. Then, create an empty file for the target, and attempt to rename. + // This seems to fix things in certain cases, so we do it. + try + { + { + std::ofstream targetFile{ to }; + } + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + + // 3. Then, try repeatedly for 500ms in case it is a timing thing. + for (int i = 0; i < 5; ++i) + { + try + { + std::this_thread::sleep_for(100ms); + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + } + + // 4. Attempt to use a hard link if available. + if (SupportsHardLinks(from)) + { + try + { + // Create a hard link to the file; the installer will be left in the temp directory afterward + // but it is better to succeed the operation and leave a file around than to fail. + // First we have to remove the target file as the function will not overwrite. + std::filesystem::remove(to); + std::filesystem::create_hard_link(from, to); + return; + } + CATCH_LOG(); + } + + // 5. Copy the file if nothing else has worked so far. + // Create a copy of the file; the installer will be left in the temp directory afterward + // but it is better to succeed the operation and leave a file around than to fail. + std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -430,7 +430,8 @@ namespace AppInstaller::Manifest installerType == InstallerTypeEnum::Msi || installerType == InstallerTypeEnum::Nullsoft || installerType == InstallerTypeEnum::Wix || - installerType == InstallerTypeEnum::Burn + installerType == InstallerTypeEnum::Burn || + installerType == InstallerTypeEnum::Portable ); } @@ -442,7 +443,8 @@ namespace AppInstaller::Manifest installerType == InstallerTypeEnum::Msi || installerType == InstallerTypeEnum::Nullsoft || installerType == InstallerTypeEnum::Wix || - installerType == InstallerTypeEnum::Burn + installerType == InstallerTypeEnum::Burn || + installerType == InstallerTypeEnum::Portable ); } diff --git a/src/AppInstallerCommonCore/PortableARPEntry.cpp b/src/AppInstallerCommonCore/PortableARPEntry.cpp @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/PortableARPEntry.h" +#include "winget/Manifest.h" + +using namespace AppInstaller::Utility; + +#define VALUENAMECASE(valueName) case PortableValueName::valueName: return s_##valueName; + +namespace AppInstaller::Registry::Portable +{ + namespace + { + constexpr std::wstring_view s_UninstallRegistryX64 = L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + constexpr std::wstring_view s_UninstallRegistryX86 = L"Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + constexpr std::wstring_view s_DisplayName = L"DisplayName"; + constexpr std::wstring_view s_DisplayVersion = L"DisplayVersion"; + constexpr std::wstring_view s_Publisher = L"Publisher"; + constexpr std::wstring_view s_InstallDate = L"InstallDate"; + constexpr std::wstring_view s_URLInfoAbout = L"URLInfoAbout"; + constexpr std::wstring_view s_HelpLink = L"HelpLink"; + constexpr std::wstring_view s_UninstallString = L"UninstallString"; + constexpr std::wstring_view s_WinGetInstallerType = L"WinGetInstallerType"; + constexpr std::wstring_view s_InstallLocation = L"InstallLocation"; + constexpr std::wstring_view s_PortableTargetFullPath = L"TargetFullPath"; + constexpr std::wstring_view s_PortableSymlinkFullPath = L"SymlinkFullPath"; + constexpr std::wstring_view s_SHA256 = L"SHA256"; + constexpr std::wstring_view s_WinGetPackageIdentifier = L"WinGetPackageIdentifier"; + constexpr std::wstring_view s_WinGetSourceIdentifier = L"WinGetSourceIdentifier"; + constexpr std::wstring_view s_InstallDirectoryCreated = L"InstallDirectoryCreated"; + } + + PortableARPEntry::PortableARPEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::wstring& productCode) + { + HKEY root; + std::wstring subKey; + if (scope == Manifest::ScopeEnum::Machine) + { + root = HKEY_LOCAL_MACHINE; + if (arch == Utility::Architecture::X64) + { + subKey = s_UninstallRegistryX64; + } + else + { + subKey = s_UninstallRegistryX86; + } + } + else + { + // HKCU uninstall registry share the x64 registry view. + root = HKEY_CURRENT_USER; + subKey = s_UninstallRegistryX64; + } + + subKey += L"\\" + productCode; + m_key = Key::OpenIfExists(root, subKey, 0, KEY_ALL_ACCESS); + if (m_key != NULL) + { + m_exists = true; + } + else + { + m_key = Key::Create(root, subKey); + } + } + + std::wstring_view ToString(PortableValueName valueName) + { + switch (valueName) + { + VALUENAMECASE(DisplayName); + VALUENAMECASE(DisplayVersion); + VALUENAMECASE(Publisher); + VALUENAMECASE(InstallDate); + VALUENAMECASE(URLInfoAbout); + VALUENAMECASE(HelpLink); + VALUENAMECASE(UninstallString); + VALUENAMECASE(WinGetInstallerType); + VALUENAMECASE(InstallLocation); + VALUENAMECASE(PortableTargetFullPath); + VALUENAMECASE(PortableSymlinkFullPath); + VALUENAMECASE(SHA256); + VALUENAMECASE(WinGetPackageIdentifier); + VALUENAMECASE(WinGetSourceIdentifier); + VALUENAMECASE(InstallDirectoryCreated); + default: return {}; + } + } + + 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<Value::Type::String>() == packageId; + } + + if (existingWinGetSourceId.has_value()) + { + isSamePackageSource = existingWinGetSourceId.value().GetValue<Value::Type::String>() == sourceId; + } + + return isSamePackageId && isSamePackageSource; + } + + void PortableARPEntry::SetValue(PortableValueName valueName, const std::wstring& value) + { + m_key.SetValue(std::wstring{ ToString(valueName) }, value, REG_SZ); + } + + void PortableARPEntry::SetValue(PortableValueName valueName, const std::string_view& value) + { + m_key.SetValue(std::wstring{ ToString(valueName) }, ConvertToUTF16(value), REG_SZ); + } + + void PortableARPEntry::SetValue(PortableValueName valueName, bool& value) + { + m_key.SetValue(std::wstring{ ToString(valueName) }, value); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h b/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h @@ -15,6 +15,9 @@ namespace AppInstaller::Utility // Gets the current time as a string. Can be used as a file name. std::string GetCurrentTimeForFilename(); + // Gets the current date as a string to be used in the ARP registry. + std::string GetCurrentDateForARP(); + // Gets the current time as a unix epoch value. int64_t GetCurrentUnixEpoch(); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -94,6 +94,10 @@ #define APPINSTALLER_CLI_ERROR_UPGRADE_VERSION_NOT_NEWER ((HRESULT)0x8A15004F) #define APPINSTALLER_CLI_ERROR_UPGRADE_VERSION_UNKNOWN ((HRESULT)0x8A150050) #define APPINSTALLER_CLI_ERROR_ICU_CONVERSION_ERROR ((HRESULT)0x8A150051) +#define APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED ((HRESULT)0x8A150052) +#define APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED ((HRESULT)0x8A150053) +#define APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS ((HRESULT)0x8A150054) +#define APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY ((HRESULT)0x8A150055) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -48,11 +48,15 @@ namespace AppInstaller::Runtime // The value of %USERPROFILE%. UserProfile, // The location where portable packages are installed to with user scope. - PortableAppUserRoot, + PortablePackageUserRoot, // The location where portable packages are installed to with machine scope (x64). - PortableAppMachineRootX64, + PortablePackageMachineRootX64, // The location where portable packages are installed to with machine scope (x86). - PortableAppMachineRootX86, + PortablePackageMachineRootX86, + // The location where symlinks to portable packages are stored under user scope. + PortableLinksUserLocation, + // The location where symlinks to portable packages are stored under machine scope. + PortableLinksMachineLocation, }; void SetRuntimePathStateName(std::string name); @@ -67,12 +71,6 @@ namespace AppInstaller::Runtime // Determines whether the process is running with administrator privileges. bool IsRunningAsAdmin(); - // Checks if the file system at path supports named streams/ADS - bool SupportsNamedStreams(const std::filesystem::path& path); - - // Checks if the file system at path supports hard links - bool SupportsHardLinks(const std::filesystem::path& path); - // Returns true if this is a release build; false if not. inline constexpr bool IsReleaseBuild(); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h b/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h @@ -55,6 +55,8 @@ namespace AppInstaller::Utility { static std::string ConvertToString(const HashBuffer& hashBuffer); + static std::wstring ConvertToWideString(const HashBuffer& hashBuffer); + static HashBuffer ConvertToBytes(const std::string& hashStr); // Returns a value indicating whether the two hashes are equal. diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" + +namespace AppInstaller::Filesystem +{ + // Checks if the file system at path supports named streams/ADS + bool SupportsNamedStreams(const std::filesystem::path& path); + + // Checks if the file system at path supports hard links + bool SupportsHardLinks(const std::filesystem::path& path); + + // Checks if the file system at path support reparse points + bool SupportsReparsePoints(const std::filesystem::path& path); + + // Renames the file to a new path. + void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to); +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Registry.h" +#include "Manifest.h" + +namespace AppInstaller::Registry::Portable +{ + enum class PortableValueName + { + DisplayName, + DisplayVersion, + HelpLink, + InstallDate, + InstallDirectoryCreated, + InstallLocation, + PortableSymlinkFullPath, + PortableTargetFullPath, + Publisher, + SHA256, + URLInfoAbout, + UninstallString, + WinGetInstallerType, + WinGetPackageIdentifier, + WinGetSourceIdentifier, + }; + + std::wstring_view ToString(PortableValueName valueName); + + struct PortableARPEntry : Registry::Key + { + PortableARPEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::wstring& productCode); + + bool IsSamePortablePackageEntry(const std::string& packageId, const std::string& sourceId); + + bool Exists() { return m_exists; } + + void SetValue(PortableValueName valueName, const std::wstring& value); + + void SetValue(PortableValueName valueName, const std::string_view& value); + + void SetValue(PortableValueName valueName, bool& value); + + Registry::Key GetKey() { return m_key; }; + + private: + bool m_exists; + Key m_key; + }; + +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Registry.h b/src/AppInstallerCommonCore/Public/winget/Registry.h @@ -8,6 +8,7 @@ #include <string_view> #include <vector> +#define AICLI_REGISTRY_UTF16_FLAG 0x08000000 namespace AppInstaller::Registry { @@ -42,6 +43,13 @@ namespace AppInstaller::Registry }; template <> + struct ValueTypeSpecifics<REG_SZ | AICLI_REGISTRY_UTF16_FLAG> + { + using value_t = std::wstring; + static value_t Convert(const std::vector<BYTE>& data); + }; + + template <> struct ValueTypeSpecifics<REG_EXPAND_SZ> { using value_t = std::string; @@ -49,6 +57,13 @@ namespace AppInstaller::Registry }; template <> + struct ValueTypeSpecifics<REG_EXPAND_SZ | AICLI_REGISTRY_UTF16_FLAG> + { + using value_t = std::wstring; + static value_t Convert(const std::vector<BYTE>& data); + }; + + template <> struct ValueTypeSpecifics<REG_BINARY> { using value_t = std::vector<BYTE>; @@ -77,7 +92,10 @@ namespace AppInstaller::Registry { None = REG_NONE, String = REG_SZ, + UTF16Flag = AICLI_REGISTRY_UTF16_FLAG, + UTF16String = REG_SZ | UTF16Flag, ExpandString = REG_EXPAND_SZ, + UTF16ExpandString = REG_EXPAND_SZ | UTF16Flag, Binary = REG_BINARY, DWord = REG_DWORD, DWordLittleEndian = REG_DWORD_LITTLE_ENDIAN, @@ -251,6 +269,11 @@ namespace AppInstaller::Registry std::optional<Key> SubKey(std::string_view name, DWORD options = 0) const; std::optional<Key> SubKey(const std::wstring& name, DWORD options = 0) const; + // Set registry values. + void SetValue(const std::wstring& name, const std::wstring& value, DWORD type = REG_SZ) const; + void SetValue(const std::wstring& name, const std::vector<BYTE>& value, DWORD type = REG_BINARY) const; + void SetValue(const std::wstring& name, DWORD value) const; + ValueList Values() const; operator bool() const { return m_key.operator bool(); } @@ -259,10 +282,21 @@ namespace AppInstaller::Registry static Key OpenIfExists(HKEY key, std::string_view subKey = {}, DWORD options = 0, REGSAM access = KEY_READ); static Key OpenIfExists(HKEY key, const std::wstring& subKey = {}, DWORD options = 0, REGSAM access = KEY_READ); + // Creates a new Key or returns one if it already existed. + static Key Create(HKEY key, std::string_view subkey = {}, DWORD options = REG_OPTION_NON_VOLATILE, REGSAM access = KEY_ALL_ACCESS); + static Key Create(HKEY key, const std::wstring& subKey = {}, DWORD options = REG_OPTION_NON_VOLATILE, REGSAM access = KEY_ALL_ACCESS); + + // Delete a key + static bool Delete(HKEY key, std::string_view subkey, DWORD samDesired); + static bool Delete(HKEY key, const std::wstring& subKey, DWORD samDesired); + private: // When ignoring error, returns whether the key existed bool Initialize(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access, bool ignoreErrorIfDoesNotExist); + // Returns whether the key was created successfully. + bool CreateAndOpen(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access); + wil::shared_hkey m_key; REGSAM m_access = KEY_READ; }; diff --git a/src/AppInstallerCommonCore/Registry.cpp b/src/AppInstallerCommonCore/Registry.cpp @@ -146,11 +146,21 @@ namespace AppInstaller::Registry return ConvertBytesToString(data); } + ValueTypeSpecifics<REG_SZ | AICLI_REGISTRY_UTF16_FLAG>::value_t ValueTypeSpecifics<REG_SZ | AICLI_REGISTRY_UTF16_FLAG>::Convert(const std::vector<BYTE>& data) + { + return ConvertBytesToWideString(data); + } + ValueTypeSpecifics<REG_EXPAND_SZ>::value_t ValueTypeSpecifics<REG_EXPAND_SZ>::Convert(const std::vector<BYTE>& data) { return Utility::ConvertToUTF8(Utility::ExpandEnvironmentVariables(ConvertBytesToWideString(data))); } + ValueTypeSpecifics<REG_EXPAND_SZ | AICLI_REGISTRY_UTF16_FLAG>::value_t ValueTypeSpecifics<REG_EXPAND_SZ | AICLI_REGISTRY_UTF16_FLAG>::Convert(const std::vector<BYTE>& data) + { + return ConvertBytesToWideString(data); + } + ValueTypeSpecifics<REG_BINARY>::value_t ValueTypeSpecifics<REG_BINARY>::Convert(const std::vector<BYTE>& data) { return data; @@ -169,7 +179,8 @@ namespace AppInstaller::Registry bool Value::HasCompatibleType(Type type) const { // Allow interop between String and ExpandString - if ((m_type == Type::String || m_type == Type::ExpandString) && (type == Type::String || type == Type::ExpandString)) + if ((m_type == Type::String || m_type == Type::ExpandString || m_type == Type::UTF16String || m_type == Type::UTF16ExpandString) && + (type == Type::String || type == Type::ExpandString || type == Type::UTF16String || type == Type::UTF16ExpandString)) { return true; } @@ -422,6 +433,26 @@ namespace AppInstaller::Registry } } + void Key::SetValue(const std::wstring& name, const std::wstring& value, DWORD type) const + { + THROW_IF_WIN32_ERROR(RegSetValueExW(m_key.get(), name.c_str(), 0, type, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>(sizeof(wchar_t) * (value.size() + 1)))); + AICLI_LOG(Core, Verbose, << "Setting '" << Utility::ConvertToUTF8(name) << "' with the value: " << Utility::ConvertToUTF8(value)); + } + + void Key::SetValue(const std::wstring& name, const std::vector<BYTE>& value, DWORD type) const + { + THROW_IF_WIN32_ERROR(RegSetValueExW(m_key.get(), name.c_str(), 0, type, reinterpret_cast<const BYTE*>(value.data()), static_cast<DWORD>(value.size()))); + AICLI_LOG(Core, Verbose, << "Setting '" << Utility::ConvertToUTF8(name) << "' with the value: " << ConvertBytesToString(value)); + + } + + void Key::SetValue(const std::wstring& name, DWORD value) const + { + THROW_IF_WIN32_ERROR(RegSetValueExW(m_key.get(), name.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(DWORD))); + AICLI_LOG(Core, Verbose, << "Setting '" << Utility::ConvertToUTF8(name) << "' with the value: " << value); + + } + ValueList Key::Values() const { return { m_key }; @@ -439,6 +470,62 @@ namespace AppInstaller::Registry return result; } + Key Key::Create(HKEY key, std::string_view subKey, DWORD options, REGSAM access) + { + return Create(key, Utility::ConvertToUTF16(subKey), options, access); + } + + Key Key::Create(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access) + { + Key result; + result.CreateAndOpen(key, subKey, options, access); + return result; + } + + bool Key::Delete(HKEY key, std::string_view subKey, DWORD samDesired) + { + return Delete(key, Utility::ConvertToUTF16(subKey), samDesired); + } + + bool Key::Delete(HKEY key, const std::wstring& subKey, DWORD samDesired) + { + LSTATUS status = RegDeleteKeyExW(key, subKey.c_str(), samDesired, 0); + if (status == ERROR_SUCCESS) + { + AICLI_LOG(Core, Verbose, << "Subkey '" << Utility::ConvertToUTF8(subKey) << "' was deleted successfully."); + return true; + } + else if (status == ERROR_FILE_NOT_FOUND) + { + AICLI_LOG(Core, Verbose, << "Subkey '" << Utility::ConvertToUTF8(subKey) << "' was not found."); + } + else + { + THROW_IF_WIN32_ERROR(status); + } + + return false; + } + + bool Key::CreateAndOpen(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access) + { + m_access = access; + LPDWORD disposition = {}; + LSTATUS status = RegCreateKeyExW(key, subKey.c_str(), 0, nullptr, options, access, NULL, &m_key, disposition); + + if (disposition == (LPDWORD)REG_CREATED_NEW_KEY) + { + AICLI_LOG(Core, Verbose, << "Subkey '" << Utility::ConvertToUTF8(subKey) << "' was created."); + } + else if (disposition == (LPDWORD)REG_OPENED_EXISTING_KEY) + { + AICLI_LOG(Core, Verbose, << "Subkey '" << Utility::ConvertToUTF8(subKey) << "' already existed and was opened."); + } + + THROW_IF_WIN32_ERROR(status); + return true; + } + bool Key::Initialize(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access, bool ignoreErrorIfDoesNotExist) { m_access = access; diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -24,9 +24,10 @@ namespace AppInstaller::Runtime constexpr std::string_view s_SecureSettings_Base = "Microsoft/WinGet"sv; constexpr std::string_view s_SecureSettings_UserRelative = "settings"sv; constexpr std::string_view s_SecureSettings_Relative_Unpackaged = "win"sv; - constexpr std::string_view s_PortableAppUserRoot = "Microsoft/WinGet"sv; - constexpr std::string_view s_PortableAppMachineRoot = "WinGet"sv; + constexpr std::string_view s_PortablePackageUserRoot_Base = "Microsoft"sv; + constexpr std::string_view s_PortablePackageRoot = "WinGet"sv; constexpr std::string_view s_PortablePackagesDirectory = "Packages"sv; + constexpr std::string_view s_LinksDirectory = "Links"sv; #ifndef WINGET_DISABLE_FOR_FUZZING constexpr std::string_view s_SecureSettings_Relative_Packaged = "pkg"sv; #endif @@ -343,36 +344,50 @@ namespace AppInstaller::Runtime result = GetKnownFolderPath(FOLDERID_Profile); create = false; break; - case PathName::PortableAppUserRoot: + case PathName::PortablePackageUserRoot: result = Settings::User().Get<Setting::PortableAppUserRoot>(); if (result.empty()) { result = GetKnownFolderPath(FOLDERID_LocalAppData); - result /= s_PortableAppUserRoot; + result /= s_PortablePackageUserRoot_Base; + result /= s_PortablePackageRoot; result /= s_PortablePackagesDirectory; } create = true; break; - case PathName::PortableAppMachineRootX64: + case PathName::PortablePackageMachineRootX64: result = Settings::User().Get<Setting::PortableAppMachineRoot>(); if (result.empty()) { result = GetKnownFolderPath(FOLDERID_ProgramFilesX64); - result /= s_PortableAppMachineRoot; + result /= s_PortablePackageRoot; result /= s_PortablePackagesDirectory; } create = true; break; - case PathName::PortableAppMachineRootX86: + case PathName::PortablePackageMachineRootX86: result = Settings::User().Get<Setting::PortableAppMachineRoot>(); if (result.empty()) { result = GetKnownFolderPath(FOLDERID_ProgramFilesX86); - result /= s_PortableAppMachineRoot; + result /= s_PortablePackageRoot; result /= s_PortablePackagesDirectory; } create = true; break; + case PathName::PortableLinksUserLocation: + result = GetKnownFolderPath(FOLDERID_LocalAppData); + result /= s_PortablePackageUserRoot_Base; + result /= s_PortablePackageRoot; + result /= s_LinksDirectory; + create = true; + break; + case PathName::PortableLinksMachineLocation: + result = GetKnownFolderPath(FOLDERID_ProgramFilesX64); + result /= s_PortablePackageRoot; + result /= s_LinksDirectory; + create = true; + break; default: THROW_HR(E_UNEXPECTED); } @@ -418,36 +433,50 @@ namespace AppInstaller::Runtime result = GetKnownFolderPath(FOLDERID_Profile); create = false; break; - case PathName::PortableAppUserRoot: + case PathName::PortablePackageUserRoot: result = Settings::User().Get<Setting::PortableAppUserRoot>(); if (result.empty()) { result = GetKnownFolderPath(FOLDERID_LocalAppData); - result /= s_PortableAppUserRoot; + result /= s_PortablePackageUserRoot_Base; + result /= s_PortablePackageRoot; result /= s_PortablePackagesDirectory; } create = true; break; - case PathName::PortableAppMachineRootX64: + case PathName::PortablePackageMachineRootX64: result = Settings::User().Get<Setting::PortableAppMachineRoot>(); if (result.empty()) { result = GetKnownFolderPath(FOLDERID_ProgramFilesX64); - result /= s_PortableAppMachineRoot; + result /= s_PortablePackageRoot; result /= s_PortablePackagesDirectory; } create = true; break; - case PathName::PortableAppMachineRootX86: + case PathName::PortablePackageMachineRootX86: result = Settings::User().Get<Setting::PortableAppMachineRoot>(); if (result.empty()) { result = GetKnownFolderPath(FOLDERID_ProgramFilesX86); - result /= s_PortableAppMachineRoot; + result /= s_PortablePackageRoot; result /= s_PortablePackagesDirectory; } create = true; break; + case PathName::PortableLinksUserLocation: + result = GetKnownFolderPath(FOLDERID_LocalAppData); + result /= s_PortablePackageUserRoot_Base; + result /= s_PortablePackageRoot; + result /= s_LinksDirectory; + create = true; + break; + case PathName::PortableLinksMachineLocation: + result = GetKnownFolderPath(FOLDERID_ProgramFilesX64); + result /= s_PortablePackageRoot; + result /= s_LinksDirectory; + create = true; + break; default: THROW_HR(E_UNEXPECTED); } @@ -514,55 +543,6 @@ namespace AppInstaller::Runtime return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); } - DWORD GetVolumeInformationFlagsByHandle(HANDLE anyFileHandle) - { - DWORD flags = 0; - wchar_t fileSystemName[MAX_PATH]; - THROW_LAST_ERROR_IF(!GetVolumeInformationByHandleW( - anyFileHandle, /*hFile*/ - NULL, /*lpVolumeNameBuffer*/ - 0, /*nVolumeNameSize*/ - NULL, /*lpVolumeSerialNumber*/ - NULL, /*lpMaximumComponentLength*/ - &flags, /*lpFileSystemFlags*/ - fileSystemName, /*lpFileSystemNameBuffer*/ - MAX_PATH /*nFileSystemNameSize*/)); - - // Vista and older does not report all flags, fix them up here - if (!(flags & FILE_SUPPORTS_HARD_LINKS) && !_wcsicmp(fileSystemName, L"NTFS")) - { - flags |= FILE_SUPPORTS_HARD_LINKS|FILE_SUPPORTS_EXTENDED_ATTRIBUTES|FILE_SUPPORTS_OPEN_BY_FILE_ID|FILE_SUPPORTS_USN_JOURNAL; - } - - return flags; - } - - DWORD GetVolumeInformationFlags(const std::filesystem::path& anyPath) - { - wil::unique_hfile fileHandle{ CreateFileW( - anyPath.c_str(), /*lpFileName*/ - 0, /*dwDesiredAccess*/ - FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, /*dwShareMode*/ - NULL, /*lpSecurityAttributes*/ - OPEN_EXISTING, /*dwCreationDisposition*/ - FILE_ATTRIBUTE_NORMAL, /*dwFlagsAndAttributes*/ - NULL /*hTemplateFile*/) }; - - THROW_LAST_ERROR_IF(fileHandle.get() == INVALID_HANDLE_VALUE); - - return GetVolumeInformationFlagsByHandle(fileHandle.get()); - } - - bool SupportsNamedStreams(const std::filesystem::path& path) - { - return (GetVolumeInformationFlags(path) & FILE_NAMED_STREAMS) != 0; - } - - bool SupportsHardLinks(const std::filesystem::path& path) - { - return (GetVolumeInformationFlags(path) & FILE_SUPPORTS_HARD_LINKS) != 0; - } - constexpr bool IsReleaseBuild() { #ifdef WINGET_ENABLE_RELEASE_BUILD diff --git a/src/AppInstallerCommonCore/SHA256.cpp b/src/AppInstallerCommonCore/SHA256.cpp @@ -6,6 +6,7 @@ #include "Public/AppInstallerSHA256.h" #include "Public/AppInstallerRuntime.h" #include "Public/AppInstallerErrors.h" +#include "Public/AppInstallerStrings.h" using namespace AppInstaller::Runtime; @@ -108,6 +109,11 @@ namespace AppInstaller::Utility { return std::string(resultBuffer); } + std::wstring SHA256::ConvertToWideString(const HashBuffer& hashBuffer) + { + return ConvertToUTF16(SHA256::ConvertToString(hashBuffer)); + } + SHA256::HashBuffer SHA256::ConvertToBytes(const std::string& hashStr) { if (hashStr.size() != HashStringSizeInChars)