diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index e1646184d6..382162dc12 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -180,6 +180,7 @@ FOF
foldc
foldcase
FOLDERID
+FONTHASH
FORPARSING
foundfr
fsanitize
@@ -427,6 +428,7 @@ pid
pidl
pidlist
PKCS
+PKEY
pkgmgr
pkindex
pkix
@@ -440,7 +442,10 @@ PRIMARYKEY
processthreads
productcode
PRODUCTICON
+propkey
+PROPVARIANT
proxystub
+pwsz
pscustomobject
pseudocode
PSHOST
diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md
index caad7a5ef4..2acacdd1b4 100644
--- a/doc/windows/package-manager/winget/returnCodes.md
+++ b/doc/windows/package-manager/winget/returnCodes.md
@@ -146,6 +146,8 @@ ms.localizationpriority: medium
| 0x8A150084 | -1978335100 | APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED | The Microsoft Store package does not support download command. |
| 0x8A150085 | -1978335099 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN | Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have required privilege. |
| 0x8A150086 | -1978335098 | APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE | Downloaded zero byte installer; ensure that your network connection is working properly. |
+| 0x8A150087 | -1978335097 | APPINSTALLER_CLI_ERROR_FONT_INSTALL_FAILED | Failed to install font package. |
+| 0x8A150088 | -1978335096 | APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED | Font file is not supported and cannot be installed. |
## Install errors.
@@ -170,7 +172,7 @@ ms.localizationpriority: medium
| 0x8A150111 | -1978334959 | APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE_BY_APPLICATION | Application is currently in use by another application. |
| 0x8A150112 | -1978334958 | APPINSTALLER_CLI_ERROR_INSTALL_INVALID_PARAMETER | Invalid parameter. |
| 0x8A150113 | -1978334957 | APPINSTALLER_CLI_ERROR_INSTALL_SYSTEM_NOT_SUPPORTED | Package not supported by the system. |
-| 0x8A150114 | -1978334956 | APPINSTALLER_CLI_ERROR_INSTALL_UPGRADE_NOT_SUPPORTED | The installer does not support upgrading an existing package. |
+| 0x8A150114 | -1978334956 | APPINSTALLER_CLI_ERROR_INSTALL_UPGRADE_NOT_SUPPORTED | The installer does not support upgrading an existing package. |
| 0x8A150115 | -1978334955 | APPINSTALLER_CLI_ERROR_INSTALL_CUSTOM_ERROR | Installation failed with installer custom error. |
## Check for package installed status
diff --git a/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json b/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json
index 98da3b2c34..e7523e92c7 100644
--- a/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json
+++ b/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json
@@ -65,7 +65,8 @@
"wix",
"burn",
"pwa",
- "portable"
+ "portable",
+ "font"
],
"description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level"
},
@@ -80,7 +81,8 @@
"nullsoft",
"wix",
"burn",
- "portable"
+ "portable",
+ "font"
],
"description": "Enumeration of supported nested installer types contained inside an archive file"
},
diff --git a/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json b/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json
index dd48208c79..380c62b227 100644
--- a/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json
+++ b/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json
@@ -167,7 +167,8 @@
"wix",
"burn",
"pwa",
- "portable"
+ "portable",
+ "font"
],
"description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level"
},
@@ -182,7 +183,8 @@
"nullsoft",
"wix",
"burn",
- "portable"
+ "portable",
+ "font"
],
"description": "Enumeration of supported nested installer types contained inside an archive file"
},
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
index 45c9c0fa04..cfff631fb3 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
@@ -398,6 +398,7 @@
+
@@ -483,6 +484,7 @@
+
Create
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
index a8f48edd34..e5942dd17c 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
@@ -266,6 +266,9 @@
Workflows
+
+ Header Files
+
@@ -502,6 +505,9 @@
Workflows
+
+ Source Files
+
diff --git a/src/AppInstallerCLICore/Commands/FontCommand.cpp b/src/AppInstallerCLICore/Commands/FontCommand.cpp
index c6faae5b44..7eb2f8e108 100644
--- a/src/AppInstallerCLICore/Commands/FontCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/FontCommand.cpp
@@ -5,6 +5,7 @@
#include "Workflows/CompletionFlow.h"
#include "Workflows/WorkflowBase.h"
#include "Workflows/FontFlow.h"
+#include "Workflows/InstallFlow.h"
#include "Resources.h"
namespace AppInstaller::CLI
@@ -20,6 +21,7 @@ namespace AppInstaller::CLI
{
return InitializeFromMoveOnly>>({
std::make_unique(FullName()),
+ std::make_unique(FullName()),
});
}
@@ -43,6 +45,52 @@ namespace AppInstaller::CLI
OutputHelp(context.Reporter);
}
+ std::vector FontInstallCommand::GetArguments() const
+ {
+ return {
+ Argument::ForType(Args::Type::Manifest),
+ Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help },
+ };
+ }
+
+ Resource::LocString FontInstallCommand::ShortDescription() const
+ {
+ return { Resource::String::FontInstallCommandShortDescription };
+ }
+
+ Resource::LocString FontInstallCommand::LongDescription() const
+ {
+ return { Resource::String::FontInstallCommandLongDescription };
+ }
+
+ void FontInstallCommand::Complete(Execution::Context& context, Args::Type valueType) const
+ {
+ UNREFERENCED_PARAMETER(valueType);
+ context.Reporter.Error() << Resource::String::PendingWorkError << std::endl;
+ }
+
+ Utility::LocIndView FontInstallCommand::HelpLink() const
+ {
+ return s_FontCommand_HelpLink;
+ }
+
+ void FontInstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const
+ {
+ Argument::ValidateCommonArguments(execArgs);
+ }
+
+ void FontInstallCommand::ExecuteInternal(Execution::Context& context) const
+ {
+ if (context.Args.Contains(Execution::Args::Type::Manifest))
+ {
+ context <<
+ Workflow::ReportExecutionStage(ExecutionStage::Discovery) <<
+ Workflow::GetManifestFromArg <<
+ Workflow::SelectInstaller <<
+ Workflow::InstallSinglePackage;
+ }
+ }
+
std::vector FontListCommand::GetArguments() const
{
return {
diff --git a/src/AppInstallerCLICore/Commands/FontCommand.h b/src/AppInstallerCLICore/Commands/FontCommand.h
index c29f703f6c..741821cd9c 100644
--- a/src/AppInstallerCLICore/Commands/FontCommand.h
+++ b/src/AppInstallerCLICore/Commands/FontCommand.h
@@ -21,6 +21,24 @@ namespace AppInstaller::CLI
void ExecuteInternal(Execution::Context& context) const override;
};
+ struct FontInstallCommand final : public Command
+ {
+ FontInstallCommand(std::string_view parent) : Command("install", parent) {}
+
+ std::vector GetArguments() const override;
+
+ Resource::LocString ShortDescription() const override;
+ Resource::LocString LongDescription() const override;
+
+ void Complete(Execution::Context& context, Execution::Args::Type valueType) const override;
+
+ Utility::LocIndView HelpLink() const override;
+
+ protected:
+ void ValidateArgumentsInternal(Execution::Args& execArgs) const override;
+ void ExecuteInternal(Execution::Context& context) const override;
+ };
+
struct FontListCommand final : public Command
{
FontListCommand(std::string_view parent) : Command("list", parent) {}
diff --git a/src/AppInstallerCLICore/FontInstaller.cpp b/src/AppInstallerCLICore/FontInstaller.cpp
new file mode 100644
index 0000000000..7cd4e38602
--- /dev/null
+++ b/src/AppInstallerCLICore/FontInstaller.cpp
@@ -0,0 +1,144 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "ExecutionContext.h"
+#include "FontInstaller.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace AppInstaller::CLI::Font
+{
+ namespace
+ {
+ constexpr std::wstring_view s_FontsPathSubkey = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts";
+ constexpr std::wstring_view s_TrueType = L" (TrueType)";
+
+ bool IsTrueTypeFont(DWRITE_FONT_FILE_TYPE fileType)
+ {
+ return (
+ fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE ||
+ fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE_COLLECTION
+ );
+ }
+ }
+
+ FontFile::FontFile(std::filesystem::path filePath, DWRITE_FONT_FILE_TYPE fileType)
+ : FilePath(std::move(filePath)), FileType(fileType)
+ {
+ Title = AppInstaller::Fonts::GetFontFileTitle(FilePath);
+
+ if (IsTrueTypeFont(FileType))
+ {
+ Title += s_TrueType;
+ }
+ }
+
+ FontInstaller::FontInstaller(Manifest::ScopeEnum scope) : m_scope(scope)
+ {
+ if (scope == Manifest::ScopeEnum::Machine)
+ {
+ m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsMachineInstallLocation);
+ m_key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_FontsPathSubkey });
+ }
+ else
+ {
+ m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsUserInstallLocation);
+ m_key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_FontsPathSubkey });
+ }
+ }
+
+ bool FontInstaller::EnsureInstall()
+ {
+ for (auto& fontFile : m_fontFiles)
+ {
+ if (m_key[fontFile.Title].has_value())
+ {
+ if (!std::filesystem::exists(m_key[fontFile.Title]->GetValue()))
+ {
+ AICLI_LOG(CLI, Info, << "Removing existing font value as font file does not exist.");
+ m_key.DeleteValue(fontFile.Title);
+ }
+ else
+ {
+ AICLI_LOG(CLI, Info, << "Existing font value found: " << AppInstaller::Utility::ConvertToUTF8(fontFile.Title));
+ return false;
+ }
+ }
+
+ std::filesystem::path destinationPath = m_installLocation / fontFile.FilePath.filename();
+ auto initialStem = fontFile.FilePath.stem();
+ auto extension = fontFile.FilePath.extension();
+
+ // If a file exists at the destination path, make the filename unique.
+ int index = 0;
+ while (std::filesystem::exists(destinationPath))
+ {
+ std::filesystem::path unique = { "_" + std::to_string(index) };
+ auto duplicateStem = initialStem;
+ duplicateStem += unique;
+ duplicateStem += extension;
+ destinationPath = m_installLocation / duplicateStem;
+ index++;
+ }
+
+ fontFile.DestinationPath = std::move(destinationPath);
+ }
+
+ return true;
+ }
+
+ void FontInstaller::Install()
+ {
+ bool isMachineScope = m_scope == Manifest::ScopeEnum::Machine;
+
+ for (const auto& fontFile : m_fontFiles)
+ {
+ AICLI_LOG(CLI, Info, << "Creating font value with name : " << AppInstaller::Utility::ConvertToUTF8(fontFile.Title));
+ if (isMachineScope)
+ {
+ m_key.SetValue(fontFile.Title, fontFile.DestinationPath.filename(), REG_SZ);
+ }
+ else
+ {
+ m_key.SetValue(fontFile.Title, fontFile.DestinationPath, REG_SZ);
+ }
+ }
+
+ for (const auto& fontFile : m_fontFiles)
+ {
+ AICLI_LOG(CLI, Info, << "Moving font file to: " << fontFile.DestinationPath);
+ AppInstaller::Filesystem::RenameFile(fontFile.FilePath, fontFile.DestinationPath);
+ }
+ }
+
+ void FontInstaller::Uninstall()
+ {
+ for (const auto& fontFile : m_fontFiles)
+ {
+ if (m_key[fontFile.Title].has_value())
+ {
+ AICLI_LOG(CLI, Info, << "Existing font value found:" << AppInstaller::Utility::ConvertToUTF8(fontFile.Title));
+ std::filesystem::path existingFontFilePath = { m_key[fontFile.Title]->GetValue() };
+
+ if (m_scope == Manifest::ScopeEnum::Machine)
+ {
+ // Font entries in the HKEY_LOCAL_MACHINE hive only have the filename specified as the value. Prepend install location.
+ existingFontFilePath = m_installLocation / existingFontFilePath;
+ }
+
+ if (std::filesystem::exists(existingFontFilePath))
+ {
+ AICLI_LOG(CLI, Info, << "Removing existing font file at:" << existingFontFilePath);
+ std::filesystem::remove(existingFontFilePath);
+ }
+
+ AICLI_LOG(CLI, Info, << "Deleting registry value:" << existingFontFilePath);
+ m_key.DeleteValue(fontFile.Title);
+ }
+ }
+ }
+}
diff --git a/src/AppInstallerCLICore/FontInstaller.h b/src/AppInstallerCLICore/FontInstaller.h
new file mode 100644
index 0000000000..0e4395ddad
--- /dev/null
+++ b/src/AppInstallerCLICore/FontInstaller.h
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include
+#include
+
+namespace AppInstaller::CLI::Font
+{
+ struct FontFile
+ {
+ FontFile(std::filesystem::path filePath, DWRITE_FONT_FILE_TYPE fileType);
+
+ std::filesystem::path FilePath;
+ DWRITE_FONT_FILE_TYPE FileType;
+ std::wstring Title;
+ std::filesystem::path DestinationPath;
+ };
+
+ struct FontInstaller
+ {
+ FontInstaller(Manifest::ScopeEnum scope);
+
+ std::filesystem::path FontFileLocation;
+
+ void SetFontFiles(const std::vector& fontFiles)
+ {
+ m_fontFiles = fontFiles;
+ }
+
+ // Checks if all expected registry values and font files can be installed prior to installation.
+ bool EnsureInstall();
+
+ void Install();
+
+ void Uninstall();
+
+ private:
+ Manifest::ScopeEnum m_scope;
+ std::filesystem::path m_installLocation;
+ Registry::Key m_key;
+
+ std::vector m_fontFiles;
+ };
+}
diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h
index cd56f587c2..c87a166f79 100644
--- a/src/AppInstallerCLICore/Resources.h
+++ b/src/AppInstallerCLICore/Resources.h
@@ -250,13 +250,18 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(FileNotFound);
WINGET_DEFINE_RESOURCE_STRINGID(FilesRemainInInstallDirectory);
WINGET_DEFINE_RESOURCE_STRINGID(FlagContainAdjoinedError);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontAlreadyInstalled);
WINGET_DEFINE_RESOURCE_STRINGID(FontCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontFace);
WINGET_DEFINE_RESOURCE_STRINGID(FontFaces);
WINGET_DEFINE_RESOURCE_STRINGID(FontFamily);
WINGET_DEFINE_RESOURCE_STRINGID(FontFamilyNameArgumentDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontFileNotSupported);
WINGET_DEFINE_RESOURCE_STRINGID(FontFilePaths);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandLongDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandShortDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontInstallFailed);
WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontVersion);
diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp
index 084cbb4d3f..d97d046a46 100644
--- a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp
@@ -71,23 +71,30 @@ namespace AppInstaller::CLI::Workflow
// Gets the file name that can be used to ShellExecute the file.
std::filesystem::path GetInstallerPostHashValidationFileName(Execution::Context& context)
{
+ const auto& installer = context.Get();
+
// Get file name from download URI
- std::filesystem::path filename = GetFileNameFromURI(context.Get()->Url);
- std::wstring_view installerExtension = GetInstallerFileExtension(context);
+ std::filesystem::path filename = GetFileNameFromURI(installer->Url);
- // Assuming that we find a safe stem value in the URI, use it.
- // This should be extremely common, but just in case fall back to the older name style.
- if (filename.has_stem() && ((filename.wstring().size() + installerExtension.size()) < MAX_PATH))
+ // Default to URI for fonts since fonts can have multiple file extensions.
+ if (!DoesInstallerTypeSupportMultipleFileExtensions(installer->BaseInstallerType))
{
- filename = filename.stem();
- }
- else
- {
- const auto& manifest = context.Get();
- filename = Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version);
- }
+ std::wstring_view installerExtension = GetInstallerFileExtension(context);
- filename += installerExtension;
+ // Assuming that we find a safe stem value in the URI, use it.
+ // This should be extremely common, but just in case fall back to the older name style.
+ if (filename.has_stem() && ((filename.wstring().size() + installerExtension.size()) < MAX_PATH))
+ {
+ filename = filename.stem();
+ }
+ else
+ {
+ const auto& manifest = context.Get();
+ filename = Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version);
+ }
+
+ filename += installerExtension;
+ }
// Make file name suitable for file system path
filename = Utility::ConvertToUTF16(Utility::MakeSuitablePathPart(filename.u8string()));
@@ -223,6 +230,7 @@ namespace AppInstaller::CLI::Workflow
case InstallerTypeEnum::Portable:
case InstallerTypeEnum::Wix:
case InstallerTypeEnum::Zip:
+ case InstallerTypeEnum::Font:
context << DownloadInstallerFile;
break;
case InstallerTypeEnum::Msix:
diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp
index ce9bb7a233..dc9bb827ec 100644
--- a/src/AppInstallerCLICore/Workflows/FontFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp
@@ -5,10 +5,12 @@
#include "TableOutput.h"
#include
#include
+#include
namespace AppInstaller::CLI::Workflow
{
using namespace AppInstaller::CLI::Execution;
+ using namespace AppInstaller::CLI::Font;
namespace
{
@@ -121,4 +123,74 @@ namespace AppInstaller::CLI::Workflow
OutputInstalledFontFamiliesTable(context, lines);
}
}
+
+ void FontInstallImpl(Execution::Context& context)
+ {
+ Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown;
+ if (context.Args.Contains(Execution::Args::Type::InstallScope))
+ {
+ scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
+ }
+
+ FontInstaller fontInstaller = FontInstaller(scope);
+
+ context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl;
+
+ try
+ {
+ const auto& installerPath = context.Get();
+ std::vector filePaths;
+
+ // InstallerPath will point to a directory if extracted from an archive.
+ if (std::filesystem::is_directory(installerPath))
+ {
+ const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles;
+ for (const auto& nestedInstallerFile : nestedInstallerFiles)
+ {
+ filePaths.emplace_back(installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath));
+ }
+ }
+ else
+ {
+ filePaths.emplace_back(installerPath);
+ }
+
+ Fonts::FontCatalog fontCatalog;
+ std::vector fontFiles;
+
+ for (std::filesystem::path filePath : filePaths)
+ {
+ DWRITE_FONT_FILE_TYPE fileType;
+ if (!fontCatalog.IsFontFileSupported(filePath, fileType))
+ {
+ AICLI_LOG(CLI, Warning, << "Font file is not supported: " << filePath);
+ context.Reporter.Error() << Resource::String::FontFileNotSupported << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED);
+ }
+ else
+ {
+ AICLI_LOG(CLI, Verbose, << "Font file is supported: " << filePath);
+ fontFiles.emplace_back(FontFile(filePath, fileType));
+ }
+ }
+
+ fontInstaller.SetFontFiles(fontFiles);
+
+ if (!fontInstaller.EnsureInstall())
+ {
+ context.Reporter.Warn() << Resource::String::FontAlreadyInstalled << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_FONT_ALREADY_INSTALLED);
+ }
+
+ fontInstaller.Install();
+ context.Add(S_OK);
+ }
+ catch (...)
+ {
+ context.Add(Workflow::HandleException(context, std::current_exception()));
+ context.Reporter.Warn() << Resource::String::FontInstallFailed << std::endl;
+ fontInstaller.Uninstall();
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED);
+ }
+ }
}
diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.h b/src/AppInstallerCLICore/Workflows/FontFlow.h
index c3346e1a60..5ad5e0a2e8 100644
--- a/src/AppInstallerCLICore/Workflows/FontFlow.h
+++ b/src/AppInstallerCLICore/Workflows/FontFlow.h
@@ -10,4 +10,10 @@ namespace AppInstaller::CLI::Workflow
// Inputs: None
// Outputs: None
void ReportInstalledFonts(Execution::Context& context);
+
+ // Installs the font package.
+ // Required Args: None
+ // Inputs: Manifest, Scope, Rename, Location
+ // Outputs: None
+ void FontInstallImpl(Execution::Context& context);
}
diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
index e85540c379..dcd299340e 100644
--- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
+++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp
@@ -3,6 +3,7 @@
#include "pch.h"
#include "InstallFlow.h"
#include "DownloadFlow.h"
+#include "FontFlow.h"
#include "UninstallFlow.h"
#include "UpdateFlow.h"
#include "ResumeFlow.h"
@@ -276,6 +277,17 @@ namespace AppInstaller::CLI::Workflow
VerifyAndSetNestedInstaller <<
ExecuteInstallerForType(context.Get().value().NestedInstallerType);
}
+
+ // Runs the flow for installing a font package.
+ // Required Args: None
+ // Inputs: Installer, InstallerPath
+ // Outputs: None
+ void FontInstall(Execution::Context& context)
+ {
+ context <<
+ FontInstallImpl <<
+ ReportInstallerResult("Font"sv, APPINSTALLER_CLI_ERROR_FONT_INSTALL_FAILED, true);
+ }
}
bool ExemptFromSingleInstallLocking(InstallerTypeEnum type)
@@ -443,6 +455,9 @@ namespace AppInstaller::CLI::Workflow
case InstallerTypeEnum::Zip:
context << details::ArchiveInstall;
break;
+ case InstallerTypeEnum::Font:
+ context << details::FontInstall;
+ break;
default:
THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
}
diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h
index cbc0ce70bc..cc3df3810f 100644
--- a/src/AppInstallerCLICore/Workflows/InstallFlow.h
+++ b/src/AppInstallerCLICore/Workflows/InstallFlow.h
@@ -49,6 +49,12 @@ namespace AppInstaller::CLI::Workflow
// Inputs: Installer, InstallerPath, Manifest
// Outputs: None
void ArchiveInstall(Execution::Context& context);
+
+ // Runs the flow for installing a font package.
+ // Required Args: None
+ // Inputs: Installer, InstallerPath, Manifest
+ // Outputs: None
+ void FontInstall(Execution::Context& context);
}
// Ensures that there is an applicable installer.
diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj
index 179ffb1583..5806dd9080 100644
--- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj
+++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj
@@ -61,6 +61,8 @@
+
+
diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs
index 70d3023a18..9586c7950b 100644
--- a/src/AppInstallerCLIE2ETests/Constants.cs
+++ b/src/AppInstallerCLIE2ETests/Constants.cs
@@ -27,6 +27,7 @@ public class Constants
public const string MsiInstallerPathParameter = "MsiTestInstallerPath";
public const string MsiInstallerV2PathParameter = "MsiTestInstallerV2Path";
public const string MsixInstallerPathParameter = "MsixTestInstallerPath";
+ public const string FontPathParameter = "FontTestPath";
public const string PackageCertificatePathParameter = "PackageCertificatePath";
public const string PowerShellModulePathParameter = "PowerShellModulePath";
public const string SkipTestSourceParameter = "SkipTestSource";
@@ -55,11 +56,13 @@ public class Constants
public const string MsiInstaller = "AppInstallerTestMsiInstaller";
public const string MsixInstaller = "AppInstallerTestMsixInstaller";
public const string ZipInstaller = "AppInstallerTestZipInstaller";
+ public const string Font = "AppInstallerTestFont";
public const string ExeInstallerFileName = "AppInstallerTestExeInstaller.exe";
public const string MsiInstallerFileName = "AppInstallerTestMsiInstaller.msi";
public const string MsiInstallerV2FileName = "AppInstallerTestMsiInstallerV2.msi";
public const string MsixInstallerFileName = "AppInstallerTestMsixInstaller.msix";
public const string ZipInstallerFileName = "AppInstallerTestZipInstaller.zip";
+ public const string FontFileName = "AppInstallerTestFont.ttf";
public const string ModifyRepairInstaller = "AppInstallerTest.TestModifyRepair";
public const string IndexPackage = "source.msix";
public const string MakeAppx = "makeappx.exe";
@@ -118,6 +121,8 @@ public class Constants
public const string UninstallSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall";
public const string PathSubKey_User = @"Environment";
public const string PathSubKey_Machine = @"SYSTEM\CurrentControlSet\Control\Session Manager\Environment";
+ public const string FontsSubKey = @"Software\Microsoft\Windows NT\CurrentVersion\Fonts";
+ public const string TestFontSubKeyName = "Cascadia Code PL Regular (TrueType)";
// User settings
public const string ArchiveExtractionMethod = "archiveExtractionMethod";
@@ -272,6 +277,8 @@ public class ErrorCode
public const int ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED = unchecked((int)0x8A15007D);
public const int ERROR_INSTALLER_ZERO_BYTE_FILE = unchecked((int)0x8A150086);
+ public const int ERROR_FONT_INSTALL_FAILED = unchecked((int)0x8A150087);
+ public const int ERROR_FONT_FILE_NOT_SUPPORTED = unchecked((int)0x8A150088);
public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101);
public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102);
diff --git a/src/AppInstallerCLIE2ETests/FontCommand.cs b/src/AppInstallerCLIE2ETests/FontCommand.cs
new file mode 100644
index 0000000000..1292caad24
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/FontCommand.cs
@@ -0,0 +1,61 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace AppInstallerCLIE2ETests
+{
+ using AppInstallerCLIE2ETests.Helpers;
+ using NUnit.Framework;
+
+ ///
+ /// Test font command.
+ ///
+ public class FontCommand : BaseCommand
+ {
+ ///
+ /// One time set up.
+ ///
+ [OneTimeSetUp]
+ public void OneTimeSetup()
+ {
+ WinGetSettingsHelper.ConfigureFeature("fonts", true);
+ }
+
+ ///
+ /// Test install a font with user scope.
+ ///
+ [Test]
+ public void InstallFont_UserScope()
+ {
+ var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestFont");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Successfully installed"));
+ TestCommon.VerifyFontPackage(Constants.TestFontSubKeyName, Constants.FontFileName, TestCommon.Scope.User);
+ }
+
+ ///
+ /// Test install a font with machine scope.
+ ///
+ [Test]
+ public void InstallFont_MachineScope()
+ {
+ var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestFont --scope Machine");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Successfully installed"));
+ TestCommon.VerifyFontPackage(Constants.TestFontSubKeyName, Constants.FontFileName, TestCommon.Scope.Machine);
+ }
+
+ ///
+ /// Test install an invalid font file.
+ ///
+ [Test]
+ public void InstallInvalidFont()
+ {
+ var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestInvalidFont");
+ Assert.AreEqual(Constants.ErrorCode.ERROR_FONT_FILE_NOT_SUPPORTED, result.ExitCode);
+ Assert.True(result.StdOut.Contains("The font file is not supported and cannot be installed."));
+ }
+ }
+}
diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
index 88fb61d5c0..f0b640abff 100644
--- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
+++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
@@ -344,6 +344,81 @@ public static string GetCheckpointsDirectory()
}
}
+ ///
+ /// Gets the fonts directory based on scope.
+ ///
+ /// Scope.
+ /// The path of the fonts directory.
+ public static string GetFontsDirectory(Scope scope)
+ {
+ if (scope == Scope.Machine)
+ {
+ return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts");
+ }
+ else
+ {
+ return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "Windows", "Fonts");
+ }
+ }
+
+ ///
+ /// Verify font package.
+ ///
+ /// Name of the font registry subkey entry.
+ /// Filename of the installed font file.
+ /// Scope.
+ /// Should exist.
+ public static void VerifyFontPackage(
+ string fontSubKeyName,
+ string fontFileName,
+ Scope scope = Scope.User,
+ bool shouldExist = true)
+ {
+ // Expected font file path.
+ string expectedFontInstallPath = Path.Combine(GetFontsDirectory(scope), fontFileName);
+ bool fontFileExists = File.Exists(expectedFontInstallPath);
+
+ // Expected font registry entry.
+ bool fontEntryExists = false;
+ RegistryKey baseKey = (scope == Scope.Machine) ? Registry.LocalMachine : Registry.CurrentUser;
+ string expectedSubKeyValue = (scope == Scope.Machine) ? Path.GetFileName(expectedFontInstallPath) : expectedFontInstallPath;
+ using (RegistryKey fontsRegistryKey = baseKey.OpenSubKey(Constants.FontsSubKey, true))
+ {
+ var fontSubKeyValue = fontsRegistryKey.GetValue(fontSubKeyName);
+ if (fontSubKeyValue != null)
+ {
+ fontEntryExists = fontSubKeyValue.Equals(expectedSubKeyValue);
+ }
+ }
+
+ if (shouldExist)
+ {
+ // TODO: Replace with font uninstall when implemented.
+ if (fontEntryExists)
+ {
+ using (RegistryKey fontsRegistryKey = baseKey.OpenSubKey(Constants.FontsSubKey, true))
+ {
+ fontsRegistryKey.DeleteValue(fontSubKeyName);
+ }
+ }
+
+ if (fontFileExists)
+ {
+ try
+ {
+ File.Delete(expectedFontInstallPath);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // TODO: This error occurs for user mode if the font is in use. Skip cleanup.
+ }
+ }
+ }
+
+ Assert.AreEqual(shouldExist, fontFileExists, $"Expected font path: {expectedFontInstallPath}");
+ Assert.AreEqual(shouldExist, fontEntryExists, $"Expected {fontSubKeyName} subkey with value {expectedSubKeyValue} in registry path: {Constants.FontsSubKey}");
+ }
+
///
/// Verify portable package.
///
@@ -351,7 +426,7 @@ public static string GetCheckpointsDirectory()
/// Command alias.
/// File name.
/// Product code.
- /// Should exists.
+ /// Should exist.
/// Scope.
/// Install directory added to path instead of the symlink directory.
public static void VerifyPortablePackage(
diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs
index 2728661e86..d2eb625e7a 100644
--- a/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs
+++ b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs
@@ -8,7 +8,6 @@ namespace AppInstallerCLIE2ETests.Helpers
{
using System;
using System.IO;
- using System.Text.Json;
using Microsoft.WinGetSourceCreator;
using WinGetSourceCreator.Model;
@@ -25,6 +24,7 @@ static TestIndex()
TestIndex.MsiInstallerV2 = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsiInstaller, Constants.MsiInstallerV2FileName);
TestIndex.MsixInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsixInstaller, Constants.MsixInstallerFileName);
TestIndex.ZipInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.ZipInstaller, Constants.ZipInstallerFileName);
+ TestIndex.Font = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.Font, Constants.FontFileName);
}
///
@@ -52,6 +52,11 @@ static TestIndex()
///
public static string ZipInstaller { get; private set; }
+ ///
+ /// Gets the font file path used by the manifests in the E2E test.
+ ///
+ public static string Font { get; private set; }
+
///
/// Generate test source.
///
@@ -99,6 +104,16 @@ public static void GenerateE2ESource()
throw new FileNotFoundException(testParams.MsixInstallerPath);
}
+ if (string.IsNullOrEmpty(testParams.FontPath))
+ {
+ throw new ArgumentNullException($"{Constants.FontPathParameter} is required");
+ }
+
+ if (!File.Exists(testParams.FontPath))
+ {
+ throw new FileNotFoundException(testParams.FontPath);
+ }
+
if (string.IsNullOrEmpty(testParams.PackageCertificatePath))
{
throw new ArgumentNullException($"{Constants.PackageCertificatePathParameter} is required");
@@ -148,6 +163,13 @@ public static void GenerateE2ESource()
HashToken = "",
SignatureToken = "",
},
+ new LocalInstaller
+ {
+ Type = InstallerType.Font,
+ Name = Path.Combine(Constants.Font, Constants.FontFileName),
+ Input = testParams.FontPath,
+ HashToken = "",
+ },
},
DynamicInstallers = new ()
{
diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs
index b681fc710f..73faa5afcd 100644
--- a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs
+++ b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs
@@ -44,6 +44,7 @@ private TestSetup()
this.MsiInstallerPath = this.InitializeFileParam(Constants.MsiInstallerPathParameter);
this.MsixInstallerPath = this.InitializeFileParam(Constants.MsixInstallerPathParameter);
this.MsiInstallerV2Path = this.InitializeFileParam(Constants.MsiInstallerV2PathParameter);
+ this.MsiInstallerV2Path = this.InitializeFileParam(Constants.FontPathParameter);
this.ForcedExperimentalFeatures = this.InitializeStringArrayParam(Constants.ForcedExperimentalFeaturesParameter);
}
@@ -119,6 +120,11 @@ public static TestSetup Parameters
///
public string ZipInstallerPath { get; }
+ ///
+ /// Gets the font path.
+ ///
+ public string FontPath { get; }
+
///
/// Gets the package cert path.
///
diff --git a/src/AppInstallerCLIE2ETests/Test.runsettings b/src/AppInstallerCLIE2ETests/Test.runsettings
index 6f6079e24e..6c4742a3f4 100644
--- a/src/AppInstallerCLIE2ETests/Test.runsettings
+++ b/src/AppInstallerCLIE2ETests/Test.runsettings
@@ -1,4 +1,4 @@
-
+