Merge topic 'export-refactor-more-for-cps'
[kiteware-cmake.git] / Source / CPack / cmCPackArchiveGenerator.cxx
blob8bd3aa05fb88b3c1dd150aee1b43577021b600ca
1 /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 file Copyright.txt or https://cmake.org/licensing for details. */
3 #include "cmCPackArchiveGenerator.h"
5 #include <cstring>
6 #include <map>
7 #include <ostream>
8 #include <unordered_map>
9 #include <unordered_set>
10 #include <utility>
11 #include <vector>
13 #include "cmCPackComponentGroup.h"
14 #include "cmCPackGenerator.h"
15 #include "cmCPackLog.h"
16 #include "cmGeneratedFileStream.h"
17 #include "cmStringAlgorithms.h"
18 #include "cmSystemTools.h"
19 #include "cmValue.h"
20 #include "cmWorkingDirectory.h"
22 enum class DeduplicateStatus
24 Skip,
25 Add,
26 Error
29 /**
30 * @class cmCPackArchiveGenerator::Deduplicator
31 * @brief A utility class for deduplicating files, folders, and symlinks.
33 * This class is responsible for identifying duplicate files, folders, and
34 * symlinks when generating an archive. It keeps track of the paths that have
35 * been processed and helps in deciding whether a new path should be added,
36 * skipped, or flagged as an error.
38 class cmCPackArchiveGenerator::Deduplicator
40 private:
41 /**
42 * @brief Compares a file with already processed files.
44 * @param path The path of the file to compare.
45 * @param localTopLevel The top-level directory for the file.
46 * @return DeduplicateStatus indicating whether to add, skip, or flag an
47 * error for the file.
49 DeduplicateStatus CompareFile(const std::string& path,
50 const std::string& localTopLevel)
52 auto fileItr = this->Files.find(path);
53 if (fileItr != this->Files.end()) {
54 return cmSystemTools::FilesDiffer(path, fileItr->second)
55 ? DeduplicateStatus::Error
56 : DeduplicateStatus::Skip;
59 this->Files[path] = cmStrCat(localTopLevel, "/", path);
60 return DeduplicateStatus::Add;
63 /**
64 * @brief Compares a folder with already processed folders.
66 * @param path The path of the folder to compare.
67 * @return DeduplicateStatus indicating whether to add or skip the folder.
69 DeduplicateStatus CompareFolder(const std::string& path)
71 if (this->Folders.find(path) != this->Folders.end()) {
72 return DeduplicateStatus::Skip;
75 this->Folders.emplace(path);
76 return DeduplicateStatus::Add;
79 /**
80 * @brief Compares a symlink with already processed symlinks.
82 * @param path The path of the symlink to compare.
83 * @return DeduplicateStatus indicating whether to add, skip, or flag an
84 * error for the symlink.
86 DeduplicateStatus CompareSymlink(const std::string& path)
88 auto symlinkItr = this->Symlink.find(path);
89 std::string symlinkValue;
90 auto status = cmSystemTools::ReadSymlink(path, symlinkValue);
91 if (!status.IsSuccess()) {
92 return DeduplicateStatus::Error;
95 if (symlinkItr != this->Symlink.end()) {
96 return symlinkValue == symlinkItr->second ? DeduplicateStatus::Skip
97 : DeduplicateStatus::Error;
100 this->Symlink[path] = symlinkValue;
101 return DeduplicateStatus::Add;
104 public:
106 * @brief Determines the deduplication status of a given path.
108 * This method identifies whether the given path is a file, folder, or
109 * symlink and then delegates to the appropriate comparison method.
111 * @param path The path to check for deduplication.
112 * @param localTopLevel The top-level directory for the path.
113 * @return DeduplicateStatus indicating the action to take for the given
114 * path.
116 DeduplicateStatus IsDeduplicate(const std::string& path,
117 const std::string& localTopLevel)
119 DeduplicateStatus status;
120 if (cmSystemTools::FileIsDirectory(path)) {
121 status = this->CompareFolder(path);
122 } else if (cmSystemTools::FileIsSymlink(path)) {
123 status = this->CompareSymlink(path);
124 } else {
125 status = this->CompareFile(path, localTopLevel);
128 return status;
131 private:
132 std::unordered_map<std::string, std::string> Symlink;
133 std::unordered_set<std::string> Folders;
134 std::unordered_map<std::string, std::string> Files;
137 cmCPackGenerator* cmCPackArchiveGenerator::Create7ZGenerator()
139 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressNone, "7zip",
140 ".7z");
143 cmCPackGenerator* cmCPackArchiveGenerator::CreateTBZ2Generator()
145 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressBZip2, "paxr",
146 ".tar.bz2");
149 cmCPackGenerator* cmCPackArchiveGenerator::CreateTGZGenerator()
151 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressGZip, "paxr",
152 ".tar.gz");
155 cmCPackGenerator* cmCPackArchiveGenerator::CreateTXZGenerator()
157 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressXZ, "paxr",
158 ".tar.xz");
161 cmCPackGenerator* cmCPackArchiveGenerator::CreateTZGenerator()
163 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressCompress, "paxr",
164 ".tar.Z");
167 cmCPackGenerator* cmCPackArchiveGenerator::CreateTZSTGenerator()
169 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressZstd, "paxr",
170 ".tar.zst");
173 cmCPackGenerator* cmCPackArchiveGenerator::CreateZIPGenerator()
175 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressNone, "zip",
176 ".zip");
179 cmCPackArchiveGenerator::cmCPackArchiveGenerator(
180 cmArchiveWrite::Compress compress, std::string format, std::string extension)
181 : Compress(compress)
182 , ArchiveFormat(std::move(format))
183 , OutputExtension(std::move(extension))
187 cmCPackArchiveGenerator::~cmCPackArchiveGenerator() = default;
189 std::string cmCPackArchiveGenerator::GetArchiveComponentFileName(
190 const std::string& component, bool isGroupName)
192 std::string componentUpper(cmSystemTools::UpperCase(component));
193 std::string packageFileName;
195 if (this->IsSet("CPACK_ARCHIVE_" + componentUpper + "_FILE_NAME")) {
196 packageFileName +=
197 *this->GetOption("CPACK_ARCHIVE_" + componentUpper + "_FILE_NAME");
198 } else if (this->IsSet("CPACK_ARCHIVE_FILE_NAME")) {
199 packageFileName += this->GetComponentPackageFileName(
200 *this->GetOption("CPACK_ARCHIVE_FILE_NAME"), component, isGroupName);
201 } else {
202 packageFileName += this->GetComponentPackageFileName(
203 *this->GetOption("CPACK_PACKAGE_FILE_NAME"), component, isGroupName);
206 packageFileName += this->GetOutputExtension();
208 return packageFileName;
211 int cmCPackArchiveGenerator::InitializeInternal()
213 this->SetOptionIfNotSet("CPACK_INCLUDE_TOPLEVEL_DIRECTORY", "1");
214 cmValue newExtensionValue = this->GetOption("CPACK_ARCHIVE_FILE_EXTENSION");
215 if (!newExtensionValue.IsEmpty()) {
216 std::string newExtension = *newExtensionValue;
217 if (!cmHasLiteralPrefix(newExtension, ".")) {
218 newExtension = cmStrCat('.', newExtension);
220 cmCPackLogger(cmCPackLog::LOG_DEBUG,
221 "Using user-provided file extension "
222 << newExtension << " instead of the default "
223 << this->OutputExtension << std::endl);
224 this->OutputExtension = std::move(newExtension);
226 return this->Superclass::InitializeInternal();
229 int cmCPackArchiveGenerator::addOneComponentToArchive(
230 cmArchiveWrite& archive, cmCPackComponent* component,
231 Deduplicator* deduplicator)
233 cmCPackLogger(cmCPackLog::LOG_VERBOSE,
234 " - packaging component: " << component->Name << std::endl);
235 // Add the files of this component to the archive
236 std::string localToplevel(this->GetOption("CPACK_TEMPORARY_DIRECTORY"));
237 localToplevel += "/" + this->GetSanitizedDirOrFileName(component->Name);
238 // Change to local toplevel
239 cmWorkingDirectory workdir(localToplevel);
240 if (workdir.Failed()) {
241 cmCPackLogger(cmCPackLog::LOG_ERROR,
242 "Failed to change working directory to "
243 << localToplevel << " : "
244 << std::strerror(workdir.GetLastResult()) << std::endl);
245 return 0;
247 std::string filePrefix;
248 if (this->IsOn("CPACK_COMPONENT_INCLUDE_TOPLEVEL_DIRECTORY")) {
249 filePrefix = cmStrCat(this->GetOption("CPACK_PACKAGE_FILE_NAME"), '/');
251 cmValue installPrefix = this->GetOption("CPACK_PACKAGING_INSTALL_PREFIX");
252 if (installPrefix && installPrefix->size() > 1 &&
253 (*installPrefix)[0] == '/') {
254 // add to file prefix and remove the leading '/'
255 filePrefix += installPrefix->substr(1);
256 filePrefix += "/";
258 for (std::string const& file : component->Files) {
259 std::string rp = filePrefix + file;
261 DeduplicateStatus status = DeduplicateStatus::Add;
262 if (deduplicator != nullptr) {
263 status = deduplicator->IsDeduplicate(rp, localToplevel);
266 if (deduplicator == nullptr || status == DeduplicateStatus::Add) {
267 cmCPackLogger(cmCPackLog::LOG_DEBUG, "Adding file: " << rp << std::endl);
268 archive.Add(rp, 0, nullptr, false);
269 } else if (status == DeduplicateStatus::Error) {
270 cmCPackLogger(cmCPackLog::LOG_ERROR,
271 "ERROR The data in files with the "
272 "same filename is different.");
273 return 0;
274 } else {
275 cmCPackLogger(cmCPackLog::LOG_DEBUG,
276 "Passing file: " << rp << std::endl);
279 if (!archive) {
280 cmCPackLogger(cmCPackLog::LOG_ERROR,
281 "ERROR while packaging files: " << archive.GetError()
282 << std::endl);
283 return 0;
286 return 1;
290 * The macro will open/create a file 'filename'
291 * an declare and open the associated
292 * cmArchiveWrite 'archive' object.
294 #define DECLARE_AND_OPEN_ARCHIVE(filename, archive) \
295 cmGeneratedFileStream gf; \
296 gf.Open((filename), false, true); \
297 if (!GenerateHeader(&gf)) { \
298 cmCPackLogger(cmCPackLog::LOG_ERROR, \
299 "Problem to generate Header for archive <" \
300 << (filename) << ">." << std::endl); \
301 return 0; \
303 cmArchiveWrite archive(gf, this->Compress, this->ArchiveFormat, 0, \
304 this->GetThreadCount()); \
305 do { \
306 if (!archive.Open()) { \
307 cmCPackLogger(cmCPackLog::LOG_ERROR, \
308 "Problem to open archive <" \
309 << (filename) << ">, ERROR = " << (archive).GetError() \
310 << std::endl); \
311 return 0; \
313 if (!(archive)) { \
314 cmCPackLogger(cmCPackLog::LOG_ERROR, \
315 "Problem to create archive <" \
316 << (filename) << ">, ERROR = " << (archive).GetError() \
317 << std::endl); \
318 return 0; \
320 } while (false)
322 int cmCPackArchiveGenerator::PackageComponents(bool ignoreGroup)
324 this->packageFileNames.clear();
325 // The default behavior is to have one package by component group
326 // unless CPACK_COMPONENTS_IGNORE_GROUP is specified.
327 if (!ignoreGroup) {
328 for (auto const& compG : this->ComponentGroups) {
329 cmCPackLogger(cmCPackLog::LOG_VERBOSE,
330 "Packaging component group: " << compG.first << std::endl);
331 // Begin the archive for this group
332 std::string packageFileName = std::string(this->toplevel) + "/" +
333 this->GetArchiveComponentFileName(compG.first, true);
335 Deduplicator deduplicator;
337 // open a block in order to automatically close archive
338 // at the end of the block
340 DECLARE_AND_OPEN_ARCHIVE(packageFileName, archive);
341 // now iterate over the component of this group
342 for (cmCPackComponent* comp : (compG.second).Components) {
343 // Add the files of this component to the archive
344 this->addOneComponentToArchive(archive, comp, &deduplicator);
347 // add the generated package to package file names list
348 this->packageFileNames.push_back(std::move(packageFileName));
350 // Handle Orphan components (components not belonging to any groups)
351 for (auto& comp : this->Components) {
352 // Does the component belong to a group?
353 if (comp.second.Group == nullptr) {
354 cmCPackLogger(
355 cmCPackLog::LOG_VERBOSE,
356 "Component <"
357 << comp.second.Name
358 << "> does not belong to any group, package it separately."
359 << std::endl);
360 std::string packageFileName = std::string(this->toplevel);
361 packageFileName +=
362 "/" + this->GetArchiveComponentFileName(comp.first, false);
365 DECLARE_AND_OPEN_ARCHIVE(packageFileName, archive);
366 // Add the files of this component to the archive
367 this->addOneComponentToArchive(archive, &(comp.second), nullptr);
369 // add the generated package to package file names list
370 this->packageFileNames.push_back(std::move(packageFileName));
374 // CPACK_COMPONENTS_IGNORE_GROUPS is set
375 // We build 1 package per component
376 else {
377 for (auto& comp : this->Components) {
378 std::string packageFileName = std::string(this->toplevel);
379 packageFileName +=
380 "/" + this->GetArchiveComponentFileName(comp.first, false);
383 DECLARE_AND_OPEN_ARCHIVE(packageFileName, archive);
384 // Add the files of this component to the archive
385 this->addOneComponentToArchive(archive, &(comp.second), nullptr);
387 // add the generated package to package file names list
388 this->packageFileNames.push_back(std::move(packageFileName));
391 return 1;
394 int cmCPackArchiveGenerator::PackageComponentsAllInOne()
396 // reset the package file names
397 this->packageFileNames.clear();
398 this->packageFileNames.emplace_back(this->toplevel);
399 this->packageFileNames[0] += "/";
401 if (this->IsSet("CPACK_ARCHIVE_FILE_NAME")) {
402 this->packageFileNames[0] += *this->GetOption("CPACK_ARCHIVE_FILE_NAME");
403 } else {
404 this->packageFileNames[0] += *this->GetOption("CPACK_PACKAGE_FILE_NAME");
407 this->packageFileNames[0] += this->GetOutputExtension();
409 cmCPackLogger(cmCPackLog::LOG_VERBOSE,
410 "Packaging all groups in one package..."
411 "(CPACK_COMPONENTS_ALL_GROUPS_IN_ONE_PACKAGE is set)"
412 << std::endl);
413 DECLARE_AND_OPEN_ARCHIVE(packageFileNames[0], archive);
415 Deduplicator deduplicator;
417 // The ALL COMPONENTS in ONE package case
418 for (auto& comp : this->Components) {
419 // Add the files of this component to the archive
420 this->addOneComponentToArchive(archive, &(comp.second), &deduplicator);
423 // archive goes out of scope so it will finalized and closed.
424 return 1;
427 int cmCPackArchiveGenerator::PackageFiles()
429 cmCPackLogger(cmCPackLog::LOG_DEBUG,
430 "Toplevel: " << this->toplevel << std::endl);
432 if (this->WantsComponentInstallation()) {
433 // CASE 1 : COMPONENT ALL-IN-ONE package
434 // If ALL COMPONENTS in ONE package has been requested
435 // then the package file is unique and should be open here.
436 if (this->componentPackageMethod == ONE_PACKAGE) {
437 return this->PackageComponentsAllInOne();
439 // CASE 2 : COMPONENT CLASSICAL package(s) (i.e. not all-in-one)
440 // There will be 1 package for each component group
441 // however one may require to ignore component group and
442 // in this case you'll get 1 package for each component.
443 return this->PackageComponents(this->componentPackageMethod ==
444 ONE_PACKAGE_PER_COMPONENT);
447 // CASE 3 : NON COMPONENT package.
448 DECLARE_AND_OPEN_ARCHIVE(packageFileNames[0], archive);
449 cmWorkingDirectory workdir(this->toplevel);
450 if (workdir.Failed()) {
451 cmCPackLogger(cmCPackLog::LOG_ERROR,
452 "Failed to change working directory to "
453 << this->toplevel << " : "
454 << std::strerror(workdir.GetLastResult()) << std::endl);
455 return 0;
457 for (std::string const& file : this->files) {
458 // Get the relative path to the file
459 std::string rp = cmSystemTools::RelativePath(this->toplevel, file);
460 archive.Add(rp, 0, nullptr, false);
461 if (!archive) {
462 cmCPackLogger(cmCPackLog::LOG_ERROR,
463 "Problem while adding file <"
464 << file << "> to archive <" << this->packageFileNames[0]
465 << ">, ERROR = " << archive.GetError() << std::endl);
466 return 0;
469 // The destructor of cmArchiveWrite will close and finish the write
470 return 1;
473 int cmCPackArchiveGenerator::GenerateHeader(std::ostream* /*unused*/)
475 return 1;
478 bool cmCPackArchiveGenerator::SupportsComponentInstallation() const
480 // The Component installation support should only
481 // be activated if explicitly requested by the user
482 // (for backward compatibility reason)
483 return this->IsOn("CPACK_ARCHIVE_COMPONENT_INSTALL");
486 int cmCPackArchiveGenerator::GetThreadCount() const
488 int threads = 1;
490 // CPACK_ARCHIVE_THREADS overrides CPACK_THREADS
491 if (this->IsSet("CPACK_ARCHIVE_THREADS")) {
492 threads = std::stoi(*this->GetOption("CPACK_ARCHIVE_THREADS"));
493 } else if (this->IsSet("CPACK_THREADS")) {
494 threads = std::stoi(*this->GetOption("CPACK_THREADS"));
497 return threads;