/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmDependsC.h" #include "cmsys/FStream.hxx" #include #include "cmAlgorithms.h" #include "cmFileTimeComparison.h" #include "cmLocalGenerator.h" #include "cmMakefile.h" #include "cmSystemTools.h" #define INCLUDE_REGEX_LINE \ "^[ \t]*[#%][ \t]*(include|import)[ \t]*[<\"]([^\">]+)([\">])" #define INCLUDE_REGEX_LINE_MARKER "#IncludeRegexLine: " #define INCLUDE_REGEX_SCAN_MARKER "#IncludeRegexScan: " #define INCLUDE_REGEX_COMPLAIN_MARKER "#IncludeRegexComplain: " #define INCLUDE_REGEX_TRANSFORM_MARKER "#IncludeRegexTransform: " cmDependsC::cmDependsC() = default; cmDependsC::cmDependsC( cmLocalGenerator* lg, const std::string& targetDir, const std::string& lang, const std::map* validDeps) : cmDepends(lg, targetDir) , ValidDeps(validDeps) { cmMakefile* mf = lg->GetMakefile(); // Configure the include file search path. this->SetIncludePathFromLanguage(lang); // Configure regular expressions. std::string scanRegex = "^.*$"; std::string complainRegex = "^$"; { std::string scanRegexVar = "CMAKE_"; scanRegexVar += lang; scanRegexVar += "_INCLUDE_REGEX_SCAN"; if (const char* sr = mf->GetDefinition(scanRegexVar)) { scanRegex = sr; } std::string complainRegexVar = "CMAKE_"; complainRegexVar += lang; complainRegexVar += "_INCLUDE_REGEX_COMPLAIN"; if (const char* cr = mf->GetDefinition(complainRegexVar)) { complainRegex = cr; } } this->IncludeRegexLine.compile(INCLUDE_REGEX_LINE); this->IncludeRegexScan.compile(scanRegex); this->IncludeRegexComplain.compile(complainRegex); this->IncludeRegexLineString = INCLUDE_REGEX_LINE_MARKER INCLUDE_REGEX_LINE; this->IncludeRegexScanString = INCLUDE_REGEX_SCAN_MARKER; this->IncludeRegexScanString += scanRegex; this->IncludeRegexComplainString = INCLUDE_REGEX_COMPLAIN_MARKER; this->IncludeRegexComplainString += complainRegex; this->SetupTransforms(); this->CacheFileName = this->TargetDirectory; this->CacheFileName += "/"; this->CacheFileName += lang; this->CacheFileName += ".includecache"; this->ReadCacheFile(); } cmDependsC::~cmDependsC() { this->WriteCacheFile(); cmDeleteAll(this->FileCache); } bool cmDependsC::WriteDependencies(const std::set& sources, const std::string& obj, std::ostream& makeDepends, std::ostream& internalDepends) { // Make sure this is a scanning instance. if (sources.empty() || sources.begin()->empty()) { cmSystemTools::Error("Cannot scan dependencies without a source file."); return false; } if (obj.empty()) { cmSystemTools::Error("Cannot scan dependencies without an object file."); return false; } std::set dependencies; bool haveDeps = false; std::string binDir = this->LocalGenerator->GetBinaryDirectory(); // Compute a path to the object file to write to the internal depend file. // Any existing content of the internal depend file has already been // loaded in ValidDeps with this path as a key. std::string obj_i = this->LocalGenerator->ConvertToRelativePath(binDir, obj); if (this->ValidDeps != nullptr) { std::map::const_iterator tmpIt = this->ValidDeps->find(obj_i); if (tmpIt != this->ValidDeps->end()) { dependencies.insert(tmpIt->second.begin(), tmpIt->second.end()); haveDeps = true; } } if (!haveDeps) { // Walk the dependency graph starting with the source file. int srcFiles = static_cast(sources.size()); this->Encountered.clear(); for (std::string const& src : sources) { UnscannedEntry root; root.FileName = src; this->Unscanned.push(root); this->Encountered.insert(src); } std::set scanned; // Use reserve to allocate enough memory for tempPathStr // so that during the loops no memory is allocated or freed std::string tempPathStr; tempPathStr.reserve(4 * 1024); while (!this->Unscanned.empty()) { // Get the next file to scan. UnscannedEntry current = this->Unscanned.front(); this->Unscanned.pop(); // If not a full path, find the file in the include path. std::string fullName; if ((srcFiles > 0) || cmSystemTools::FileIsFullPath(current.FileName)) { if (cmSystemTools::FileExists(current.FileName, true)) { fullName = current.FileName; } } else if (!current.QuotedLocation.empty() && cmSystemTools::FileExists(current.QuotedLocation, true)) { // The include statement producing this entry was a double-quote // include and the included file is present in the directory of // the source containing the include statement. fullName = current.QuotedLocation; } else { std::map::iterator headerLocationIt = this->HeaderLocationCache.find(current.FileName); if (headerLocationIt != this->HeaderLocationCache.end()) { fullName = headerLocationIt->second; } else { for (std::string const& i : this->IncludePath) { // Construct the name of the file as if it were in the current // include directory. Avoid using a leading "./". tempPathStr = cmSystemTools::CollapseCombinedPath(i, current.FileName); // Look for the file in this location. if (cmSystemTools::FileExists(tempPathStr, true)) { fullName = tempPathStr; HeaderLocationCache[current.FileName] = fullName; break; } } } } // Complain if the file cannot be found and matches the complain // regex. if (fullName.empty() && this->IncludeRegexComplain.find(current.FileName)) { cmSystemTools::Error("Cannot find file \"", current.FileName.c_str(), "\"."); return false; } // Scan the file if it was found and has not been scanned already. if (!fullName.empty() && (scanned.find(fullName) == scanned.end())) { // Record scanned files. scanned.insert(fullName); // Check whether this file is already in the cache std::map::iterator fileIt = this->FileCache.find(fullName); if (fileIt != this->FileCache.end()) { fileIt->second->Used = true; dependencies.insert(fullName); for (UnscannedEntry const& inc : fileIt->second->UnscannedEntries) { if (this->Encountered.find(inc.FileName) == this->Encountered.end()) { this->Encountered.insert(inc.FileName); this->Unscanned.push(inc); } } } else { // Try to scan the file. Just leave it out if we cannot find // it. cmsys::ifstream fin(fullName.c_str()); if (fin) { cmsys::FStream::BOM bom = cmsys::FStream::ReadBOM(fin); if (bom == cmsys::FStream::BOM_None || bom == cmsys::FStream::BOM_UTF8) { // Add this file as a dependency. dependencies.insert(fullName); // Scan this file for new dependencies. Pass the directory // containing the file to handle double-quote includes. std::string dir = cmSystemTools::GetFilenamePath(fullName); this->Scan(fin, dir, fullName); } else { // Skip file with encoding we do not implement. } } } } srcFiles--; } } // Write the dependencies to the output stream. Makefile rules // written by the original local generator for this directory // convert the dependencies to paths relative to the home output // directory. We must do the same here. std::string obj_m = cmSystemTools::ConvertToOutputPath(obj_i); internalDepends << obj_i << std::endl; for (std::string const& dep : dependencies) { makeDepends << obj_m << ": " << cmSystemTools::ConvertToOutputPath( this->LocalGenerator->ConvertToRelativePath(binDir, dep)) << std::endl; internalDepends << " " << dep << std::endl; } makeDepends << std::endl; return true; } void cmDependsC::ReadCacheFile() { if (this->CacheFileName.empty()) { return; } cmsys::ifstream fin(this->CacheFileName.c_str()); if (!fin) { return; } std::string line; cmIncludeLines* cacheEntry = nullptr; bool haveFileName = false; while (cmSystemTools::GetLineFromStream(fin, line)) { if (line.empty()) { cacheEntry = nullptr; haveFileName = false; continue; } // the first line after an empty line is the name of the parsed file if (!haveFileName) { haveFileName = true; int newer = 0; cmFileTimeComparison comp; bool res = comp.FileTimeCompare(this->CacheFileName.c_str(), line.c_str(), &newer); if (res && newer == 1) // cache is newer than the parsed file { cacheEntry = new cmIncludeLines; this->FileCache[line] = cacheEntry; } // file doesn't exist, check that the regular expressions // haven't changed else if (!res) { if (line.find(INCLUDE_REGEX_LINE_MARKER) == 0) { if (line != this->IncludeRegexLineString) { return; } } else if (line.find(INCLUDE_REGEX_SCAN_MARKER) == 0) { if (line != this->IncludeRegexScanString) { return; } } else if (line.find(INCLUDE_REGEX_COMPLAIN_MARKER) == 0) { if (line != this->IncludeRegexComplainString) { return; } } else if (line.find(INCLUDE_REGEX_TRANSFORM_MARKER) == 0) { if (line != this->IncludeRegexTransformString) { return; } } } } else if (cacheEntry != nullptr) { UnscannedEntry entry; entry.FileName = line; if (cmSystemTools::GetLineFromStream(fin, line)) { if (line != "-") { entry.QuotedLocation = line; } cacheEntry->UnscannedEntries.push_back(std::move(entry)); } } } } void cmDependsC::WriteCacheFile() const { if (this->CacheFileName.empty()) { return; } cmsys::ofstream cacheOut(this->CacheFileName.c_str()); if (!cacheOut) { return; } cacheOut << this->IncludeRegexLineString << "\n\n"; cacheOut << this->IncludeRegexScanString << "\n\n"; cacheOut << this->IncludeRegexComplainString << "\n\n"; cacheOut << this->IncludeRegexTransformString << "\n\n"; for (auto const& fileIt : this->FileCache) { if (fileIt.second->Used) { cacheOut << fileIt.first << std::endl; for (UnscannedEntry const& inc : fileIt.second->UnscannedEntries) { cacheOut << inc.FileName << std::endl; if (inc.QuotedLocation.empty()) { cacheOut << "-" << std::endl; } else { cacheOut << inc.QuotedLocation << std::endl; } } cacheOut << std::endl; } } } void cmDependsC::Scan(std::istream& is, const std::string& directory, const std::string& fullName) { cmIncludeLines* newCacheEntry = new cmIncludeLines; newCacheEntry->Used = true; this->FileCache[fullName] = newCacheEntry; // Read one line at a time. std::string line; while (cmSystemTools::GetLineFromStream(is, line)) { // Transform the line content first. if (!this->TransformRules.empty()) { this->TransformLine(line); } // Match include directives. if (this->IncludeRegexLine.find(line)) { // Get the file being included. UnscannedEntry entry; entry.FileName = this->IncludeRegexLine.match(2); cmSystemTools::ConvertToUnixSlashes(entry.FileName); if (this->IncludeRegexLine.match(3) == "\"" && !cmSystemTools::FileIsFullPath(entry.FileName)) { // This was a double-quoted include with a relative path. We // must check for the file in the directory containing the // file we are scanning. entry.QuotedLocation = cmSystemTools::CollapseCombinedPath(directory, entry.FileName); } // Queue the file if it has not yet been encountered and it // matches the regular expression for recursive scanning. Note // that this check does not account for the possibility of two // headers with the same name in different directories when one // is included by double-quotes and the other by angle brackets. // It also does not work properly if two header files with the same // name exist in different directories, and both are included from a // file their own directory by simply using "filename.h" (#12619) // This kind of problem will be fixed when a more // preprocessor-like implementation of this scanner is created. if (this->IncludeRegexScan.find(entry.FileName)) { newCacheEntry->UnscannedEntries.push_back(entry); if (this->Encountered.find(entry.FileName) == this->Encountered.end()) { this->Encountered.insert(entry.FileName); this->Unscanned.push(entry); } } } } } void cmDependsC::SetupTransforms() { // Get the transformation rules. std::vector transformRules; cmMakefile* mf = this->LocalGenerator->GetMakefile(); if (const char* xform = mf->GetDefinition("CMAKE_INCLUDE_TRANSFORMS")) { cmSystemTools::ExpandListArgument(xform, transformRules, true); } for (std::string const& tr : transformRules) { this->ParseTransform(tr); } this->IncludeRegexTransformString = INCLUDE_REGEX_TRANSFORM_MARKER; if (!this->TransformRules.empty()) { // Construct the regular expression to match lines to be // transformed. std::string xform = "^([ \t]*[#%][ \t]*(include|import)[ \t]*)("; const char* sep = ""; for (auto const& tr : this->TransformRules) { xform += sep; xform += tr.first; sep = "|"; } xform += ")[ \t]*\\(([^),]*)\\)"; this->IncludeRegexTransform.compile(xform); // Build a string that encodes all transformation rules and will // change when rules are changed. this->IncludeRegexTransformString += xform; for (auto const& tr : this->TransformRules) { this->IncludeRegexTransformString += " "; this->IncludeRegexTransformString += tr.first; this->IncludeRegexTransformString += "(%)="; this->IncludeRegexTransformString += tr.second; } } } void cmDependsC::ParseTransform(std::string const& xform) { // A transform rule is of the form SOME_MACRO(%)=value-with-% // We can simply separate with "(%)=". std::string::size_type pos = xform.find("(%)="); if (pos == std::string::npos || pos == 0) { return; } std::string name = xform.substr(0, pos); std::string value = xform.substr(pos + 4); this->TransformRules[name] = value; } void cmDependsC::TransformLine(std::string& line) { // Check for a transform rule match. Return if none. if (!this->IncludeRegexTransform.find(line)) { return; } TransformRulesType::const_iterator tri = this->TransformRules.find(this->IncludeRegexTransform.match(3)); if (tri == this->TransformRules.end()) { return; } // Construct the transformed line. std::string newline = this->IncludeRegexTransform.match(1); std::string arg = this->IncludeRegexTransform.match(4); for (char c : tri->second) { if (c == '%') { newline += arg; } else { newline += c; } } // Return the transformed line. line = newline; }