From 1ba2234e81343dfbd57576f6323732d37f5bb0ea Mon Sep 17 00:00:00 2001 From: Diego Argueta Date: Sun, 16 May 2021 08:48:31 -0700 Subject: [PATCH] Add library version information and configuration safeguards (#21) --- CHANGELOG.rst | 12 ++++++ configure | 74 +++++++++++++++++++++++++++++++-- docs/api.rst | 12 ++++-- include/unicornlua/unicornlua.h | 50 +++++++++++++++++++++- src/unicorn.cpp | 31 +++++++++++++- tests/lua/imports.lua | 17 ++++++++ 6 files changed, 186 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0ac2c2c9..469a608f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,18 @@ Changes ======= +1.1.1 (2021-05-15) +------------------ + +New Features +~~~~~~~~~~~~ + +* Added a global constant to the ``unicorn`` module named ``UNICORNLUA_VERSION``. + This is a three-element table giving the major, minor, and patch versions of + the Lua binding. +* Added certain protections and better error messages in the ``configure`` script + to aid setting up your dev environment and debugging certain problems. + 1.1.0 (2021-01-18) ------------------ diff --git a/configure b/configure index 06f433af..8a8fe4d8 100755 --- a/configure +++ b/configure @@ -5,6 +5,8 @@ import configparser import json import logging import os +import textwrap +import re import shutil import subprocess import sys @@ -24,8 +26,8 @@ DEFAULT_LUAROCKS_VERSION = CONFIG["luarocks"]["default_version"] DEFAULT_LUA_VERSION = CONFIG["lua"]["default_version"] -# Detect if we're in a CI environment. This works for Travis, CircleCI, and -# AppVeyor. (AppVeyor uses both "True" and "true" hence this .lower() fiddling.) +# Detect if we're in a CI environment. This works for Travis, CircleCI, and AppVeyor. +# (AppVeyor uses both "True" and "true" hence this .lower() fiddling.) IN_CI_ENVIRONMENT = os.getenv("CI", "").lower() == "true" @@ -96,7 +98,9 @@ def parse_args(): " to the system's LuaRocks executable.", ) parser.add_argument( - "--lua-headers", metavar="PATH", help="The path to the Lua headers files.", + "--lua-headers", + metavar="PATH", + help="The path to the Lua headers files.", ) parser.add_argument( "--lua-library", @@ -131,6 +135,29 @@ def get_luarocks_version(luarocks_exe): return version +def get_lua_header_version(header_dir): + file_path = os.path.join(header_dir, "lua.h") + if not os.path.exists(file_path): + raise ErrorExit( + "Directory doesn't exist or header file `lua.h` is missing: " + header_dir + ) + + LOG.debug("Determining Lua version from header file at %s", file_path) + with open(file_path, "r") as fd: + contents = fd.read() + + match = re.search(r"LUA_VERSION_NUM\s+(\d+)", contents) + if not match: + raise ErrorExit("Couldn't determine version of Lua header at: " + file_path) + + integer_version = int(match[1]) + major_version = integer_version // 100 + minor_version = integer_version % 100 + + LOG.debug("Lua header defines version as %d.%d.", major_version, minor_version) + return "%d.%d" % (major_version, minor_version) + + def set_defaults_from_config(args): result = vars(args) if not args.venv_config: @@ -184,6 +211,41 @@ def generate_cmake_parameters(settings, install_version, platform): "IN_CI_ENVIRONMENT": "YES" if IN_CI_ENVIRONMENT else "NO", } + if not settings["lua_library"] or not settings["lua_headers"]: + raise ErrorExit( + "The LUA_LIBRARIES and/or LUA_INCLUDE_DIR variables are empty. This usually" + " happens when Lua isn't installed locally or if your include paths aren't" + " set. You can either use a virtual environment (see tools/lua_env.py) or," + " if you do have Lua already installed, you'll need to provide the include" + " and library paths to this script via the --lua-headers and --lua-library" + " options. If you're on a *NIX system, Lua headers are usually in" + " `/usr/include/lua/` and the library is at" + " `/usr/lib//liblua{.a, .so}`." + ) + + header_version = get_lua_header_version(settings["lua_headers"]) + + if settings["use_venv"]: + version_source = "Virtual environment" + else: + version_source = "OS" + + # For LuaJIT, `short_version` is the version of LuaJIT, *not* Lua! Thus we have to + # special-case the version number check. + if is_luajit and header_version != "5.1": + raise ErrorExit( + "Mismatch between %s's LuaJIT and header version. LuaJIT implements 5.1," + " but the header says %s. Either this is the wrong header, or this version" + " of LuaJIT implements an unexpected version of Lua. Header path: %s" + % (version_source, header_version, settings["lua_headers"]) + ) + elif not is_luajit and (header_version != short_version): + raise ErrorExit( + "Mismatch between Lua executable and header version. %s Lua version: %s;" + " header says %s. The path to the headers is likely wrong: %s" + % (version_source, short_version, header_version, settings["lua_headers"]) + ) + if settings["uc_lib"]: values["UNICORN_LIBRARY"] = settings["uc_lib"] if settings["uc_headers"]: @@ -244,7 +306,11 @@ if __name__ == "__main__": LOG.error("Killed.") sys.exit(2) except ErrorExit as exc: - LOG.error(str(exc)) + if str(exc): + print() + multiline_message = textwrap.wrap(str(exc), width=80) + for line in multiline_message: + LOG.error(line) sys.exit(1) else: sys.exit(0) diff --git a/docs/api.rst b/docs/api.rst index 71c8ecdd..5ecd312a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -385,10 +385,16 @@ Note: (This still works correctly if the library is compiled against Unicorn *New in 1.1.0* -Global Functions ----------------- +Globals +------- -These functions live in the ``unicorn`` namespace. +These live in the ``unicorn`` namespace. + +``LUA_LIBRARY_VERSION`` +~~~~~~~~~~~~~~~~~~~~~~~ + +This is a three-element table giving the major, minor, and patch versions of the +Lua binding. ``arch_supported(architecture)`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/include/unicornlua/unicornlua.h b/include/unicornlua/unicornlua.h index 72e6ef4e..f84f4ac7 100644 --- a/include/unicornlua/unicornlua.h +++ b/include/unicornlua/unicornlua.h @@ -17,7 +17,55 @@ #error "Library must be compiled against version 1.x of Unicorn." #endif +/** + * The major version number of this Lua library (first part, 1.x.x). + */ +#define UNICORNLUA_VERSION_MAJOR 1 + +/** + * The minor version number of this Lua library (second part, x.1.x). + */ +#define UNICORNLUA_VERSION_MINOR 1 -#define UNICORNLUA_UNICORN_MAJOR_MINOR_PATCH ((UC_VERSION_MAJOR << 16) | (UC_VERSION_MINOR << 8) | UC_VERSION_EXTRA) +/** + * The patch version number of this Lua library (third part, x.x.1). + */ +#define UNICORNLUA_VERSION_PATCH 1 + +/** + * Create a 24-bit number from a release's major, minor, and patch numbers. + * + * You can use this for comparing a version number in your C/C++ code: + * + * #if UNICORNLUA_VERSION < MAKE_VERSION(1, 1, 0) + * // Executes on versions below 1.1.0 + * #else + * // Executes on versions 1.1.0 and above (including 1.1.1) + * #endif + */ +#define MAKE_VERSION(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) + + +/** + * The full version number of this Lua library, as an integer. + * + * This is a 24-bit number, where each version component is bit-shifted so that + * it occupies a single byte. The major version is the most-significant 8 bits, + * the minor version is the 8 bits below that, and the patch number is below + * that. Thus, release 1.10.16 would be represented by 0x010A10. + */ +#define UNICORNLUA_VERSION MAKE_VERSION(UNICORNLUA_VERSION_MAJOR, \ + UNICORNLUA_VERSION_MINOR, \ + UNICORNLUA_VERSION_PATCH) + +/** + * The full version number of the Unicorn library this was compiled with, as an integer. + * + * The construction and semantics of this version number are the same as in + * @ref UNICORNLUA_VERSION. + */ +#define UNICORNLUA_UNICORN_MAJOR_MINOR_PATCH MAKE_VERSION(UC_VERSION_MAJOR, \ + UC_VERSION_MINOR, \ + UC_VERSION_EXTRA) #endif /* INCLUDE_UNICORNLUA_UNICORNLUA_H_ */ diff --git a/src/unicorn.cpp b/src/unicorn.cpp index 80fda6fa..20ba5f86 100644 --- a/src/unicorn.cpp +++ b/src/unicorn.cpp @@ -4,10 +4,11 @@ #include "unicornlua/engine.h" #include "unicornlua/lua.h" #include "unicornlua/registers.h" +#include "unicornlua/unicornlua.h" #include "unicornlua/utils.h" -static int ul_version(lua_State *L) { +static int ul_unicorn_version(lua_State *L) { unsigned major, minor; uc_version(&major, &minor); @@ -17,6 +18,23 @@ static int ul_version(lua_State *L) { } +// Create a three-element table that indicates the major, minor, and patch +// versions of this Lua binding. +static int ul_create_unicornlua_version_table(lua_State *L) { + lua_createtable(L, 3, 0); + + lua_pushinteger(L, UNICORNLUA_VERSION_MAJOR); + lua_seti(L, -2, 1); + + lua_pushinteger(L, UNICORNLUA_VERSION_MINOR); + lua_seti(L, -2, 2); + + lua_pushinteger(L, UNICORNLUA_VERSION_PATCH); + lua_seti(L, -2, 3); + return 1; +} + + static int ul_arch_supported(lua_State *L) { auto architecture = static_cast(luaL_checkinteger(L, -1)); lua_pushboolean(L, uc_arch_supported(architecture)); @@ -64,13 +82,22 @@ static const luaL_Reg kUnicornLibraryFunctions[] = { {"arch_supported", ul_arch_supported}, {"open", ul_open}, {"strerror", ul_strerror}, - {"version", ul_version}, + {"version", ul_unicorn_version}, {nullptr, nullptr} }; extern "C" UNICORN_EXPORT int luaopen_unicorn(lua_State *L) { + // Initialize the engine bits, such as the metatables that engine and context + // instances use. ul_init_engines_lib(L); + + // Create the main library table with all of the global functions in it. luaL_newlib(L, kUnicornLibraryFunctions); + + // Create a table in the library that contains the major, minor, and patch + // numbers of the Lua binding. These are positional values, not fields. + ul_create_unicornlua_version_table(L); + lua_setfield(L, -2, "LUA_LIBRARY_VERSION"); return 1; } diff --git a/tests/lua/imports.lua b/tests/lua/imports.lua index 71d76d38..a22f1a65 100644 --- a/tests/lua/imports.lua +++ b/tests/lua/imports.lua @@ -8,3 +8,20 @@ describe("Ensure library loads don't crash.", function () it('[sparc] require', function () require 'unicorn.sparc_const' end) it('[x86] require', function () require 'unicorn.x86_const' end) end) + + +describe('Ensure binding version number looks correct.', function () + it('Check existence of version table', function() + local unicorn = require 'unicorn' + assert.is_not_nil(unicorn.LUA_LIBRARY_VERSION) + end) + + it('Checks version table looks correct', function () + local unicorn = require 'unicorn' + local major, minor, patch = table.unpack(unicorn.LUA_LIBRARY_VERSION) + + assert.is_equal("number", type(major), 'Major version is borked') + assert.is_equal("number", type(minor), 'Minor version is borked') + assert.is_equal("number", type(patch), 'Patch version is borked') + end) +end)