2
0

XmlBuilderWorker.cpp 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  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 "XmlBuilderWorker.h"
  9. #include <AssetBuilderSDK/SerializationDependencies.h>
  10. #include <AzCore/Component/ComponentApplication.h>
  11. #include <AzCore/IO/SystemFile.h>
  12. #include <AzCore/std/string/wildcard.h>
  13. #include <AzCore/Dependency/Dependency.h>
  14. #include <AzFramework/FileFunc/FileFunc.h>
  15. #include <AzFramework/IO/LocalFileIO.h>
  16. #include <AzFramework/StringFunc/StringFunc.h>
  17. #include "Builders/CopyDependencyBuilder/SchemaBuilderWorker/SchemaUtils.h"
  18. namespace CopyDependencyBuilder
  19. {
  20. namespace Internal
  21. {
  22. bool AddFileExtention(const AZStd::string expectedExtension, AZStd::string& fileName, bool isOptional)
  23. {
  24. if (!AzFramework::StringFunc::Path::HasExtension(fileName.c_str()))
  25. {
  26. // Open 3D Engine makes use of some files without extensions, only replace the extension if there is an expected extension.
  27. if (!expectedExtension.empty())
  28. {
  29. AzFramework::StringFunc::Path::ReplaceExtension(fileName, expectedExtension.c_str());
  30. }
  31. }
  32. else if (!expectedExtension.empty())
  33. {
  34. AZStd::string existingExtension;
  35. AzFramework::StringFunc::Path::GetExtension(fileName.c_str(), existingExtension, false);
  36. if (expectedExtension.front() == '.')
  37. {
  38. existingExtension = '.' + existingExtension;
  39. }
  40. if (existingExtension != expectedExtension)
  41. {
  42. if (!isOptional)
  43. {
  44. AZ_Warning("XmlBuilderWorker", false, "Dependency %s already has an extension %s and the expected extension %s is different."
  45. "The original extension is not replaced.", fileName.c_str(), existingExtension.c_str(), expectedExtension.c_str());
  46. }
  47. return false;
  48. }
  49. }
  50. return true;
  51. }
  52. AZ::Data::AssetId TextToAssetId(const char* text)
  53. {
  54. // Parse the asset id
  55. // Asset data could look like "id={00000000-0000-0000-0000-000000000000}:0,type={00000000-0000-0000-0000-000000000000},hint={asset_path}"
  56. // SubId is 16 based according to AssetSerializer
  57. AZ::Data::AssetId assetId;
  58. if (!text)
  59. {
  60. AZ_Error("XmlBuilderWorker", false, "Null string given to TextToAssetId");
  61. return assetId;
  62. }
  63. const char* idGuidStart = strchr(text, '{');
  64. if (!idGuidStart)
  65. {
  66. AZ_Error("XmlBuilderWorker", false, "Invalid asset guid data! %s", text);
  67. return assetId;
  68. }
  69. const char* idGuidEnd = strchr(idGuidStart, ':');
  70. AZ_Error("XmlBuilderWorker", idGuidEnd, "Invalid asset guid data! %s", idGuidStart);
  71. const char* idSubIdStart = idGuidEnd + 1;
  72. assetId.m_guid = AZ::Uuid::CreateString(idGuidStart, idGuidEnd - idGuidStart);
  73. assetId.m_subId = static_cast<AZ::u32>(strtoul(idSubIdStart, nullptr, 16));
  74. return assetId;
  75. }
  76. bool ParseAttributeNode(
  77. const AzFramework::XmlSchemaAttribute& schemaAttribute,
  78. const AZ::rapidxml::xml_node<char>* xmlFileNode,
  79. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies,
  80. AssetBuilderSDK::ProductPathDependencySet& pathDependencies,
  81. const AZStd::string& sourceAssetFolder,
  82. const AZStd::string& nodeContent = "",
  83. const AZStd::string& watchFolder = "")
  84. {
  85. // Attribute nodes of the XML schema specify the attributes which are used to store product dependency info in the actual XML nodes
  86. AZStd::string schemaAttributeName = schemaAttribute.GetName();
  87. // The attribute name could be empty only if the XML element content specifies the product dependency
  88. // e.g. <entry>example.dds</entry>
  89. if (schemaAttributeName.empty() && nodeContent.empty())
  90. {
  91. return schemaAttribute.IsOptional();
  92. }
  93. AZ::rapidxml::xml_attribute<char>* xmlNodeNameAttr = xmlFileNode->first_attribute(schemaAttributeName.c_str(), 0, false);
  94. // Return early if the attribute specified in the schema doesn't exist
  95. if (!schemaAttributeName.empty() && !xmlNodeNameAttr)
  96. {
  97. return schemaAttribute.IsOptional();
  98. }
  99. AZStd::string dependencyValue = !schemaAttributeName.empty() ? xmlNodeNameAttr->value() : nodeContent;
  100. switch (schemaAttribute.GetType())
  101. {
  102. case AzFramework::XmlSchemaAttribute::AttributeType::RelativePath:
  103. {
  104. AssetBuilderSDK::ProductPathDependencyType productPathDependencyType =
  105. static_cast<AZ::u32>(schemaAttribute.GetPathDependencyType()) == static_cast<AZ::u32>(AssetBuilderSDK::ProductPathDependencyType::SourceFile)
  106. ? AssetBuilderSDK::ProductPathDependencyType::SourceFile
  107. : AssetBuilderSDK::ProductPathDependencyType::ProductFile;
  108. if (dependencyValue.empty())
  109. {
  110. return true;
  111. }
  112. // Reject values that don't pass the match pattern
  113. if(!schemaAttribute.GetMatchPattern().empty())
  114. {
  115. AZStd::regex match(schemaAttribute.GetMatchPattern(), AZStd::regex_constants::ECMAScript | AZStd::regex_constants::icase);
  116. if(!AZStd::regex_search(dependencyValue, match))
  117. {
  118. return true;
  119. }
  120. }
  121. if(!schemaAttribute.GetFindPattern().empty())
  122. {
  123. AZStd::regex find(schemaAttribute.GetFindPattern(), AZStd::regex_constants::ECMAScript | AZStd::regex_constants::icase);
  124. dependencyValue = AZStd::regex_replace(dependencyValue, find, schemaAttribute.GetReplacePattern());
  125. }
  126. if (AddFileExtention(schemaAttribute.GetExpectedExtension(), dependencyValue, schemaAttribute.IsOptional()))
  127. {
  128. if (schemaAttribute.IsRelativeToSourceAssetFolder())
  129. {
  130. AzFramework::StringFunc::AssetDatabasePath::Join(sourceAssetFolder.c_str(), dependencyValue.c_str(), dependencyValue);
  131. }
  132. else if (schemaAttribute.CacheRelativePath())
  133. {
  134. AZStd::string_view depFolder{ sourceAssetFolder.c_str() };
  135. if (watchFolder.length() && sourceAssetFolder.starts_with(watchFolder))
  136. {
  137. depFolder = &sourceAssetFolder[watchFolder.length()];
  138. if (depFolder.front() == '/')
  139. {
  140. depFolder = &sourceAssetFolder[watchFolder.length() + 1];
  141. }
  142. }
  143. AzFramework::StringFunc::AssetDatabasePath::Join(depFolder.data(), dependencyValue.c_str(), dependencyValue);
  144. }
  145. pathDependencies.emplace(dependencyValue, productPathDependencyType);
  146. }
  147. break;
  148. }
  149. case AzFramework::XmlSchemaAttribute::AttributeType::Asset:
  150. {
  151. productDependencies.emplace_back(AssetBuilderSDK::ProductDependency(TextToAssetId(dependencyValue.c_str()), {}));
  152. break;
  153. }
  154. default:
  155. AZ_Error("XmlBuilderWorker", false, "Unsupported schema attribute type. Choose from RelativePath and Asset.");
  156. break;
  157. }
  158. return true;
  159. }
  160. bool ParseElementNode(
  161. const AzFramework::XmlSchemaElement& xmlSchemaElement,
  162. const AZ::rapidxml::xml_node<char>* xmlFileNode,
  163. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies,
  164. AssetBuilderSDK::ProductPathDependencySet& pathDependencies,
  165. const AZStd::string& sourceAssetFolder,
  166. const AZStd::string& watchFolderPath)
  167. {
  168. // Check whether the XML node matches the schema
  169. AZStd::string schemaElementName = xmlSchemaElement.GetName();
  170. if (strcmp(schemaElementName.c_str(), xmlFileNode->name()) != 0 && schemaElementName != "*")
  171. {
  172. return false;
  173. }
  174. AZStd::vector<AssetBuilderSDK::ProductDependency> localProductDependencies;
  175. AssetBuilderSDK::ProductPathDependencySet localPathDependencies;
  176. // Continue parsing the source XML using the child element and attribute nodes
  177. for (const AzFramework::XmlSchemaElement& childSchemaElement : xmlSchemaElement.GetChildElements())
  178. {
  179. bool findMatchedChildNode = false;
  180. for (AZ::rapidxml::xml_node<char>* xmlFileChildNode = xmlFileNode->first_node(); xmlFileChildNode; xmlFileChildNode = xmlFileChildNode->next_sibling())
  181. {
  182. findMatchedChildNode = ParseElementNode(childSchemaElement, xmlFileChildNode, localProductDependencies, localPathDependencies, sourceAssetFolder, watchFolderPath) || findMatchedChildNode;
  183. }
  184. if (!findMatchedChildNode && !childSchemaElement.IsOptional())
  185. {
  186. return false;
  187. }
  188. }
  189. for (const AzFramework::XmlSchemaAttribute& schemaAttribute : xmlSchemaElement.GetAttributes())
  190. {
  191. AZStd::string xmlNodeValue = schemaAttribute.GetName().empty() ? xmlFileNode->value() : "";
  192. if (!ParseAttributeNode(schemaAttribute, xmlFileNode, localProductDependencies, localPathDependencies, sourceAssetFolder, xmlNodeValue, watchFolderPath))
  193. {
  194. return false;
  195. }
  196. }
  197. // Only merge the dependencies if the attributes parsed cleanly. If a required dependency was missing, then don't add anything that was found.
  198. productDependencies.insert(productDependencies.end(), localProductDependencies.begin(), localProductDependencies.end());
  199. pathDependencies.insert(localPathDependencies.begin(), localPathDependencies.end());
  200. return true;
  201. }
  202. AZ::Outcome <AZ::rapidxml::xml_node<char>*, AZStd::string> GetSourceFileRootNode(
  203. const AZStd::string& filePath,
  204. AZStd::vector<char>& charBuffer,
  205. AZ::rapidxml::xml_document<char>& xmlDoc)
  206. {
  207. AZ::IO::FileIOStream fileStream;
  208. if (!fileStream.Open(filePath.c_str(), AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary))
  209. {
  210. return AZ::Failure(AZStd::string::format("Failed to open source file %s.", filePath.c_str()));
  211. }
  212. AZ::IO::SizeType length = fileStream.GetLength();
  213. if (length == 0)
  214. {
  215. return AZ::Failure(AZStd::string("Failed to get the file stream length."));
  216. }
  217. charBuffer.resize_no_construct(length + 1);
  218. fileStream.Read(length, charBuffer.data());
  219. charBuffer.back() = 0;
  220. if (!xmlDoc.parse<AZ::rapidxml::parse_default>(charBuffer.data()))
  221. {
  222. return AZ::Failure(AZStd::string::format("Failed to parse the source file %s.", filePath.c_str()));
  223. }
  224. AZ::rapidxml::xml_node<char>* xmlRootNode = xmlDoc.first_node();
  225. if (!xmlRootNode)
  226. {
  227. return AZ::Failure(AZStd::string::format("Failed to get the root node of the source file %s.", filePath.c_str()));
  228. }
  229. return AZ::Success(AZStd::move(xmlRootNode));
  230. }
  231. AZStd::string GetSourceFileVersion(
  232. const AZ::rapidxml::xml_node<char>* xmlFileRootNode,
  233. const AZStd::string& rootNodeAttributeName)
  234. {
  235. if (!rootNodeAttributeName.empty())
  236. {
  237. AZ::rapidxml::xml_attribute<char>* xmlNodeNameAttr = xmlFileRootNode->first_attribute(rootNodeAttributeName.c_str(), 0, false);
  238. if (xmlNodeNameAttr)
  239. {
  240. return xmlNodeNameAttr->value();
  241. }
  242. }
  243. AZStd::string result = "0";
  244. for (size_t count = 1; count < MaxVersionPartsCount; ++count)
  245. {
  246. result += ".0";
  247. }
  248. return result;
  249. }
  250. bool MatchesVersionConstraints(const AZ::Version<MaxVersionPartsCount>& version, const AZStd::vector<AZStd::string>& versionConstraints)
  251. {
  252. if (versionConstraints.size() == 0)
  253. {
  254. return true;
  255. }
  256. AZ::Dependency<MaxVersionPartsCount> dependency;
  257. AZ::Outcome<void, AZStd::string> parseOutcome = dependency.ParseVersions(versionConstraints);
  258. if (!parseOutcome.IsSuccess())
  259. {
  260. AZ_Error("XmlBuilderWorker", false, parseOutcome.TakeError().c_str());
  261. return false;
  262. }
  263. return dependency.IsFullfilledBy(AZ::Specifier<MaxVersionPartsCount>(AZ::Uuid::CreateNull(), version));
  264. }
  265. AZ::Outcome<AZ::Version<MaxVersionPartsCount>, AZStd::string> ParseFromString(const AZStd::string& versionStr)
  266. {
  267. AZ::Version<MaxVersionPartsCount> result;
  268. AZStd::vector<AZStd::string> versionParts;
  269. AzFramework::StringFunc::Tokenize(versionStr.c_str(), versionParts, VERSION_SEPARATOR_CHAR);
  270. size_t versionPartsCount = versionParts.size();
  271. if (versionPartsCount > MaxVersionPartsCount)
  272. {
  273. return AZ::Failure(AZStd::string::format(
  274. "Failed to parse invalid version string \"%s\". "
  275. "Only version number with at most %zu parts is supported. "
  276. , versionStr.c_str(), MaxVersionPartsCount));
  277. }
  278. for (int index = 0; index < MaxVersionPartsCount; ++index)
  279. {
  280. if (index >= versionPartsCount)
  281. {
  282. result.m_parts[index] = 0;
  283. }
  284. else
  285. {
  286. AZStd::string versionPart = versionParts[index];
  287. if (!AZStd::all_of(versionPart.begin(), versionPart.end(), isdigit))
  288. {
  289. return AZ::Failure(AZStd::string::format(
  290. "Failed to parse invalid version string \"%s\". "
  291. "Unexpected separator character encountered. "
  292. "Expected: \"%d\""
  293. , versionStr.c_str(), VERSION_SEPARATOR_CHAR));
  294. }
  295. result.m_parts[index] = AZStd::stoi(versionParts[index]);
  296. }
  297. }
  298. return AZ::Success(result);
  299. }
  300. }
  301. // m_skipServer (3rd Param) should be false - we want to process xml files on the server as it's a generic data format which could
  302. // have meaningful data for a server
  303. XmlBuilderWorker::XmlBuilderWorker()
  304. : CopyDependencyBuilderWorker("xml", true, false)
  305. {
  306. }
  307. void XmlBuilderWorker::RegisterBuilderWorker()
  308. {
  309. AssetBuilderSDK::AssetBuilderDesc xmlSchemaBuilderDescriptor;
  310. xmlSchemaBuilderDescriptor.m_name = "XmlBuilderWorker";
  311. xmlSchemaBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("(?!.*libs\\/gameaudio\\/).*\\.xml", AssetBuilderSDK::AssetBuilderPattern::PatternType::Regex));
  312. xmlSchemaBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.vegdescriptorlist", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
  313. xmlSchemaBuilderDescriptor.m_busId = azrtti_typeid<XmlBuilderWorker>();
  314. xmlSchemaBuilderDescriptor.m_version = 9;
  315. xmlSchemaBuilderDescriptor.m_createJobFunction =
  316. AZStd::bind(&XmlBuilderWorker::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
  317. xmlSchemaBuilderDescriptor.m_processJobFunction =
  318. AZStd::bind(&XmlBuilderWorker::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
  319. BusConnect(xmlSchemaBuilderDescriptor.m_busId);
  320. AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBusTraits::RegisterBuilderInformation, xmlSchemaBuilderDescriptor);
  321. bool success;
  322. AZStd::vector<AZStd::string> assetSafeFolders;
  323. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(success, &AzToolsFramework::AssetSystemRequestBus::Events::GetAssetSafeFolders, assetSafeFolders);
  324. for (const AZStd::string& assetSafeFolder : assetSafeFolders)
  325. {
  326. AZStd::string schemaFolder = "Schema";
  327. AzFramework::StringFunc::AssetDatabasePath::Join(assetSafeFolder.c_str(), schemaFolder.c_str(), schemaFolder);
  328. AddSchemaFileDirectory(schemaFolder);
  329. }
  330. }
  331. void XmlBuilderWorker::AddSchemaFileDirectory(const AZStd::string& schemaFileDirectory)
  332. {
  333. if (!AZ::IO::FileIOBase::GetInstance()->Exists(schemaFileDirectory.c_str()))
  334. {
  335. return;
  336. }
  337. char resolvedPath[AZ_MAX_PATH_LEN];
  338. AZ::IO::FileIOBase::GetInstance()->ResolvePath(schemaFileDirectory.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
  339. m_schemaFileDirectories.emplace_back(resolvedPath);
  340. }
  341. void XmlBuilderWorker::UnregisterBuilderWorker()
  342. {
  343. BusDisconnect();
  344. }
  345. AZ::Outcome<AZStd::vector<AssetBuilderSDK::SourceFileDependency>, AZStd::string> XmlBuilderWorker::GetSourceDependencies(
  346. const AssetBuilderSDK::CreateJobsRequest& request) const
  347. {
  348. AZStd::vector<AssetBuilderSDK::SourceFileDependency> sourceDependencies;
  349. // Iterate through each schema file and check whether the source XML matches its file path pattern
  350. for (const AZStd::string& schemaFileDirectory : m_schemaFileDirectories)
  351. {
  352. AZ::Outcome<AZStd::list<AZStd::string>, AZStd::string> findFilesResult = AzFramework::FileFunc::FindFilesInPath(schemaFileDirectory, SchemaNamePattern, true);
  353. if (!findFilesResult.IsSuccess())
  354. {
  355. return AZ::Failure(AZStd::string::format("Failed to find schema files in directory %s.", schemaFileDirectory.c_str()));
  356. }
  357. for (const AZStd::string& schemaPath : findFilesResult.GetValue())
  358. {
  359. AzFramework::XmlSchemaAsset schemaAsset;
  360. AZ::ObjectStream::FilterDescriptor loadFilter = AZ::ObjectStream::FilterDescriptor(&AZ::Data::AssetFilterNoAssetLoading, AZ::ObjectStream::FILTERFLAG_IGNORE_UNKNOWN_CLASSES);
  361. if (!AZ::Utils::LoadObjectFromFileInPlace(schemaPath, schemaAsset, nullptr, loadFilter))
  362. {
  363. return AZ::Failure(AZStd::string::format("Failed to load schema file: %s.", schemaPath.c_str()));
  364. }
  365. AZStd::string fullPath;
  366. AzFramework::StringFunc::AssetDatabasePath::Join(request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), fullPath);
  367. if (SourceFileDependsOnSchema(schemaAsset, fullPath.c_str()))
  368. {
  369. AssetBuilderSDK::SourceFileDependency sourceFileDependency;
  370. sourceFileDependency.m_sourceFileDependencyPath = schemaPath;
  371. sourceDependencies.emplace_back(sourceFileDependency);
  372. }
  373. }
  374. }
  375. return AZ::Success(sourceDependencies);
  376. }
  377. bool XmlBuilderWorker::ParseProductDependencies(
  378. const AssetBuilderSDK::ProcessJobRequest& request,
  379. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies,
  380. AssetBuilderSDK::ProductPathDependencySet& pathDependencies)
  381. {
  382. AZStd::vector<AZStd::string> matchedSchemas;
  383. // We've already iterate through all the schemas and found source dependencies in CreateJobs
  384. // Retrieve the matched schemas from the job parameters in ProcessJob to avoid redundant work
  385. const auto& paramMap = request.m_jobDescription.m_jobParameters;
  386. auto startIter = paramMap.find(AZ_CRC("sourceDependencyStartPoint", 0xdfa24dde));
  387. auto sourceNumIter = paramMap.find(AZ_CRC("sourceDependenciesNum", 0xf52e721a));
  388. if (startIter != paramMap.end() && sourceNumIter != paramMap.end())
  389. {
  390. int startPoint = AzFramework::StringFunc::ToInt(startIter->second.c_str());
  391. int sourceDependenciesNum = AzFramework::StringFunc::ToInt(sourceNumIter->second.c_str());
  392. for (int index = 0; index < sourceDependenciesNum; ++index)
  393. {
  394. auto thisSchemaIter = paramMap.find(startPoint + index);
  395. if (thisSchemaIter != paramMap.end())
  396. {
  397. matchedSchemas.emplace_back(thisSchemaIter->second);
  398. }
  399. }
  400. }
  401. // If a schema is found or not found, the result is valid. Return false if there was an error.
  402. return MatchExistingSchema(request.m_fullPath, matchedSchemas, productDependencies, pathDependencies, request.m_watchFolder) != SchemaMatchResult::Error;
  403. }
  404. XmlBuilderWorker::SchemaMatchResult XmlBuilderWorker::MatchLastUsedSchema(
  405. [[maybe_unused]] const AZStd::string& sourceFilePath,
  406. [[maybe_unused]] AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies,
  407. [[maybe_unused]] AssetBuilderSDK::ProductPathDependencySet& pathDependencies,
  408. [[maybe_unused]] const AZStd::string& watchFolderPath) const
  409. {
  410. // Check the existing schema info stored in the asset database
  411. // LY-99056
  412. return SchemaMatchResult::NoMatchFound;
  413. }
  414. XmlBuilderWorker::SchemaMatchResult XmlBuilderWorker::MatchExistingSchema(
  415. const AZStd::string& sourceFilePath,
  416. AZStd::vector<AZStd::string>& sourceDependencyPaths,
  417. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies,
  418. AssetBuilderSDK::ProductPathDependencySet& pathDependencies,
  419. const AZStd::string& watchFolderPath) const
  420. {
  421. if (m_printDebug)
  422. {
  423. printf("Searching %zu source dependency paths\n", sourceDependencyPaths.size());
  424. }
  425. if (sourceDependencyPaths.empty())
  426. {
  427. // Iterate through all the schema files if no source dependencies are detected in CreateJobs
  428. for (const AZStd::string& schemaFileDirectory : m_schemaFileDirectories)
  429. {
  430. if (m_printDebug)
  431. {
  432. printf("Finding files in %s\n", schemaFileDirectory.c_str());
  433. }
  434. AZ::Outcome<AZStd::list<AZStd::string>, AZStd::string> searchResult = AzFramework::FileFunc::FindFilesInPath(schemaFileDirectory, SchemaNamePattern, true);
  435. if (searchResult.IsSuccess())
  436. {
  437. AZStd::list<AZStd::string> newSchemaFiles = searchResult.GetValue();
  438. if (m_printDebug)
  439. {
  440. printf("Found %zu files\n", newSchemaFiles.size());
  441. }
  442. for (const AZStd::string& newSchemaFile : newSchemaFiles)
  443. {
  444. if (m_printDebug)
  445. {
  446. printf("Adding %s\n", newSchemaFile.c_str());
  447. }
  448. sourceDependencyPaths.emplace_back(newSchemaFile);
  449. }
  450. }
  451. }
  452. }
  453. for (const AZStd::string& schemaFilePath : sourceDependencyPaths)
  454. {
  455. SchemaMatchResult matchResult = ParseXmlFile(schemaFilePath, sourceFilePath, productDependencies, pathDependencies, watchFolderPath);
  456. if (m_printDebug)
  457. {
  458. printf("Match on %s returns %d\n", schemaFilePath.c_str(), (int)matchResult);
  459. }
  460. switch (matchResult)
  461. {
  462. case SchemaMatchResult::MatchFound:
  463. // Update the LastUsedSchema info stored in the asset database
  464. // LY-99056
  465. AZ_Printf("XmlBuilderWorker", "Schema file %s found for source %s.", schemaFilePath.c_str(), sourceFilePath.c_str());
  466. return matchResult;
  467. case SchemaMatchResult::NoMatchFound:
  468. // Continue searching through schemas if this one didn't match.
  469. break;
  470. case SchemaMatchResult::Error:
  471. default:
  472. return matchResult;
  473. }
  474. }
  475. return SchemaMatchResult::NoMatchFound;
  476. }
  477. XmlBuilderWorker::SchemaMatchResult XmlBuilderWorker::ParseXmlFile(
  478. const AZStd::string& schemaFilePath,
  479. const AZStd::string& sourceFilePath,
  480. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies,
  481. AssetBuilderSDK::ProductPathDependencySet& pathDependencies,
  482. const AZStd::string& watchFolderPath) const
  483. {
  484. if (schemaFilePath.empty())
  485. {
  486. return SchemaMatchResult::NoMatchFound;
  487. }
  488. AzFramework::XmlSchemaAsset schemaAsset;
  489. AZ::ObjectStream::FilterDescriptor loadFilter = AZ::ObjectStream::FilterDescriptor(&AZ::Data::AssetFilterNoAssetLoading, AZ::ObjectStream::FILTERFLAG_IGNORE_UNKNOWN_CLASSES);
  490. if (!AZ::Utils::LoadObjectFromFileInPlace(schemaFilePath, schemaAsset, nullptr, loadFilter))
  491. {
  492. AZ_Error("XmlBuilderWorker", false, "Failed to load schema file: %s.", schemaFilePath.c_str());
  493. // This isn't a blocking error, the error was on this schema, so try checking the next schema for a match.
  494. return SchemaMatchResult::NoMatchFound;
  495. }
  496. // Get the source file root node and version info
  497. AZStd::vector<char> xmlFileBuffer;
  498. AZ::rapidxml::xml_document<char> xmlFileDoc;
  499. AZ::Outcome <AZ::rapidxml::xml_node<char>*, AZStd::string> rootNodeOutcome = Internal::GetSourceFileRootNode(sourceFilePath, xmlFileBuffer, xmlFileDoc);
  500. if (!rootNodeOutcome.IsSuccess())
  501. {
  502. AZ_Error("XmlBuilderWorker", false, rootNodeOutcome.TakeError().c_str());
  503. // The XML file couldn't be loaded.
  504. // We can't know whether this is intentionally an empty file any more than if it were an empty xml with a root node that that were incorrect
  505. // So we leave it as "nothing will match this" and emit the above error
  506. return SchemaMatchResult::NoMatchFound;
  507. }
  508. AZ::rapidxml::xml_node<char>* xmlFileRootNode = rootNodeOutcome.GetValue();
  509. AZStd::string sourceFileVersionStr = Internal::GetSourceFileVersion(xmlFileRootNode, schemaAsset.GetVersionSearchRule().GetRootNodeAttributeName());
  510. AZ::Outcome <AZ::Version<MaxVersionPartsCount>, AZStd::string> SourceFileVersionOutcome = Internal::ParseFromString(sourceFileVersionStr);
  511. if (!SourceFileVersionOutcome.IsSuccess())
  512. {
  513. AZ_Warning("XmlBuilderWorker", false, SourceFileVersionOutcome.TakeError().c_str());
  514. // This isn't a blocking error, the error was on this schema, so try checking the next schema for a match.
  515. return SchemaMatchResult::NoMatchFound;
  516. }
  517. AZ::Version<MaxVersionPartsCount> version = SourceFileVersionOutcome.GetValue();
  518. AZ::Outcome <void, bool> matchingRuleOutcome = SearchForMatchingRule(sourceFilePath, schemaFilePath, version, schemaAsset.GetMatchingRules(), watchFolderPath);
  519. if (!matchingRuleOutcome.IsSuccess())
  520. {
  521. // This isn't a blocking error, the error was on this schema, so try checking the next schema for a match.
  522. return SchemaMatchResult::NoMatchFound;
  523. }
  524. bool dependencySearchRuleResult = false;
  525. if (schemaAsset.UseAZSerialization())
  526. {
  527. AZ::SerializeContext* context = nullptr;
  528. AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
  529. dependencySearchRuleResult = AssetBuilderSDK::GatherProductDependenciesForFile(
  530. *context,
  531. sourceFilePath,
  532. productDependencies,
  533. pathDependencies);
  534. }
  535. else
  536. {
  537. AZStd::string sourceAssetFolder = sourceFilePath;
  538. AzFramework::StringFunc::Path::GetFullPath(sourceAssetFolder.c_str(), sourceAssetFolder);
  539. dependencySearchRuleResult = SearchForDependencySearchRule(xmlFileRootNode, schemaFilePath, version,
  540. schemaAsset.GetDependencySearchRules(), productDependencies, pathDependencies, sourceAssetFolder, watchFolderPath);
  541. if (!dependencySearchRuleResult)
  542. {
  543. AZ_Warning("XmlBuilderWorker", false, "File %s matches schema %s's maching rules defined for version %s,"
  544. "but has no matching dependency search rules. No dependencies will be emitted for this file."
  545. "To resolve this warning, add a new dependency search rule that matches this version and leave it empty if no dependencies need to be emitted.",
  546. sourceFilePath.c_str(), schemaFilePath.c_str(), sourceFileVersionStr.c_str());
  547. }
  548. }
  549. // The schema matched, so return either a match was found or there was an error.
  550. return dependencySearchRuleResult ? SchemaMatchResult::MatchFound : SchemaMatchResult::Error;
  551. }
  552. AZ::Outcome <void, bool> XmlBuilderWorker::SearchForMatchingRule(
  553. const AZStd::string& sourceFilePath,
  554. [[maybe_unused]] const AZStd::string& schemaFilePath,
  555. const AZ::Version<MaxVersionPartsCount>& version,
  556. const AZStd::vector<AzFramework::MatchingRule>& matchingRules,
  557. [[maybe_unused]] const AZStd::string& watchFolderPath) const
  558. {
  559. if (matchingRules.size() == 0)
  560. {
  561. AZ_Error("XmlBuilderWorker", false, "Matching rules are missing.");
  562. return AZ::Failure(false);
  563. }
  564. // Check each matching rule
  565. for (const AzFramework::MatchingRule& matchingRule : matchingRules)
  566. {
  567. if (!matchingRule.Valid())
  568. {
  569. AZ_Error("XmlBuilderWorker", false, "Matching rules defined in schema file %s are invalid.", schemaFilePath.c_str());
  570. return AZ::Failure(false);
  571. }
  572. AZStd::string filePathPattern = matchingRule.GetFilePathPattern();
  573. AZStd::string excludedFilePathPattern = matchingRule.GetExcludedFilePathPattern();
  574. if (!Internal::MatchesVersionConstraints(version, matchingRule.GetVersionConstraints()) ||
  575. !AZStd::wildcard_match(filePathPattern.c_str(), sourceFilePath.c_str()) ||
  576. (!excludedFilePathPattern.empty() && AZStd::wildcard_match(excludedFilePathPattern.c_str(), sourceFilePath.c_str())))
  577. {
  578. continue;
  579. }
  580. else
  581. {
  582. return AZ::Success();
  583. }
  584. }
  585. return AZ::Failure(true);
  586. }
  587. bool XmlBuilderWorker::SearchForDependencySearchRule(
  588. AZ::rapidxml::xml_node<char>* xmlFileRootNode,
  589. [[maybe_unused]] const AZStd::string& schemaFilePath,
  590. const AZ::Version<MaxVersionPartsCount>& version,
  591. const AZStd::vector<AzFramework::DependencySearchRule>& dependencySearchRules,
  592. AZStd::vector<AssetBuilderSDK::ProductDependency>& productDependencies,
  593. AssetBuilderSDK::ProductPathDependencySet& pathDependencies,
  594. const AZStd::string& sourceAssetFolder,
  595. const AZStd::string& watchFolderPath) const
  596. {
  597. if (dependencySearchRules.size() == 0)
  598. {
  599. AZ_Error("XmlBuilderWorker", false, "Dependency search rules are missing.");
  600. return false;
  601. }
  602. for (const AzFramework::DependencySearchRule& dependencySearchRule : dependencySearchRules)
  603. {
  604. if (!Internal::MatchesVersionConstraints(version, dependencySearchRule.GetVersionConstraints()))
  605. {
  606. continue;
  607. }
  608. // Pre-calculate the list of all the XML nodes and mappings from node names to the corresponding nodes
  609. // This could help to reduce the number of traversals when we need to find a match which could appear multiple times in the source file
  610. AZStd::unordered_map<AZStd::string, AZStd::vector<const AZ::rapidxml::xml_node<char>*>> xmlNodeMappings;
  611. AZStd::vector<const AZ::rapidxml::xml_node<char>*> xmlNodeList;
  612. TraverseSourceFile(xmlFileRootNode, xmlNodeMappings, xmlNodeList);
  613. AZStd::vector<AzFramework::SearchRuleDefinition> searchRuleDefinitions = dependencySearchRule.GetSearchRules();
  614. for (const AzFramework::SearchRuleDefinition& searchRuleDefinition : searchRuleDefinitions)
  615. {
  616. AzFramework::XmlSchemaElement searchRuleRootNode = searchRuleDefinition.GetSearchRuleStructure();
  617. AZStd::vector<const AZ::rapidxml::xml_node<char>*> validNodes;
  618. if (searchRuleRootNode.GetName() == "*")
  619. {
  620. // If the schema element node name is "*", it could match any node in the source XML file
  621. // We can use this to specify an attribute which contains product dependency info and could exist in any XML node
  622. validNodes = xmlNodeList;
  623. }
  624. else
  625. {
  626. if (searchRuleDefinition.IsRelativeToXmlRoot())
  627. {
  628. // If the dependency search rule is relative to the root, we will only care about the match at the root level
  629. validNodes = { xmlFileRootNode };
  630. }
  631. else
  632. {
  633. // Otherwise we need to check for any match that appears in the XML file structure
  634. validNodes = xmlNodeMappings[searchRuleRootNode.GetName()];
  635. }
  636. }
  637. for (const AZ::rapidxml::xml_node<char>* validNode : validNodes)
  638. {
  639. Internal::ParseElementNode(searchRuleRootNode, validNode, productDependencies, pathDependencies, sourceAssetFolder, watchFolderPath);
  640. }
  641. }
  642. return true;
  643. }
  644. return false;
  645. }
  646. void XmlBuilderWorker::TraverseSourceFile(
  647. const AZ::rapidxml::xml_node<char>* currentNode,
  648. AZStd::unordered_map<AZStd::string, AZStd::vector<const AZ::rapidxml::xml_node<char>*>>& xmlNodeMappings,
  649. AZStd::vector<const AZ::rapidxml::xml_node<char>*>& xmlNodeList) const
  650. {
  651. if (!currentNode)
  652. {
  653. return;
  654. }
  655. xmlNodeMappings[currentNode->name()].emplace_back(currentNode);
  656. xmlNodeList.emplace_back(currentNode);
  657. for (AZ::rapidxml::xml_node<char>* childNode = currentNode->first_node(); childNode; childNode = childNode->next_sibling())
  658. {
  659. TraverseSourceFile(childNode, xmlNodeMappings, xmlNodeList);
  660. }
  661. }
  662. }