From 4e7e6928cb9256ceb7529c1e2f42d7ae8d65329f Mon Sep 17 00:00:00 2001 From: Tyler Yankee Date: Thu, 5 Feb 2026 15:26:54 -0500 Subject: [PATCH] install: Fix bugs around empty directories The conversion between internal list representations led to empty directories being left behind when processing generator expressions in the destination. This ensures the intended behavior of creating a destination directory but installing nothing into it when no are specified, regardless of the presence or absence of genex. Further, an explicit empty string (via `""`, or an undefined variable) led to the current source directory being expanded as a file to install, leading to the generation of an infinitely recursive install command. Disallow this behavior, forcing projects to explicitly specify the current source directory if that is their intention. Fixes: #27568 --- Source/cmInstallCommand.cxx | 5 +++-- Source/cmInstallDirectoryGenerator.cxx | 20 ++++++++++++++----- .../install/DIRECTORY-PATTERN-all-check.cmake | 2 +- .../RunCMake/install/DIRECTORY-PATTERN.cmake | 10 ++++++++++ 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Source/cmInstallCommand.cxx b/Source/cmInstallCommand.cxx index 2d632e7dbe..1449f3483c 100644 --- a/Source/cmInstallCommand.cxx +++ b/Source/cmInstallCommand.cxx @@ -1791,10 +1791,11 @@ bool HandleDirectoryMode(std::vector const& args, exclude_from_all = true; doing = DoingNone; } else if (doing == DoingDirs) { - // Convert this directory to a full path. + // If the given directory is not a full path, convert it to one by + // assuming it's relative to the current source directory. std::string dir = args[i]; std::string::size_type gpos = cmGeneratorExpression::Find(dir); - if (gpos != 0 && !cmSystemTools::FileIsFullPath(dir)) { + if (!dir.empty() && gpos != 0 && !cmSystemTools::FileIsFullPath(dir)) { dir = cmStrCat(helper.Makefile->GetCurrentSourceDirectory(), '/', args[i]); } diff --git a/Source/cmInstallDirectoryGenerator.cxx b/Source/cmInstallDirectoryGenerator.cxx index a388d917d8..cf1fac91fc 100644 --- a/Source/cmInstallDirectoryGenerator.cxx +++ b/Source/cmInstallDirectoryGenerator.cxx @@ -2,6 +2,7 @@ file LICENSE.rst or https://cmake.org/licensing for details. */ #include "cmInstallDirectoryGenerator.h" +#include #include #include "cmGeneratorExpression.h" @@ -54,6 +55,13 @@ bool cmInstallDirectoryGenerator::Compute(cmLocalGenerator* lg) std::vector cmInstallDirectoryGenerator::GetDirectories( std::string const& config) const { + // If given only empty directories, collapse into a single specification to + // avoid redundant calls. This supports the use case of installing an empty + // directory into a destination when a directory is not specified. + if (std::all_of(this->Directories.begin(), this->Directories.end(), + [](std::string const& d) { return d.empty(); })) { + return std::vector{ "" }; + } cmList directories; if (this->ActionsPerConfig) { for (std::string const& f : this->Directories) { @@ -81,11 +89,13 @@ void cmInstallDirectoryGenerator::GenerateScriptForConfig( { std::vector dirs = this->GetDirectories(config); - // Make sure all dirs have absolute paths. - cmMakefile const& mf = *this->LocalGenerator->GetMakefile(); - for (std::string& d : dirs) { - if (!cmSystemTools::FileIsFullPath(d)) { - d = cmStrCat(mf.GetCurrentSourceDirectory(), '/', d); + if (!(dirs.size() == 1 && dirs[0].empty())) { + // Make sure all dirs have absolute paths. + cmMakefile const& mf = *this->LocalGenerator->GetMakefile(); + for (std::string& d : dirs) { + if (!cmSystemTools::FileIsFullPath(d)) { + d = cmStrCat(mf.GetCurrentSourceDirectory(), '/', d); + } } } diff --git a/Tests/RunCMake/install/DIRECTORY-PATTERN-all-check.cmake b/Tests/RunCMake/install/DIRECTORY-PATTERN-all-check.cmake index 7a20edc364..504d1fde35 100644 --- a/Tests/RunCMake/install/DIRECTORY-PATTERN-all-check.cmake +++ b/Tests/RunCMake/install/DIRECTORY-PATTERN-all-check.cmake @@ -1 +1 @@ -check_installed([[^dir1;dir1/empty\.c;dir1/empty\.h;dir2;dir2/pattern;dir2/pattern/empty\.txt;dir3;dir3/empty\.c;dir3/empty\.h;dir3/empty\.txt;dir4;dir4/empty\.c;dir4/empty\.h;dir4/empty\.txt;empty$]]) +check_installed([[^dir1;dir1/empty\.c;dir1/empty\.h;dir2;dir2/pattern;dir2/pattern/empty\.txt;dir3;dir3/empty\.c;dir3/empty\.h;dir3/empty\.txt;dir4;dir4/empty\.c;dir4/empty\.h;dir4/empty\.txt;empty;empty_str;empty_with_genex$]]) diff --git a/Tests/RunCMake/install/DIRECTORY-PATTERN.cmake b/Tests/RunCMake/install/DIRECTORY-PATTERN.cmake index 74d804351e..61007ac44f 100644 --- a/Tests/RunCMake/install/DIRECTORY-PATTERN.cmake +++ b/Tests/RunCMake/install/DIRECTORY-PATTERN.cmake @@ -34,3 +34,13 @@ install( DIRECTORY DESTINATION empty ) + +install( + DIRECTORY "" "" + DESTINATION empty_str + ) + +install( + DIRECTORY + DESTINATION $ + )