MissingDependencyScanner.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "MissingDependencyScanner.h"
  9. AZ_PUSH_DISABLE_WARNING(4244 4251, "-Wunknown-warning-option")
  10. AZ_POP_DISABLE_WARNING
  11. #include "LineByLineDependencyScanner.h"
  12. #include "PotentialDependencies.h"
  13. #include "native/AssetDatabase/AssetDatabase.h"
  14. #include "native/assetprocessor.h"
  15. #include <AzCore/Component/TickBus.h>
  16. #include <AzCore/IO/FileIO.h>
  17. #include <AzCore/IO/Path/Path.h>
  18. #include <AzCore/std/smart_ptr/make_shared.h>
  19. #include <AzCore/std/smart_ptr/shared_ptr.h>
  20. #include <AzCore/std/string/wildcard.h>
  21. #include <AzCore/Utils/Utils.h>
  22. #include <AzCore/XML/rapidxml.h>
  23. #include <AzFramework/API/ApplicationAPI.h>
  24. #include <AzFramework/FileTag/FileTag.h>
  25. #include <AzFramework/FileTag/FileTagBus.h>
  26. namespace AssetProcessor
  27. {
  28. constexpr auto EngineFolder = AZ::IO::FixedMaxPath("Assets") / "Engine";
  29. AZStd::string GetXMLDependenciesFile(const AZStd::string& fullPath, const AZStd::vector<AzFramework::GemInfo>& gemInfoList, AZStd::string& tokenName)
  30. {
  31. AZ::IO::Path xmlDependenciesFileFullPath;
  32. tokenName = EngineFolder.String();
  33. for (const AzFramework::GemInfo& gemElement : gemInfoList)
  34. {
  35. for (const AZ::IO::Path& absoluteSourcePath : gemElement.m_absoluteSourcePaths)
  36. {
  37. if (AZ::IO::PathView(fullPath).IsRelativeTo(absoluteSourcePath))
  38. {
  39. xmlDependenciesFileFullPath = absoluteSourcePath;
  40. xmlDependenciesFileFullPath /= AzFramework::GemInfo::GetGemAssetFolder();
  41. xmlDependenciesFileFullPath /= AZStd::string::format("%s_Dependencies.xml", gemElement.m_gemName.c_str());;
  42. if (AZ::IO::FileIOBase::GetInstance()->Exists(xmlDependenciesFileFullPath.c_str()))
  43. {
  44. tokenName = gemElement.m_gemName;
  45. return xmlDependenciesFileFullPath.Native();
  46. }
  47. }
  48. }
  49. }
  50. // if we are here than either the %gemName%_Dependencies.xml file does not exists or the user inputted path is not inside a gems folder,
  51. // in both the cases we will return the engine dependencies file
  52. xmlDependenciesFileFullPath = AZ::Utils::GetEnginePath();
  53. xmlDependenciesFileFullPath /= EngineFolder;
  54. xmlDependenciesFileFullPath /= "Engine_Dependencies.xml";
  55. return xmlDependenciesFileFullPath.Native();
  56. }
  57. const int MissingDependencyScanner::DefaultMaxScanIteration = 800;
  58. class MissingDependency
  59. {
  60. public:
  61. MissingDependency(const AZ::Data::AssetId& assetId, const PotentialDependencyMetaData& metaData) :
  62. m_assetId(assetId),
  63. m_metaData(metaData)
  64. {
  65. }
  66. // Allows MissingDependency to be in a sorted container, which stabilizes log output.
  67. bool operator<(const MissingDependency& rhs) const
  68. {
  69. return m_assetId < rhs.m_assetId;
  70. }
  71. AZ::Data::AssetId m_assetId;
  72. PotentialDependencyMetaData m_metaData;
  73. };
  74. MissingDependencyScanner::MissingDependencyScanner()
  75. {
  76. m_defaultScanner = AZStd::make_shared<LineByLineDependencyScanner>();
  77. ApplicationManagerNotifications::Bus::Handler::BusConnect();
  78. MissingDependencyScannerRequestBus::Handler::BusConnect();
  79. }
  80. MissingDependencyScanner::~MissingDependencyScanner()
  81. {
  82. MissingDependencyScannerRequestBus::Handler::BusDisconnect();
  83. ApplicationManagerNotifications::Bus::Handler::BusDisconnect();
  84. }
  85. void MissingDependencyScanner::ApplicationShutdownRequested()
  86. {
  87. // Do not add any new functions to the SystemTickBus queue
  88. m_shutdownRequested = true;
  89. // Finish up previously queued work
  90. AZ::SystemTickBus::ExecuteQueuedEvents();
  91. }
  92. void MissingDependencyScanner::ScanFile(const AZStd::string& fullPath, int maxScanIteration, AZStd::shared_ptr<AssetDatabaseConnection> databaseConnection, const AZStd::string& dependencyTokenName, bool queueDbCommandsOnMainThread, scanFileCallback callback)
  93. {
  94. AZ::s64 productPK = -1;
  95. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer dependencies = {};
  96. ScanFile(fullPath, maxScanIteration, productPK, dependencies, databaseConnection, dependencyTokenName, ScannerMatchType::ExtensionOnlyFirstMatch, nullptr, queueDbCommandsOnMainThread, callback);
  97. }
  98. void MissingDependencyScanner::ScanFile(
  99. const AZStd::string& fullPath,
  100. int maxScanIteration,
  101. AZ::s64 productPK,
  102. const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencies,
  103. AZStd::shared_ptr<AssetDatabaseConnection> databaseConnection,
  104. bool queueDbCommandsOnMainThread,
  105. scanFileCallback callback)
  106. {
  107. ScanFile(fullPath, maxScanIteration, productPK, dependencies, databaseConnection, "", ScannerMatchType::ExtensionOnlyFirstMatch, nullptr, queueDbCommandsOnMainThread, callback);
  108. }
  109. void MissingDependencyScanner::ScanFile(
  110. const AZStd::string& fullPath,
  111. int maxScanIteration,
  112. AZ::s64 productPK,
  113. const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencies,
  114. AZStd::shared_ptr<AssetDatabaseConnection> databaseConnection,
  115. AZStd::string dependencyTokenName,
  116. ScannerMatchType matchType,
  117. AZ::Crc32* forceScanner,
  118. bool queueDbCommandsOnMainThread,
  119. scanFileCallback callback)
  120. {
  121. using namespace AzFramework::FileTag;
  122. AZ_Printf(AssetProcessor::ConsoleChannel, "Scanning for missing dependencies:\t%s\n", fullPath.c_str());
  123. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
  124. if (productPK != -1)
  125. {
  126. AZStd::vector<AZStd::vector<AZStd::string>> excludedTagsList = {
  127. {
  128. FileTags[static_cast<unsigned int>(FileTagsIndex::EditorOnly)]
  129. },
  130. {
  131. FileTags[static_cast<unsigned int>(FileTagsIndex::Shader)]
  132. } };
  133. databaseConnection->GetSourceByProductID(productPK, sourceEntry);
  134. for (const AZStd::vector<AZStd::string>& tags : excludedTagsList)
  135. {
  136. bool shouldIgnore = false;
  137. QueryFileTagsEventBus::EventResult(shouldIgnore, FileTagType::Exclude,
  138. &QueryFileTagsEventBus::Events::Match, fullPath.c_str(), tags);
  139. if (shouldIgnore)
  140. {
  141. // Record that this file was ignored in the database, so the asset tab can display this information.
  142. AZStd::string ignoredByTagText("File matches EditorOnly or Shader tag, ignoring for missing dependencies search.");
  143. AZ_Printf(AssetProcessor::ConsoleChannel, "\t%s\n", ignoredByTagText.c_str());
  144. SetDependencyScanResultStatus(
  145. ignoredByTagText,
  146. productPK,
  147. sourceEntry.m_analysisFingerprint,
  148. databaseConnection,
  149. queueDbCommandsOnMainThread,
  150. callback);
  151. return;
  152. }
  153. }
  154. }
  155. else
  156. {
  157. // if we are here than it implies that this file is not an asset
  158. AZStd::vector<AZStd::string> tags{
  159. FileTags[static_cast<unsigned int>(FileTagsIndex::Ignore)],
  160. FileTags[static_cast<unsigned int>(FileTagsIndex::ProductDependency)] };
  161. bool shouldIgnore = false;
  162. QueryFileTagsEventBus::EventResult(shouldIgnore, FileTagType::Exclude, &QueryFileTagsEventBus::Events::Match, fullPath.c_str(), tags);
  163. if (shouldIgnore)
  164. {
  165. AZ_Printf(AssetProcessor::ConsoleChannel, "File ( %s ) will be skipped by the missing dependency scanner.\n", fullPath.c_str());
  166. return;
  167. }
  168. }
  169. AZ::IO::FileIOStream fileStream;
  170. if (!fileStream.Open(fullPath.c_str(), AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary))
  171. {
  172. AZ_Error(AssetProcessor::ConsoleChannel, false, "File at path %s could not be opened.", fullPath.c_str());
  173. // Record that this file was ignored in the database, so the asset tab can display this information.
  174. SetDependencyScanResultStatus(
  175. "The file could not be opened.",
  176. productPK,
  177. sourceEntry.m_analysisFingerprint,
  178. databaseConnection,
  179. queueDbCommandsOnMainThread,
  180. callback);
  181. return;
  182. }
  183. PotentialDependencies potentialDependencies;
  184. bool scanSuccessful = RunScan(fullPath, maxScanIteration, fileStream, potentialDependencies, matchType, forceScanner);
  185. fileStream.Close();
  186. if (!scanSuccessful)
  187. {
  188. // RunScan will report an error on what caused the scan to fail.
  189. SetDependencyScanResultStatus(
  190. "An error occured, see log for details.",
  191. productPK,
  192. sourceEntry.m_analysisFingerprint,
  193. databaseConnection,
  194. queueDbCommandsOnMainThread,
  195. callback);
  196. return;
  197. }
  198. MissingDependencies missingDependencies;
  199. PopulateMissingDependencies(productPK, databaseConnection, dependencies, missingDependencies, potentialDependencies);
  200. if (queueDbCommandsOnMainThread && !m_shutdownRequested)
  201. {
  202. AZ::SystemTickBus::QueueFunction([=]()
  203. {
  204. ReportMissingDependencies(productPK, databaseConnection, dependencyTokenName, missingDependencies, callback);
  205. });
  206. }
  207. else
  208. {
  209. ReportMissingDependencies(productPK, databaseConnection, dependencyTokenName, missingDependencies, callback);
  210. }
  211. }
  212. void MissingDependencyScanner::SetDependencyScanResultStatus(
  213. AZStd::string status,
  214. AZ::s64 productPK,
  215. const AZStd::string& analysisFingerprint,
  216. AZStd::shared_ptr<AssetDatabaseConnection> databaseConnection,
  217. bool queueDbCommandsOnMainThread,
  218. scanFileCallback callback)
  219. {
  220. QDateTime currentTime = QDateTime::currentDateTime();
  221. auto finalizeMissingDependency = [=]() {
  222. AzToolsFramework::AssetDatabase::MissingProductDependencyDatabaseEntry missingDependencyEntry(
  223. productPK,
  224. /*Scanner*/ "",
  225. /*Scanner Version*/ "",
  226. analysisFingerprint,
  227. AZ::Uuid::CreateNull(),
  228. /*Product ID*/ 0,
  229. status,
  230. currentTime.toString().toUtf8().constData(),
  231. currentTime.toSecsSinceEpoch());
  232. databaseConnection->SetMissingProductDependency(missingDependencyEntry);
  233. callback(missingDependencyEntry.m_missingDependencyString);
  234. };
  235. if (queueDbCommandsOnMainThread && !m_shutdownRequested)
  236. {
  237. AZ::SystemTickBus::QueueFunction(finalizeMissingDependency);
  238. }
  239. else
  240. {
  241. finalizeMissingDependency();
  242. }
  243. }
  244. void MissingDependencyScanner::RegisterSpecializedScanner(AZStd::shared_ptr<SpecializedDependencyScanner> scanner)
  245. {
  246. m_specializedScanners.insert(AZStd::pair<AZ::Crc32, AZStd::shared_ptr<SpecializedDependencyScanner>>(scanner->GetScannerCRC(), scanner));
  247. }
  248. bool MissingDependencyScanner::RunScan(
  249. const AZStd::string& fullPath,
  250. int maxScanIteration,
  251. AZ::IO::GenericStream& fileStream,
  252. PotentialDependencies& potentialDependencies,
  253. ScannerMatchType matchType,
  254. AZ::Crc32* forceScanner)
  255. {
  256. // If a scanner is given to specifically use, then use that scanner and only that scanner.
  257. if (forceScanner)
  258. {
  259. AZ_Printf(
  260. AssetProcessor::ConsoleChannel,
  261. "\tForcing scanner with CRC %d\n",
  262. *forceScanner);
  263. DependencyScannerMap::iterator scannerToUse = m_specializedScanners.find(*forceScanner);
  264. if (scannerToUse != m_specializedScanners.end())
  265. {
  266. scannerToUse->second->ScanFileForPotentialDependencies(fileStream, potentialDependencies, maxScanIteration);
  267. return true;
  268. }
  269. else
  270. {
  271. AZ_Error(AssetProcessor::ConsoleChannel, false, "Attempted to force dependency scan using CRC %d, which is not registered.",
  272. *forceScanner);
  273. return false;
  274. }
  275. }
  276. // Check if a specialized scanner should be used, based on the given scanner matching type rule.
  277. for (const AZStd::pair<AZ::Crc32, AZStd::shared_ptr<SpecializedDependencyScanner>>&
  278. scanner : m_specializedScanners)
  279. {
  280. switch (matchType)
  281. {
  282. case ScannerMatchType::ExtensionOnlyFirstMatch:
  283. if (scanner.second->DoesScannerMatchFileExtension(fullPath))
  284. {
  285. return scanner.second->ScanFileForPotentialDependencies(fileStream, potentialDependencies, maxScanIteration);
  286. }
  287. break;
  288. case ScannerMatchType::FileContentsFirstMatch:
  289. if (scanner.second->DoesScannerMatchFileData(fileStream))
  290. {
  291. return scanner.second->ScanFileForPotentialDependencies(fileStream, potentialDependencies, maxScanIteration);
  292. }
  293. break;
  294. case ScannerMatchType::Deep:
  295. // A deep scan has every matching scanner scan the file, and uses the default scan.
  296. if (scanner.second->DoesScannerMatchFileData(fileStream))
  297. {
  298. scanner.second->ScanFileForPotentialDependencies(fileStream, potentialDependencies, maxScanIteration);
  299. }
  300. break;
  301. default:
  302. AZ_Error(AssetProcessor::ConsoleChannel, false, "Scan match type %d is not available.", matchType);
  303. break;
  304. };
  305. }
  306. // No specialized scanner was found (or a deep scan is being performed), so use the default scanner.
  307. return m_defaultScanner->ScanFileForPotentialDependencies(fileStream, potentialDependencies, maxScanIteration);
  308. }
  309. void MissingDependencyScanner::PopulateMissingDependencies(
  310. AZ::s64 productPK,
  311. AZStd::shared_ptr<AssetDatabaseConnection> databaseConnection,
  312. const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencies,
  313. MissingDependencies& missingDependencies,
  314. const PotentialDependencies& potentialDependencies)
  315. {
  316. // If a file references itself, don't report it.
  317. AzToolsFramework::AssetDatabase::SourceDatabaseEntry fileWithPotentialMissingDependencies;
  318. databaseConnection->GetSourceByProductID(productPK, fileWithPotentialMissingDependencies);
  319. AZStd::map<AZ::Uuid, PotentialDependencyMetaData> uuids(potentialDependencies.m_uuids);
  320. AZStd::map<AZ::Data::AssetId, PotentialDependencyMetaData> assetIds(potentialDependencies.m_assetIds);
  321. // Check if any products exist for the given job, and those products have a sub ID that matches
  322. // the expected sub ID.
  323. AzToolsFramework::AssetDatabase::ProductDatabaseEntry productWithPotentialMissingDependencies;
  324. databaseConnection->GetProductByProductID(productPK, productWithPotentialMissingDependencies);
  325. QString scannedProductPath( productWithPotentialMissingDependencies.m_productName.c_str() );
  326. auto lastSeparatorIndex = scannedProductPath.lastIndexOf(AZ_CORRECT_DATABASE_SEPARATOR_STRING);
  327. scannedProductPath = scannedProductPath.remove(lastSeparatorIndex + 1, scannedProductPath.length());
  328. // Check the existing product dependency list for the file that is being scanned, remove
  329. // any potential UUIDs that match dependencies already being emitted.
  330. for (const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry&
  331. existingDependency : dependencies)
  332. {
  333. AZStd::map<AZ::Uuid, PotentialDependencyMetaData>::iterator matchingDependency =
  334. uuids.find(existingDependency.m_dependencySourceGuid);
  335. if (matchingDependency != uuids.end())
  336. {
  337. uuids.erase(matchingDependency);
  338. }
  339. }
  340. // Remove all UUIDs that don't match an asset in the database.
  341. for (AZStd::map<AZ::Uuid, PotentialDependencyMetaData>::iterator uuidIter = uuids.begin();
  342. uuidIter != uuids.end();
  343. ++uuidIter)
  344. {
  345. if (fileWithPotentialMissingDependencies.m_sourceGuid == uuidIter->first)
  346. {
  347. // This product references itself, or the source it comes from. Don't report it as a missing dependency.
  348. continue;
  349. }
  350. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
  351. if (!databaseConnection->GetSourceBySourceGuid(uuidIter->first, sourceEntry))
  352. {
  353. // The UUID isn't in the asset database, don't add it to the list of missing dependencies.
  354. continue;
  355. }
  356. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  357. if (!databaseConnection->GetJobsBySourceID(sourceEntry.m_sourceID, jobs))
  358. {
  359. // No jobs existed for that source asset, so there are no products for this asset.
  360. // With no products, there is no way there can be a missing product dependency.
  361. continue;
  362. }
  363. // The dependency only referenced the source UUID, so add all products as missing dependencies.
  364. for (const AzToolsFramework::AssetDatabase::JobDatabaseEntry& job : jobs)
  365. {
  366. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  367. if (!databaseConnection->GetProductsByJobID(job.m_jobID, products))
  368. {
  369. continue;
  370. }
  371. for (const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& product : products)
  372. {
  373. // This match was for a UUID with no product ID, so add all products as missing dependencies.
  374. MissingDependency missingDependency(
  375. AZ::Data::AssetId(uuidIter->first, product.m_subID),
  376. uuidIter->second);
  377. missingDependencies.insert(missingDependency);
  378. }
  379. }
  380. }
  381. // Validate the asset ID list, removing anything that is already a dependency, or does not exist in the asset database.
  382. for (AZStd::map<AZ::Data::AssetId, PotentialDependencyMetaData>::iterator assetIdIter = assetIds.begin();
  383. assetIdIter != assetIds.end();
  384. ++assetIdIter)
  385. {
  386. bool foundUUID = false;
  387. // Strip out all existing, matching dependencies
  388. for (const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry&
  389. existingDependency : dependencies)
  390. {
  391. if (existingDependency.m_dependencySourceGuid == assetIdIter->first.m_guid &&
  392. existingDependency.m_dependencySubID == assetIdIter->first.m_subId)
  393. {
  394. foundUUID = true;
  395. break;
  396. }
  397. }
  398. // There is already a dependency with this UUID, so it's not a missing dependency.
  399. if (foundUUID)
  400. {
  401. continue;
  402. }
  403. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
  404. if (!databaseConnection->GetSourceBySourceGuid(assetIdIter->first.m_guid, sourceEntry))
  405. {
  406. // The UUID isn't in the asset database. Don't report it as a missing dependency
  407. // because UUIDs are used for tracking many things that are not assets.
  408. continue;
  409. }
  410. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  411. if (!databaseConnection->GetJobsBySourceID(sourceEntry.m_sourceID, jobs))
  412. {
  413. // No jobs existed for that source asset, so there are no products for this asset.
  414. // With no products, there is no way there can be a missing product dependency.
  415. continue;
  416. }
  417. bool isProductOfFileWithPotentialMissingDependencies = fileWithPotentialMissingDependencies.m_sourceGuid == assetIdIter->first.m_guid;
  418. bool foundMatchingProduct = false;
  419. for (const AzToolsFramework::AssetDatabase::JobDatabaseEntry& job : jobs)
  420. {
  421. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  422. if (!databaseConnection->GetProductsByJobID(job.m_jobID, products))
  423. {
  424. continue;
  425. }
  426. for (const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& product : products)
  427. {
  428. if (product.m_subID == assetIdIter->first.m_subId)
  429. {
  430. // This product references itself. Don't report it as a missing dependency.
  431. // If the product references a different product of the same source and that isn't
  432. // a dependency, then do report that.
  433. // We have to check against more than the productPK to catch identical products across multiple
  434. // platforms.
  435. if (productPK == product.m_productID ||
  436. (isProductOfFileWithPotentialMissingDependencies && productWithPotentialMissingDependencies.m_subID == product.m_subID))
  437. {
  438. continue;
  439. }
  440. MissingDependency missingDependency(
  441. assetIdIter->first,
  442. assetIdIter->second);
  443. missingDependencies.insert(missingDependency);
  444. foundMatchingProduct = true;
  445. break;
  446. }
  447. }
  448. if (foundMatchingProduct)
  449. {
  450. break;
  451. }
  452. }
  453. }
  454. for (const PotentialDependencyMetaData& path : potentialDependencies.m_paths)
  455. {
  456. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer searchSources;
  457. QString searchName(path.m_sourceString.c_str());
  458. // The paths in the file may have had slashes in either direction, or double slashes.
  459. searchName.replace(AZ_WRONG_DATABASE_SEPARATOR_STRING, AZ_CORRECT_DATABASE_SEPARATOR_STRING);
  460. searchName.replace(AZ_DOUBLE_CORRECT_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR_STRING);
  461. if (databaseConnection->GetSourcesBySourceName(searchName, searchSources))
  462. {
  463. // A source matched the path, look up products and add them as resolved path dependencies.
  464. for (const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& source : searchSources)
  465. {
  466. if (fileWithPotentialMissingDependencies.m_sourceGuid == source.m_sourceGuid)
  467. {
  468. // This product references itself, or the source it comes from. Don't report it as a missing dependency.
  469. continue;
  470. }
  471. bool dependencyExistsForSource = false;
  472. for (const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry&
  473. existingDependency : dependencies)
  474. {
  475. if (existingDependency.m_dependencySourceGuid == source.m_sourceGuid)
  476. {
  477. dependencyExistsForSource = true;
  478. break;
  479. }
  480. }
  481. if (dependencyExistsForSource)
  482. {
  483. continue;
  484. }
  485. AzToolsFramework::AssetDatabase::JobDatabaseEntryContainer jobs;
  486. if (!databaseConnection->GetJobsBySourceID(source.m_sourceID, jobs))
  487. {
  488. // No jobs exist for this source, which means there is no matching product dependency.
  489. continue;
  490. }
  491. for (const AzToolsFramework::AssetDatabase::JobDatabaseEntry& job : jobs)
  492. {
  493. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  494. if (!databaseConnection->GetProductsByJobID(job.m_jobID, products))
  495. {
  496. // No products, no product dependencies.
  497. continue;
  498. }
  499. for (const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& product : products)
  500. {
  501. MissingDependency missingDependency(
  502. AZ::Data::AssetId(source.m_sourceGuid, product.m_subID),
  503. path);
  504. missingDependencies.insert(missingDependency);
  505. }
  506. }
  507. }
  508. }
  509. else
  510. {
  511. // Product paths in the asset database include the platform and additional pathing information that
  512. // makes this check more complex than the source path check.
  513. // Examples:
  514. // pc/usersettings.xml
  515. // pc/ProjectName/file.xml
  516. // Taking all results from this EndsWith check can lead to an over-emission of potential missing dependencies.
  517. // For example, if a file has a comment like "Something about .dds files", then EndsWith would return
  518. // every single dds file in the database.
  519. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
  520. if (!databaseConnection->GetProductsLikeProductName(searchName, AssetDatabaseConnection::LikeType::EndsWith, products))
  521. {
  522. continue;
  523. }
  524. for (const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& product : products)
  525. {
  526. if (productPK == product.m_productID)
  527. {
  528. // Don't report if a file has a reference to itself.
  529. continue;
  530. }
  531. // Cull the platform from the product path to perform a more confident comparison against the given path.
  532. QString culledProductPath = QString(product.m_productName.c_str());
  533. // If this appears to be a valid path to a product relative to the product being checked
  534. if (culledProductPath.compare(scannedProductPath.append(searchName), Qt::CaseInsensitive) != 0)
  535. {
  536. culledProductPath = culledProductPath.remove(0, culledProductPath.indexOf(AZ_CORRECT_DATABASE_SEPARATOR_STRING) + 1);
  537. // This first check will catch paths that include the project name, as well as references to assets that include a scan folder in the path.
  538. if (culledProductPath.compare(searchName, Qt::CaseInsensitive) != 0)
  539. {
  540. int nextFolderIndex = culledProductPath.indexOf(AZ_CORRECT_DATABASE_SEPARATOR_STRING);
  541. if (nextFolderIndex == -1)
  542. {
  543. continue;
  544. }
  545. // Perform a second check with the scan folder removed. Many asset references are relevant to scan folder roots.
  546. // For example, a material may have a relative path reference to a texture as "textures/SomeTexture.dds".
  547. // This relative path resolves in many systems based on scan folder root, so if this file is in "platform/project/textures/SomeTexture.dds",
  548. // this check is intended to find that reference.
  549. culledProductPath = culledProductPath.remove(0, nextFolderIndex + 1);
  550. if (culledProductPath.compare(searchName, Qt::CaseInsensitive) != 0)
  551. {
  552. continue;
  553. }
  554. }
  555. }
  556. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer productSources;
  557. if (!databaseConnection->GetSourcesByProductName(QString(product.m_productName.c_str()), productSources))
  558. {
  559. AZ_Error(
  560. AssetProcessor::ConsoleChannel,
  561. false,
  562. "Product %s does not have a matching source. Your database may be corrupted.", product.m_productName.c_str());
  563. continue;
  564. }
  565. for (const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& source : productSources)
  566. {
  567. bool dependencyExistsForProduct = false;
  568. for (const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry&
  569. existingDependency : dependencies)
  570. {
  571. if (existingDependency.m_dependencySourceGuid == source.m_sourceGuid &&
  572. existingDependency.m_dependencySubID == product.m_subID)
  573. {
  574. dependencyExistsForProduct = true;
  575. break;
  576. }
  577. }
  578. if (!dependencyExistsForProduct)
  579. {
  580. AZ::Data::AssetId assetId(source.m_sourceGuid, product.m_subID);
  581. MissingDependency missingDependency(
  582. AZ::Data::AssetId(source.m_sourceGuid, product.m_subID),
  583. path);
  584. missingDependencies.insert(missingDependency);
  585. }
  586. }
  587. }
  588. }
  589. }
  590. }
  591. void MissingDependencyScanner::ReportMissingDependencies(
  592. AZ::s64 productPK,
  593. AZStd::shared_ptr<AssetDatabaseConnection> databaseConnection,
  594. const AZStd::string& dependencyTokenName,
  595. const MissingDependencies& missingDependencies,
  596. scanFileCallback callback)
  597. {
  598. using namespace AzFramework::FileTag;
  599. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
  600. databaseConnection->GetSourceByProductID(productPK, sourceEntry);
  601. AZStd::vector<AZStd::string> tags{
  602. FileTags[static_cast<unsigned int>(FileTagsIndex::Ignore)],
  603. FileTags[static_cast<unsigned int>(FileTagsIndex::ProductDependency)] };
  604. QDateTime currentTime = QDateTime::currentDateTime();
  605. // If there were no missing dependencies, add a row to the table so we know it was scanned.
  606. if (productPK != -1 && missingDependencies.empty())
  607. {
  608. SetDependencyScanResultStatus(
  609. "No missing dependencies found",
  610. productPK,
  611. sourceEntry.m_analysisFingerprint,
  612. databaseConnection,
  613. /*queueDbCommandsOnMainThread*/ false, // ReportMissingDependencies was already queued to run on the main thread.
  614. callback);
  615. return;
  616. }
  617. for (const MissingDependency& missingDependency : missingDependencies)
  618. {
  619. bool shouldIgnore = false;
  620. QueryFileTagsEventBus::EventResult(shouldIgnore, FileTagType::Exclude,
  621. &QueryFileTagsEventBus::Events::Match, missingDependency.m_metaData.m_sourceString.c_str(), tags);
  622. if (!shouldIgnore && !dependencyTokenName.empty())
  623. {
  624. // if one of the rules in the xml dependency file match then skip the missing dependency
  625. auto rulesFound = m_dependenciesRulesMap.find(dependencyTokenName);
  626. if (rulesFound != m_dependenciesRulesMap.end())
  627. {
  628. for (const auto& rule : rulesFound->second)
  629. {
  630. if (AZStd::wildcard_match(rule, missingDependency.m_metaData.m_sourceString))
  631. {
  632. shouldIgnore = true;
  633. break;
  634. }
  635. }
  636. }
  637. }
  638. if (!shouldIgnore)
  639. {
  640. AZStd::string assetIdStr = missingDependency.m_assetId.ToString<AZStd::string>();
  641. AZ_Printf(
  642. AssetProcessor::ConsoleChannel,
  643. "\t\tMissing dependency: String \"%s\" matches asset: %s\n",
  644. missingDependency.m_metaData.m_sourceString.c_str(),
  645. assetIdStr.c_str());
  646. if (productPK != -1)
  647. {
  648. AzToolsFramework::AssetDatabase::MissingProductDependencyDatabaseEntry missingDependencyEntry(
  649. productPK,
  650. missingDependency.m_metaData.m_scanner->GetName(),
  651. missingDependency.m_metaData.m_scanner->GetVersion(),
  652. sourceEntry.m_analysisFingerprint,
  653. missingDependency.m_assetId.m_guid,
  654. missingDependency.m_assetId.m_subId,
  655. missingDependency.m_metaData.m_sourceString,
  656. currentTime.toString().toUtf8().constData(),
  657. currentTime.toSecsSinceEpoch());
  658. databaseConnection->SetMissingProductDependency(missingDependencyEntry);
  659. }
  660. callback(missingDependency.m_metaData.m_sourceString);
  661. }
  662. }
  663. }
  664. bool MissingDependencyScanner::PopulateRulesForScanFolder(const AZStd::string& scanFolderPath, const AZStd::vector<AzFramework::GemInfo>& gemInfoList, AZStd::string& dependencyTokenName)
  665. {
  666. AZStd::string xmlDependenciesFullFilePath = GetXMLDependenciesFile(scanFolderPath, gemInfoList, dependencyTokenName);
  667. if (xmlDependenciesFullFilePath.empty())
  668. {
  669. AZ_Printf(AssetProcessor::ConsoleChannel, "Unable to find xml dependency file for the directory scan %s\n", scanFolderPath.c_str());
  670. }
  671. auto found = m_dependenciesRulesMap.find(dependencyTokenName);
  672. if (found != m_dependenciesRulesMap.end())
  673. {
  674. // this imply that we have already parsed this file and populated the rules,
  675. // therefore we can exit early.
  676. return true;
  677. }
  678. if (!AZ::IO::FileIOBase::GetInstance()->Exists(xmlDependenciesFullFilePath.c_str()))
  679. {
  680. AZ_Printf(AssetProcessor::ConsoleChannel, "Unable to find xml dependency file (%s). \n", xmlDependenciesFullFilePath.c_str());
  681. return false;
  682. }
  683. AZ::IO::FileIOStream fileStream;
  684. AZStd::vector<AZStd::string> dependenciesRuleList;
  685. if (fileStream.Open(xmlDependenciesFullFilePath.c_str(), AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary))
  686. {
  687. if (!fileStream.CanRead())
  688. {
  689. return false;
  690. }
  691. AZ::IO::SizeType length = fileStream.GetLength();
  692. if (length == 0)
  693. {
  694. return false;
  695. }
  696. AZStd::vector<char> charBuffer;
  697. charBuffer.resize_no_construct(length + 1);
  698. fileStream.Read(length, charBuffer.data());
  699. charBuffer.back() = 0;
  700. AZ::rapidxml::xml_document<char> xmlDoc;
  701. xmlDoc.parse<AZ::rapidxml::parse_no_data_nodes>(charBuffer.data());
  702. auto engineDependenciesNode = xmlDoc.first_node("EngineDependencies");
  703. if (!engineDependenciesNode)
  704. {
  705. return false;
  706. }
  707. auto dependencyNode = engineDependenciesNode->first_node("Dependency");
  708. while (dependencyNode)
  709. {
  710. auto pathAttr = dependencyNode->first_attribute("path");
  711. if (pathAttr)
  712. {
  713. dependenciesRuleList.emplace_back(AZStd::string(pathAttr->value()));
  714. }
  715. dependencyNode = dependencyNode->next_sibling();
  716. }
  717. m_dependenciesRulesMap[dependencyTokenName] = dependenciesRuleList;
  718. return true;
  719. }
  720. return false;
  721. }
  722. }