/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmCPackArchiveGenerator.h" #include #include #include #include #include #include #include #include "cmCPackComponentGroup.h" #include "cmCPackGenerator.h" #include "cmCPackLog.h" #include "cmGeneratedFileStream.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmValue.h" #include "cmWorkingDirectory.h" enum class DeduplicateStatus { Skip, Add, Error }; /** * @class cmCPackArchiveGenerator::Deduplicator * @brief A utility class for deduplicating files, folders, and symlinks. * * This class is responsible for identifying duplicate files, folders, and * symlinks when generating an archive. It keeps track of the paths that have * been processed and helps in deciding whether a new path should be added, * skipped, or flagged as an error. */ class cmCPackArchiveGenerator::Deduplicator { private: /** * @brief Compares a file with already processed files. * * @param path The path of the file to compare. * @param localTopLevel The top-level directory for the file. * @return DeduplicateStatus indicating whether to add, skip, or flag an * error for the file. */ DeduplicateStatus CompareFile(const std::string& path, const std::string& localTopLevel) { auto fileItr = this->Files.find(path); if (fileItr != this->Files.end()) { return cmSystemTools::FilesDiffer(path, fileItr->second) ? DeduplicateStatus::Error : DeduplicateStatus::Skip; } this->Files[path] = cmStrCat(localTopLevel, "/", path); return DeduplicateStatus::Add; } /** * @brief Compares a folder with already processed folders. * * @param path The path of the folder to compare. * @return DeduplicateStatus indicating whether to add or skip the folder. */ DeduplicateStatus CompareFolder(const std::string& path) { if (this->Folders.find(path) != this->Folders.end()) { return DeduplicateStatus::Skip; } this->Folders.emplace(path); return DeduplicateStatus::Add; } /** * @brief Compares a symlink with already processed symlinks. * * @param path The path of the symlink to compare. * @return DeduplicateStatus indicating whether to add, skip, or flag an * error for the symlink. */ DeduplicateStatus CompareSymlink(const std::string& path) { auto symlinkItr = this->Symlink.find(path); std::string symlinkValue; auto status = cmSystemTools::ReadSymlink(path, symlinkValue); if (!status.IsSuccess()) { return DeduplicateStatus::Error; } if (symlinkItr != this->Symlink.end()) { return symlinkValue == symlinkItr->second ? DeduplicateStatus::Skip : DeduplicateStatus::Error; } this->Symlink[path] = symlinkValue; return DeduplicateStatus::Add; } public: /** * @brief Determines the deduplication status of a given path. * * This method identifies whether the given path is a file, folder, or * symlink and then delegates to the appropriate comparison method. * * @param path The path to check for deduplication. * @param localTopLevel The top-level directory for the path. * @return DeduplicateStatus indicating the action to take for the given * path. */ DeduplicateStatus IsDeduplicate(const std::string& path, const std::string& localTopLevel) { DeduplicateStatus status; if (cmSystemTools::FileIsDirectory(path)) { status = this->CompareFolder(path); } else if (cmSystemTools::FileIsSymlink(path)) { status = this->CompareSymlink(path); } else { status = this->CompareFile(path, localTopLevel); } return status; } private: std::unordered_map Symlink; std::unordered_set Folders; std::unordered_map Files; }; cmCPackGenerator* cmCPackArchiveGenerator::Create7ZGenerator() { return new cmCPackArchiveGenerator(cmArchiveWrite::CompressNone, "7zip", ".7z"); } cmCPackGenerator* cmCPackArchiveGenerator::CreateTBZ2Generator() { return new cmCPackArchiveGenerator(cmArchiveWrite::CompressBZip2, "paxr", ".tar.bz2"); } cmCPackGenerator* cmCPackArchiveGenerator::CreateTGZGenerator() { return new cmCPackArchiveGenerator(cmArchiveWrite::CompressGZip, "paxr", ".tar.gz"); } cmCPackGenerator* cmCPackArchiveGenerator::CreateTXZGenerator() { return new cmCPackArchiveGenerator(cmArchiveWrite::CompressXZ, "paxr", ".tar.xz"); } cmCPackGenerator* cmCPackArchiveGenerator::CreateTZGenerator() { return new cmCPackArchiveGenerator(cmArchiveWrite::CompressCompress, "paxr", ".tar.Z"); } cmCPackGenerator* cmCPackArchiveGenerator::CreateTZSTGenerator() { return new cmCPackArchiveGenerator(cmArchiveWrite::CompressZstd, "paxr", ".tar.zst"); } cmCPackGenerator* cmCPackArchiveGenerator::CreateZIPGenerator() { return new cmCPackArchiveGenerator(cmArchiveWrite::CompressNone, "zip", ".zip"); } cmCPackArchiveGenerator::cmCPackArchiveGenerator( cmArchiveWrite::Compress compress, std::string format, std::string extension) : Compress(compress) , ArchiveFormat(std::move(format)) , OutputExtension(std::move(extension)) { } cmCPackArchiveGenerator::~cmCPackArchiveGenerator() = default; std::string cmCPackArchiveGenerator::GetArchiveComponentFileName( const std::string& component, bool isGroupName) { std::string componentUpper(cmSystemTools::UpperCase(component)); std::string packageFileName; if (this->IsSet("CPACK_ARCHIVE_" + componentUpper + "_FILE_NAME")) { packageFileName += *this->GetOption("CPACK_ARCHIVE_" + componentUpper + "_FILE_NAME"); } else if (this->IsSet("CPACK_ARCHIVE_FILE_NAME")) { packageFileName += this->GetComponentPackageFileName( *this->GetOption("CPACK_ARCHIVE_FILE_NAME"), component, isGroupName); } else { packageFileName += this->GetComponentPackageFileName( *this->GetOption("CPACK_PACKAGE_FILE_NAME"), component, isGroupName); } packageFileName += this->GetOutputExtension(); return packageFileName; } int cmCPackArchiveGenerator::InitializeInternal() { this->SetOptionIfNotSet("CPACK_INCLUDE_TOPLEVEL_DIRECTORY", "1"); cmValue newExtensionValue = this->GetOption("CPACK_ARCHIVE_FILE_EXTENSION"); if (!newExtensionValue.IsEmpty()) { std::string newExtension = *newExtensionValue; if (!cmHasLiteralPrefix(newExtension, ".")) { newExtension = cmStrCat('.', newExtension); } cmCPackLogger(cmCPackLog::LOG_DEBUG, "Using user-provided file extension " << newExtension << " instead of the default " << this->OutputExtension << std::endl); this->OutputExtension = std::move(newExtension); } return this->Superclass::InitializeInternal(); } int cmCPackArchiveGenerator::addOneComponentToArchive( cmArchiveWrite& archive, cmCPackComponent* component, Deduplicator* deduplicator) { cmCPackLogger(cmCPackLog::LOG_VERBOSE, " - packaging component: " << component->Name << std::endl); // Add the files of this component to the archive std::string localToplevel(this->GetOption("CPACK_TEMPORARY_DIRECTORY")); localToplevel += "/" + this->GetSanitizedDirOrFileName(component->Name); // Change to local toplevel cmWorkingDirectory workdir(localToplevel); if (workdir.Failed()) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Failed to change working directory to " << localToplevel << " : " << std::strerror(workdir.GetLastResult()) << std::endl); return 0; } std::string filePrefix; if (this->IsOn("CPACK_COMPONENT_INCLUDE_TOPLEVEL_DIRECTORY")) { filePrefix = cmStrCat(this->GetOption("CPACK_PACKAGE_FILE_NAME"), '/'); } cmValue installPrefix = this->GetOption("CPACK_PACKAGING_INSTALL_PREFIX"); if (installPrefix && installPrefix->size() > 1 && (*installPrefix)[0] == '/') { // add to file prefix and remove the leading '/' filePrefix += installPrefix->substr(1); filePrefix += "/"; } for (std::string const& file : component->Files) { std::string rp = filePrefix + file; DeduplicateStatus status = DeduplicateStatus::Add; if (deduplicator) { status = deduplicator->IsDeduplicate(rp, localToplevel); } if (!deduplicator || status == DeduplicateStatus::Add) { cmCPackLogger(cmCPackLog::LOG_DEBUG, "Adding file: " << rp << std::endl); archive.Add(rp, 0, nullptr, false); } else if (status == DeduplicateStatus::Error) { cmCPackLogger(cmCPackLog::LOG_ERROR, "ERROR The data in files with the " "same filename is different."); return 0; } else { cmCPackLogger(cmCPackLog::LOG_DEBUG, "Passing file: " << rp << std::endl); } if (!archive) { cmCPackLogger(cmCPackLog::LOG_ERROR, "ERROR while packaging files: " << archive.GetError() << std::endl); return 0; } } return 1; } /* * The macro will open/create a file 'filename' * an declare and open the associated * cmArchiveWrite 'archive' object. */ #define DECLARE_AND_OPEN_ARCHIVE(filename, archive) \ cmGeneratedFileStream gf; \ gf.Open((filename), false, true); \ if (!GenerateHeader(&gf)) { \ cmCPackLogger(cmCPackLog::LOG_ERROR, \ "Problem to generate Header for archive <" \ << (filename) << ">." << std::endl); \ return 0; \ } \ cmArchiveWrite archive(gf, this->Compress, this->ArchiveFormat, 0, \ this->GetThreadCount()); \ do { \ if (!archive.Open()) { \ cmCPackLogger(cmCPackLog::LOG_ERROR, \ "Problem to open archive <" \ << (filename) << ">, ERROR = " << (archive).GetError() \ << std::endl); \ return 0; \ } \ if (!(archive)) { \ cmCPackLogger(cmCPackLog::LOG_ERROR, \ "Problem to create archive <" \ << (filename) << ">, ERROR = " << (archive).GetError() \ << std::endl); \ return 0; \ } \ } while (false) int cmCPackArchiveGenerator::PackageComponents(bool ignoreGroup) { this->packageFileNames.clear(); // The default behavior is to have one package by component group // unless CPACK_COMPONENTS_IGNORE_GROUP is specified. if (!ignoreGroup) { for (auto const& compG : this->ComponentGroups) { cmCPackLogger(cmCPackLog::LOG_VERBOSE, "Packaging component group: " << compG.first << std::endl); // Begin the archive for this group std::string packageFileName = std::string(this->toplevel) + "/" + this->GetArchiveComponentFileName(compG.first, true); Deduplicator deduplicator; // open a block in order to automatically close archive // at the end of the block { DECLARE_AND_OPEN_ARCHIVE(packageFileName, archive); // now iterate over the component of this group for (cmCPackComponent* comp : (compG.second).Components) { // Add the files of this component to the archive this->addOneComponentToArchive(archive, comp, &deduplicator); } } // add the generated package to package file names list this->packageFileNames.push_back(std::move(packageFileName)); } // Handle Orphan components (components not belonging to any groups) for (auto& comp : this->Components) { // Does the component belong to a group? if (!comp.second.Group) { cmCPackLogger( cmCPackLog::LOG_VERBOSE, "Component <" << comp.second.Name << "> does not belong to any group, package it separately." << std::endl); std::string packageFileName = std::string(this->toplevel); packageFileName += "/" + this->GetArchiveComponentFileName(comp.first, false); { DECLARE_AND_OPEN_ARCHIVE(packageFileName, archive); // Add the files of this component to the archive this->addOneComponentToArchive(archive, &(comp.second), nullptr); } // add the generated package to package file names list this->packageFileNames.push_back(std::move(packageFileName)); } } } // CPACK_COMPONENTS_IGNORE_GROUPS is set // We build 1 package per component else { for (auto& comp : this->Components) { std::string packageFileName = std::string(this->toplevel); packageFileName += "/" + this->GetArchiveComponentFileName(comp.first, false); { DECLARE_AND_OPEN_ARCHIVE(packageFileName, archive); // Add the files of this component to the archive this->addOneComponentToArchive(archive, &(comp.second), nullptr); } // add the generated package to package file names list this->packageFileNames.push_back(std::move(packageFileName)); } } return 1; } int cmCPackArchiveGenerator::PackageComponentsAllInOne() { // reset the package file names this->packageFileNames.clear(); this->packageFileNames.emplace_back(this->toplevel); this->packageFileNames[0] += "/"; if (this->IsSet("CPACK_ARCHIVE_FILE_NAME")) { this->packageFileNames[0] += *this->GetOption("CPACK_ARCHIVE_FILE_NAME"); } else { this->packageFileNames[0] += *this->GetOption("CPACK_PACKAGE_FILE_NAME"); } this->packageFileNames[0] += this->GetOutputExtension(); cmCPackLogger(cmCPackLog::LOG_VERBOSE, "Packaging all groups in one package..." "(CPACK_COMPONENTS_ALL_GROUPS_IN_ONE_PACKAGE is set)" << std::endl); DECLARE_AND_OPEN_ARCHIVE(packageFileNames[0], archive); Deduplicator deduplicator; // The ALL COMPONENTS in ONE package case for (auto& comp : this->Components) { // Add the files of this component to the archive this->addOneComponentToArchive(archive, &(comp.second), &deduplicator); } // archive goes out of scope so it will finalized and closed. return 1; } int cmCPackArchiveGenerator::PackageFiles() { cmCPackLogger(cmCPackLog::LOG_DEBUG, "Toplevel: " << this->toplevel << std::endl); if (this->WantsComponentInstallation()) { // CASE 1 : COMPONENT ALL-IN-ONE package // If ALL COMPONENTS in ONE package has been requested // then the package file is unique and should be open here. if (this->componentPackageMethod == ONE_PACKAGE) { return this->PackageComponentsAllInOne(); } // CASE 2 : COMPONENT CLASSICAL package(s) (i.e. not all-in-one) // There will be 1 package for each component group // however one may require to ignore component group and // in this case you'll get 1 package for each component. return this->PackageComponents(this->componentPackageMethod == ONE_PACKAGE_PER_COMPONENT); } // CASE 3 : NON COMPONENT package. DECLARE_AND_OPEN_ARCHIVE(packageFileNames[0], archive); cmWorkingDirectory workdir(this->toplevel); if (workdir.Failed()) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Failed to change working directory to " << this->toplevel << " : " << std::strerror(workdir.GetLastResult()) << std::endl); return 0; } for (std::string const& file : this->files) { // Get the relative path to the file std::string rp = cmSystemTools::RelativePath(this->toplevel, file); archive.Add(rp, 0, nullptr, false); if (!archive) { cmCPackLogger(cmCPackLog::LOG_ERROR, "Problem while adding file <" << file << "> to archive <" << this->packageFileNames[0] << ">, ERROR = " << archive.GetError() << std::endl); return 0; } } // The destructor of cmArchiveWrite will close and finish the write return 1; } int cmCPackArchiveGenerator::GenerateHeader(std::ostream* /*unused*/) { return 1; } bool cmCPackArchiveGenerator::SupportsComponentInstallation() const { // The Component installation support should only // be activated if explicitly requested by the user // (for backward compatibility reason) return this->IsOn("CPACK_ARCHIVE_COMPONENT_INSTALL"); } int cmCPackArchiveGenerator::GetThreadCount() const { int threads = 1; // CPACK_ARCHIVE_THREADS overrides CPACK_THREADS if (this->IsSet("CPACK_ARCHIVE_THREADS")) { threads = std::stoi(*this->GetOption("CPACK_ARCHIVE_THREADS")); } else if (this->IsSet("CPACK_THREADS")) { threads = std::stoi(*this->GetOption("CPACK_THREADS")); } return threads; }