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"
8 #include <unordered_map>
9 #include <unordered_set>
13 #include "cmCPackComponentGroup.h"
14 #include "cmCPackGenerator.h"
15 #include "cmCPackLog.h"
16 #include "cmGeneratedFileStream.h"
17 #include "cmStringAlgorithms.h"
18 #include "cmSystemTools.h"
20 #include "cmWorkingDirectory.h"
22 enum class DeduplicateStatus
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
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
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
;
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
;
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
;
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
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
);
125 status
= this->CompareFile(path
, localTopLevel
);
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",
143 cmCPackGenerator
* cmCPackArchiveGenerator::CreateTBZ2Generator()
145 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressBZip2
, "paxr",
149 cmCPackGenerator
* cmCPackArchiveGenerator::CreateTGZGenerator()
151 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressGZip
, "paxr",
155 cmCPackGenerator
* cmCPackArchiveGenerator::CreateTXZGenerator()
157 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressXZ
, "paxr",
161 cmCPackGenerator
* cmCPackArchiveGenerator::CreateTZGenerator()
163 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressCompress
, "paxr",
167 cmCPackGenerator
* cmCPackArchiveGenerator::CreateTZSTGenerator()
169 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressZstd
, "paxr",
173 cmCPackGenerator
* cmCPackArchiveGenerator::CreateZIPGenerator()
175 return new cmCPackArchiveGenerator(cmArchiveWrite::CompressNone
, "zip",
179 cmCPackArchiveGenerator::cmCPackArchiveGenerator(
180 cmArchiveWrite::Compress compress
, std::string format
, std::string extension
)
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")) {
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
);
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
);
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);
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.");
275 cmCPackLogger(cmCPackLog::LOG_DEBUG
,
276 "Passing file: " << rp
<< std::endl
);
280 cmCPackLogger(cmCPackLog::LOG_ERROR
,
281 "ERROR while packaging files: " << archive
.GetError()
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); \
303 cmArchiveWrite archive(gf, this->Compress, this->ArchiveFormat, 0, \
304 this->GetThreadCount()); \
306 if (!archive.Open()) { \
307 cmCPackLogger(cmCPackLog::LOG_ERROR, \
308 "Problem to open archive <" \
309 << (filename) << ">, ERROR = " << (archive).GetError() \
314 cmCPackLogger(cmCPackLog::LOG_ERROR, \
315 "Problem to create archive <" \
316 << (filename) << ">, ERROR = " << (archive).GetError() \
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.
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) {
355 cmCPackLog::LOG_VERBOSE
,
358 << "> does not belong to any group, package it separately."
360 std::string packageFileName
= std::string(this->toplevel
);
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
377 for (auto& comp
: this->Components
) {
378 std::string packageFileName
= std::string(this->toplevel
);
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
));
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");
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)"
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.
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
);
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);
462 cmCPackLogger(cmCPackLog::LOG_ERROR
,
463 "Problem while adding file <"
464 << file
<< "> to archive <" << this->packageFileNames
[0]
465 << ">, ERROR = " << archive
.GetError() << std::endl
);
469 // The destructor of cmArchiveWrite will close and finish the write
473 int cmCPackArchiveGenerator::GenerateHeader(std::ostream
* /*unused*/)
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
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"));