From 8d943999dbe018b9820c3c1b9e8e4fc721a6e8fa Mon Sep 17 00:00:00 2001 From: hazre Date: Thu, 9 Jul 2026 04:47:24 +0200 Subject: [PATCH 1/4] feat: migrate build to zig, add cross-platform support Replace make-based build with zig, enabling Windows support and removing the need for a C compiler. Use mise's cmd module for command execution. Add shared platform and version modules. Include readline support via linenoize dependency. Signed-off-by: hazre --- .gitignore | 4 + README.md | 30 ++----- build.zig | 112 +++++++++++++++++++++++++ build.zig.zon | 12 +++ hk.pkl | 5 ++ hooks/env_keys.lua | 36 ++++---- hooks/post_install.lua | 176 ++++++++++++++++++++++++---------------- lib/platform.lua | 98 ++++++++++++++++++++++ lib/readline/history.h | 7 ++ lib/readline/readline.h | 9 ++ lib/readline_shim.c | 17 ++++ lib/version.lua | 15 ++++ metadata.lua | 39 ++++----- mise-tasks/test.mjs | 53 ++++++++++++ mise.toml | 5 +- 15 files changed, 480 insertions(+), 138 deletions(-) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 lib/platform.lua create mode 100644 lib/readline/history.h create mode 100644 lib/readline/readline.h create mode 100644 lib/readline_shim.c create mode 100644 lib/version.lua create mode 100644 mise-tasks/test.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebb9422 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.zig-cache/ +zig-out/ +zig-pkg/ +*.o diff --git a/README.md b/README.md index 766f9f6..9a273fb 100644 --- a/README.md +++ b/README.md @@ -6,34 +6,14 @@ A [vfox](https://github.com/version-fox/vfox) / [mise](https://mise.jdx.dev) plu - **Dynamic version fetching**: Automatically fetches available versions from lua.org - **Always up-to-date**: No static version list to maintain -- **Compiles from source**: Uses official Lua source releases +- **Compiles from source** via Zig: Cross-platform build with a single compiler - **LuaRocks included**: Automatically installs LuaRocks for Lua 5.x versions -- **Cross-platform**: Works on Linux and macOS +- **Cross-platform**: Works on Linux, macOS, and Windows ## Requirements -- A C compiler (gcc or clang) -- make -- curl - -### macOS - -```bash -xcode-select --install -``` - -### Debian/Ubuntu - -```bash -sudo apt-get install build-essential libreadline-dev -``` - -### RHEL/CentOS - -```bash -sudo yum groupinstall "Development Tools" -sudo yum install readline-devel -``` +- [Zig](https://ziglang.org/download/) 0.16.0 +- make (not needed on Windows) ## Installation @@ -81,7 +61,7 @@ This plugin: 1. Fetches the list of available versions from [lua.org/ftp](https://www.lua.org/ftp/) 2. Downloads the source tarball for the requested version -3. Compiles Lua from source using `make` +3. Copies `build.zig` and a readline shim into the source tree, then compiles with `zig build` 4. Installs LuaRocks (for Lua 5.x versions) ## License diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..3f0677d --- /dev/null +++ b/build.zig @@ -0,0 +1,112 @@ +const std = @import("std"); + +fn addExe(b: *std.Build, name: []const u8, src: []const u8, flags: []const []const u8, lib: *std.Build.Step.Compile, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }); + mod.addIncludePath(b.path("src")); + mod.addCSourceFile(.{ .file = b.path(src), .flags = flags }); + mod.linkLibrary(lib); + + if (target.result.os.tag == .linux) { + mod.linkSystemLibrary("dl", .{}); + } + + const exe = b.addExecutable(.{ .name = name, .root_module = mod }); + return exe; +} + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const readline = b.option(bool, "readline", "Link readline for REPL") orelse false; + + const src_dir = b.build_root.handle.openDir(b.graph.io, "src", .{ .iterate = true }) catch + @panic("src/ directory not found — run zig build from extracted Lua source root"); + defer src_dir.close(b.graph.io); + + var lib_srcs = std.ArrayList([]const u8).empty; + defer lib_srcs.deinit(b.allocator); + + var iter = src_dir.iterate(); + while (iter.next(b.graph.io) catch @panic("failed to read src/")) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.name, ".c")) continue; + if (std.mem.eql(u8, entry.name, "lua.c") or std.mem.eql(u8, entry.name, "luac.c")) continue; + try lib_srcs.append(b.allocator, b.dupe(entry.name)); + } + + const src_root = b.path("src"); + const os_tag = target.result.os.tag; + + var flags = std.ArrayList([]const u8).empty; + defer flags.deinit(b.allocator); + + try flags.append(b.allocator, "-std=gnu99"); + + if (os_tag == .linux or os_tag == .windows) { + try flags.append(b.allocator, "-fvisibility=hidden"); + } + + switch (os_tag) { + .linux => try flags.append(b.allocator, "-DLUA_USE_LINUX"), + .macos => try flags.append(b.allocator, "-DLUA_USE_MACOSX"), + .windows => {}, + else => try flags.append(b.allocator, "-DLUA_USE_POSIX"), + } + + if (readline) { + try flags.append(b.allocator, "-DLUA_USE_READLINE"); + } + + if (optimize == .Debug) { + try flags.append(b.allocator, "-DLUA_USE_APICHECK"); + } + + const lib_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }); + lib_module.addIncludePath(src_root); + lib_module.addCSourceFiles(.{ + .root = src_root, + .files = lib_srcs.items, + .flags = flags.items, + .language = .c, + }); + + const lib = b.addLibrary(.{ + .name = "lua", + .linkage = .static, + .root_module = lib_module, + }); + + const lua_exe = addExe(b, "lua", "src/lua.c", flags.items, lib, target, optimize); + const luac_exe = addExe(b, "luac", "src/luac.c", flags.items, lib, target, optimize); + + if (readline) { + const linenoize = b.dependency("linenoize", .{ + .target = target, + .optimize = optimize, + }); + lua_exe.root_module.addIncludePath(linenoize.path("include")); + lua_exe.root_module.addIncludePath(b.path("lib")); + lua_exe.root_module.addCSourceFile(.{ .file = b.path("lib/readline_shim.c") }); + lua_exe.root_module.linkLibrary(linenoize.artifact("linenoise")); + } + + b.installArtifact(lib); + b.installArtifact(lua_exe); + b.installArtifact(luac_exe); + + const include_install = b.addInstallDirectory(.{ + .source_dir = src_root, + .install_dir = .{ .custom = "include" }, + .install_subdir = "", + .include_extensions = &.{ "lua.h", "luaconf.h", "lualib.h", "lauxlib.h" }, + }); + b.getInstallStep().dependOn(&include_install.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..976cb3e --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .vfox_lua, + .version = "0.16.0", + .dependencies = .{ + .linenoize = .{ + .url = "git+https://github.com/hazre/linenoize.git#ddfa9a28c607b77359b2b28b84495699759ae3b5", + .hash = "linenoize-0.1.1-J7HK8IT0AABVYIWXsdseNxD1rOuQmikD0tmd1BYlTnFv", + }, + }, + .paths = .{""}, + .fingerprint = 0x6d25d9ed3c490967, +} diff --git a/hk.pkl b/hk.pkl index e4c759b..de5e3a5 100644 --- a/hk.pkl +++ b/hk.pkl @@ -11,6 +11,11 @@ local linters = new Mapping { check = "stylua --check {{files}}" fix = "stylua {{files}}" } + ["zig-fmt"] { + glob = "*.zig" + check = "zig fmt --check {{files}}" + fix = "zig fmt {{files}}" + } ["actionlint"] { glob = ".github/workflows/*.{yml,yaml}" check = "actionlint {{files}}" diff --git a/hooks/env_keys.lua b/hooks/env_keys.lua index 9d775e5..8d6cba7 100644 --- a/hooks/env_keys.lua +++ b/hooks/env_keys.lua @@ -2,33 +2,23 @@ --- @param ctx table Context provided by vfox --- @return table Environment configuration function PLUGIN:EnvKeys(ctx) - local sdkInfo = ctx.sdkInfo["lua"] - local version = sdkInfo.version - local installDir = sdkInfo.path + local version = require("version") - -- Extract major.minor version for Lua paths - local shortVersion = string.match(version, "^(%d+%.%d+)") + local sdkInfo = ctx.sdkInfo["lua"] + local installDir = sdkInfo.path:gsub("\\", "/") + local v = version.parse(sdkInfo.version) + local shortVersion = v and (v.major .. "." .. v.minor) local envs = { - { - key = "PATH", - value = installDir .. "/bin", - }, + { key = "PATH", value = installDir .. "/bin" }, + { key = "PATH", value = installDir .. "/luarocks" }, + { key = "PATH", value = installDir .. "/luarocks/bin" }, } - -- Add LuaRocks bin to PATH if it exists - local luarocksBin = installDir .. "/luarocks/bin" - local f = io.open(luarocksBin, "r") - if f ~= nil then - f:close() - table.insert(envs, { - key = "PATH", - value = luarocksBin, - }) - end - -- Set LUA_INIT for package paths (similar to asdf-lua) if shortVersion then + local soext = (RUNTIME.osType == "windows") and "dll" or "so" + local packagePath = string.format( "package.path = package.path .. ';%s/share/lua/%s/?.lua;%s/share/lua/%s/?/init.lua;%s/luarocks/share/lua/%s/?.lua;%s/luarocks/share/lua/%s/?/init.lua'", installDir, @@ -41,11 +31,13 @@ function PLUGIN:EnvKeys(ctx) shortVersion ) local packageCpath = string.format( - "package.cpath = package.cpath .. ';%s/lib/lua/%s/?.so;%s/luarocks/lib/lua/%s/?.so'", + "package.cpath = package.cpath .. ';%s/lib/lua/%s/?.%s;%s/luarocks/lib/lua/%s/?.%s'", installDir, shortVersion, + soext, installDir, - shortVersion + shortVersion, + soext ) table.insert(envs, { diff --git a/hooks/post_install.lua b/hooks/post_install.lua index 834b971..74ab7df 100644 --- a/hooks/post_install.lua +++ b/hooks/post_install.lua @@ -1,57 +1,72 @@ ---- Compiles and installs Lua from source +--- Compiles and installs Lua from source using the zig build system. --- @param ctx table Context provided by vfox --- @field ctx.sdkInfo table SDK information with version and path function PLUGIN:PostInstall(ctx) + local platform = require("platform") + local version = require("version") + local cmd = require("cmd") local http = require("http") local json = require("json") local sdkInfo = ctx.sdkInfo["lua"] local version = sdkInfo.version local sdkPath = sdkInfo.path + local sep = platform.sep - -- mise extracts tarball and strips top-level directory, so sdkPath IS the source directory - - -- Determine OS-specific make target - local os_type = RUNTIME.osType - local make_target = "guess" - - if os_type == "darwin" then - make_target = "macosx" - elseif os_type == "linux" then - -- For Lua < 5.4, use "linux", otherwise "guess" - local major, minor = string.match(version, "^(%d+)%.(%d+)") - if major and minor then - local ver_num = tonumber(major) * 100 + tonumber(minor) - if ver_num < 504 then - make_target = "linux" - end - end + local function join(...) + return table.concat({ ... }, sep) end - -- Build Lua - local major = tonumber(string.match(version, "^(%d+)")) - local buildCmd + local plug = RUNTIME.pluginDirPath + + -- Copy build.zig and readline shim into extracted source tree + platform.mkdir(join(sdkPath, "readline")) + + local files = { + "build.zig", + "build.zig.zon", + "lib/readline_shim.c", + "lib/test_readline_shim.c", + "lib/readline/readline.h", + "lib/readline/history.h", + } + for _, f in ipairs(files) do + local normal = f:gsub("/", sep) + if not platform.cp(join(plug, normal), join(sdkPath, normal)) then + error("Failed to copy " .. f) + end + end - if major and major >= 5 then - -- Lua 5.x: use make local target which creates install/ subdirectory - buildCmd = string.format("cd '%s' && make %s && make local", sdkPath, make_target) - else - -- Older versions - buildCmd = string.format("cd '%s' && make && make install INSTALL_ROOT=install", sdkPath) + -- Build Lua via zig (compiles liblua.a, lua, luac, and installs headers) + local ok, out = pcall( + cmd.exec, + "mise exec zig@0.16.0 -- zig build --build-file " + .. sdkPath + .. sep + .. "build.zig -Dreadline --prefix " + .. sdkPath + .. sep + .. "install", + { cwd = sdkPath } + ) + if not ok then + error("zig build failed:\n" .. out) end - local status = os.execute(buildCmd) - if status ~= 0 and status ~= true then - error("Failed to build Lua: make failed") + -- Move build artifacts from install/ to sdkPath root + local installDir = join(sdkPath, "install") + for _, entry in ipairs(platform.listdir(installDir)) do + platform.mv(join(installDir, entry), join(sdkPath, entry)) end - -- After make local, files are in install/ subdirectory - -- Move them to the root of sdkPath (overwriting source files is fine) - local moveCmd = string.format("cd '%s' && mv install/* . 2>/dev/null || cp -r install/* . 2>/dev/null", sdkPath) - os.execute(moveCmd) + -- Remove build artifacts and source files + for _, item in ipairs({ "install", "zig-out", ".zig-cache", "zig-pkg", "build.zig", "build.zig.zon", "readline" }) do + platform.rm(join(sdkPath, item)) + end -- Install LuaRocks for Lua 5.x - if major and major >= 5 then + local ver = version.parse(version) + if ver and ver.major >= 5 then -- Get latest LuaRocks version from GitHub releases local luarocksVersion = "3.11.1" -- Default fallback @@ -72,45 +87,70 @@ function PLUGIN:PostInstall(ctx) -- Download and install LuaRocks local luarocksUrl = "https://github.com/luarocks/luarocks/archive/refs/tags/v" .. luarocksVersion .. ".tar.gz" - local luarocksArchive = sdkPath .. "/luarocks.tar.gz" + local luarocksArchive = sdkPath .. sep .. "luarocks.tar.gz" - local downloadCmd = string.format("curl -sL '%s' -o '%s'", luarocksUrl, luarocksArchive) - status = os.execute(downloadCmd) - if status ~= 0 and status ~= true then - -- LuaRocks installation is optional, don't fail - return + local archiver = require("archiver") + local _, dlerr = http.download_file({ url = luarocksUrl }, luarocksArchive) + if dlerr then + error("failed to download luarocks: " .. tostring(dlerr)) end - local extractCmd = string.format("cd '%s' && tar xzf luarocks.tar.gz", sdkPath) - status = os.execute(extractCmd) - if status ~= 0 and status ~= true then - return + archiver.decompress(luarocksArchive, sdkPath) + + local luarocksDir = sdkPath .. sep .. "luarocks-" .. luarocksVersion + + if RUNTIME.osType == "windows" then + -- Windows: use install.bat provided by LuaRocks + platform.cp(sdkPath .. sep .. "lib" .. sep .. "lua.lib", sdkPath .. sep .. "lib" .. sep .. "lua5.4.lib") + local ok, out = pcall( + cmd.exec, + sdkPath + .. sep + .. "bin" + .. sep + .. "lua.exe install.bat /LUA " + .. sdkPath + .. " /P " + .. sdkPath + .. sep + .. "luarocks /NOADMIN /NOREG /F", + { cwd = luarocksDir } + ) + if not ok then + error("luarocks install failed:\n" .. out) + end + else + -- Unix: configure + make bootstrap + local ok, out = pcall( + cmd.exec, + "./configure --with-lua='" + .. sdkPath + .. "' --with-lua-include='" + .. sdkPath + .. "/include' --with-lua-lib='" + .. sdkPath + .. "/lib' --prefix='" + .. sdkPath + .. "/luarocks'", + { cwd = luarocksDir } + ) + if ok then + local ok2, out2 = pcall(cmd.exec, "make bootstrap", { cwd = luarocksDir }) + if not ok2 then + error("luarocks build failed:\n" .. out2) + end + end end - local luarocksDir = sdkPath .. "/luarocks-" .. luarocksVersion - local configureCmd = string.format( - "cd '%s' && ./configure --with-lua='%s' --with-lua-include='%s/include' --with-lua-lib='%s/lib' --prefix='%s/luarocks' 2>/dev/null", - luarocksDir, - sdkPath, - sdkPath, - sdkPath, - sdkPath - ) - status = os.execute(configureCmd) - if status ~= 0 and status ~= true then - -- Clean up and return without luarocks - os.execute(string.format("rm -rf '%s/luarocks.tar.gz' '%s/luarocks-'*", sdkPath, sdkPath)) - return + -- Clean up LuaRocks source and archive + platform.rm(luarocksArchive) + if platform.exists(luarocksDir) then + platform.rm(luarocksDir) end - - local bootstrapCmd = string.format("cd '%s' && make bootstrap 2>&1", luarocksDir) - os.execute(bootstrapCmd) - - -- Clean up LuaRocks source - os.execute(string.format("rm -rf '%s/luarocks.tar.gz' '%s/luarocks-'*", sdkPath, sdkPath)) end - -- Clean up Lua source files (keep only bin, lib, include, man, share, luarocks) - local cleanCmd = string.format("cd '%s' && rm -rf src doc Makefile README install 2>/dev/null", sdkPath) - os.execute(cleanCmd) + -- Clean up Lua source files, keeping only bin/, lib/, include/, share/ + for _, item in ipairs({ "src", "doc", "Makefile", "README" }) do + platform.rm(sdkPath .. sep .. item) + end end diff --git a/lib/platform.lua b/lib/platform.lua new file mode 100644 index 0000000..9dce6e5 --- /dev/null +++ b/lib/platform.lua @@ -0,0 +1,98 @@ +local os_type = RUNTIME.osType + +local M = {} + +M.sep = (os_type == "windows") and "\\" or "/" + +function M.cp(src, dst) + if os_type == "windows" then + local f = io.open(src, "rb") + if not f then + return false, "source not found: " .. src + end + local content = f:read("*a") + f:close() + f = io.open(dst, "wb") + if not f then + return false, "cannot write: " .. dst + end + f:write(content) + f:close() + return true + else + local ok = os.execute("cp '" .. src .. "' '" .. dst .. "' 2>/dev/null") + if ok ~= 0 and ok ~= true then + return false, "cp failed: " .. src .. " -> " .. dst + end + return true + end +end + +function M.mkdir(path) + if os_type == "windows" then + os.execute("mkdir " .. path .. " 2>nul") + else + os.execute("mkdir -p '" .. path .. "' 2>/dev/null") + end +end + +function M.mv(src, dst) + if os_type == "windows" then + local ok, _ = pcall(os.rename, src, dst) + if not ok then + M.cp(src, dst) + os.execute("del /f /q " .. src .. " 2>nul") + end + else + local ok = os.execute("mv '" .. src .. "' '" .. dst .. "' 2>/dev/null") + if ok ~= 0 and ok ~= true then + M.cp(src, dst) + os.execute("rm -rf '" .. src .. "' 2>/dev/null") + end + end +end + +function M.listdir(path) + local entries = {} + if os_type == "windows" then + local f = io.popen("dir /b " .. path .. " 2>nul") + if f then + for entry in f:lines() do + table.insert(entries, entry) + end + f:close() + end + else + local f = io.popen("ls -1 '" .. path .. "' 2>/dev/null") + if f then + for entry in f:lines() do + table.insert(entries, entry) + end + f:close() + end + end + return entries +end + +function M.exists(path) + local f, _, code = io.open(path) + if code == 13 or code == 5 then + return true + end + if f then + f:close() + return true + end + return false +end + +function M.rm(path) + if os_type == "windows" then + os.execute("rmdir /s /q " .. path .. " 2>nul") + os.execute("del /f /q " .. path .. " 2>nul") + else + os.execute("rm -rf '" .. path .. "' 2>/dev/null") + end +end + +return M diff --git a/lib/readline/history.h b/lib/readline/history.h new file mode 100644 index 0000000..29568c9 --- /dev/null +++ b/lib/readline/history.h @@ -0,0 +1,7 @@ +#ifndef _READLINE_HISTORY_H_ +#define _READLINE_HISTORY_H_ + +void add_history(const char *); +void using_history(void); + +#endif diff --git a/lib/readline/readline.h b/lib/readline/readline.h new file mode 100644 index 0000000..70df3d6 --- /dev/null +++ b/lib/readline/readline.h @@ -0,0 +1,9 @@ +#ifndef _READLINE_H_ +#define _READLINE_H_ + +extern char *rl_readline_name; + +char *readline(const char *); +void rl_free(void *); + +#endif diff --git a/lib/readline_shim.c b/lib/readline_shim.c new file mode 100644 index 0000000..6c10880 --- /dev/null +++ b/lib/readline_shim.c @@ -0,0 +1,17 @@ +#include +#include "linenoise.h" + +char *rl_readline_name = "linenoise"; +static int history_initialized = 0; + +char *readline(const char *prompt) { + return linenoise(prompt); +} + +void add_history(const char *line) { + if (!history_initialized) { + linenoiseHistorySetMaxLen(1000); + history_initialized = 1; + } + linenoiseHistoryAdd(line); +} diff --git a/lib/version.lua b/lib/version.lua new file mode 100644 index 0000000..e00fea6 --- /dev/null +++ b/lib/version.lua @@ -0,0 +1,15 @@ +local M = {} + +function M.parse(s) + local major, minor, patch = string.match(s, "^(%d+)%.?(%d*)%.?(%d*)$") + if not major then + return nil + end + return { + major = tonumber(major), + minor = tonumber(#minor > 0 and minor or 0), + patch = tonumber(#patch > 0 and patch or 0), + } +end + +return M diff --git a/metadata.lua b/metadata.lua index 69c12cc..fa4525c 100644 --- a/metadata.lua +++ b/metadata.lua @@ -1,24 +1,19 @@ -PLUGIN = {} -PLUGIN.name = "lua" -PLUGIN.version = "0.1.0" -PLUGIN.homepage = "https://github.com/mise-plugins/vfox-lua" -PLUGIN.license = "MIT" -PLUGIN.description = "Lua version manager - compiles from source" -PLUGIN.minRuntimeVersion = "0.3.0" -PLUGIN.notes = { - "Compiles Lua from source. Requires a C compiler (gcc/clang).", - "Automatically installs LuaRocks for Lua 5.x versions.", -} +PLUGIN = { + name = "lua", + version = "0.1.0", + homepage = "https://github.com/mise-plugins/vfox-lua", + license = "MIT", + description = "Lua version manager - compiles from source", + minRuntimeVersion = "0.3.0", + notes = { + "Compiles Lua from source. Requires zig compiler (auto-installed via mise).", + "Automatically installs LuaRocks for Lua 5.x versions.", + }, --- System prerequisites checked by mise before installing (see the `system_deps` --- setting). Detection is the source of truth; the `packages` map only provides --- remediation hints. -PLUGIN.systemDependencies = { - { bin = "cc", packages = { apt = "build-essential", dnf = "gcc" } }, - { bin = "make", packages = { brew = "make", apt = "build-essential", dnf = "make" } }, - -- LuaRocks is downloaded with curl in post_install - { bin = "curl", packages = { brew = "curl", apt = "curl", dnf = "curl" } }, - -- Lua links libreadline for interactive line editing on the default build - { pkgconfig = "readline", optional = "readline line editing", - packages = { brew = "readline", apt = "libreadline-dev", dnf = "readline-devel" } }, + -- System prerequisites checked by mise before installing (see the `system_deps` + -- setting). Detection is the source of truth; the `packages` map only provides + -- remediation hints. + systemDependencies = { + { bin = "make", packages = { brew = "make", apt = "build-essential", dnf = "make" } }, + }, } diff --git a/mise-tasks/test.mjs b/mise-tasks/test.mjs new file mode 100644 index 0000000..c340acb --- /dev/null +++ b/mise-tasks/test.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +//MISE description="Run plugin tests - install and execute tool" +//MISE tools={node="latest"} + +import { execSync, spawnSync } from "node:child_process"; + +function run(cmd, opts = {}) { + const result = execSync(cmd, { + encoding: "utf-8", + stdio: opts.silent ? "pipe" : "inherit", + timeout: 300000, + ...opts, + }); + return result?.trim() ?? ""; +} + +function test(name, fn) { + process.stdout.write(` ${name}... `); + try { + fn(); + console.log("✓"); + } catch (e) { + console.log("✗"); + console.log(` ${e.message}`); + process.exit(1); + } +} + +const LUA_VERSION = process.env.LUA_TEST_VERSION || "5.4.7"; + +run("mise plugin link --force lua ."); +run("mise cache clear"); +run(`mise install lua@${LUA_VERSION}`); + +test("lua binary reports correct version", () => { + const out = run(`mise exec lua@${LUA_VERSION} -- lua -v 2>&1`, { + stdio: "pipe", + }); + if (!out.includes(LUA_VERSION)) { + throw new Error(`expected ${LUA_VERSION} in output:\n${out}`); + } +}); + +test("luarocks is available", () => { + const out = run(`mise exec lua@${LUA_VERSION} -- luarocks --version`, { + stdio: "pipe", + }); + if (!out.toLowerCase().includes("luarocks")) { + throw new Error(`luarocks not found:\n${out}`); + } +}); + +console.log("\n✓ All tests passed"); diff --git a/mise.toml b/mise.toml index bdb971e..40e0f40 100644 --- a/mise.toml +++ b/mise.toml @@ -6,12 +6,14 @@ actionlint = "latest" hk = "latest" lua = "5.4" lua-language-server = "latest" +make = "latest" pkl = "latest" stylua = "latest" +zig = "0.16.0" [tasks.format] description = "Format Lua scripts" -run = "stylua metadata.lua hooks/" +run = "stylua metadata.lua hooks/ lib/" [tasks.lint] description = "Lint Lua scripts and GitHub Actions using hk" @@ -24,3 +26,4 @@ run = "hk fix" [tasks.ci] description = "Run all CI checks" depends = ["lint", "test"] + From 33ad9f2940103b87607a2363bb8ae0938b8bdf16 Mon Sep 17 00:00:00 2001 From: hazre Date: Fri, 10 Jul 2026 16:53:00 +0200 Subject: [PATCH 2/4] fix: address code review issues Signed-off-by: hazre --- build.zig | 4 ++-- hooks/post_install.lua | 16 +++++----------- lib/platform.lua | 12 ++++++------ lib/readline_shim.c | 6 +++++- metadata.lua | 1 + 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/build.zig b/build.zig index 3f0677d..be9fb6e 100644 --- a/build.zig +++ b/build.zig @@ -27,7 +27,7 @@ pub fn build(b: *std.Build) !void { @panic("src/ directory not found — run zig build from extracted Lua source root"); defer src_dir.close(b.graph.io); - var lib_srcs = std.ArrayList([]const u8).empty; + var lib_srcs = std.ArrayListUnmanaged([]const u8).empty; defer lib_srcs.deinit(b.allocator); var iter = src_dir.iterate(); @@ -41,7 +41,7 @@ pub fn build(b: *std.Build) !void { const src_root = b.path("src"); const os_tag = target.result.os.tag; - var flags = std.ArrayList([]const u8).empty; + var flags = std.ArrayListUnmanaged([]const u8).empty; defer flags.deinit(b.allocator); try flags.append(b.allocator, "-std=gnu99"); diff --git a/hooks/post_install.lua b/hooks/post_install.lua index 74ab7df..7c3bfbc 100644 --- a/hooks/post_install.lua +++ b/hooks/post_install.lua @@ -9,7 +9,7 @@ function PLUGIN:PostInstall(ctx) local json = require("json") local sdkInfo = ctx.sdkInfo["lua"] - local version = sdkInfo.version + local luaVersion = sdkInfo.version local sdkPath = sdkInfo.path local sep = platform.sep @@ -26,7 +26,6 @@ function PLUGIN:PostInstall(ctx) "build.zig", "build.zig.zon", "lib/readline_shim.c", - "lib/test_readline_shim.c", "lib/readline/readline.h", "lib/readline/history.h", } @@ -40,13 +39,7 @@ function PLUGIN:PostInstall(ctx) -- Build Lua via zig (compiles liblua.a, lua, luac, and installs headers) local ok, out = pcall( cmd.exec, - "mise exec zig@0.16.0 -- zig build --build-file " - .. sdkPath - .. sep - .. "build.zig -Dreadline --prefix " - .. sdkPath - .. sep - .. "install", + "zig build --build-file " .. sdkPath .. sep .. "build.zig -Dreadline --prefix " .. sdkPath .. sep .. "install", { cwd = sdkPath } ) if not ok then @@ -65,7 +58,7 @@ function PLUGIN:PostInstall(ctx) end -- Install LuaRocks for Lua 5.x - local ver = version.parse(version) + local ver = version.parse(luaVersion) if ver and ver.major >= 5 then -- Get latest LuaRocks version from GitHub releases local luarocksVersion = "3.11.1" -- Default fallback @@ -101,7 +94,8 @@ function PLUGIN:PostInstall(ctx) if RUNTIME.osType == "windows" then -- Windows: use install.bat provided by LuaRocks - platform.cp(sdkPath .. sep .. "lib" .. sep .. "lua.lib", sdkPath .. sep .. "lib" .. sep .. "lua5.4.lib") + local libName = "lua" .. luaVersion:match("^(%d+%.%d+)") .. ".lib" + platform.cp(sdkPath .. sep .. "lib" .. sep .. "lua.lib", sdkPath .. sep .. "lib" .. sep .. libName) local ok, out = pcall( cmd.exec, sdkPath diff --git a/lib/platform.lua b/lib/platform.lua index 9dce6e5..54e69d2 100644 --- a/lib/platform.lua +++ b/lib/platform.lua @@ -30,7 +30,7 @@ end function M.mkdir(path) if os_type == "windows" then - os.execute("mkdir " .. path .. " 2>nul") + os.execute('mkdir "' .. path .. '" 2>nul') else os.execute("mkdir -p '" .. path .. "' 2>/dev/null") end @@ -41,7 +41,7 @@ function M.mv(src, dst) local ok, _ = pcall(os.rename, src, dst) if not ok then M.cp(src, dst) - os.execute("del /f /q " .. src .. " 2>nul") + os.execute('del /f /q "' .. src .. '" 2>nul') end else local ok = os.execute("mv '" .. src .. "' '" .. dst .. "' 2>/dev/null") @@ -55,7 +55,7 @@ end function M.listdir(path) local entries = {} if os_type == "windows" then - local f = io.popen("dir /b " .. path .. " 2>nul") + local f = io.popen('dir /b "' .. path .. '" 2>nul') if f then for entry in f:lines() do table.insert(entries, entry) @@ -76,7 +76,7 @@ end function M.exists(path) local f, _, code = io.open(path) - if code == 13 or code == 5 then + if code == 13 or code == 5 or code == 21 then return true end if f then @@ -88,8 +88,8 @@ end function M.rm(path) if os_type == "windows" then - os.execute("rmdir /s /q " .. path .. " 2>nul") - os.execute("del /f /q " .. path .. " 2>nul") + os.execute('rmdir /s /q "' .. path .. '" 2>nul') + os.execute('del /f /q "' .. path .. '" 2>nul') else os.execute("rm -rf '" .. path .. "' 2>/dev/null") end diff --git a/lib/readline_shim.c b/lib/readline_shim.c index 6c10880..5035289 100644 --- a/lib/readline_shim.c +++ b/lib/readline_shim.c @@ -8,10 +8,14 @@ char *readline(const char *prompt) { return linenoise(prompt); } -void add_history(const char *line) { +void using_history(void) { if (!history_initialized) { linenoiseHistorySetMaxLen(1000); history_initialized = 1; } +} + +void add_history(const char *line) { + using_history(); linenoiseHistoryAdd(line); } diff --git a/metadata.lua b/metadata.lua index fa4525c..04425e8 100644 --- a/metadata.lua +++ b/metadata.lua @@ -14,6 +14,7 @@ PLUGIN = { -- setting). Detection is the source of truth; the `packages` map only provides -- remediation hints. systemDependencies = { + { bin = "zig", packages = { brew = "zig", apt = "zig", dnf = "zig" } }, { bin = "make", packages = { brew = "make", apt = "build-essential", dnf = "make" } }, }, } From eeae17933b341aae8c7d404ae14b597cdf10c783 Mon Sep 17 00:00:00 2001 From: hazre Date: Fri, 10 Jul 2026 17:11:02 +0200 Subject: [PATCH 3/4] fix: resolve Windows compatibility issues Signed-off-by: hazre --- build.zig | 4 +-- hooks/post_install.lua | 57 +++++++++++++++++++++++++++++++++--------- lib/platform.lua | 22 +++++++++++++--- mise-tasks/test.mjs | 1 + 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/build.zig b/build.zig index be9fb6e..b73e75e 100644 --- a/build.zig +++ b/build.zig @@ -93,8 +93,8 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); lua_exe.root_module.addIncludePath(linenoize.path("include")); - lua_exe.root_module.addIncludePath(b.path("lib")); - lua_exe.root_module.addCSourceFile(.{ .file = b.path("lib/readline_shim.c") }); + lua_exe.root_module.addIncludePath(b.path(".")); + lua_exe.root_module.addCSourceFile(.{ .file = b.path("readline_shim.c") }); lua_exe.root_module.linkLibrary(linenoize.artifact("linenoise")); } diff --git a/hooks/post_install.lua b/hooks/post_install.lua index 7c3bfbc..cbb5025 100644 --- a/hooks/post_install.lua +++ b/hooks/post_install.lua @@ -20,30 +20,54 @@ function PLUGIN:PostInstall(ctx) local plug = RUNTIME.pluginDirPath -- Copy build.zig and readline shim into extracted source tree + -- Shim files are flattened to root level to avoid creating lib/ before the zig install move platform.mkdir(join(sdkPath, "readline")) local files = { "build.zig", "build.zig.zon", - "lib/readline_shim.c", - "lib/readline/readline.h", - "lib/readline/history.h", + { src = "lib/readline_shim.c", dst = "readline_shim.c" }, + { src = "lib/readline/readline.h", dst = "readline/readline.h" }, + { src = "lib/readline/history.h", dst = "readline/history.h" }, } for _, f in ipairs(files) do - local normal = f:gsub("/", sep) - if not platform.cp(join(plug, normal), join(sdkPath, normal)) then - error("Failed to copy " .. f) + local src, dst + if type(f) == "string" then + src = plug .. "/" .. f + dst = sdkPath .. "/" .. f + else + src = plug .. "/" .. f.src + dst = sdkPath .. "/" .. f.dst + end + if not platform.cp(src, dst) then + error("Failed to copy " .. (type(f) == "string" and f or f.src)) end end - -- Build Lua via zig (compiles liblua.a, lua, luac, and installs headers) + -- Resolve zig path: mise which works for mise-managed zig; fallback to plain "zig" for standalone vfox + local zig_bin = "zig" + local f = io.popen("mise which zig 2>nul") + if f then + local path = f:read("*l") + f:close() + if path and #path > 0 then + zig_bin = path + end + end local ok, out = pcall( cmd.exec, - "zig build --build-file " .. sdkPath .. sep .. "build.zig -Dreadline --prefix " .. sdkPath .. sep .. "install", + zig_bin + .. " build --build-file " + .. sdkPath + .. sep + .. "build.zig -Dreadline --prefix " + .. sdkPath + .. sep + .. "install", { cwd = sdkPath } ) if not ok then - error("zig build failed:\n" .. out) + error("zig build failed: " .. tostring(out)) end -- Move build artifacts from install/ to sdkPath root @@ -53,7 +77,16 @@ function PLUGIN:PostInstall(ctx) end -- Remove build artifacts and source files - for _, item in ipairs({ "install", "zig-out", ".zig-cache", "zig-pkg", "build.zig", "build.zig.zon", "readline" }) do + for _, item in ipairs({ + "install", + "zig-out", + ".zig-cache", + "zig-pkg", + "build.zig", + "build.zig.zon", + "readline", + "readline_shim.c", + }) do platform.rm(join(sdkPath, item)) end @@ -111,7 +144,7 @@ function PLUGIN:PostInstall(ctx) { cwd = luarocksDir } ) if not ok then - error("luarocks install failed:\n" .. out) + error("luarocks install failed:\n" .. tostring(out)) end else -- Unix: configure + make bootstrap @@ -131,7 +164,7 @@ function PLUGIN:PostInstall(ctx) if ok then local ok2, out2 = pcall(cmd.exec, "make bootstrap", { cwd = luarocksDir }) if not ok2 then - error("luarocks build failed:\n" .. out2) + error("luarocks build failed:\n" .. tostring(out2)) end end end diff --git a/lib/platform.lua b/lib/platform.lua index 54e69d2..9ad4a18 100644 --- a/lib/platform.lua +++ b/lib/platform.lua @@ -6,6 +6,8 @@ M.sep = (os_type == "windows") and "\\" or "/" function M.cp(src, dst) if os_type == "windows" then + src = src:gsub("\\", "/") + dst = dst:gsub("\\", "/") local f = io.open(src, "rb") if not f then return false, "source not found: " .. src @@ -30,7 +32,10 @@ end function M.mkdir(path) if os_type == "windows" then - os.execute('mkdir "' .. path .. '" 2>nul') + local p = io.popen('mkdir "' .. path .. '" 2>nul') + if p then + p:close() + end else os.execute("mkdir -p '" .. path .. "' 2>/dev/null") end @@ -41,7 +46,10 @@ function M.mv(src, dst) local ok, _ = pcall(os.rename, src, dst) if not ok then M.cp(src, dst) - os.execute('del /f /q "' .. src .. '" 2>nul') + local p = io.popen('del /f /q "' .. src .. '" 2>nul') + if p then + p:close() + end end else local ok = os.execute("mv '" .. src .. "' '" .. dst .. "' 2>/dev/null") @@ -88,8 +96,14 @@ end function M.rm(path) if os_type == "windows" then - os.execute('rmdir /s /q "' .. path .. '" 2>nul') - os.execute('del /f /q "' .. path .. '" 2>nul') + local p = io.popen('rmdir /s /q "' .. path .. '" 2>nul') + if p then + p:close() + end + p = io.popen('del /f /q "' .. path .. '" 2>nul') + if p then + p:close() + end else os.execute("rm -rf '" .. path .. "' 2>/dev/null") end diff --git a/mise-tasks/test.mjs b/mise-tasks/test.mjs index c340acb..4c11880 100644 --- a/mise-tasks/test.mjs +++ b/mise-tasks/test.mjs @@ -30,6 +30,7 @@ const LUA_VERSION = process.env.LUA_TEST_VERSION || "5.4.7"; run("mise plugin link --force lua ."); run("mise cache clear"); +run(`mise uninstall lua@${LUA_VERSION}`); run(`mise install lua@${LUA_VERSION}`); test("lua binary reports correct version", () => { From bf0681e13d47753bdc0b29d992cc13933b68950a Mon Sep 17 00:00:00 2001 From: hazre Date: Fri, 10 Jul 2026 17:58:34 +0200 Subject: [PATCH 4/4] fix: disable UBSan in debug builds for luarocks module compatibility Signed-off-by: hazre --- build.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/build.zig b/build.zig index b73e75e..9adf6a2 100644 --- a/build.zig +++ b/build.zig @@ -63,6 +63,7 @@ pub fn build(b: *std.Build) !void { if (optimize == .Debug) { try flags.append(b.allocator, "-DLUA_USE_APICHECK"); + try flags.append(b.allocator, "-fno-sanitize=undefined"); } const lib_module = b.createModule(.{