From 359590e50c5678562547e0eb5cb7b1c0e097df7a Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sat, 20 Jun 2026 21:23:05 -0500 Subject: [PATCH 1/3] Use hardlinks for aliasing in portable installations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/ReleaseNotes.md | 6 +- src/AppInstallerCLICore/PortableInstaller.cpp | 58 +++++++++ .../Workflows/PortableFlow.cpp | 42 +++++-- src/AppInstallerCLIE2ETests/InstallCommand.cs | 112 ++++++++++++++++++ .../UninstallCommand.cs | 35 ++++++ .../Public/winget/PortableFileEntry.h | 14 ++- src/AppInstallerSharedLib/Filesystem.cpp | 32 +++++ .../Public/winget/Filesystem.h | 4 + 8 files changed, 294 insertions(+), 9 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 57c970aeb6..760d49ff11 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -4,4 +4,8 @@ Nothing yet. ## Bug Fixes -* None yet +### Portable installer alias handling + +Portable installs now preserve the original executable filename instead of renaming it when an alias is needed. For aliases requested through `--rename`, `Commands`, or `PortableCommandAlias`, WinGet creates a hardlink alias and keeps the original file as the source executable. + +This change resolves alias failures in non-symlinked scenarios, including cases where WinGet adds the install directory to `PATH` instead of creating links. Because the alias is now created as an executable hardlink in the install location, command aliases remain available and consistent even when symlink creation is skipped. diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 1e3498e55a..d3ade3233a 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -72,6 +72,22 @@ namespace AppInstaller::CLI::Portable } } } + else if (fileType == PortableFileType::Hardlink) + { + if (std::filesystem::exists(filePath)) + { + // Only verify hash if one was stored + if (!entry.SHA256.empty()) + { + SHA256::HashBuffer fileHash = SHA256::ComputeHashFromFile(filePath); + if (!SHA256::AreEqual(fileHash, SHA256::ConvertToBytes(entry.SHA256))) + { + AICLI_LOG(CLI, Warning, << "Hardlink hash does not match ARP Entry. Expected: " << entry.SHA256 << " Actual: " << SHA256::ConvertToString(fileHash)); + return false; + } + } + } + } else if (fileType == PortableFileType::Symlink) { std::filesystem::path symlinkTargetPath{ AppInstaller::Utility::ConvertToUTF16(entry.SymlinkTarget) }; @@ -122,6 +138,33 @@ namespace AppInstaller::CLI::Portable std::filesystem::copy(entry.CurrentPath, filePath, std::filesystem::copy_options::overwrite_existing | std::filesystem::copy_options::recursive); } } + else if (fileType == PortableFileType::Hardlink) + { + if (std::filesystem::exists(filePath)) + { + AICLI_LOG(Core, Info, << "Removing existing portable hardlink at: " << filePath); + std::filesystem::remove(filePath); + } + + AICLI_LOG(Core, Info, << "Creating hardlink at: " << filePath << " pointing to: " << entry.CurrentPath); + + // Try to create hardlink + if (Filesystem::CreateHardlink(entry.CurrentPath, filePath)) + { + AICLI_LOG(Core, Info, << "Hardlink created successfully at: " << filePath); + } + else + { + // Fallback: copy the file if hardlinks not supported + AICLI_LOG(Core, Info, << "Hardlink creation failed, falling back to copy: " << filePath); + std::filesystem::copy_file(entry.CurrentPath, filePath, std::filesystem::copy_options::overwrite_existing); + } + + if (!RecordToIndex) + { + CommitToARPEntry(PortableValueName::SHA256, entry.SHA256); + } + } else if (entry.FileType == PortableFileType::Symlink) { std::filesystem::path symlinkTargetPath{ Utility::ConvertToUTF16(entry.SymlinkTarget) }; @@ -181,6 +224,11 @@ namespace AppInstaller::CLI::Portable AICLI_LOG(CLI, Info, << "Deleting portable exe at: " << filePath); std::filesystem::remove(filePath); } + else if (fileType == PortableFileType::Hardlink && std::filesystem::exists(filePath)) + { + AICLI_LOG(CLI, Info, << "Deleting portable hardlink at: " << filePath); + std::filesystem::remove(filePath); + } else if (fileType == PortableFileType::Symlink) { if (Filesystem::SymlinkExists(filePath)) @@ -462,6 +510,16 @@ namespace AppInstaller::CLI::Portable if (!symlinkFullPath.empty()) { + // If alias differs from original filename, a hardlink exists in the install directory. + // Track it so uninstall removes it even when state is reconstructed from ARP values. + if (!targetFullPath.empty() && targetFullPath.filename() != symlinkFullPath.filename()) + { + std::filesystem::path hardlinkPath = InstallLocation / symlinkFullPath.filename(); + if (hardlinkPath != targetFullPath && std::filesystem::exists(hardlinkPath)) + { + m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateHardlinkEntry(hardlinkPath, targetFullPath, SHA256))); + } + } m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); } } diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 7d58b77110..b372ea82aa 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -188,7 +188,6 @@ namespace AppInstaller::CLI::Workflow for (const auto& entry : std::filesystem::directory_iterator(installerPath)) { std::filesystem::path entryPath = entry.path(); - PortableFileEntry portableFile; std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path()); std::filesystem::path targetPath = targetInstallDirectory / relativePath; @@ -203,10 +202,12 @@ namespace AppInstaller::CLI::Workflow } const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; - + for (const auto& nestedInstallerFile : nestedInstallerFiles) { - const std::filesystem::path& targetPath = targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + const std::filesystem::path& relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + const std::filesystem::path& targetPath = targetInstallDirectory / relativeFilePath; + std::filesystem::path originalFilename = targetPath.filename(); std::filesystem::path commandAlias; if (nestedInstallerFile.PortableCommandAlias.empty()) @@ -219,15 +220,30 @@ namespace AppInstaller::CLI::Workflow } Filesystem::AppendExtension(commandAlias, ".exe"); + + // If alias differs from original filename, create hardlink + // Hardlink will be placed in the same directory as the original file to avoid pathing issues and same-volume restrictions + if (commandAlias != originalFilename) + { + std::filesystem::path sourcePath = installerPath / relativeFilePath; + std::filesystem::path hardlinkPath = targetPath.parent_path() / commandAlias; + // Compute SHA256 from source file for the hardlink entry + std::string sha256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(sourcePath)); + entries.emplace_back(std::move(PortableFileEntry::CreateHardlinkEntry(hardlinkPath, targetPath, sha256))); + } entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetPath))); } } else { + // Non-archive portable case: single executable file std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); const std::vector& commands = context.Get()->Commands; - std::filesystem::path commandAlias = installerPath.filename(); + + std::filesystem::path originalFilename = installerPath.filename(); + std::filesystem::path commandAlias = originalFilename; + // Determine the command alias from rename arg, commands, or use original filename if (!commands.empty()) { commandAlias = ConvertToUTF16(commands[0]); @@ -237,10 +253,22 @@ namespace AppInstaller::CLI::Workflow { commandAlias = ConvertToUTF16(renameArg); } - AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); - const std::filesystem::path& targetFullPath = targetInstallDirectory / commandAlias; - entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, {}))); + Filesystem::AppendExtension(commandAlias, ".exe"); + + // Target path for the original file (keeps its original name) + const std::filesystem::path& targetFullPath = targetInstallDirectory / originalFilename; + + // Create file entry for original (with original name) - this computes SHA256 + std::string fileSha256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(installerPath)); + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, fileSha256))); + + // If alias differs from original filename, create hardlink + if (commandAlias != originalFilename) + { + std::filesystem::path hardlinkPath = targetInstallDirectory / commandAlias; + entries.emplace_back(std::move(PortableFileEntry::CreateHardlinkEntry(hardlinkPath, targetFullPath, fileSha256))); + } entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetFullPath))); } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index cdbc171082..7fed6323da 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -544,6 +544,118 @@ public void InstallZip_ArchivePortableWithBinariesDependentOnPath() TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.User, true); } + /// + /// Test install portable with rename creates hardlink instead of renaming original. + /// + [Test] + public void InstallPortableWithRename_VerifyHardlink() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExeWithCommand"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + string renameArgValue = "customAlias.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId} --rename {renameArgValue}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + string installPath = Path.Combine(installDir, packageDirName); + string originalFile = Path.Combine(installPath, "AppInstallerTestExeInstaller.exe"); + string hardlinkFile = Path.Combine(installPath, renameArgValue); + + // Verify original file exists with original name (not renamed) + Assert.True(File.Exists(originalFile), $"Original file should exist at: {originalFile}"); + + // Verify hardlink exists + Assert.True(File.Exists(hardlinkFile), $"Hardlink should exist at: {hardlinkFile}"); + + // Verify hardlink and original point to same content (equivalence) + byte[] originalBytes = File.ReadAllBytes(originalFile); + byte[] hardlinkBytes = File.ReadAllBytes(hardlinkFile); + Assert.AreEqual(originalBytes.Length, hardlinkBytes.Length, "File sizes should be equal"); + Assert.True(originalBytes.AsSpan().SequenceEqual(hardlinkBytes.AsSpan()), "Hardlink should be equivalent to original file"); + + // Verify uninstall removes both original and hardlink + var uninstallResult = TestCommon.RunAICLICommand("uninstall", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, uninstallResult.ExitCode); + Assert.False(File.Exists(originalFile), $"Original file should be removed after uninstall"); + Assert.False(File.Exists(hardlinkFile), $"Hardlink should be removed after uninstall"); + } + + /// + /// Test install portable with Commands field creates hardlinks for all command aliases. + /// + [Test] + public void InstallPortableWithCommands_VerifyHardlinks() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExeWithCommand"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + string commandAlias = "testCommand.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + string installPath = Path.Combine(installDir, packageDirName); + string originalFile = Path.Combine(installPath, "AppInstallerTestExeInstaller.exe"); + string hardlinkFile = Path.Combine(installPath, commandAlias); + + // Verify original file exists with original name + Assert.True(File.Exists(originalFile), $"Original file should exist at: {originalFile}"); + + // Verify command alias hardlink exists + Assert.True(File.Exists(hardlinkFile), $"Command alias hardlink should exist at: {hardlinkFile}"); + + // Verify hardlink is equivalent to original + byte[] originalBytes = File.ReadAllBytes(originalFile); + byte[] hardlinkBytes = File.ReadAllBytes(hardlinkFile); + Assert.AreEqual(originalBytes.Length, hardlinkBytes.Length, "File sizes should be equal"); + Assert.True(originalBytes.AsSpan().SequenceEqual(hardlinkBytes.AsSpan()), "Command alias hardlink should be equivalent to original file"); + + // Cleanup + TestCommon.RunAICLICommand("uninstall", $"{packageId}"); + } + + /// + /// Test install zip portable with PortableCommandAlias creates hardlinks for nested files. + /// + [Test] + public void InstallZip_PortableWithCommandAlias_VerifyHardlinks() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, packageDirName, productCode; + packageId = "AppInstallerTest.TestZipInstallerWithPortable"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + string originalFileName = "AppInstallerTestExeInstaller.exe"; + string commandAlias = "TestPortable.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + string installPath = Path.Combine(installDir, packageDirName); + string originalFile = Path.Combine(installPath, originalFileName); + string hardlinkFile = Path.Combine(installPath, commandAlias); + + // Verify original extracted file exists with original name + Assert.True(File.Exists(originalFile), $"Original extracted file should exist at: {originalFile}"); + + // Verify hardlink for command alias exists + Assert.True(File.Exists(hardlinkFile), $"Command alias hardlink should exist at: {hardlinkFile}"); + + // Verify hardlink is equivalent to original + byte[] originalBytes = File.ReadAllBytes(originalFile); + byte[] hardlinkBytes = File.ReadAllBytes(hardlinkFile); + Assert.AreEqual(originalBytes.Length, hardlinkBytes.Length, "File sizes should be equal"); + Assert.True(originalBytes.AsSpan().SequenceEqual(hardlinkBytes.AsSpan()), "Archive portable hardlink should be equivalent to original file"); + + // Cleanup + TestCommon.RunAICLICommand("uninstall", $"{packageId}"); + } + /// /// Test install zip with invalid relative file path. /// diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs index 4a8f70134d..7e129273c4 100644 --- a/src/AppInstallerCLIE2ETests/UninstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -188,6 +188,41 @@ public void UninstallZip_Portable() TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); } + /// + /// Test uninstall portable package removes hardlinks. + /// + [Test] + public void UninstallPortableWithCommands_RemovesHardlinks() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExeWithCommand"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + + // Install + var installResult = TestCommon.RunAICLICommand("install", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, installResult.ExitCode); + Assert.True(installResult.StdOut.Contains("Successfully installed")); + + string installPath = Path.Combine(installDir, packageDirName); + string originalFile = Path.Combine(installPath, "AppInstallerTestExeInstaller.exe"); + string hardlinkFile = Path.Combine(installPath, "testCommand.exe"); + + // Verify files exist after install + Assert.True(File.Exists(originalFile), "Original file should exist after install"); + Assert.True(File.Exists(hardlinkFile), "Hardlink should exist after install"); + + // Uninstall + var uninstallResult = TestCommon.RunAICLICommand("uninstall", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, uninstallResult.ExitCode); + Assert.True(uninstallResult.StdOut.Contains("Successfully uninstalled")); + + // Verify both original and hardlink are removed + Assert.False(File.Exists(originalFile), "Original file should be removed after uninstall"); + Assert.False(File.Exists(hardlinkFile), "Hardlink should be removed after uninstall"); + Assert.False(Directory.Exists(installPath), "Installation directory should be removed after uninstall"); + } + /// /// Test uninstall not indexed. /// diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h index 26c834ff7c..4067b1afcc 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -13,7 +13,8 @@ namespace AppInstaller::Portable Unknown, File, Directory, - Symlink + Symlink, + Hardlink }; // Metadata representation of a portable file placed down during installation @@ -66,6 +67,17 @@ namespace AppInstaller::Portable return symlinkEntry; } + static PortableFileEntry CreateHardlinkEntry(const std::filesystem::path& hardlinkPath, const std::filesystem::path& targetPath, const std::string& sha256 = {}) + { + PortableFileEntry hardlinkEntry; + hardlinkEntry.FileType = PortableFileType::Hardlink; + hardlinkEntry.CurrentPath = targetPath; + hardlinkEntry.SetFilePath(hardlinkPath); + // Use provided SHA256 or empty (will be computed from target after install) + hardlinkEntry.SHA256 = sha256; + return hardlinkEntry; + } + static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& currentPath, const std::filesystem::path& directoryPath) { PortableFileEntry directoryEntry; diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp index 85acbf09a3..1373860850 100644 --- a/src/AppInstallerSharedLib/Filesystem.cpp +++ b/src/AppInstallerSharedLib/Filesystem.cpp @@ -439,6 +439,38 @@ namespace AppInstaller::Filesystem std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing); } + bool CreateHardlink(const std::filesystem::path& target, const std::filesystem::path& link) + { + try + { + // Remove the link path if it already exists + if (std::filesystem::exists(link)) + { + std::filesystem::remove(link); + } + + // Attempt to create the hardlink + std::filesystem::create_hard_link(target, link); + AICLI_LOG(Core, Info, << "Hardlink created successfully from " << target << " to " << link); + return true; + } + catch (const std::filesystem::filesystem_error& e) + { + AICLI_LOG(Core, Info, << "Hardlink not supported or creation failed: " << e.what()); + return false; + } + catch (const std::exception& e) + { + AICLI_LOG(Core, Info, << "Hardlink creation failed: " << e.what()); + return false; + } + catch (...) + { + AICLI_LOG(Core, Info, << "Hardlink creation failed with unknown error"); + return false; + } + } + #ifndef AICLI_DISABLE_TEST_HOOKS static bool* s_CreateSymlinkResult_TestHook_Override = nullptr; diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index f450444234..865881938a 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -27,6 +27,10 @@ namespace AppInstaller::Filesystem // Renames the file to a new path. void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to); + // Creates a hardlink at linkPath pointing to targetPath. + // Returns true if successful, false if hardlinks are not supported or creation failed. + bool CreateHardlink(const std::filesystem::path& target, const std::filesystem::path& link); + // Creates a symlink that points to the target path. bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link); From e42e642109530dbbe919d6af7cc783a0105a05e4 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sat, 20 Jun 2026 21:34:34 -0500 Subject: [PATCH 2/3] Add spelling allowlist entries for hardlinks and pathing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/spelling/allow.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 3666150db0..e1ef76bb8e 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -122,6 +122,7 @@ hcertstore HCRYPTMSG HGlobal HGLOBAL +hardlinks HIDECANCEL hinternet HKCU @@ -228,6 +229,7 @@ Params params parentidx pathpart +pathing Pathto PBYTE pch From 86f8a3c0d32290cca9dc7d59f287369f3b451a02 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sat, 20 Jun 2026 21:38:55 -0500 Subject: [PATCH 3/3] Fix whitespace --- src/AppInstallerCLICore/Workflows/PortableFlow.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index b372ea82aa..2f02a3d9f1 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -202,7 +202,7 @@ namespace AppInstaller::CLI::Workflow } const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; - + for (const auto& nestedInstallerFile : nestedInstallerFiles) { const std::filesystem::path& relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); @@ -222,7 +222,7 @@ namespace AppInstaller::CLI::Workflow Filesystem::AppendExtension(commandAlias, ".exe"); // If alias differs from original filename, create hardlink - // Hardlink will be placed in the same directory as the original file to avoid pathing issues and same-volume restrictions + // Hardlink will be placed in the same directory as the original file to avoid pathing issues and same-volume restrictions if (commandAlias != originalFilename) { std::filesystem::path sourcePath = installerPath / relativeFilePath; @@ -239,7 +239,7 @@ namespace AppInstaller::CLI::Workflow // Non-archive portable case: single executable file std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); const std::vector& commands = context.Get()->Commands; - + std::filesystem::path originalFilename = installerPath.filename(); std::filesystem::path commandAlias = originalFilename;