PathDependencyManager.cpp 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  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 "PathDependencyManager.h"
  9. #include <AzCore/std/string/wildcard.h>
  10. #include <AzCore/Asset/AssetCommon.h>
  11. #include <AzCore/Jobs/Algorithms.h>
  12. #include <utilities/PlatformConfiguration.h>
  13. #include <utilities/assetUtils.h>
  14. #include <AzFramework/StringFunc/StringFunc.h>
  15. #include <utilities/StatsCapture.h>
  16. namespace AssetProcessor
  17. {
  18. void SanitizeForDatabase(AZStd::string& str)
  19. {
  20. AZStd::to_lower(str.begin(), str.end());
  21. // Not calling normalize because wildcards should be preserved.
  22. if (AZ::StringFunc::Contains(str, AZ_WRONG_DATABASE_SEPARATOR, true))
  23. {
  24. AZStd::replace(str.begin(), str.end(), AZ_WRONG_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR);
  25. AzFramework::StringFunc::Replace(str, AZ_DOUBLE_CORRECT_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR_STRING);
  26. }
  27. }
  28. PathDependencyManager::PathDependencyManager(AZStd::shared_ptr<AssetDatabaseConnection> stateData, PlatformConfiguration* platformConfig)
  29. : m_stateData(stateData), m_platformConfig(platformConfig)
  30. {
  31. }
  32. void PathDependencyManager::QueueSourceForDependencyResolution(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry)
  33. {
  34. m_queuedForResolve.push_back(sourceEntry);
  35. }
  36. void PathDependencyManager::ProcessQueuedDependencyResolves()
  37. {
  38. if (m_queuedForResolve.empty())
  39. {
  40. return;
  41. }
  42. auto queuedForResolve = m_queuedForResolve;
  43. m_queuedForResolve.clear();
  44. // Grab every product from the database and map to Source PK -> [products]
  45. AZStd::unordered_map<AZ::s64, AZStd::vector<AzToolsFramework::AssetDatabase::ProductDatabaseEntry>> productMap;
  46. m_stateData->QueryCombined([&productMap](const AzToolsFramework::AssetDatabase::CombinedDatabaseEntry& entry)
  47. {
  48. productMap[entry.m_sourcePK].push_back(entry);
  49. return true;
  50. });
  51. // Build up a list of all the paths we need to search for: products + 2 variations of the source path
  52. AZStd::vector<SearchEntry> searches;
  53. for (const auto& entry : queuedForResolve)
  54. {
  55. // Search for each product
  56. for (const auto& productEntry : productMap[entry.m_sourceID])
  57. {
  58. const AZStd::string& productName = productEntry.m_productName;
  59. // strip path of the <platform>/
  60. AZStd::string_view result = AssetUtilities::StripAssetPlatformNoCopy(productName);
  61. searches.emplace_back(result, false, &entry, &productEntry);
  62. }
  63. // Search for the source path
  64. AZStd::string sourceNameWithScanFolder =
  65. ToScanFolderPrefixedPath(aznumeric_cast<int>(entry.m_scanFolderPK), entry.m_sourceName.c_str());
  66. AZStd::string sanitizedSourceName = entry.m_sourceName;
  67. SanitizeForDatabase(sourceNameWithScanFolder);
  68. SanitizeForDatabase(sanitizedSourceName);
  69. searches.emplace_back(sourceNameWithScanFolder, true, &entry, nullptr);
  70. searches.emplace_back(sanitizedSourceName, true, &entry, nullptr);
  71. }
  72. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer unresolvedDependencies;
  73. m_stateData->GetUnresolvedProductDependencies(unresolvedDependencies);
  74. AZStd::recursive_mutex mapMutex;
  75. // Map of <Source PK => Map of <Matched SearchEntry => Product Dependency>>
  76. AZStd::unordered_map<AZ::s64, AZStd::unordered_map<const SearchEntry*, AZStd::unordered_set<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>>> sourceIdToMatchedSearchDependencies;
  77. // For every search path we created, we're going to see if it matches up against any of the unresolved dependencies
  78. AZ::parallel_for_each(
  79. searches.begin(), searches.end(),
  80. [&sourceIdToMatchedSearchDependencies, &mapMutex, &unresolvedDependencies](const SearchEntry& search)
  81. {
  82. AZStd::unordered_set<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry> matches;
  83. for (const auto& entry: unresolvedDependencies)
  84. {
  85. AZ::IO::PathView searchPath(search.m_path);
  86. if(((entry.m_dependencyType == AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::ProductDep_SourceFile && search.m_isSourcePath)
  87. || (entry.m_dependencyType == AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::ProductDep_ProductFile && !search.m_isSourcePath))
  88. && searchPath.Match(entry.m_unresolvedPath))
  89. {
  90. matches.insert(entry);
  91. }
  92. }
  93. if (!matches.empty())
  94. {
  95. AZStd::scoped_lock lock(mapMutex);
  96. auto& productDependencyDatabaseEntries = sourceIdToMatchedSearchDependencies[search.m_sourceEntry->m_sourceID][&search];
  97. productDependencyDatabaseEntries.insert(matches.begin(), matches.end());
  98. }
  99. });
  100. for (const auto& entry : queuedForResolve)
  101. {
  102. RetryDeferredDependencies(entry, sourceIdToMatchedSearchDependencies[entry.m_sourceID], productMap[entry.m_sourceID]);
  103. }
  104. }
  105. void PathDependencyManager::SaveUnresolvedDependenciesToDatabase(AssetBuilderSDK::ProductPathDependencySet& unresolvedDependencies,
  106. const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry, const AZStd::string& platform)
  107. {
  108. using namespace AzToolsFramework::AssetDatabase;
  109. ProductDependencyDatabaseEntryContainer dependencyContainer;
  110. for (const auto& unresolvedPathDep : unresolvedDependencies)
  111. {
  112. auto dependencyType = unresolvedPathDep.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::SourceFile ?
  113. ProductDependencyDatabaseEntry::ProductDep_SourceFile :
  114. ProductDependencyDatabaseEntry::ProductDep_ProductFile;
  115. ProductDependencyDatabaseEntry placeholderDependency(
  116. productEntry.m_productID,
  117. AZ::Uuid::CreateNull(),
  118. 0,
  119. AZStd::bitset<64>(),
  120. platform,
  121. 0,
  122. // Use a string that will make it easy to route errors back here correctly. An empty string can be a symptom of many
  123. // other problems. This string says that something went wrong in this function.
  124. AZStd::string("INVALID_PATH"),
  125. dependencyType);
  126. AZStd::string path = AssetUtilities::NormalizeFilePath(unresolvedPathDep.m_dependencyPath.c_str()).toUtf8().constData();
  127. bool isExactDependency = IsExactDependency(path);
  128. if (isExactDependency && unresolvedPathDep.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::SourceFile)
  129. {
  130. QString relativePath, scanFolder;
  131. if (!AzFramework::StringFunc::Path::IsRelative(path.c_str()))
  132. {
  133. if (m_platformConfig->ConvertToRelativePath(QString::fromUtf8(path.c_str()), relativePath, scanFolder))
  134. {
  135. auto* scanFolderInfo = m_platformConfig->GetScanFolderByPath(scanFolder);
  136. path = ToScanFolderPrefixedPath(aznumeric_cast<int>(scanFolderInfo->ScanFolderID()), relativePath.toUtf8().constData());
  137. }
  138. }
  139. }
  140. SanitizeForDatabase(path);
  141. placeholderDependency.m_unresolvedPath = path;
  142. dependencyContainer.push_back(placeholderDependency);
  143. }
  144. if (!m_stateData->UpdateProductDependencies(dependencyContainer))
  145. {
  146. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to save unresolved dependencies to database for product %d (%s)",
  147. productEntry.m_productID, productEntry.m_productName.c_str());
  148. }
  149. }
  150. void PathDependencyManager::SetDependencyResolvedCallback(const DependencyResolvedCallback& callback)
  151. {
  152. m_dependencyResolvedCallback = callback;
  153. }
  154. bool PathDependencyManager::IsExactDependency(AZStd::string_view path)
  155. {
  156. return path.find('*') == AZStd::string_view::npos;
  157. }
  158. void PathDependencyManager::GetMatchedExclusions(
  159. const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry,
  160. const AzToolsFramework::AssetDatabase::ProductDatabaseEntry& productEntry,
  161. AZStd::vector<AZStd::pair<DependencyProductIdInfo, bool>>& excludedDependencies,
  162. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType dependencyType,
  163. const MapSet& exclusionMaps) const
  164. {
  165. bool handleProductDependencies = dependencyType == AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType::ProductDep_ProductFile;
  166. AZStd::string_view assetName = handleProductDependencies ? productEntry.m_productName : sourceEntry.m_sourceName;
  167. const DependencyProductMap& excludedPathDependencyIds = handleProductDependencies ? exclusionMaps.m_productPathDependencyIds : exclusionMaps.m_sourcePathDependencyIds;
  168. const DependencyProductMap& excludedWildcardPathDependencyIds = handleProductDependencies ? exclusionMaps.m_wildcardProductPathDependencyIds : exclusionMaps.m_wildcardSourcePathDependencyIds;
  169. // strip asset platform from path
  170. AZStd::string strippedPath = handleProductDependencies ? AssetUtilities::StripAssetPlatform(assetName).toUtf8().constData() : sourceEntry.m_sourceName;
  171. SanitizeForDatabase(strippedPath);
  172. auto unresolvedIter = excludedPathDependencyIds.find(ExcludedDependenciesSymbol + strippedPath);
  173. if (unresolvedIter != excludedPathDependencyIds.end())
  174. {
  175. for (const auto& dependencyProductIdInfo : unresolvedIter->second)
  176. {
  177. excludedDependencies.emplace_back(dependencyProductIdInfo, true); // true = is exact dependency
  178. }
  179. }
  180. for (const auto& pair : excludedWildcardPathDependencyIds)
  181. {
  182. AZStd::string filter = pair.first.substr(1);
  183. if (wildcard_match(filter, strippedPath))
  184. {
  185. for (const auto& dependencyProductIdInfo : pair.second)
  186. {
  187. excludedDependencies.emplace_back(dependencyProductIdInfo, false); // false = wildcard dependency
  188. }
  189. }
  190. }
  191. }
  192. PathDependencyManager::DependencyProductMap& PathDependencyManager::SelectMap(MapSet& mapSet, bool wildcard, AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType type)
  193. {
  194. const bool isSource = type == AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry::DependencyType::ProductDep_SourceFile;
  195. if (wildcard)
  196. {
  197. if (isSource)
  198. {
  199. return mapSet.m_wildcardSourcePathDependencyIds;
  200. }
  201. return mapSet.m_wildcardProductPathDependencyIds;
  202. }
  203. if (isSource)
  204. {
  205. return mapSet.m_sourcePathDependencyIds;
  206. }
  207. return mapSet.m_productPathDependencyIds;
  208. }
  209. PathDependencyManager::MapSet PathDependencyManager::PopulateExclusionMaps() const
  210. {
  211. using namespace AzToolsFramework::AssetDatabase;
  212. MapSet mapSet;
  213. m_stateData->QueryProductDependencyExclusions([&mapSet](ProductDependencyDatabaseEntry& unresolvedDep)
  214. {
  215. DependencyProductIdInfo idPair;
  216. idPair.m_productDependencyId = unresolvedDep.m_productDependencyID;
  217. idPair.m_productId = unresolvedDep.m_productPK;
  218. idPair.m_platform = unresolvedDep.m_platform;
  219. AZStd::string path = unresolvedDep.m_unresolvedPath;
  220. AZStd::to_lower(path.begin(), path.end());
  221. const bool isExactDependency = IsExactDependency(path);
  222. auto& map = SelectMap(mapSet, !isExactDependency, unresolvedDep.m_dependencyType);
  223. map[path].push_back(AZStd::move(idPair));
  224. return true;
  225. });
  226. return mapSet;
  227. }
  228. void PathDependencyManager::NotifyResolvedDependencies(const AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer& dependencyContainer) const
  229. {
  230. if (!m_dependencyResolvedCallback)
  231. {
  232. return;
  233. }
  234. for (const auto& dependency : dependencyContainer)
  235. {
  236. AzToolsFramework::AssetDatabase::ProductDatabaseEntry productEntry;
  237. if (!m_stateData->GetProductByProductID(dependency.m_productPK, productEntry))
  238. {
  239. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to get existing product with productId %i from the database", dependency.m_productPK);
  240. }
  241. AzToolsFramework::AssetDatabase::SourceDatabaseEntry dependentSource;
  242. if (!m_stateData->GetSourceByJobID(productEntry.m_jobPK, dependentSource))
  243. {
  244. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to get existing product from job ID of product %i from the database", dependency.m_productPK);
  245. }
  246. m_dependencyResolvedCallback(AZ::Data::AssetId(dependentSource.m_sourceGuid, productEntry.m_subID), dependency);
  247. }
  248. }
  249. void PathDependencyManager::SaveResolvedDependencies(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry, const MapSet& exclusionMaps, const AZStd::string& sourceNameWithScanFolder,
  250. const AZStd::unordered_set<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>& dependencyEntries,
  251. AZStd::string_view matchedPath, bool isSourceDependency, const AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer& matchedProducts,
  252. AZStd::vector<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>& dependencyContainer) const
  253. {
  254. for (const auto& productDependencyDatabaseEntry : dependencyEntries)
  255. {
  256. const bool isExactDependency = IsExactDependency(productDependencyDatabaseEntry.m_unresolvedPath);
  257. AZ::s64 dependencyId = isExactDependency ? productDependencyDatabaseEntry.m_productDependencyID : AzToolsFramework::AssetDatabase::InvalidEntryId;
  258. if (isSourceDependency && !isExactDependency && matchedPath == sourceNameWithScanFolder)
  259. {
  260. // Since we did a search for the source 2 different ways, filter one out
  261. // Scanfolder-prefixes are only for exact dependencies
  262. break;
  263. }
  264. for (const auto& matchedProduct : matchedProducts)
  265. {
  266. // Check if this match is excluded before continuing
  267. AZStd::vector<AZStd::pair<DependencyProductIdInfo, bool>> exclusions; // bool = is exact dependency
  268. GetMatchedExclusions(sourceEntry, matchedProduct, exclusions, productDependencyDatabaseEntry.m_dependencyType, exclusionMaps);
  269. if (!exclusions.empty())
  270. {
  271. bool isExclusionForThisProduct = false;
  272. bool isExclusionExact = false;
  273. for (const auto& exclusionPair : exclusions)
  274. {
  275. if (exclusionPair.first.m_productId == productDependencyDatabaseEntry.m_productPK && exclusionPair.first.m_platform == productDependencyDatabaseEntry.m_platform)
  276. {
  277. isExclusionExact = exclusionPair.second;
  278. isExclusionForThisProduct = true;
  279. break;
  280. }
  281. }
  282. if (isExclusionForThisProduct)
  283. {
  284. if (isExactDependency && isExclusionExact)
  285. {
  286. AZ_Error("PathDependencyManager", false, "Dependency exclusion found for an exact dependency. It is not valid to both include and exclude a file by the same rule. File: %s", isSourceDependency ? sourceEntry.m_sourceName.c_str() : matchedProduct.m_productName.c_str());
  287. }
  288. continue;
  289. }
  290. }
  291. // We need to make sure this product is for the same platform the dependency is for
  292. AzToolsFramework::AssetDatabase::JobDatabaseEntry jobEntry;
  293. if (!m_stateData->GetJobByJobID(matchedProduct.m_jobPK, jobEntry))
  294. {
  295. AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to get job entry for product %s", matchedProduct.ToString().c_str());
  296. }
  297. if (jobEntry.m_platform != productDependencyDatabaseEntry.m_platform)
  298. {
  299. continue;
  300. }
  301. // All checks passed, this is a valid dependency we need to save to the db
  302. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry entry;
  303. entry.m_productDependencyID = dependencyId;
  304. entry.m_productPK = productDependencyDatabaseEntry.m_productPK;
  305. entry.m_dependencySourceGuid = sourceEntry.m_sourceGuid;
  306. entry.m_dependencySubID = matchedProduct.m_subID;
  307. entry.m_platform = productDependencyDatabaseEntry.m_platform;
  308. entry.m_dependencyFlags = AZ::Data::ProductDependencyInfo::CreateFlags(AZ::Data::AssetLoadBehavior::NoLoad);
  309. dependencyContainer.push_back(AZStd::move(entry));
  310. // If there's more than 1 product, reset the ID so further products create new db entries
  311. dependencyId = AzToolsFramework::AssetDatabase::InvalidEntryId;
  312. }
  313. }
  314. }
  315. void PathDependencyManager::RetryDeferredDependencies(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry,
  316. const AZStd::unordered_map<const SearchEntry*, AZStd::unordered_set<AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntry>>& matches,
  317. const AZStd::vector<AzToolsFramework::AssetDatabase::ProductDatabaseEntry>& products)
  318. {
  319. MapSet exclusionMaps = PopulateExclusionMaps();
  320. AZStd::string sourceNameWithScanFolder = ToScanFolderPrefixedPath(aznumeric_cast<int>(sourceEntry.m_scanFolderPK), sourceEntry.m_sourceName.c_str());
  321. SanitizeForDatabase(sourceNameWithScanFolder);
  322. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer dependencyVector;
  323. // Go through all the matched dependencies
  324. for (const auto& pair : matches)
  325. {
  326. const SearchEntry* searchEntry = pair.first;
  327. const bool isSourceDependency = searchEntry->m_isSourcePath;
  328. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer matchedProducts;
  329. // Figure out the list of products to work with, for a source match, use all the products, otherwise just use the matched products
  330. if (isSourceDependency)
  331. {
  332. matchedProducts = products;
  333. }
  334. else
  335. {
  336. matchedProducts.push_back(*searchEntry->m_productEntry);
  337. }
  338. // Go through each dependency we're resolving and create a db entry for each product that resolved it (wildcard/source dependencies will generally create more than 1)
  339. SaveResolvedDependencies(
  340. sourceEntry, exclusionMaps, sourceNameWithScanFolder, pair.second, searchEntry->m_path, isSourceDependency, matchedProducts,
  341. dependencyVector);
  342. }
  343. AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer existingDependencies;
  344. if (!m_stateData->GetDirectReverseProductDependenciesBySourceGuidAllPlatforms(sourceEntry.m_sourceGuid, existingDependencies))
  345. {
  346. AZ_Error("PathDependencyManager", false, "Failed to query existing product dependencies for source `%s` (%s)",
  347. sourceEntry.m_sourceName.c_str(),
  348. sourceEntry.m_sourceGuid.ToString<AZStd::string>().c_str());
  349. }
  350. else
  351. {
  352. // Remove any existing dependencies from the list of dependencies we're about to save
  353. dependencyVector.erase(AZStd::remove_if(dependencyVector.begin(), dependencyVector.end(), [&existingDependencies](const auto& entry) -> bool
  354. {
  355. return AZStd::find(existingDependencies.begin(), existingDependencies.end(), entry) != existingDependencies.end();
  356. }), dependencyVector.end());
  357. }
  358. // Save everything to the db, this will update matched non-wildcard dependencies and add new records for wildcard matches
  359. if (!m_stateData->UpdateProductDependencies(dependencyVector))
  360. {
  361. AZ_Error("PathDependencyManager", false, "Failed to update product dependencies");
  362. }
  363. else
  364. {
  365. // Send a notification for each dependency that has been resolved
  366. NotifyResolvedDependencies(dependencyVector);
  367. }
  368. }
  369. void CleanupPathDependency(AssetBuilderSDK::ProductPathDependency& pathDependency)
  370. {
  371. if (pathDependency.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::SourceFile)
  372. {
  373. // Nothing to cleanup if the dependency type was already pointing at source.
  374. return;
  375. }
  376. // Many workflows use source and product extensions for textures interchangeably, assuming that a later system will clean up the path.
  377. // Multiple systems use the AZ Serialization system to reference assets and collect these asset references. Not all of these systems
  378. // check if the references are to source or product asset types.
  379. // Instead requiring each of these systems to handle this (and failing in hard to track down ways later when they don't), check here, and clean things up.
  380. const AZStd::vector<AZStd::string> sourceImageExtensions = { ".tif", ".tiff", ".bmp", ".gif", ".jpg", ".jpeg", ".tga", ".png" };
  381. for (const AZStd::string& sourceImageExtension : sourceImageExtensions)
  382. {
  383. if (AzFramework::StringFunc::Path::IsExtension(pathDependency.m_dependencyPath.c_str(), sourceImageExtension.c_str()))
  384. {
  385. // This was a source format image reported initially as a product file dependency. Fix that to be a source file dependency.
  386. pathDependency.m_dependencyType = AssetBuilderSDK::ProductPathDependencyType::SourceFile;
  387. break;
  388. }
  389. }
  390. }
  391. void PathDependencyManager::ResolveDependencies(AssetBuilderSDK::ProductPathDependencySet& pathDeps, AZStd::vector<AssetBuilderSDK::ProductDependency>& resolvedDeps, const AZStd::string& platform, [[maybe_unused]] const AZStd::string& productName)
  392. {
  393. const AZ::Data::ProductDependencyInfo::ProductDependencyFlags productDependencyFlags =
  394. AZ::Data::ProductDependencyInfo::CreateFlags(AZ::Data::AssetLoadBehavior::NoLoad);
  395. AZStd::vector<AssetBuilderSDK::ProductDependency> excludedDeps;
  396. // Check the path dependency set and find any conflict (include and exclude the same path dependency)
  397. AssetBuilderSDK::ProductPathDependencySet conflicts;
  398. for (const AssetBuilderSDK::ProductPathDependency& pathDep : pathDeps)
  399. {
  400. auto conflictItr = find_if(pathDeps.begin(), pathDeps.end(),
  401. [&pathDep](const AssetBuilderSDK::ProductPathDependency& pathDepForComparison)
  402. {
  403. return (pathDep.m_dependencyPath == ExcludedDependenciesSymbol + pathDepForComparison.m_dependencyPath ||
  404. pathDepForComparison.m_dependencyPath == ExcludedDependenciesSymbol + pathDep.m_dependencyPath) &&
  405. pathDep.m_dependencyType == pathDepForComparison.m_dependencyType;
  406. });
  407. if (conflictItr != pathDeps.end())
  408. {
  409. conflicts.insert(pathDep);
  410. }
  411. }
  412. auto pathIter = pathDeps.begin();
  413. while (pathIter != pathDeps.end())
  414. {
  415. if (conflicts.find(*pathIter) != conflicts.end())
  416. {
  417. // Ignore conflicted path dependencies
  418. AZ_Error(AssetProcessor::DebugChannel, false,
  419. "Cannot resolve path dependency %s for product %s since there's a conflict\n",
  420. pathIter->m_dependencyPath.c_str(), productName.c_str());
  421. ++pathIter;
  422. continue;
  423. }
  424. AssetBuilderSDK::ProductPathDependency cleanedupDependency(*pathIter);
  425. CleanupPathDependency(cleanedupDependency);
  426. AZStd::string dependencyPathSearch = cleanedupDependency.m_dependencyPath;
  427. bool isExcludedDependency = dependencyPathSearch.starts_with(ExcludedDependenciesSymbol);
  428. dependencyPathSearch = isExcludedDependency ? dependencyPathSearch.substr(1) : dependencyPathSearch;
  429. // The database uses % for wildcards, both path based searching uses *, so keep a copy of the path with the * wildcard for later
  430. // use.
  431. AZStd::string pathWildcardSearchPath(dependencyPathSearch);
  432. bool isExactDependency = !AzFramework::StringFunc::Replace(dependencyPathSearch, '*', '%');
  433. if (cleanedupDependency.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::ProductFile)
  434. {
  435. SanitizeForDatabase(dependencyPathSearch);
  436. SanitizeForDatabase(pathWildcardSearchPath);
  437. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer productInfoContainer;
  438. QString productNameWithPlatform = QString("%1%2%3").arg(platform.c_str(), AZ_CORRECT_DATABASE_SEPARATOR_STRING, dependencyPathSearch.c_str());
  439. if (AzFramework::StringFunc::Equal(productNameWithPlatform.toUtf8().data(), productName.c_str()))
  440. {
  441. AZ_Warning(AssetProcessor::ConsoleChannel, false,
  442. "Invalid dependency: Product Asset ( %s ) has listed itself as one of its own Product Dependencies.",
  443. productName.c_str());
  444. pathIter = pathDeps.erase(pathIter);
  445. continue;
  446. }
  447. if (isExactDependency)
  448. {
  449. // Search for products in the cache platform folder
  450. // Example: If a path dependency is "test1.asset" in AutomatedTesting on PC, this would search
  451. // "AutomatedTesting/Cache/pc/test1.asset"
  452. m_stateData->GetProductsByProductName(productNameWithPlatform, productInfoContainer);
  453. }
  454. else
  455. {
  456. m_stateData->GetProductsLikeProductName(productNameWithPlatform, AzToolsFramework::AssetDatabase::AssetDatabaseConnection::LikeType::Raw, productInfoContainer);
  457. }
  458. // See if path matches any product files
  459. if (!productInfoContainer.empty())
  460. {
  461. AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceDatabaseEntry;
  462. for (const auto& productDatabaseEntry : productInfoContainer)
  463. {
  464. if (m_stateData->GetSourceByJobID(productDatabaseEntry.m_jobPK, sourceDatabaseEntry))
  465. {
  466. // The SQL wildcard search is greedy and doesn't match the path based, glob style wildcard search that is
  467. // expected in this case. This also matches the behavior of resolving unmet dependencies later. There are two
  468. // cases that wildcard dependencies resolve:
  469. // 1. When the product with the wildcard dependency is first created, it resolves those dependencies against
  470. // what's already in the database. That's this case.
  471. // 2. When another product is created, all existing wildcard dependencies are compared against that product to
  472. // see if it matches them.
  473. // This check here makes sure that the filter for 1 matches 2.
  474. if (!isExactDependency)
  475. {
  476. AZ::IO::PathView searchPath(productDatabaseEntry.m_productName);
  477. if (!searchPath.Match(pathWildcardSearchPath))
  478. {
  479. continue;
  480. }
  481. }
  482. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencyList = isExcludedDependency ? excludedDeps : resolvedDeps;
  483. productDependencyList.emplace_back(AZ::Data::AssetId(sourceDatabaseEntry.m_sourceGuid, productDatabaseEntry.m_subID), productDependencyFlags);
  484. }
  485. else
  486. {
  487. AZ_Error(AssetProcessor::ConsoleChannel, false, "Source for JobID %i not found (from product %s)", productDatabaseEntry.m_jobPK, dependencyPathSearch.c_str());
  488. }
  489. // For exact dependencies we expect that there is only 1 match. Even if we processed more than 1, the results could be inconsistent since the other assets may not be finished processing yet
  490. if (isExactDependency)
  491. {
  492. break;
  493. }
  494. }
  495. // Wildcard and excluded dependencies never get removed since they can be fulfilled by a future product
  496. if (isExactDependency && !isExcludedDependency)
  497. {
  498. pathIter = pathDeps.erase(pathIter);
  499. continue;
  500. }
  501. }
  502. }
  503. else
  504. {
  505. // For source assets, the casing of the input path must be maintained. Just fix up the path separators.
  506. AZStd::replace(dependencyPathSearch.begin(), dependencyPathSearch.end(), AZ_WRONG_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR);
  507. AzFramework::StringFunc::Replace(dependencyPathSearch, AZ_DOUBLE_CORRECT_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR_STRING);
  508. // See if path matches any source files
  509. AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sourceInfoContainer;
  510. if (isExactDependency)
  511. {
  512. QString databaseName;
  513. QString scanFolder;
  514. if (ProcessInputPathToDatabasePathAndScanFolder(dependencyPathSearch.c_str(), databaseName, scanFolder))
  515. {
  516. AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
  517. if(m_stateData->GetSourceBySourceNameScanFolderId(databaseName, m_platformConfig->GetScanFolderByPath(scanFolder)->ScanFolderID(), source))
  518. {
  519. sourceInfoContainer.push_back(AZStd::move(source));
  520. }
  521. }
  522. }
  523. else
  524. {
  525. m_stateData->GetSourcesLikeSourceName(dependencyPathSearch.c_str(), AzToolsFramework::AssetDatabase::AssetDatabaseConnection::LikeType::Raw, sourceInfoContainer);
  526. }
  527. if (!sourceInfoContainer.empty())
  528. {
  529. bool productsAvailable = false;
  530. for (const auto& sourceDatabaseEntry : sourceInfoContainer)
  531. {
  532. AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer productInfoContainer;
  533. if (m_stateData->GetProductsBySourceID(sourceDatabaseEntry.m_sourceID, productInfoContainer, AZ::Uuid::CreateNull(), "", platform.c_str()))
  534. {
  535. productsAvailable = true;
  536. // Add a dependency on every product of this source file
  537. for (const auto& productDatabaseEntry : productInfoContainer)
  538. {
  539. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencyList = isExcludedDependency ? excludedDeps : resolvedDeps;
  540. productDependencyList.emplace_back(AZ::Data::AssetId(sourceDatabaseEntry.m_sourceGuid, productDatabaseEntry.m_subID), productDependencyFlags);
  541. }
  542. }
  543. // For exact dependencies we expect that there is only 1 match. Even if we processed more than 1, the results could be inconsistent since the other assets may not be finished processing yet
  544. if (isExactDependency)
  545. {
  546. break;
  547. }
  548. }
  549. if (isExactDependency && productsAvailable && !isExcludedDependency)
  550. {
  551. pathIter = pathDeps.erase(pathIter);
  552. continue;
  553. }
  554. }
  555. }
  556. pathIter->m_dependencyPath = cleanedupDependency.m_dependencyPath;
  557. pathIter->m_dependencyType = cleanedupDependency.m_dependencyType;
  558. ++pathIter;
  559. }
  560. // Remove the excluded dependency from the resolved dependency list and leave them unresolved
  561. resolvedDeps.erase(AZStd::remove_if(resolvedDeps.begin(), resolvedDeps.end(),
  562. [&excludedDeps](const AssetBuilderSDK::ProductDependency& resolvedDependency)
  563. {
  564. auto excludedDependencyItr = AZStd::find_if(excludedDeps.begin(), excludedDeps.end(),
  565. [&resolvedDependency](const AssetBuilderSDK::ProductDependency& excludedDependency)
  566. {
  567. return resolvedDependency.m_dependencyId == excludedDependency.m_dependencyId &&
  568. resolvedDependency.m_flags == excludedDependency.m_flags;
  569. });
  570. return excludedDependencyItr != excludedDeps.end();
  571. }), resolvedDeps.end());
  572. }
  573. bool PathDependencyManager::ProcessInputPathToDatabasePathAndScanFolder(const char* dependencyPathSearch, QString& databaseName, QString& scanFolder) const
  574. {
  575. if (!AzFramework::StringFunc::Path::IsRelative(dependencyPathSearch))
  576. {
  577. // absolute paths just get converted directly
  578. return m_platformConfig->ConvertToRelativePath(QString::fromUtf8(dependencyPathSearch), databaseName, scanFolder);
  579. }
  580. else
  581. {
  582. // relative paths get the first matching asset, and then they get the usual call.
  583. QString absolutePath = m_platformConfig->FindFirstMatchingFile(QString::fromUtf8(dependencyPathSearch));
  584. if (!absolutePath.isEmpty())
  585. {
  586. return m_platformConfig->ConvertToRelativePath(absolutePath, databaseName, scanFolder);
  587. }
  588. }
  589. return false;
  590. }
  591. AZStd::string PathDependencyManager::ToScanFolderPrefixedPath(int scanFolderId, const char* relativePath) const
  592. {
  593. static constexpr char ScanFolderSeparator = '$';
  594. return AZStd::string::format("%c%d%c%s", ScanFolderSeparator, scanFolderId, ScanFolderSeparator, relativePath);
  595. }
  596. } // namespace AssetProcessor