From b98e1799c54fb2f108b2a2f68c6ce9307b802715 Mon Sep 17 00:00:00 2001 From: Avril112113 Date: Wed, 24 Apr 2024 14:14:52 +0100 Subject: [PATCH] Added config for single or multiple projects under 1 or many config files --- tool/BuildActions.lua | 37 +++ tool/build.lua | 135 --------- tool/cli.lua | 12 +- tool/cli_print.lua | 3 +- tool/config.lua | 63 ++++ tool/multi_project.lua | 84 ++++++ tool/project.lua | 280 ++++++++++++++++++ ..._to_swaddon.lua => transform_combiner.lua} | 7 +- tool/transform_swaddon_tracing.lua | 8 +- 9 files changed, 481 insertions(+), 148 deletions(-) create mode 100644 tool/BuildActions.lua delete mode 100644 tool/build.lua create mode 100644 tool/config.lua create mode 100644 tool/multi_project.lua create mode 100644 tool/project.lua rename tool/{transform_lua_to_swaddon.lua => transform_combiner.lua} (92%) diff --git a/tool/BuildActions.lua b/tool/BuildActions.lua new file mode 100644 index 0000000..6abc689 --- /dev/null +++ b/tool/BuildActions.lua @@ -0,0 +1,37 @@ +local BuildActions = {} + + +--- Called before any of the build process starts. +---@param config SSSWTool.Config +function BuildActions.pre_build(config) +end + +--- Called before a file is parsed. +---@param config SSSWTool.Config +---@param path string +function BuildActions.pre_parse(config, path) +end + +--- Called after a file is parsed but before it's transformed. +---@param config SSSWTool.Config +---@param path string +---@param ast ASTNodeSource +function BuildActions.post_parse(config, path, ast) +end + +--- Called after a file is transformed. +---@param config SSSWTool.Config +---@param path string +---@param ast ASTNodeSource +function BuildActions.post_transform(config, path, ast) +end + +--- Called after the entire build process finishes. +--- This is the very last build action to be called. +--- This is still called before `script.lua` is copied to it's output directory. +---@param config SSSWTool.Config +function BuildActions.post_build(config) +end + + +return BuildActions diff --git a/tool/build.lua b/tool/build.lua deleted file mode 100644 index d9896a2..0000000 --- a/tool/build.lua +++ /dev/null @@ -1,135 +0,0 @@ -local lfs = require "lfs" - -local Parser = require "SelenScript.parser.parser" -local Transformer = require "SelenScript.transformer.transformer" -local Emitter = require "SelenScript.emitter.emitter" -local Utils = require "SelenScript.utils" -local ASTHelpers = require "SelenScript.transformer.ast_helpers" -local AST = require "SelenScript.parser.ast" - -local SWAddonTransformerDefs = require "tool.transform_lua_to_swaddon" -local SWAddonTracerTransformerDefs = require "tool.transform_swaddon_tracing" - - ----@param path string ----@param mode LuaFileSystem.AttributeMode -local function path_is(path, mode) - local attributes = lfs.attributes(path) - return attributes ~= nil and attributes.mode == mode -end - - -local Build = {} - - ----@param addon_dir string -function Build.build(addon_dir) - addon_dir = addon_dir:gsub("\\", "/"):gsub("^./", ""):gsub("/$", "") - if #addon_dir <= 0 then - addon_dir = "." - end - if not path_is(addon_dir, "directory") then - print(("Invalid addon directory '%s'"):format(addon_dir)) - return -1 - end - - local enable_tracing = false - - local entry_file = "script.lua" - local entry_file_path = addon_dir .. "/" .. entry_file - if not path_is(entry_file_path, "file") then - print(("Missing entry file '%s'"):format(entry_file)) - return -1 - end - local entry_file_src = Utils.readFile(entry_file_path) - - local time_start = os.clock() - - local parser - do - local errors - print_info("Creating parser") - parser, errors = Parser.new() - if #errors > 0 then - print_error("-- Parser creation Errors: " .. #errors .. " --") - for _, v in ipairs(errors) do - print_error((v.id or "NO_ID") .. ": " .. v.msg) - end - os.exit(-1) - end - if parser == nil or #errors > 0 then - print_error("Failed to create parser.") - os.exit(-1) - end - end - - local ast, comments - do - local errors - print_info("Parsing 'script.lua'") - ast, errors, comments = parser:parse(entry_file_src, entry_file_path) - if #errors > 0 then - print_error("-- Parse Errors: " .. #errors .. " --") - for _, v in ipairs(errors) do - print_error(v.id .. ": " .. v.msg) - end - os.exit(-1) - end - end - - do - print_info("Transforming AST") - local transformer = Transformer.new(SWAddonTransformerDefs) - local errors = transformer:transform(ast, { - addon_dir=addon_dir, - parser=parser, - }) - if #errors > 0 then - print_error("-- Transformer Errors: " .. #errors .. " --") - for _, v in ipairs(errors) do - print_error(v.id .. ": " .. v.msg) - end - os.exit(-1) - end - end - - if enable_tracing then - print_info("Transforming AST (DBG Tracer)") - local transformer = Transformer.new(SWAddonTracerTransformerDefs) - local errors = transformer:transform(ast, { - addon_dir=addon_dir, - parser=parser, - }) - if #errors > 0 then - print_error("-- Transformer Errors: " .. #errors .. " --") - for _, v in ipairs(errors) do - print_error(v.id .. ": " .. v.msg) - end - os.exit(-1) - end - end - - do - -- Add comment at beginning of file to disable all diagnostics of the file. - -- This isn't required but is nice to have. - table.insert(ast.block.block, 1, ASTHelpers.Nodes.LineComment(ast.block.block[1], "---", "@diagnostic disable")) - end - - do - print_info("Emitting Lua") - local emitter_lua = Emitter.new("lua", {}) - local script_out, script_out_source_map = emitter_lua:generate(ast, { - base_path = addon_dir, - luacats_source_prefix = "..", - }) - - lfs.mkdir(addon_dir .. "/_build") - Utils.writeFile(addon_dir .. "/_build/script.lua", script_out) - end - - local time_finish = os.clock() - print_info(("Finished in %ss."):format(time_finish-time_start)) -end - - -return Build diff --git a/tool/cli.lua b/tool/cli.lua index e6b078c..8380f55 100644 --- a/tool/cli.lua +++ b/tool/cli.lua @@ -1,5 +1,6 @@ require "tool.cli_print" -local Build = require "tool.build" + +local MultiProject = require "tool.multi_project" local CLI = {} @@ -59,7 +60,14 @@ CLI.actions = { handler = function(args, pos) local addon_dir = args[pos] or "./" pos = pos + 1 - Build.build(addon_dir) + local multi_project, err = MultiProject.new(addon_dir .. "/ssswtool.json") + if not multi_project or err then + print_error(err or "FAIL project ~= nil") + return -1 + end + multi_project:build() + -- TODO: Check for any projects that failed to build and return -1 if so. + return 0 end, }, } diff --git a/tool/cli_print.lua b/tool/cli_print.lua index 563849e..4aa7acb 100644 --- a/tool/cli_print.lua +++ b/tool/cli_print.lua @@ -40,7 +40,8 @@ local function _print(mode, ...) for i=1,select("#", ...) do values[i] = tostring(values[i]) end - print(("%s[%s%s%s]:%s%s %s"):format(Colors.fix, MODE_COLORS[mode], mode, Colors.fix, Colors.reset, string.rep(" ", 5-#mode), table.concat(values, "\t"))) + local s = table.concat(values, "\t"):gsub("\n", "%0" .. string.rep(" ", 9)) + print(("%s[%s%s%s]:%s%s %s"):format(Colors.fix, MODE_COLORS[mode], mode, Colors.fix, Colors.reset, string.rep(" ", 5-#mode), s)) end ---@param ... any diff --git a/tool/config.lua b/tool/config.lua new file mode 100644 index 0000000..b0e7204 --- /dev/null +++ b/tool/config.lua @@ -0,0 +1,63 @@ +local json = require "json" + +local Utils = require "SelenScript.utils" + + +---@class SSSWTool.Config +---@field _validate fun(config:SSSWTool.Config, data:table)? +local Config = {} +Config.__index = Config + + +---@param default table? +---@param validate fun(config:SSSWTool.Config, data:table)? +function Config.new(default, validate) + local self = setmetatable({}, Config) + self._validate = validate + self.data = default and Utils.shallowcopy(default) or {} + if validate then + local ok, err = pcall(validate, self, self.data) + if not ok then + error(("Failed to validate config '%s'\n%s"):format("", err:gsub(".-:.-: ", ""))) + end + end + return self +end + +--- Siliently ignores file not found errors and does not reset config. +---@param path string +function Config:read(path) + local f, msg, code = io.open(path, "r") + if not f then + if code == 2 then + return true + end + return false, msg, code + end + local src = f:read("*a") + local ok, data = pcall(json.decode, src) + if not ok then + return false, ("Failed to parse config '%s'\n%s"):format(path, data:gsub(".-:.-: ", "")) + end + if self._validate then + local ok, err = pcall(self._validate, self, data) + if not ok then + return false, ("Failed to validate config '%s'\n%s"):format(path, err:gsub(".-:.-: ", "")) + end + end + self.data = data + return true +end + +-- ---@param path string +-- function Config:write(path) +-- local f, msg, code = io.open(path, "w") +-- if not f then +-- return false, msg, code +-- end +-- error("TODO") +-- return true +-- end + + +return Config diff --git a/tool/multi_project.lua b/tool/multi_project.lua new file mode 100644 index 0000000..099d56b --- /dev/null +++ b/tool/multi_project.lua @@ -0,0 +1,84 @@ +local Utils = require "SelenScript.utils" + +local Config = require "tool.config" +local Project = require "tool.project" + + +---@class SSSWTool.MultiProject +---@field config_path string # Normalized path to the project config file. +---@field project_path string # Normalized path to the project. +---@field projects (SSSWTool.Project|SSSWTool.MultiProject)[] +---@field config SSSWTool.Config +local MultiPorject = {} +MultiPorject.__index = MultiPorject + + +---@param config_path string +function MultiPorject.new(config_path) + local self = setmetatable({}, MultiPorject) + self.config_path = config_path:gsub("\\", "/"):gsub("^%./", ""):gsub("/$", "") + self.project_path = self.config_path:match("^(.*)/") or error("Failed to get parent dir from config path.") + + self.projects = {} + self.config = Config.new() + local ok, err, code = self.config:read(config_path) + if not ok then + return nil, err, code + end + if type(self.config.data) == "table" and type(self.config.data[1]) == "table" then + print_info(("Multi Project config '%s'"):format(config_path)) + for i, v in ipairs(self.config.data) do + if type(v) == "string" then + self:createMultiProject(self.project_path .. "/" .. v, ("'%s' index %s"):format(config_path, i)) + else + Utils.merge(Project.getDefaultConfig(self, false), v, false) + self:createProject(v, ("'%s' index %s"):format(config_path, i)) + end + end + else + Utils.merge(Project.getDefaultConfig(self, true), self.config.data, false) + self:createProject(self.config.data, config_path) + end + return self +end + +---@param project_config SSSWTool.Project.Config +---@param project_path string +function MultiPorject:createProject(project_config, project_path) + local project, err = Project.new(self, project_config) + if not project or err then + if err then + print_error(("%s: %s"):format(project_config.name or project_path, err:gsub(".-:.-: ", ""))) + return nil + else + error("ERROR project == nil") + end + else + print_info(("Loaded config for %s"):format(project_config.name or project_path)) + end + table.insert(self.projects, project) + return project +end + +---@param config_path string +---@param project_path string +function MultiPorject:createMultiProject(config_path, project_path) + local multiproject = MultiPorject.new(config_path) + table.insert(self.projects, multiproject) + return multiproject +end + +---@return {[1]:SSSWTool.Project|SSSWTool.MultiProject,[2]:any[]}[] +function MultiPorject:build() + local results = {} + for _, project in ipairs(self.projects) do + print() + print_info(("Buidling '%s'"):format(project.config.name)) + local result = {project:build()} + table.insert(results, {project, result}) + end + return results +end + + +return MultiPorject diff --git a/tool/project.lua b/tool/project.lua new file mode 100644 index 0000000..d85bd1e --- /dev/null +++ b/tool/project.lua @@ -0,0 +1,280 @@ +local lfs = require "lfs" + +local Parser = require "SelenScript.parser.parser" +local Transformer = require "SelenScript.transformer.transformer" +local Emitter = require "SelenScript.emitter.emitter" +local Utils = require "SelenScript.utils" +local ASTHelpers = require "SelenScript.transformer.ast_helpers" +local AST = require "SelenScript.parser.ast" + + +---@param path string +---@param mode LuaFileSystem.AttributeMode +local function path_is(path, mode) + local attributes = lfs.attributes(path) + return attributes ~= nil and attributes.mode == mode +end + + +---@class SSSWTool.Transformer : Transformer +---@field multiproject SSSWTool.MultiProject +---@field project SSSWTool.Project +---@field parser Parser + + +---@class SSSWTool.Project.Config +---@field name string +---@field entrypoint string? +---@field src string|string[] +---@field out string|string[]|nil +---@field transformers table + + +---@class SSSWTool.Project +---@field multiproject SSSWTool.MultiProject +---@field config SSSWTool.Project.Config +local Project = {} +Project.__index = Project + +Project.TRANSFORMERS = { + combiner = require "tool.transform_combiner", + tracing = require "tool.transform_swaddon_tracing", +} +Project.TRANSFORM_ORDER = { + "combiner", + "tracing", +} +Project.DEFAULT_TRANSFORMERS = { + combiner = true, + tracing = false, +} + +---@param data table +function Project.validate_config(data) + ---@param tbl any + ---@param key_type type|(fun(value:any):boolean) + ---@param value_type type|(fun(value:any):boolean) + local function table_of(tbl, key_type, value_type) + if type(tbl) ~= "table" then return false end + for i, v in pairs(tbl) do + if type(key_type) == "function" then + if not key_type(i) then return false end + else + if type(i) ~= key_type then return false end + end + if type(value_type) == "function" then + if not value_type(v) then return false end + else + if type(v) ~= value_type then return false end + end + end + return true + end + assert(type(data) == "table", "Config ROOT expected table") + assert(type(data.name) == "string", "Config field 'name' expected string") + assert(type(data.src) == "string" or table_of(data.src, "number", "string"), "Config field 'src' expected string or string[]") + assert(data.out == nil or type(data.out) == "string" or table_of(data.out, "number", "string"), "Config field 'out' expected string or string[] or nil/null") +end + +---@param multiproject SSSWTool.MultiProject +---@param infer_name boolean +function Project.getDefaultConfig(multiproject, infer_name) + return { + name = infer_name and multiproject.project_path:match("/(.-)$") or nil, + src = ".", + out = nil, + transformers = Project.DEFAULT_TRANSFORMERS, + } +end + + +---@param multiproject SSSWTool.MultiProject +---@param config SSSWTool.Project.Config +function Project.new(multiproject, config) + local self = setmetatable({}, Project) + self.multiproject = multiproject + local ok, err = pcall(Project.validate_config, config) + if not ok then return nil, err end + self.config = config + return self +end + +---@param path string # Project local path to file. +---@return string, string, nil +---@overload fun(path:string): nil, nil, string +function Project:findSrcFile(path) + local searched = {} + local function check(base) + local src_path = (base .. "/" .. path):gsub("\\", "/"):gsub("^%./", ""):gsub("/$", "") + local full_path = self.multiproject.project_path .. "/" .. src_path + table.insert(searched, ("no file '%s'"):format(full_path)) + if path_is(full_path, "file") then + return full_path, src_path + end + end + + local srcs = type(self.config.src) == "string" and {self.config.src} or self.config.src + ---@diagnostic disable-next-line: param-type-mismatch + for _, src in ipairs(srcs) do + local full_path, src_path = check(src) + if full_path and src_path then + return full_path, src_path + end + end + return nil, nil, table.concat(searched, "\n") +end + +---@param modpath string # Project local mod path to file. +---@return string, string, nil +---@overload fun(modpath:string): nil, nil, string +function Project:findModFile(modpath, path) + path = path or "?.lua;?/init.lua;" + local path_parts = {} + local srcs = type(self.config.src) == "string" and {self.config.src} or self.config.src + ---@diagnostic disable-next-line: param-type-mismatch + for _, src in ipairs(srcs) do + if #src > 0 then + local new_repl = path:gsub( + "%?", + ((self.multiproject.project_path .. "/" .. src .. "/?"):gsub("\\", "/"):gsub("^%./", ""):gsub("%/./", "/"):gsub("/$", "")) + ) + table.insert(path_parts, new_repl) + end + end + local full_path, err = package.searchpath(modpath, table.concat(path_parts, ";")) + if full_path then + full_path = full_path:gsub("\\", "/"):gsub("^%./", ""):gsub("/%./", "/"):gsub("/$", "") + local src_path = full_path:sub(#self.multiproject.project_path+2, -1) + return full_path, src_path, nil + end + return nil, nil, err +end + +function Project:build() + local entry_file_name = self.config.entrypoint or "script.lua" + local entry_file_path = self:findSrcFile(entry_file_name) + if not entry_file_path then + print(("Missing entry file '%s'"):format(entry_file_name)) + return -1 + end + local entry_file_src = Utils.readFile(entry_file_path) + + local time_start = os.clock() + + local parser + do + local errors + print_info("Creating parser") + parser, errors = Parser.new() + if #errors > 0 then + print_error("-- Parser creation Errors: " .. #errors .. " --") + for _, v in ipairs(errors) do + print_error((v.id or "NO_ID") .. ": " .. v.msg) + end + return false + end + if parser == nil or #errors > 0 then + print_error("Failed to create parser.") + return false + end + end + + local ast, comments + do + local errors + print_info(("Parsing '%s'"):format(entry_file_name)) + ast, errors, comments = parser:parse(entry_file_src, entry_file_path) + if #errors > 0 then + print_error("-- Parse Errors: " .. #errors .. " --") + for _, v in ipairs(errors) do + print_error(v.id .. ": " .. v.msg) + end + return false + end + end + + for _, transformer_name in ipairs(Project.TRANSFORM_ORDER) do + if self.config.transformers[transformer_name] then + print_info(("Transforming AST with '%s'"):format(transformer_name)) + local TransformerDefs = assert(Project.TRANSFORMERS[transformer_name], ("Missing transformer"):format(transformer_name)) + local transformer = Transformer.new(TransformerDefs) + local errors = transformer:transform(ast, { + multiproject = self.multiproject, + project = self, + parser = parser, + }) + if #errors > 0 then + print_error("-- Transformer Errors: " .. #errors .. " --") + for _, v in ipairs(errors) do + print_error(v.id .. ": " .. v.msg) + end + return false + end + -- else + -- print_info(("Transforming AST skipped '%s' (disabled)"):format(transformer_name)) + end + end + + do + -- Add comment at beginning of file to disable all diagnostics of the file. + -- This isn't required but is nice to have. + table.insert(ast.block.block, 1, ASTHelpers.Nodes.LineComment(ast.block.block[1], "---", "@diagnostic disable")) + end + + local script_out + do + print_info("Emitting Lua") + local emitter_lua = Emitter.new("lua", {}) + local script_out_source_map + script_out, script_out_source_map = emitter_lua:generate(ast, { + get_source_path = function(path) + if path:match("^/") then + return path + end + return ("../%s"):format(path:sub(#self.multiproject.project_path+2, -1)) + end + }) + + print_info("Writing to '_build'") + lfs.mkdir(self.multiproject.project_path .. "/_build") + Utils.writeFile(self.multiproject.project_path .. "/_build/" .. self.config.name .. ".lua", script_out) + end + + if self.config.out ~= nil then + local sw_save_path + if jit.os == "Windows" then + local appdata_path = os.getenv("appdata") + if appdata_path and #appdata_path > 0 then + sw_save_path = appdata_path .. "\\StormWorks" + end + end + local outs = type(self.config.out) == "string" and {self.config.out} or self.config.out + if #outs > 0 then + print_info("Writing configured output files.") + end + ---@diagnostic disable-next-line: param-type-mismatch + for _, out in ipairs(outs) do + ---@cast out string + if out:find("{SW_SAVE}") and not sw_save_path then + print_warn(("Writing to '%s' failed as '{SW_SAVE}' is not available. (Currently, only Windows is supported for this)"):format(out)) + else + local SEP = jit.os == "Windows" and "\\" or "/" + out = out:gsub("{SW_SAVE}", sw_save_path):gsub("{NAME}", self.config.name):gsub("[\\/]", SEP) + local dir_path = out:match("^(.*)"..SEP) + if not path_is(dir_path, "directory") then + print_warn(("Directory does not exist '%s'"):format(dir_path)) + else + print_info(("Writing '%s'"):format(out)) + Utils.writeFile(out, script_out) + end + end + end + end + + local time_finish = os.clock() + print_info(("Finished build in %ss."):format(time_finish-time_start)) + return true +end + + +return Project diff --git a/tool/transform_lua_to_swaddon.lua b/tool/transform_combiner.lua similarity index 92% rename from tool/transform_lua_to_swaddon.lua rename to tool/transform_combiner.lua index b11a004..f539463 100644 --- a/tool/transform_lua_to_swaddon.lua +++ b/tool/transform_combiner.lua @@ -9,9 +9,7 @@ local ASTNodes = ASTHelpers.Nodes local AST = require "SelenScript.parser.ast" -- Used for debugging. ----@class Transformer_Lua_to_SWAddon : Transformer ----@field parser Parser ----@field addon_dir string +---@class SSSWTool.Transformer_Combiner : SSSWTool.Transformer ---@field required_files table local TransformerDefs = {} @@ -92,7 +90,7 @@ function TransformerDefs:index(node) else return node end - local filepath, err = package.searchpath(modpath, ("%s/?.lua;%s/?/init.lua;"):format(self.addon_dir, self.addon_dir)) + local filepath, filepath_local, err = self.project:findModFile(modpath) self.required_files = self.required_files or {} if err or not filepath then print_error(("Failed to find '%s'%s"):format(modpath, err)) @@ -100,7 +98,6 @@ function TransformerDefs:index(node) else filepath = filepath:gsub("\\", "/") if self.required_files[filepath] == nil then - local filepath_local = filepath:gsub("^"..Utils.escape_pattern(self.addon_dir).."/?", "") print_info(("Parsing '%s'"):format(filepath_local)) local ast, errors, comments = self.parser:parse(Utils.readFile(filepath), filepath) if #errors > 0 then diff --git a/tool/transform_swaddon_tracing.lua b/tool/transform_swaddon_tracing.lua index dfe0c4d..f5ca3fe 100644 --- a/tool/transform_swaddon_tracing.lua +++ b/tool/transform_swaddon_tracing.lua @@ -34,9 +34,7 @@ local EVENT_HOOKS = { -- ["onSpawnAddonComponent"]=1, } ----@class Transformer_SWAddon_Tracing : Transformer ----@field parser Parser ----@field addon_dir string +---@class SSSWTool.Transformer_Tracing : SSSWTool.Transformer local TransformerDefs = {} @@ -187,10 +185,10 @@ function TransformerDefs:funcbody(node) assert(local_source_node ~= nil, "local_source_node ~= nil") local local_file_path = "" if local_source_node.file then - if local_source_node.file:find("^/") then + if local_source_node.file:find("^[\\/]") then local_file_path = local_source_node.file:gsub("\\", "/") else - local_file_path = local_source_node.file:sub(#self.addon_dir+2):gsub("\\", "/") + local_file_path = local_source_node.file:sub(#self.multiproject.project_path+2):gsub("\\", "/") end end self:_add_trace_info(node, name, start_line, start_column, local_file_path)