file(ARCHIVE_CREATE): Add PATTERNS_EXCLUDE option

Exclude files and directories matching the given patterns while creating
an archive, using libarchive's matcher for parity with file(ARCHIVE_EXTRACT).

Fixes: #27877.
This commit is contained in:
Daksh Mamodiya
2026-06-16 15:33:06 +02:00
parent 6f7584e5e1
commit 9800bd2098
15 changed files with 145 additions and 9 deletions
+12
View File
@@ -937,6 +937,7 @@ Archiving
[MTIME <mtime>]
[THREADS <number>]
[WORKING_DIRECTORY <dir>]
[PATTERNS_EXCLUDE <pattern>...]
[VERBOSE])
:target: ARCHIVE_CREATE
:break: verbatim
@@ -1077,6 +1078,17 @@ Archiving
this directory. If this option is not provided, the current working
directory will be used by default.
``PATTERNS_EXCLUDE <pattern>...``
.. versionadded:: 4.5
Do not add files or directories that match one of the given patterns.
Wildcards are supported. When a directory matches a pattern, it is
excluded together with everything beneath it.
Exclusion patterns are not anchored to the start of an entry's path: a
pattern matches if it matches any portion of the path. This is consistent
with the ``--exclude`` option of command-line ``tar``.
``VERBOSE``
Enable verbose output from the archive operation.
@@ -0,0 +1,6 @@
file-ARCHIVE_CREATE-patterns-exclude
------------------------------------
* The :command:`file(ARCHIVE_CREATE)` command gained a ``PATTERNS_EXCLUDE``
option to omit files and directories matching the given patterns from the
created archive.
+53
View File
@@ -406,8 +406,44 @@ bool cmArchiveWrite::Open()
return true;
}
bool cmArchiveWrite::SetExcludePatterns(
std::vector<std::string> const& patterns)
{
if (patterns.empty()) {
return true;
}
if (!this->MatchObject) {
this->MatchObject = archive_match_new();
if (!this->MatchObject) {
this->Error = "archive_match_new: out of memory";
return false;
}
}
// NOLINTNEXTLINE(readability-use-anyofallof)
for (std::string const& pattern : patterns) {
// Reject empty patterns ourselves. libarchive would reject them too, but
// only after allocating an error message on the match object that
// archive_match_free() does not release.
if (pattern.empty()) {
this->Error = "exclusion pattern must not be empty";
return false;
}
if (archive_match_exclude_pattern(this->MatchObject, pattern.c_str()) !=
ARCHIVE_OK) {
this->Error =
cmStrCat("Failed to add to exclusion list: ", pattern, " (",
cm_archive_error_string(this->MatchObject), ')');
return false;
}
}
return true;
}
cmArchiveWrite::~cmArchiveWrite()
{
if (this->MatchObject) {
archive_match_free(this->MatchObject);
}
archive_read_free(this->Disk);
archive_write_free(this->Archive);
}
@@ -425,6 +461,23 @@ bool cmArchiveWrite::Add(std::string path, size_t skip, char const* prefix,
bool cmArchiveWrite::AddPath(std::string const& path, size_t skip,
char const* prefix, bool recursive)
{
// Skip paths whose archive entry name matches an exclusion pattern. For a
// directory this also prevents descending into it, pruning the subtree.
if (this->MatchObject && skip < path.length()) {
cm::string_view out = cm::string_view(path).substr(skip);
std::string dest = cmStrCat(prefix ? prefix : "", out);
Entry e;
cm_archive_entry_copy_pathname(e, dest.c_str());
int matched = archive_match_path_excluded(this->MatchObject, e);
if (matched < 0) {
this->Error = cmStrCat("archive_match_path_excluded: ",
cm_archive_error_string(this->MatchObject));
return false;
}
if (matched > 0) {
return true;
}
}
if (path != "." || (this->Format != "zip" && this->Format != "7zip")) {
if (!this->AddFile(path, skip, prefix)) {
return false;
+7
View File
@@ -7,6 +7,7 @@
#include <cstddef>
#include <iosfwd>
#include <string>
#include <vector>
#if defined(CMAKE_BOOTSTRAP)
# error "cmArchiveWrite not allowed during bootstrap build!"
@@ -143,6 +144,11 @@ public:
this->Gname = "";
}
//! Sets exclusion patterns. Any path whose archive entry name matches one
//! of the patterns is skipped, and excluded directories are not descended
//! into. Returns false and sets the error if a pattern cannot be added.
bool SetExcludePatterns(std::vector<std::string> const& patterns);
private:
bool Okay() const { return this->Error.empty(); }
bool AddPath(std::string const& path, size_t skip, char const* prefix,
@@ -158,6 +164,7 @@ private:
std::ostream& Stream;
struct archive* Archive;
struct archive* Disk;
struct archive* MatchObject = nullptr;
bool Verbose = false;
std::string Format;
std::string Error;
+3 -2
View File
@@ -1338,8 +1338,9 @@ std::string cmCTest::Base64GzipEncodeFile(std::string const& file)
std::vector<std::string> files;
files.push_back(file);
if (!cmSystemTools::CreateTar(
tarFile, files, {}, cmSystemTools::TarCompressGZip, "UTF-8", false)) {
if (!cmSystemTools::CreateTar(tarFile, files, {}, {},
cmSystemTools::TarCompressGZip, "UTF-8",
false)) {
cmCTestLog(this, ERROR_MESSAGE,
"Error creating tar while "
"encoding file: "
+7 -4
View File
@@ -3720,6 +3720,7 @@ bool HandleArchiveCreateCommand(std::vector<std::string> const& args,
bool Verbose = false;
// "PATHS" requires at least one value, but use a custom check below.
ArgumentParser::MaybeEmpty<std::vector<std::string>> Paths;
ArgumentParser::MaybeEmpty<std::vector<std::string>> PatternsExclude;
};
static auto const parser =
@@ -3733,7 +3734,8 @@ bool HandleArchiveCreateCommand(std::vector<std::string> const& args,
.Bind("THREADS"_s, &Arguments::Threads)
.Bind("WORKING_DIRECTORY"_s, &Arguments::WorkingDirectory)
.Bind("VERBOSE"_s, &Arguments::Verbose)
.Bind("PATHS"_s, &Arguments::Paths);
.Bind("PATHS"_s, &Arguments::Paths)
.Bind("PATTERNS_EXCLUDE"_s, &Arguments::PatternsExclude);
std::vector<std::string> unrecognizedArguments;
auto parsedArgs =
@@ -3866,9 +3868,10 @@ bool HandleArchiveCreateCommand(std::vector<std::string> const& args,
}
if (!cmSystemTools::CreateTar(
parsedArgs.Output, parsedArgs.Paths, parsedArgs.WorkingDirectory,
compress, parsedArgs.Encoding, parsedArgs.Verbose, parsedArgs.MTime,
parsedArgs.Format, compressionLevel, threads)) {
parsedArgs.Output, parsedArgs.Paths, parsedArgs.PatternsExclude,
parsedArgs.WorkingDirectory, compress, parsedArgs.Encoding,
parsedArgs.Verbose, parsedArgs.MTime, parsedArgs.Format,
compressionLevel, threads)) {
status.SetError(cmStrCat("failed to compress: ", parsedArgs.Output));
cmSystemTools::SetFatalErrorOccurred();
return false;
+6
View File
@@ -2238,6 +2238,7 @@ bool cmSystemTools::IsPathToMacOSSharedLibrary(std::string const& path)
bool cmSystemTools::CreateTar(
std::string const& arFileName, std::vector<std::string> const& files,
std::vector<std::string> const& excludeFiles,
std::string const& workingDirectory, cmTarCompression compressType,
std::string const& encoding, bool verbose, std::string const& mtime,
std::string const& format, int compressionLevel, int numThreads)
@@ -2300,6 +2301,10 @@ bool cmSystemTools::CreateTar(
}
a.SetMTime(mtime);
a.SetVerbose(verbose);
if (!a.SetExcludePatterns(excludeFiles)) {
cmSystemTools::Error(a.GetError());
return false;
}
bool tarCreatedSuccessfully = true;
for (auto path : files) {
if (cmSystemTools::FileIsFullPath(path)) {
@@ -2315,6 +2320,7 @@ bool cmSystemTools::CreateTar(
#else
(void)arFileName;
(void)files;
(void)excludeFiles;
(void)encoding;
(void)verbose;
return false;
+1
View File
@@ -527,6 +527,7 @@ public:
std::string const& encoding, bool verbose);
static bool CreateTar(std::string const& arFileName,
std::vector<std::string> const& files,
std::vector<std::string> const& excludeFiles,
std::string const& workingDirectory,
cmTarCompression compressType,
std::string const& encoding, bool verbose,
+3 -3
View File
@@ -2129,9 +2129,9 @@ int cmcmd::ExecuteCMakeCommand(std::vector<std::string> const& args,
if (files.empty()) {
std::cerr << "tar: No files or directories specified\n";
}
if (!cmSystemTools::CreateTar(outFile, files, {}, compress, encoding,
verbose, mtime, format, compressionLevel,
numThreads)) {
if (!cmSystemTools::CreateTar(outFile, files, {}, {}, compress,
encoding, verbose, mtime, format,
compressionLevel, numThreads)) {
cmSystemTools::Error(cmStrCat("Problem creating tar:\n ", outFile));
return 1;
}
@@ -72,6 +72,10 @@ run_cmake(zip-filtered)
run_cmake(zip-filtered-exclude)
run_cmake(zip-filtered-exclude-precedence)
# Excluding selected files or directories from creation
run_cmake(create-filtered-exclude)
run_cmake(create-empty-pattern-exclude)
run_cmake(create-missing-args)
run_cmake(extract-missing-args)
@@ -0,0 +1,3 @@
^CMake Error: exclusion pattern must not be empty
CMake Error at create-empty-pattern-exclude\.cmake:[0-9]+ \(file\):
file failed to compress:
@@ -0,0 +1,8 @@
set(COMPRESS_DIR ${CMAKE_CURRENT_BINARY_DIR}/compress_dir)
file(WRITE ${COMPRESS_DIR}/f1.txt "f1")
file(ARCHIVE_CREATE
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/out.tar
FORMAT gnutar
PATHS ${COMPRESS_DIR}
PATTERNS_EXCLUDE "*.o" "") # empty pattern is rejected
@@ -0,0 +1,30 @@
set(OUTPUT_NAME "test.zip")
set(ARCHIVE_FORMAT zip)
# Exclude entries matching PATTERNS_EXCLUDE while *creating* the archive.
set(COMPRESSION_OPTIONS
PATTERNS_EXCLUDE
"compress_dir/d 2/*" # exclude everything under a directory
"d-4" # exclude by unanchored name (dir + contents)
"no_such_entry" # matches nothing: must not be an error
)
# Everything that was not excluded must still be packed and extracted.
set(CUSTOM_CHECK_FILES
"f1.txt"
"d1/f1.txt"
"d + 3/f1.txt"
"d_4/f1.txt" # underscore, not the excluded "d-4"
"My Special Directory/f1.txt"
)
# The excluded entries must not be present in the created archive.
set(NOT_EXISTING_FILES_CHECK
"d 2/f1.txt"
"d-4/f1.txt"
)
include(${CMAKE_CURRENT_LIST_DIR}/roundtrip.cmake)
check_magic("504b0304" LIMIT 4 HEX)
@@ -65,6 +65,7 @@ file(ARCHIVE_CREATE
COMPRESSION "${COMPRESSION_TYPE}"
WORKING_DIRECTORY "${WORKING_DIRECTORY}"
${ENCODING_OPTIONS}
${COMPRESSION_OPTIONS}
VERBOSE
PATHS ${FULL_COMPRESS_DIR})