winget-cli

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

PortableInstaller.cpp (22260B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "ExecutionContext.h"
      5 #include "PortableInstaller.h"
      6 #include <winget/Manifest.h>
      7 #include <winget/ManifestCommon.h>
      8 #include <winget/Filesystem.h>
      9 #include <winget/PathVariable.h>
     10 #include <winget/PortableIndex.h>
     11 #include <AppInstallerErrors.h>
     12 #include <AppInstallerRuntime.h>
     13 #include <Workflows/WorkflowBase.h>
     14 
     15 using namespace AppInstaller::Utility;
     16 using namespace AppInstaller::Registry;
     17 using namespace AppInstaller::Registry::Portable;
     18 using namespace AppInstaller::Registry::Environment;
     19 using namespace AppInstaller::Repository;
     20 using namespace AppInstaller::SQLite;
     21 using namespace AppInstaller::Repository::Microsoft;
     22 using namespace AppInstaller::Repository::Microsoft::Schema;
     23 using namespace AppInstaller::CLI::Workflow;
     24 
     25 namespace AppInstaller::CLI::Portable
     26 {
     27     std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope)
     28     {
     29         if (scope == Manifest::ScopeEnum::Machine)
     30         {
     31             return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation);
     32         }
     33         else
     34         {
     35             return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation);
     36         }
     37     }
     38 
     39     std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch)
     40     {
     41         if (scope == Manifest::ScopeEnum::Machine)
     42         {
     43             if (arch == Utility::Architecture::X86)
     44             {
     45                 return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86);
     46             }
     47             else
     48             {
     49                 return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRoot);
     50             }
     51         }
     52         else
     53         {
     54             return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot);
     55         }
     56     }
     57 
     58     bool VerifyPortableFile(AppInstaller::Portable::PortableFileEntry& entry)
     59     {
     60         std::filesystem::path filePath = entry.GetFilePath();
     61         PortableFileType fileType = entry.FileType;
     62 
     63         if (fileType == PortableFileType::File)
     64         {
     65             if (std::filesystem::exists(filePath))
     66             {
     67                 SHA256::HashBuffer fileHash = SHA256::ComputeHashFromFile(filePath);
     68                 if (!SHA256::AreEqual(fileHash, SHA256::ConvertToBytes(entry.SHA256)))
     69                 {
     70                     AICLI_LOG(CLI, Warning, << "File hash does not match ARP Entry. Expected: " << entry.SHA256 << " Actual: " << SHA256::ConvertToString(fileHash));
     71                     return false;
     72                 }
     73             }
     74         }
     75         else if (fileType == PortableFileType::Symlink)
     76         {
     77             std::filesystem::path symlinkTargetPath{ AppInstaller::Utility::ConvertToUTF16(entry.SymlinkTarget) };
     78             if (Filesystem::SymlinkExists(filePath) && !Filesystem::VerifySymlink(filePath, symlinkTargetPath))
     79             {
     80                 AICLI_LOG(CLI, Warning, << "Symlink target does not match ARP Entry. Expected: " << symlinkTargetPath << " Actual: " << std::filesystem::read_symlink(filePath));
     81                 return false;
     82             }
     83         }
     84 
     85         return true;
     86     }
     87 
     88     void PortableInstaller::InstallFile(AppInstaller::Portable::PortableFileEntry& entry)
     89     {
     90         PortableFileType fileType = entry.FileType;
     91         std::filesystem::path filePath = entry.GetFilePath();
     92 
     93         if (entry.FileType == PortableFileType::File)
     94         {
     95             if (std::filesystem::exists(filePath))
     96             {
     97                 AICLI_LOG(Core, Info, << "Removing existing portable file at: " << filePath);
     98                 std::filesystem::remove(filePath);
     99             }
    100 
    101             AICLI_LOG(Core, Info, << "Moving portable exe to: " << filePath);
    102 
    103             if (!RecordToIndex)
    104             {
    105                 CommitToARPEntry(PortableValueName::PortableTargetFullPath, filePath);
    106                 CommitToARPEntry(PortableValueName::SHA256, entry.SHA256);
    107             }
    108 
    109             Filesystem::RenameFile(entry.CurrentPath, filePath);
    110         }
    111         else if (fileType == PortableFileType::Directory)
    112         {
    113             if (Filesystem::IsSameVolume(entry.CurrentPath, filePath))
    114             {
    115                 AICLI_LOG(Core, Info, << "Renaming directory to: " << filePath);
    116                 Filesystem::RenameFile(entry.CurrentPath, filePath);
    117             }
    118             else
    119             {
    120                 // Copy directory instead of renaming as there is a known issue with renaming across drives.
    121                 AICLI_LOG(Core, Info, << "Copying directory to: " << filePath);
    122                 std::filesystem::copy(entry.CurrentPath, filePath, std::filesystem::copy_options::overwrite_existing | std::filesystem::copy_options::recursive);
    123             }
    124         }
    125         else if (entry.FileType == PortableFileType::Symlink)
    126         {
    127             std::filesystem::path symlinkTargetPath{ Utility::ConvertToUTF16(entry.SymlinkTarget) };
    128 
    129             if (BinariesDependOnPath && !InstallDirectoryAddedToPath)
    130             {
    131                 // Scenario indicated by 'ArchiveBinariesDependOnPath' manifest entry.
    132                 // Skip symlink creation for portables dependent on binaries that require the install directory to be added to PATH.
    133                 std::filesystem::path installDirectory = symlinkTargetPath.parent_path();
    134                 AddToPathVariable(installDirectory);
    135                 AICLI_LOG(Core, Info, << "Install directory added to PATH: " << installDirectory);
    136                 CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true);
    137             }
    138             else if (!InstallDirectoryAddedToPath)
    139             {
    140                 std::filesystem::file_status status = std::filesystem::status(filePath);
    141                 if (std::filesystem::is_directory(status))
    142                 {
    143                     AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << filePath << "points to an existing directory.");
    144                     THROW_HR(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY);
    145                 }
    146 
    147                 if (!RecordToIndex)
    148                 {
    149                     CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, filePath);
    150                 }
    151 
    152                 if (std::filesystem::remove(filePath))
    153                 {
    154                     AICLI_LOG(CLI, Info, << "Removed existing file at " << filePath);
    155                     m_stream << Resource::String::OverwritingExistingFileAtMessage(Utility::LocIndView{ filePath.u8string() }) << std::endl;
    156                 }
    157 
    158                 if (Filesystem::CreateSymlink(symlinkTargetPath, filePath))
    159                 {
    160                     AICLI_LOG(Core, Info, << "Symlink created at: " << filePath << " with target path: " << symlinkTargetPath);
    161                 }
    162                 else
    163                 {
    164                     // If symlink creation fails, resort to adding the package directory to PATH.
    165                     AICLI_LOG(Core, Info, << "Failed to create symlink at: " << filePath);
    166                     AddToPathVariable(symlinkTargetPath.parent_path());
    167                     CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true);
    168                 }
    169             }
    170             m_stream << Resource::String::PortableAliasAdded << ' ' << filePath.stem() << std::endl;
    171         }
    172     }
    173 
    174     void PortableInstaller::RemoveFile(AppInstaller::Portable::PortableFileEntry& entry)
    175     {
    176         const auto& filePath = entry.GetFilePath();
    177         PortableFileType fileType = entry.FileType;
    178 
    179         if (fileType == PortableFileType::File && std::filesystem::exists(filePath))
    180         {
    181             AICLI_LOG(CLI, Info, << "Deleting portable exe at: " << filePath);
    182             std::filesystem::remove(filePath);
    183         }
    184         else if (fileType == PortableFileType::Symlink)
    185         {
    186             if (Filesystem::SymlinkExists(filePath))
    187             {
    188                 AICLI_LOG(CLI, Info, << "Deleting portable symlink at: " << filePath);
    189                 std::filesystem::remove(filePath);
    190             }
    191             else if (InstallDirectoryAddedToPath)
    192             {
    193                 // If symlink doesn't exist, check if install directory was added to PATH directly and remove.
    194                 RemoveFromPathVariable(std::filesystem::path(Utility::ConvertToUTF16(entry.SymlinkTarget)).parent_path());
    195             }
    196         }
    197         else if (fileType == PortableFileType::Symlink && Filesystem::SymlinkExists(filePath))
    198         {
    199             AICLI_LOG(CLI, Info, << "Deleting portable symlink at: " << filePath);
    200             std::filesystem::remove(filePath);
    201         }
    202         else if (fileType == PortableFileType::Directory && std::filesystem::exists(filePath))
    203         {
    204             AICLI_LOG(CLI, Info, << "Removing directory at " << filePath);
    205             std::filesystem::remove_all(filePath);
    206         }
    207     }
    208 
    209     // TODO: Optimize by applying the difference between expected and desired state.
    210     void PortableInstaller::ApplyDesiredState()
    211     {
    212         std::filesystem::path existingIndexPath = InstallLocation / GetPortableIndexFileName();
    213         if (std::filesystem::exists(existingIndexPath))
    214         {
    215             bool deleteIndex = false;
    216             {
    217                 PortableIndex existingIndex = PortableIndex::Open(existingIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite);
    218 
    219                 for (auto expectedEntry : m_expectedEntries)
    220                 {
    221                     RemoveFile(expectedEntry);
    222                     existingIndex.RemovePortableFile(expectedEntry);
    223                 }
    224 
    225                 deleteIndex = existingIndex.IsEmpty();
    226             }
    227 
    228             if (deleteIndex)
    229             {
    230                 std::filesystem::remove(existingIndexPath);
    231                 AICLI_LOG(CLI, Info, << "Portable index deleted: " << existingIndexPath);
    232             }
    233         }
    234         else
    235         {
    236             for (auto expectedEntry : m_expectedEntries)
    237             {
    238                 RemoveFile(expectedEntry);
    239             }
    240         }
    241 
    242         // Check if existing install location differs from the target install location for proper cleanup.
    243         if (!TargetInstallLocation.empty() && TargetInstallLocation != InstallLocation)
    244         {
    245             RemoveInstallDirectory();
    246         }
    247 
    248         if (RecordToIndex)
    249         {
    250             std::filesystem::path targetIndexPath = TargetInstallLocation / GetPortableIndexFileName();
    251             PortableIndex targetIndex = std::filesystem::exists(targetIndexPath) ?
    252                 PortableIndex::Open(targetIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) :
    253                 PortableIndex::CreateNew(targetIndexPath.u8string());
    254 
    255             for (auto desiredEntry : m_desiredEntries)
    256             {
    257                 targetIndex.AddOrUpdatePortableFile(desiredEntry);
    258                 InstallFile(desiredEntry);
    259             }
    260         }
    261         else
    262         {
    263             for (auto desiredEntry : m_desiredEntries)
    264             {
    265                 InstallFile(desiredEntry);
    266             }
    267         }
    268     }
    269 
    270     bool PortableInstaller::VerifyExpectedState()
    271     {
    272         for (auto entry : m_expectedEntries)
    273         {
    274             if (!VerifyPortableFile(entry))
    275             {
    276                 AICLI_LOG(CLI, Info, << "Portable file has been modified: " << entry.GetFilePath());
    277                 return false;
    278             }
    279         }
    280 
    281         return true;
    282     }
    283 
    284     void PortableInstaller::Install(Workflow::OperationType operation)
    285     {
    286         // If the operation is an install, the ARP entry should be created first so that a catastrophic failure
    287         // leaves the system in a state where an uninstall may be possible
    288         if (operation == Workflow::OperationType::Install)
    289         {
    290             RegisterARPEntry();
    291         }
    292 
    293         CreateTargetInstallDirectory();
    294 
    295         ApplyDesiredState();
    296 
    297         if (!InstallDirectoryAddedToPath)
    298         {
    299             AddToPathVariable(GetPortableLinksLocation(GetScope()));
    300         }
    301 
    302         // If the operation is an upgrade, the ARP entry should be created last so that a catastrophic failure
    303         // leaves the system in a state where an upgrade can be re-attempted
    304         if (operation == Workflow::OperationType::Upgrade)
    305         {
    306             RegisterARPEntry();
    307         }
    308     }
    309 
    310     void PortableInstaller::Uninstall()
    311     {
    312         ApplyDesiredState();
    313 
    314         RemoveInstallDirectory();
    315 
    316         if (!InstallDirectoryAddedToPath)
    317         {
    318             RemoveFromPathVariable(GetPortableLinksLocation(GetScope()));
    319         }
    320 
    321         m_portableARPEntry.Delete();
    322         AICLI_LOG(CLI, Info, << "PortableARPEntry deleted.");
    323     }
    324 
    325     void PortableInstaller::CreateTargetInstallDirectory()
    326     {
    327         if (std::filesystem::create_directories(TargetInstallLocation))
    328         {
    329             AICLI_LOG(Core, Info, << "Created target install directory: " << TargetInstallLocation);
    330             CommitToARPEntry(PortableValueName::InstallDirectoryCreated, true);
    331         }
    332 
    333         CommitToARPEntry(PortableValueName::InstallLocation, TargetInstallLocation);
    334     }
    335 
    336     void PortableInstaller::RemoveInstallDirectory()
    337     {
    338         if (std::filesystem::exists(InstallLocation) && InstallDirectoryCreated)
    339         {
    340             if (Purge)
    341             {
    342                 m_stream << Resource::String::PurgeInstallDirectory << std::endl;
    343                 const auto& removedFilesCount = std::filesystem::remove_all(InstallLocation);
    344                 AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories");
    345             }
    346             else
    347             {
    348                 if (std::filesystem::is_empty(InstallLocation))
    349                 {
    350                     AICLI_LOG(CLI, Info, << "Removing empty install directory: " << InstallLocation);
    351                     std::filesystem::remove(InstallLocation);
    352                 }
    353                 else
    354                 {
    355                     AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << InstallLocation);
    356                     m_stream << Resource::String::FilesRemainInInstallDirectory(Utility::LocIndView{ InstallLocation.u8string() }) << std::endl;
    357                 }
    358             }
    359         }
    360     }
    361 
    362     void PortableInstaller::AddToPathVariable(const std::filesystem::path& value)
    363     {
    364         if (PathVariable(GetScope()).Append(value))
    365         {
    366             AICLI_LOG(Core, Info, << "Appending portable target directory to PATH registry: " << value);
    367             m_stream << Resource::String::ModifiedPathRequiresShellRestart << std::endl;
    368         }
    369         else
    370         {
    371             AICLI_LOG(CLI, Info, << "Portable target directory already exists in PATH registry: " << value);
    372         }
    373     }
    374 
    375     void PortableInstaller::RemoveFromPathVariable(const std::filesystem::path& value)
    376     {
    377         if (std::filesystem::exists(value) && !std::filesystem::is_empty(value))
    378         {
    379             AICLI_LOG(Core, Info, << "Install directory is not empty: " << value);
    380         }
    381         else
    382         {
    383             if (PathVariable(GetScope()).Remove(value))
    384             {
    385                 AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << value);
    386             }
    387             else
    388             {
    389                 AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << value);
    390             }
    391         }
    392     }
    393 
    394     void PortableInstaller::SetAppsAndFeaturesMetadata(const Manifest::Manifest& manifest, const std::vector<AppInstaller::Manifest::AppsAndFeaturesEntry>& entries)
    395     {
    396         AppInstaller::Manifest::AppsAndFeaturesEntry entry;
    397         if (!entries.empty())
    398         {
    399             entry = entries[0];
    400         }
    401 
    402         if (entry.DisplayName.empty())
    403         {
    404             entry.DisplayName = manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>();
    405         }
    406         if (entry.DisplayVersion.empty())
    407         {
    408             entry.DisplayVersion = manifest.Version;
    409         }
    410         if (entry.Publisher.empty())
    411         {
    412             entry.Publisher = manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>();
    413         }
    414 
    415         DisplayName = entry.DisplayName;
    416         DisplayVersion = entry.DisplayVersion;
    417         Publisher = entry.Publisher;
    418         InstallDate = Utility::GetCurrentDateForARP();
    419         URLInfoAbout = manifest.CurrentLocalization.Get<Manifest::Localization::PackageUrl>();
    420         HelpLink = manifest.CurrentLocalization.Get<Manifest::Localization::PublisherSupportUrl>();
    421     }
    422 
    423     AppInstaller::Manifest::AppsAndFeaturesEntry PortableInstaller::GetAppsAndFeaturesEntry()
    424     {
    425         Manifest::AppsAndFeaturesEntry entry;
    426 
    427         entry.DisplayName = DisplayName;
    428         entry.Publisher = Publisher;
    429         entry.DisplayVersion = DisplayVersion;
    430         entry.InstallerType = Manifest::InstallerTypeEnum::Portable;
    431         entry.ProductCode = GetProductCode();
    432 
    433         return entry;
    434     }
    435 
    436     void PortableInstaller::SetExpectedState()
    437     {
    438         const auto& indexPath = InstallLocation / GetPortableIndexFileName();
    439 
    440         if (std::filesystem::exists(indexPath))
    441         {
    442             PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite);
    443             m_expectedEntries = portableIndex.GetAllPortableFiles();
    444         }
    445         else
    446         {
    447             std::filesystem::path targetFullPath = PortableTargetFullPath;
    448             std::filesystem::path symlinkFullPath = PortableSymlinkFullPath;
    449 
    450             // Order matters here so that file entries are removed before symlink entries during uninstall from registry.
    451             // This is to ensure that the directory is fully uninstalled before attempting to remove from PATH registry.
    452             if (!targetFullPath.empty())
    453             {
    454                 m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256)));
    455             }
    456 
    457             if (!symlinkFullPath.empty())
    458             {
    459                 m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath)));
    460             }
    461         }
    462     }
    463 
    464     PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) :
    465         m_portableARPEntry(PortableARPEntry(scope, arch, productCode))
    466     {
    467         if (ARPEntryExists())
    468         {
    469             DisplayName = GetStringValue(PortableValueName::DisplayName);
    470             DisplayVersion = GetStringValue(PortableValueName::DisplayVersion);
    471             HelpLink = GetStringValue(PortableValueName::HelpLink);
    472             InstallDate = GetStringValue(PortableValueName::InstallDate);
    473             Publisher = GetStringValue(PortableValueName::Publisher);
    474             SHA256 = GetStringValue(PortableValueName::SHA256);
    475             URLInfoAbout = GetStringValue(PortableValueName::URLInfoAbout);
    476             UninstallString = GetStringValue(PortableValueName::UninstallString);
    477             WinGetInstallerType = GetStringValue(PortableValueName::WinGetInstallerType);
    478             WinGetPackageIdentifier = GetStringValue(PortableValueName::WinGetPackageIdentifier);
    479             WinGetSourceIdentifier = GetStringValue(PortableValueName::WinGetSourceIdentifier);
    480             InstallLocation = GetPathValue(PortableValueName::InstallLocation);
    481             PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath);
    482             PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath);
    483             InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath);
    484             InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated);
    485         }
    486         
    487         SetExpectedState();
    488     }
    489 
    490     void PortableInstaller::RegisterARPEntry()
    491     {
    492         CommitToARPEntry(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier);
    493         CommitToARPEntry(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier);
    494         CommitToARPEntry(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode());
    495         CommitToARPEntry(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable));
    496         CommitToARPEntry(PortableValueName::DisplayName, DisplayName);
    497         CommitToARPEntry(PortableValueName::DisplayVersion, DisplayVersion);
    498         CommitToARPEntry(PortableValueName::Publisher, Publisher);
    499         CommitToARPEntry(PortableValueName::InstallDate, InstallDate);
    500         CommitToARPEntry(PortableValueName::URLInfoAbout, URLInfoAbout);
    501         CommitToARPEntry(PortableValueName::HelpLink, HelpLink);
    502     }
    503 
    504     std::string PortableInstaller::GetStringValue(PortableValueName valueName)
    505     {
    506         if (m_portableARPEntry[valueName].has_value())
    507         {
    508             return m_portableARPEntry[valueName]->GetValue<Value::Type::String>();
    509         }
    510         else
    511         {
    512             return {};
    513         }
    514     }
    515 
    516     std::filesystem::path PortableInstaller::GetPathValue(PortableValueName valueName)
    517     {
    518         if (m_portableARPEntry[valueName].has_value())
    519         {
    520             return m_portableARPEntry[valueName]->GetValue<Value::Type::UTF16String>();
    521         }
    522         {
    523             return {};
    524         }
    525     }
    526 
    527     bool PortableInstaller::GetBoolValue(PortableValueName valueName)
    528     {
    529         if (m_portableARPEntry[valueName].has_value())
    530         {
    531             return m_portableARPEntry[valueName]->GetValue<Value::Type::DWord>();
    532         }
    533         else
    534         {
    535             return false;
    536         }
    537     }
    538 }