diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index b8f90fbb4..9ae5de4a2 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -196,6 +196,7 @@ if(ENABLE_SCRIPTING) script/properties_class.cpp script/range_class.cpp script/rectangle_class.cpp + script/require.cpp script/security.cpp script/selection_class.cpp script/site_class.cpp diff --git a/src/app/cli/default_cli_delegate.cpp b/src/app/cli/default_cli_delegate.cpp index fa65211d4..4f0e78241 100644 --- a/src/app/cli/default_cli_delegate.cpp +++ b/src/app/cli/default_cli_delegate.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2016-2018 David Capello // // This program is distributed under the terms of @@ -137,7 +137,7 @@ int DefaultCliDelegate::execScript(const std::string& filename, const Params& params) { auto engine = App::instance()->scriptEngine(); - if (!engine->evalFile(filename, params)) + if (!engine->evalUserFile(filename, params)) throw base::Exception("Error executing script %s", filename.c_str()); return engine->returnCode(); } diff --git a/src/app/commands/cmd_run_script.cpp b/src/app/commands/cmd_run_script.cpp index b04dc48f7..68f187033 100644 --- a/src/app/commands/cmd_run_script.cpp +++ b/src/app/commands/cmd_run_script.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -78,7 +78,7 @@ void RunScriptCommand::onExecute(Context* context) App::instance() ->scriptEngine() - ->evalFile(m_filename, m_params); + ->evalUserFile(m_filename, m_params); #if ENABLE_UI if (context->isUIAvailable()) diff --git a/src/app/extensions.cpp b/src/app/extensions.cpp index 4f4f2a349..da9806038 100644 --- a/src/app/extensions.cpp +++ b/src/app/extensions.cpp @@ -35,6 +35,7 @@ #ifdef ENABLE_SCRIPTING #include "app/script/engine.h" #include "app/script/luacpp.h" + #include "app/script/require.h" #endif #include "archive.h" @@ -627,6 +628,10 @@ void Extension::initScripts() script::push_plugin(L, this); m_plugin.pluginRef = luaL_ref(L, LUA_REGISTRYINDEX); + // Set the _PLUGIN global so require() can find .lua files from the + // plugin path. + script::SetPluginForRequire setPlugin(L, m_plugin.pluginRef); + // Read plugin.preferences value { std::string fn = base::join_path(m_path, kPrefLua); @@ -665,7 +670,7 @@ void Extension::initScripts() lua_pop(L, 1); } - // Call the init() function of thi sscript with a Plugin object as first parameter + // Call the init() function of this script with a Plugin object as first parameter if (lua_getglobal(L, "init") == LUA_TFUNCTION) { // Call init(plugin) lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef); diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index 9386901e2..c2812e183 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -18,6 +18,7 @@ #include "app/pref/preferences.h" #include "app/script/blend_mode.h" #include "app/script/luacpp.h" +#include "app/script/require.h" #include "app/script/security.h" #include "app/sprite_sheet_type.h" #include "app/tilemap_mode.h" @@ -217,16 +218,7 @@ Engine::Engine() #endif // Standard Lua libraries - luaL_requiref(L, LUA_GNAME, luaopen_base, 1); - luaL_requiref(L, LUA_COLIBNAME, luaopen_coroutine, 1); - luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1); - luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, 1); - luaL_requiref(L, LUA_OSLIBNAME, luaopen_os, 1); - luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1); - luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1); - luaL_requiref(L, LUA_UTF8LIBNAME, luaopen_utf8, 1); - luaL_requiref(L, LUA_DBLIBNAME, luaopen_debug, 1); - lua_pop(L, 9); + luaL_openlibs(L); // Overwrite Lua functions lua_register(L, "print", print); @@ -255,6 +247,9 @@ Engine::Engine() lua_setfield(L, -2, "execute"); lua_pop(L, 1); + // Enhance require() function for plugins + custom_require_function(L); + // Generic code used by metatables run_mt_index_code(L); @@ -579,7 +574,7 @@ bool Engine::evalFile(const std::string& filename, } std::string absFilename = base::get_absolute_path(filename); - AddScriptFilename add(absFilename); + AddScriptFilename addScript(absFilename); set_app_params(L, params); if (g_debuggerDelegate) @@ -593,6 +588,18 @@ bool Engine::evalFile(const std::string& filename, return result; } +bool Engine::evalUserFile(const std::string& filename, + const Params& params) +{ + // Set the _SCRIPT_PATH global so require() can find .lua files from + // the script path. + std::string path = + base::get_file_path( + base::get_absolute_path(filename)); + SetScriptForRequire setScript(L, path.c_str()); + return evalFile(filename, params); +} + void Engine::startDebugger(DebuggerDelegate* debuggerDelegate) { g_debuggerDelegate = debuggerDelegate; diff --git a/src/app/script/engine.h b/src/app/script/engine.h index ec7e69368..6159a5bf5 100644 --- a/src/app/script/engine.h +++ b/src/app/script/engine.h @@ -96,6 +96,8 @@ namespace app { const std::string& filename = std::string()); bool evalFile(const std::string& filename, const Params& params = Params()); + bool evalUserFile(const std::string& filename, + const Params& params = Params()); void consolePrint(const char* text) { onConsolePrint(text); diff --git a/src/app/script/luacpp.cpp b/src/app/script/luacpp.cpp index af190e794..a104e2beb 100644 --- a/src/app/script/luacpp.cpp +++ b/src/app/script/luacpp.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -44,6 +44,8 @@ void run_mt_index_code(lua_State* L) const char* s = lua_tostring(L, -1); if (s) std::puts(s); + + lua_pop(L, 1); } } diff --git a/src/app/script/plugin_class.cpp b/src/app/script/plugin_class.cpp index 466391254..45f711cb1 100644 --- a/src/app/script/plugin_class.cpp +++ b/src/app/script/plugin_class.cpp @@ -313,6 +313,20 @@ int Plugin_newMenuSeparator(lua_State* L) return 0; } +int Plugin_get_name(lua_State* L) +{ + auto plugin = get_obj(L, 1); + lua_pushstring(L, plugin->ext->name().c_str()); + return 1; +} + +int Plugin_get_path(lua_State* L) +{ + auto plugin = get_obj(L, 1); + lua_pushstring(L, plugin->ext->path().c_str()); + return 1; +} + int Plugin_get_preferences(lua_State* L) { if (!lua_getuservalue(L, 1)) { @@ -341,6 +355,8 @@ const luaL_Reg Plugin_methods[] = { }; const Property Plugin_properties[] = { + { "name", Plugin_get_name, nullptr }, + { "path", Plugin_get_path, nullptr }, { "preferences", Plugin_get_preferences, Plugin_set_preferences }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/script/require.cpp b/src/app/script/require.cpp new file mode 100644 index 000000000..6b4041709 --- /dev/null +++ b/src/app/script/require.cpp @@ -0,0 +1,105 @@ +// Aseprite +// Copyright (c) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/script/require.h" + +#include "app/extensions.h" + +#include + +namespace app { +namespace script { + +static void eval_code(lua_State* L, const char* code) +{ + if (luaL_loadbuffer(L, code, std::strlen(code), "internal") || + lua_pcall(L, 0, 0, 0)) { + // Error case + const char* s = lua_tostring(L, -1); + if (s) + std::puts(s); + + lua_pop(L, 1); + } +} + +void custom_require_function(lua_State* L) +{ + eval_code(L, R"( +_PACKAGE_PATH_STACK = {} + +local origRequire = require +function require(name) + if _PLUGIN then + local module = origRequire(_PLUGIN.name .. '/' .. name) + if module then return module end + end + return origRequire(name) +end + +local origLuaSearcher = package.searchers[2] +package.searchers[2] = function(name) + if _PLUGIN then + name = name:sub(#_PLUGIN.name+2) + end + return origLuaSearcher(name) +end +)"); +} + +SetPluginForRequire::SetPluginForRequire(lua_State* L, int pluginRef) : L(L) +{ + lua_rawgeti(L, LUA_REGISTRYINDEX, pluginRef); + lua_setglobal(L, "_PLUGIN"); + + // We could use all elements of package.config, but "/?.lua;" should work: + // + // local templateSep = package.config:sub(3, 3) + // local substitutionPoint = package.config:sub(5, 5) + // + eval_code(L, R"( +table.insert(_PACKAGE_PATH_STACK, package.path) + +local pathSep = package.config:sub(1, 1) +package.path = _PLUGIN.path .. pathSep .. '?.lua;' .. package.path +)"); +} + +SetPluginForRequire::~SetPluginForRequire() +{ + eval_code(L, R"( +package.path = table.remove(_PACKAGE_PATH_STACK) +_PLUGIN_OLDPATH = nil +_PLUGIN = nil +)"); +} + +SetScriptForRequire::SetScriptForRequire(lua_State* L, const char* path) : L(L) +{ + lua_pushstring(L, path); + lua_setglobal(L, "_SCRIPT_PATH"); + eval_code(L, R"( +table.insert(_PACKAGE_PATH_STACK, package.path) + +local pathSep = package.config:sub(1, 1) +package.path = _SCRIPT_PATH .. pathSep .. '?.lua;' .. package.path +)"); +} + +SetScriptForRequire::~SetScriptForRequire() +{ + eval_code(L, R"( +package.path = table.remove(_PACKAGE_PATH_STACK) +_SCRIPT_PATH = nil +)"); +} + +} // namespace script +} // namespace app diff --git a/src/app/script/require.h b/src/app/script/require.h new file mode 100644 index 000000000..b33a505c4 --- /dev/null +++ b/src/app/script/require.h @@ -0,0 +1,39 @@ +// Aseprite +// Copyright (c) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_SCRIPT_REQUIRE_H_INCLUDED +#define APP_SCRIPT_REQUIRE_H_INCLUDED +#pragma once + +#include "app/script/luacpp.h" + +#include + +namespace app { +namespace script { + +class SetPluginForRequire { +public: + SetPluginForRequire(lua_State* L, int pluginRef); + ~SetPluginForRequire(); +private: + lua_State* L; +}; + +class SetScriptForRequire { +public: + SetScriptForRequire(lua_State* L, const char* path); + ~SetScriptForRequire(); +private: + lua_State* L; +}; + +void custom_require_function(lua_State* L); + +} // namespace script +} // namespace app + +#endif diff --git a/tests/scripts/require.lua b/tests/scripts/require.lua new file mode 100644 index 000000000..fbadfe577 --- /dev/null +++ b/tests/scripts/require.lua @@ -0,0 +1,11 @@ +-- Copyright (c) 2023 Igara Studio S.A. +-- +-- This file is released under the terms of the MIT license. +-- Read LICENSE.txt for more information. + +local mod = require 'require_module' +assert(mod.a == 5) +mod.a = 6 + +local mod2 = require 'require_module' +assert(mod2.a == 6) -- Now mod and mod2 points to the same object diff --git a/tests/scripts/require_module.lua b/tests/scripts/require_module.lua new file mode 100644 index 000000000..cec4633b9 --- /dev/null +++ b/tests/scripts/require_module.lua @@ -0,0 +1,6 @@ +-- Copyright (c) 2023 Igara Studio S.A. +-- +-- This file is released under the terms of the MIT license. +-- Read LICENSE.txt for more information. + +return { a=5 } diff --git a/third_party/lua b/third_party/lua index db8284f5f..04abf20c9 160000 --- a/third_party/lua +++ b/third_party/lua @@ -1 +1 @@ -Subproject commit db8284f5fc6f02f8f800c99b84b5e4f640e893ce +Subproject commit 04abf20c9e8c973c62b30d0b98f6884ff665b1ba