LuaBuilderWorker.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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 <AzCore/Casting/numeric_cast.h>
  9. #include <AzCore/Component/ComponentApplicationBus.h>
  10. #include <AzCore/Debug/Trace.h>
  11. #include <AzCore/Math/MathReflection.h>
  12. #include <AzCore/Script/ScriptAsset.h>
  13. #include <AzCore/Script/ScriptContext.h>
  14. #include <AzCore/Script/lua/lua.h> // for lua_tostring
  15. #include <AzCore/std/string/conversions.h>
  16. #include <AzFramework/FileFunc/FileFunc.h>
  17. #include <AzFramework/IO/LocalFileIO.h>
  18. #include <AzFramework/StringFunc/StringFunc.h>
  19. #include <Builders/LuaBuilder/LuaHelpers.h>
  20. #include <Builders/LuaBuilder/LuaBuilderWorker.h>
  21. // Ensures condition is true, otherwise returns (and presumably fails the build job).
  22. #define LB_VERIFY(condition, ...)\
  23. if (!(condition))\
  24. {\
  25. AZ_Error(AssetBuilderSDK::ErrorWindow, false, __VA_ARGS__);\
  26. return;\
  27. }
  28. namespace LuaBuilder
  29. {
  30. namespace
  31. {
  32. AZStd::vector<AZ::Data::Asset<AZ::ScriptAsset>> ConvertToAssets(AssetBuilderSDK::ProductPathDependencySet& dependencySet)
  33. {
  34. AZStd::vector<AZ::Data::Asset<AZ::ScriptAsset>> assets;
  35. if (AzToolsFramework::AssetSystemRequestBus::Events* assetSystem = AzToolsFramework::AssetSystemRequestBus::FindFirstHandler())
  36. {
  37. for (auto dependency : dependencySet)
  38. {
  39. AZStd::string watchFolder;
  40. AZ::Data::AssetInfo assetInfo;
  41. AZ::IO::Path path(dependency.m_dependencyPath);
  42. bool isLuaDependency = !path.HasExtension() || path.Extension() == ".lua" || path.Extension() == ".luac";
  43. auto sourcePath = path.ReplaceExtension(".lua");
  44. if (assetSystem->GetSourceInfoBySourcePath(sourcePath.c_str(), assetInfo, watchFolder)
  45. && assetInfo.m_assetId.IsValid())
  46. {
  47. AZ::Data::Asset<AZ::ScriptAsset> asset(AZ::Data::AssetId
  48. ( assetInfo.m_assetId.m_guid
  49. , AZ::ScriptAsset::CompiledAssetSubId)
  50. , azrtti_typeid<AZ::ScriptAsset>());
  51. asset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::PreLoad);
  52. assets.push_back(asset);
  53. }
  54. else if(isLuaDependency)
  55. {
  56. AZ_Error("LuaBuilder", false, "Did not find dependency %s referenced by script.", dependency.m_dependencyPath.c_str());
  57. }
  58. else
  59. {
  60. AZ_Error("LuaBuilder", false, "%s referenced by script does not appear to be a lua file.\n"
  61. "References to assets should be handled using Property slots and AssetIds to ensure proper dependency tracking.\n"
  62. "This file will not be tracked as a dependency.",
  63. dependency.m_dependencyPath.c_str());
  64. }
  65. }
  66. }
  67. else
  68. {
  69. AZ_Error("LuaBuilder", false, "AssetSystemBus not available");
  70. }
  71. return assets;
  72. }
  73. //////////////////////////////////////////////////////////////////////////
  74. // Helper for writing to a generic stream
  75. template<typename T>
  76. bool WriteToStream(AZ::IO::GenericStream& stream, const T* t)
  77. {
  78. return stream.Write(sizeof(T), t) == sizeof(T);
  79. }
  80. static const AZ::u32 s_BuildTypeKey = AZ_CRC_CE("BuildType");
  81. static const char* s_BuildTypeCompiled = "Compiled";
  82. }
  83. AZStd::string LuaBuilderWorker::GetAnalysisFingerprint()
  84. {
  85. // mutating the Analysis Fingerprint will cause the CreateJobs function to run even
  86. // on files which have not changed.
  87. return AZ::ScriptDataContext::GetInterpreterVersion();
  88. }
  89. //////////////////////////////////////////////////////////////////////////
  90. // CreateJobs
  91. void LuaBuilderWorker::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
  92. {
  93. using namespace AssetBuilderSDK;
  94. // Check for shutdown
  95. if (m_isShuttingDown)
  96. {
  97. response.m_result = CreateJobsResultCode::ShuttingDown;
  98. return;
  99. }
  100. AssetBuilderSDK::ProductPathDependencySet dependencySet;
  101. AZ::IO::Path path = request.m_watchFolder;
  102. path = path / request.m_sourceFile;
  103. ParseDependencies(path.c_str(), dependencySet);
  104. auto dependentAssets = ConvertToAssets(dependencySet);
  105. for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
  106. {
  107. JobDescriptor descriptor;
  108. descriptor.m_jobKey = "Lua Compile";
  109. descriptor.SetPlatformIdentifier(info.m_identifier.c_str());
  110. descriptor.m_critical = true;
  111. // mutating the AdditionalFingerprintInfo will cause the job to run even if
  112. // nothing else has changed (i.e., files are the same, version of this builder didn't change)
  113. // by doing this, changing the version of the interpreter is enough to cause the files to rebuild
  114. // automatically.
  115. descriptor.m_additionalFingerprintInfo = GetAnalysisFingerprint();
  116. descriptor.m_jobParameters[s_BuildTypeKey] = s_BuildTypeCompiled;
  117. for (auto& dependentAsset : dependentAssets)
  118. {
  119. AssetBuilderSDK::JobDependency jobDependency;
  120. jobDependency.m_sourceFile.m_sourceFileDependencyUUID = dependentAsset.GetId().m_guid;
  121. jobDependency.m_jobKey = "Lua Compile";
  122. jobDependency.m_platformIdentifier = info.m_identifier;
  123. jobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
  124. descriptor.m_jobDependencyList.emplace_back(AZStd::move(jobDependency));
  125. }
  126. response.m_createJobOutputs.push_back(descriptor);
  127. }
  128. response.m_result = CreateJobsResultCode::Success;
  129. }
  130. //////////////////////////////////////////////////////////////////////////
  131. // ProcessJob
  132. void LuaBuilderWorker::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
  133. {
  134. using namespace AZ::IO;
  135. AZ_TracePrintf(AssetBuilderSDK::InfoWindow, "Starting Job.\n");
  136. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
  137. LB_VERIFY(!m_isShuttingDown, "Cancelled job %s because shutdown was requested.\n", request.m_sourceFile.c_str());
  138. LB_VERIFY(request.m_jobDescription.m_jobParameters.at(s_BuildTypeKey) == s_BuildTypeCompiled
  139. , "Cancelled job %s because job key was invalid.\n", request.m_sourceFile.c_str());
  140. AZ_TracePrintf(AssetBuilderSDK::InfoWindow, "Starting script compile.\n");
  141. // setup lua state
  142. AZ::ScriptContext scriptContext(AZ::DefaultScriptContextId);
  143. // reset filename to .luac, reconstruct full path
  144. AZStd::string destFileName;
  145. AzFramework::StringFunc::Path::GetFullFileName(request.m_fullPath.c_str(), destFileName);
  146. AzFramework::StringFunc::Path::ReplaceExtension(destFileName, "luac");
  147. AZStd::string debugName = "@" + request.m_sourceFile;
  148. AZStd::to_lower(debugName.begin(), debugName.end());
  149. {
  150. // read script
  151. FileIOStream inputStream;
  152. LB_VERIFY(inputStream.Open(request.m_fullPath.c_str(), OpenMode::ModeRead | OpenMode::ModeText)
  153. , "Failed to open input file %s", request.m_sourceFile.c_str());
  154. // parse script
  155. LB_VERIFY(scriptContext.LoadFromStream(&inputStream, debugName.c_str(), "t")
  156. , "%s"
  157. , lua_tostring(scriptContext.NativeContext(), -1));
  158. inputStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  159. }
  160. // initialize asset data
  161. AZ::LuaScriptData assetData;
  162. assetData.m_debugName = debugName;
  163. AssetBuilderSDK::ProductPathDependencySet dependencySet;
  164. ParseDependencies(request.m_fullPath, dependencySet);
  165. assetData.m_dependencies = ConvertToAssets(dependencySet);
  166. auto scriptStream = assetData.CreateScriptWriteStream();
  167. LB_VERIFY(LuaDumpToStream(scriptStream, scriptContext.NativeContext()), "Failed to write lua bytecode to stream.");
  168. {
  169. // write asset data to disk
  170. AZStd::string destPath;
  171. AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), destFileName.data(), destPath, true);
  172. FileIOStream outputStream;
  173. LB_VERIFY(outputStream.Open(destPath.c_str(), OpenMode::ModeWrite | OpenMode::ModeBinary)
  174. , "Failed to open output file %s", destPath.data());
  175. AZ::SerializeContext* serializeContext = nullptr;
  176. AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
  177. LB_VERIFY(serializeContext, "Unable to retrieve serialize context.");
  178. LB_VERIFY
  179. ( AZ::Utils::SaveObjectToStream<AZ::LuaScriptData>(outputStream, AZ::ObjectStream::ST_BINARY, &assetData, serializeContext)
  180. , "Failed to write asset data to disk");
  181. }
  182. AssetBuilderSDK::JobProduct compileProduct{ destFileName, azrtti_typeid<AZ::ScriptAsset>(), AZ::ScriptAsset::CompiledAssetSubId };
  183. for (auto& dependency : assetData.m_dependencies)
  184. {
  185. compileProduct.m_dependencies.push_back({ dependency.GetId(), AZ::Data::ProductDependencyInfo::CreateFlags(AZ::Data::AssetLoadBehavior::PreLoad) });
  186. }
  187. // report success
  188. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  189. response.m_outputProducts.emplace_back(compileProduct);
  190. response.m_outputProducts.back().m_dependenciesHandled = true;
  191. AZ_TracePrintf(AssetBuilderSDK::InfoWindow, "Finished job.\n");
  192. }
  193. //////////////////////////////////////////////////////////////////////////
  194. // ShutDown
  195. void LuaBuilderWorker::ShutDown()
  196. {
  197. // it is important to note that this will be called on a different thread than your process job thread
  198. m_isShuttingDown = true;
  199. }
  200. void LuaBuilderWorker::ParseDependencies(const AZStd::string& file, AssetBuilderSDK::ProductPathDependencySet& outDependencies)
  201. {
  202. bool isInsideBlockComment = false;
  203. AzFramework::FileFunc::ReadTextFileByLine(file, [&outDependencies, &isInsideBlockComment](const char* line) -> bool
  204. {
  205. AZStd::string lineCopy(line);
  206. // Block comments can be negated by adding an extra '-' to the front of the comment marker
  207. // We should strip these out of every line, as a negated block comment should be parsed like regular code
  208. AzFramework::StringFunc::Replace(lineCopy, "---[[", "");
  209. // Splitting the line into tokens with "--" will give us the following behavior:
  210. // case 1: "code to parse -- commented out line" -> {"code to parse "," commented out line"}
  211. // case 2: "code to parse --[[ contents of block comment --]] more code to parse"
  212. // -> {"code to parse ","[[ contents of block comment ","]] more code to parse"}
  213. AZStd::vector<AZStd::string> tokens;
  214. AzFramework::StringFunc::Tokenize(lineCopy, tokens, "--", true, true);
  215. if (isInsideBlockComment)
  216. {
  217. // If the block comment ends this line, we'll handle that later
  218. lineCopy.clear();
  219. }
  220. else if (!tokens.empty())
  221. {
  222. // Unless inside a block comment, all characters to the left of "--" should be parsed
  223. lineCopy = tokens[0];
  224. }
  225. for (int tokenIndex = 1; tokenIndex < tokens.size(); ++tokenIndex)
  226. {
  227. if (AzFramework::StringFunc::StartsWith(tokens[tokenIndex].c_str(), "[["))
  228. {
  229. // "--[[" indicates the start of a block comment. Ignore contents of this token.
  230. isInsideBlockComment = true;
  231. continue;
  232. }
  233. else if (AzFramework::StringFunc::StartsWith(tokens[tokenIndex].c_str(), "]]"))
  234. {
  235. // "--]]" indicates the end of a block comment. Parse contents of this token.
  236. isInsideBlockComment = false;
  237. AzFramework::StringFunc::LChop(tokens[tokenIndex], 2);
  238. lineCopy.append(tokens[tokenIndex]);
  239. }
  240. else if (!tokens[tokenIndex].empty())
  241. {
  242. // "--" (with no special characters after) indicates a whole line comment. Ignore all further tokens.
  243. break;
  244. }
  245. }
  246. // Regex to match lines looking similar to require("a") or Script.ReloadScript("a") or require "a"
  247. // Group 1: require or empty
  248. // Group 2: quotation mark ("), apostrophe ('), or empty
  249. // Group 3: specified path or variable (variable will be indicated by empty group 2)
  250. // Group 4: Same as group 2
  251. AZStd::regex requireRegex(R"(\b(?:(require)|Script\.ReloadScript)\s*(?:\(|(?="|'))\s*("|'|)([^"')]*)(\2)\s*\)?)");
  252. // Regex to match lines looking like a path (containing a /)
  253. // Group 1: the string contents
  254. AZStd::regex pathRegex(R"~("((?=[^"]*\/)[^"\n<>:"|?*]{2,})")~");
  255. // Regex to match lines looking like ExecuteConsoleCommand("exec somefile.cfg")
  256. AZStd::regex consoleCommandRegex(R"~(ExecuteConsoleCommand\("exec (.*)"\))~");
  257. AZStd::smatch match;
  258. if (AZStd::regex_search(lineCopy, match, requireRegex))
  259. {
  260. if (!match[2].matched || !match[4].matched)
  261. {
  262. // Result is not a string literal, we'll have to rely on the path regex to pick up the dependency
  263. }
  264. else
  265. {
  266. AZStd::string filePath = match[3].str();
  267. if (match[1].matched)
  268. {
  269. // This is a "require" include, which has a format that uses . instead of / and has no file extension included
  270. static constexpr char s_luaExtension[] = ".luac";
  271. // Replace '.' in module name with '/'
  272. for (auto pos = filePath.find('.'); pos != AZStd::string::npos; pos = filePath.find('.'))
  273. {
  274. filePath.replace(pos, 1, "/", 1);
  275. }
  276. // Add file extension to path
  277. if (filePath.find(s_luaExtension) == AZStd::string::npos)
  278. {
  279. filePath += s_luaExtension;
  280. }
  281. }
  282. outDependencies.emplace(filePath, AssetBuilderSDK::ProductPathDependencyType::ProductFile);
  283. }
  284. }
  285. else if (AZStd::regex_search(lineCopy, match, consoleCommandRegex))
  286. {
  287. outDependencies.emplace(match[1].str().c_str(), AssetBuilderSDK::ProductPathDependencyType::ProductFile);
  288. }
  289. else if (AZStd::regex_search(lineCopy, match, pathRegex))
  290. {
  291. AZ_TracePrintf("LuaBuilder", "Found potential dependency on file: %s\n", match[1].str().c_str());
  292. outDependencies.emplace(match[1].str().c_str(), AssetBuilderSDK::ProductPathDependencyType::ProductFile);
  293. }
  294. return true;
  295. });
  296. }
  297. #undef LB_VERIFY
  298. }