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 }