From 634c03e2f5759e9daa28f880a92f7eb4f2f1de54 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sat, 18 Apr 2026 18:55:00 -0700 Subject: [PATCH 01/13] chore: split out json, process, path, fs, env packages They now live in their own repos. --- AGENTS.md | 24 +- CLAUDE.md | 6 +- benchmarks/lde.json | 2 +- benchmarks/src/init.lua | 11 +- packages/archive/lde.json | 10 +- packages/env/lde.json | 7 - packages/env/src/init.lua | 38 - packages/env/src/raw/linux.lua | 20 - packages/env/src/raw/macos.lua | 20 - packages/env/src/raw/posix.lua | 48 - packages/env/src/raw/windows.lua | 85 -- packages/fs/lde.json | 11 - packages/fs/src/init.lua | 237 ----- packages/fs/src/raw/linux.lua | 213 ----- packages/fs/src/raw/macos.lua | 324 ------- packages/fs/src/raw/posix.lua | 142 --- packages/fs/src/raw/windows.lua | 586 ------------ packages/fs/tests/fs.test.lua | 699 -------------- packages/json/benchmarks/lde.json | 10 - packages/json/benchmarks/src/init.lua | 117 --- packages/json/lde.json | 4 - packages/json/src/init.lua | 922 ------------------- packages/json/tests/json.test.lua | 219 ----- packages/lde-core/lde.json | 10 +- packages/lde-core/src/global/init.lua | 2 +- packages/lde-core/src/package/init.lua | 2 +- packages/lde-core/src/package/rockspec.lua | 2 +- packages/lde-core/src/package/run.lua | 2 +- packages/lde-core/tests/commonrocks.test.lua | 1 - packages/lde-core/tests/compile.test.lua | 2 +- packages/lde/lde.json | 10 +- packages/lde/src/commands/compile.lua | 1 - packages/lde/src/commands/publish.lua | 2 +- packages/lde/src/commands/run.lua | 2 +- packages/lde/src/commands/uninstall.lua | 1 - packages/lde/src/init.lua | 4 +- packages/lde/src/setup.lua | 2 +- packages/lde/tests/lib/ldecli.lua | 2 +- packages/path/lde.json | 4 - packages/path/src/init.lua | 131 --- packages/process2/.gitignore | 2 - packages/process2/lde.json | 8 - packages/process2/src/init.lua | 120 --- packages/process2/src/raw/posix.lua | 214 ----- packages/process2/src/raw/windows.lua | 341 ------- packages/process2/tests/process2.test.lua | 182 ---- packages/sea/lde.json | 8 +- packages/sea/src/init.lua | 2 +- 48 files changed, 55 insertions(+), 4757 deletions(-) delete mode 100644 packages/env/lde.json delete mode 100644 packages/env/src/init.lua delete mode 100644 packages/env/src/raw/linux.lua delete mode 100644 packages/env/src/raw/macos.lua delete mode 100644 packages/env/src/raw/posix.lua delete mode 100644 packages/env/src/raw/windows.lua delete mode 100644 packages/fs/lde.json delete mode 100644 packages/fs/src/init.lua delete mode 100644 packages/fs/src/raw/linux.lua delete mode 100644 packages/fs/src/raw/macos.lua delete mode 100644 packages/fs/src/raw/posix.lua delete mode 100644 packages/fs/src/raw/windows.lua delete mode 100644 packages/fs/tests/fs.test.lua delete mode 100644 packages/json/benchmarks/lde.json delete mode 100644 packages/json/benchmarks/src/init.lua delete mode 100644 packages/json/lde.json delete mode 100644 packages/json/src/init.lua delete mode 100644 packages/json/tests/json.test.lua delete mode 100644 packages/path/lde.json delete mode 100644 packages/path/src/init.lua delete mode 100644 packages/process2/.gitignore delete mode 100644 packages/process2/lde.json delete mode 100644 packages/process2/src/init.lua delete mode 100644 packages/process2/src/raw/posix.lua delete mode 100644 packages/process2/src/raw/windows.lua delete mode 100644 packages/process2/tests/process2.test.lua diff --git a/AGENTS.md b/AGENTS.md index c6685149..1691a3e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ packages/ lde/ # The CLI binary itself (entry: src/init.lua) lde-core/ # Core library: Package, Lockfile, runtime, install logic lde-test/ # Built-in test framework - ansi/clap/env/fs/git/http/json/path/process2/semver/util/ # Internal packages + ansi/clap/env/fs/git/http/json/path/process/semver/util/ # Internal packages sea/ # Single-executable assembly (compiles bundles into binaries) archive/ # Archive extraction support luarocks/ # LuaRocks integration @@ -37,6 +37,7 @@ The key: **the require name is the key in `lde.json` `dependencies`, not the pac During `lde test`, the runner automatically exposes the package's `tests/` directory as `target/tests` (symlinked or copied). This means test files can `require("tests.lib.something")` to share helpers across test files. Example from `packages/lde/tests/main.test.lua`: + ```lua local ldecli = require("tests.lib.ldecli") ``` @@ -107,6 +108,7 @@ This outputs `packages/lde/lde` (or `lde.exe` on Windows). To install it, copy i ## Runtime Isolation (`lde-core.runtime`) `lde run` / `lde test` execute scripts in an isolated environment: + - `package.loaded` is cleared of non-builtins before and restored after each run. - A fresh `_G` metatable is created per execution (`setfenv`). - `ffi.cdef` is patched to silently ignore "attempt to redefine" errors (safe for repeated runs in the same process). @@ -116,16 +118,16 @@ This means multiple `lde run` calls in the same process don't pollute each other ## Key Packages -| Package | Purpose | -|---|---| -| `lde-core` | `Package`, `Lockfile`, install/build/run/test/compile logic | -| `lde-test` | Test framework (`require("lde-test")` in test files) | -| `clap` | CLI argument parsing (`args:option()`, `args:flag()`, `args:pop()`) | -| `ansi` | Colored terminal output (`ansi.printf("{red}msg")`) | -| `fs` | Filesystem ops (`read`, `write`, `mkdir`, `mklink`, `scan`, `stat`) | -| `process2` | Process execution (`process.exec(bin, args, opts)`) | -| `sea` | Compiles a bundled Lua string + native libs into a self-contained binary | -| `env` | Env vars, cwd, `env.execPath()` | +| Package | Purpose | +| ---------- | ------------------------------------------------------------------------ | +| `lde-core` | `Package`, `Lockfile`, install/build/run/test/compile logic | +| `lde-test` | Test framework (`require("lde-test")` in test files) | +| `clap` | CLI argument parsing (`args:option()`, `args:flag()`, `args:pop()`) | +| `ansi` | Colored terminal output (`ansi.printf("{red}msg")`) | +| `fs` | Filesystem ops (`read`, `write`, `mkdir`, `mklink`, `scan`, `stat`) | +| `process` | Process execution (`process.exec(bin, args, opts)`) | +| `sea` | Compiles a bundled Lua string + native libs into a self-contained binary | +| `env` | Env vars, cwd, `env.execPath()` | ## Monorepo Conventions diff --git a/CLAUDE.md b/CLAUDE.md index 835c206a..e49b8254 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ packages/ lde/ # The CLI binary itself (entry: src/init.lua) lde-core/ # Core library: Package, Lockfile, runtime, install logic lde-test/ # Built-in test framework - ansi/clap/env/fs/git/http/json/path/process2/semver/util/ # Internal packages + ansi/clap/env/fs/git/http/json/path/process/semver/util/ # Internal packages sea/ # Single-executable assembly (compiles bundles into binaries) archive/ # Archive extraction support luarocks/ # LuaRocks integration @@ -213,7 +213,7 @@ args:drain(start) -- returns and removes all remaining args (or from i args:count() -- number of remaining args ``` -### `process2` +### `process` ```lua -- Blocking execution @@ -228,7 +228,7 @@ child:kill(force) -- opts: { cwd, env, stdin, stdout, stderr } -- stdout/stderr: "pipe" (default for exec), "inherit", "null" -process2.platform -- "linux", "darwin", "win32", "unix" +process.platform -- "linux", "darwin", "win32", "unix" ``` ### `env` diff --git a/benchmarks/lde.json b/benchmarks/lde.json index 93576ec8..2dcea32e 100644 --- a/benchmarks/lde.json +++ b/benchmarks/lde.json @@ -2,7 +2,7 @@ "name": "benchmarks", "version": "0.1.0", "dependencies": { - "process2": { "path": "../packages/process2" }, + "process": { "git": "https://github.com/lde-org/process" }, "ansi": { "path": "../packages/ansi" } } } diff --git a/benchmarks/src/init.lua b/benchmarks/src/init.lua index 4fe07985..1cb5e236 100644 --- a/benchmarks/src/init.lua +++ b/benchmarks/src/init.lua @@ -1,6 +1,6 @@ local ffi = require("ffi") -local process = require("process2") +local process = require("process") local ansi = require("ansi") ---@type fun(): number @@ -91,7 +91,8 @@ local function runBenchmarks(tool, tmpdir) bench("install busted (cold)", function() local code, _, stderr if tool == "lde" then - code, _, stderr = process.exec("lde", { "--tree", tmpdir .. "/lde", "install", "rocks:busted" }, { stdout = "null" }) + code, _, stderr = process.exec("lde", { "--tree", tmpdir .. "/lde", "install", "rocks:busted" }, + { stdout = "null" }) elseif tool == "luarocks" then code, _, stderr = process.exec("luarocks", { "--tree", tmpdir .. "/rocks", "install", "busted" }) elseif tool == "lx" then @@ -103,7 +104,8 @@ local function runBenchmarks(tool, tmpdir) bench("install busted (warm)", function() local code, _, stderr if tool == "lde" then - code, _, stderr = process.exec("lde", { "--tree", tmpdir .. "/lde", "install", "rocks:busted" }, { stdout = "null" }) + code, _, stderr = process.exec("lde", { "--tree", tmpdir .. "/lde", "install", "rocks:busted" }, + { stdout = "null" }) elseif tool == "luarocks" then code, _, stderr = process.exec("luarocks", { "--tree", tmpdir .. "/rocks", "install", "busted" }) elseif tool == "lx" then @@ -115,7 +117,8 @@ local function runBenchmarks(tool, tmpdir) bench("build C rock (luafilesystem)", function() local code, _, stderr if tool == "lde" then - code, _, stderr = process.exec("lde", { "--tree", tmpdir .. "/lde", "install", "rocks:luafilesystem" }, { stdout = "null" }) + code, _, stderr = process.exec("lde", { "--tree", tmpdir .. "/lde", "install", "rocks:luafilesystem" }, + { stdout = "null" }) elseif tool == "luarocks" then code, _, stderr = process.exec("luarocks", { "--tree", tmpdir .. "/rocks", "install", "luafilesystem" }) elseif tool == "lx" then diff --git a/packages/archive/lde.json b/packages/archive/lde.json index bb814856..4ba58c83 100644 --- a/packages/archive/lde.json +++ b/packages/archive/lde.json @@ -3,14 +3,14 @@ "description": "Archive extraction library. Reads magic bytes to detect zip vs tar.", "version": "0.1.0", "dependencies": { - "fs": { "path": "../fs" }, - "path": { "path": "../path" }, + "fs": { "git": "https://github.com/lde-org/fs" }, + "path": { "git": "https://github.com/lde-org/path" }, "deflate-sys": { "git": "https://github.com/lde-org/deflate-sys" } }, "devDependencies": { "lde-test": { "path": "../lde-test" }, - "fs": { "path": "../fs" }, - "env": { "path": "../env" }, - "path": { "path": "../path" } + "fs": { "git": "https://github.com/lde-org/fs" }, + "env": { "git": "https://github.com/lde-org/env" }, + "path": { "git": "https://github.com/lde-org/path" } } } diff --git a/packages/env/lde.json b/packages/env/lde.json deleted file mode 100644 index 9044a423..00000000 --- a/packages/env/lde.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "env", - "version": "0.1.0", - "dependencies": { - "path": { "path": "../path" } - } -} diff --git a/packages/env/src/init.lua b/packages/env/src/init.lua deleted file mode 100644 index 66799b5c..00000000 --- a/packages/env/src/init.lua +++ /dev/null @@ -1,38 +0,0 @@ -local path = require("path") - ----@class env.raw ----@field var fun(name: string): string? ----@field set fun(name: string, value: string?): boolean ----@field tmpdir fun(): string ----@field cwd fun(): string ----@field chdir fun(dir: string): boolean ----@field execPath fun(): string? - -local rawenv ---@type env.raw -if jit.os == "Windows" then - rawenv = require("env.raw.windows") -elseif jit.os == "Linux" then - rawenv = require("env.raw.linux") -elseif jit.os == "OSX" then - rawenv = require("env.raw.macos") -else - error("Unsupported OS: " .. jit.os) -end - ----@class env: env.raw -local env = {} - -for k, v in pairs(rawenv) do - env[k] = v -end - -local tmpCounter = 0 - ---- Returns a unique temporary file path. ---- Safe replacement for os.tmpname() which can segfault in compiled LuaJIT on Windows. -function env.tmpfile() - tmpCounter = tmpCounter + 1 - return path.join(env.tmpdir(), string.format("luaenv_%d_%d.tmp", os.clock() * 1000, tmpCounter)) -end - -return env diff --git a/packages/env/src/raw/linux.lua b/packages/env/src/raw/linux.lua deleted file mode 100644 index ddddcd4a..00000000 --- a/packages/env/src/raw/linux.lua +++ /dev/null @@ -1,20 +0,0 @@ -local ffi = require("ffi") - ----@class env.raw.linux: env.raw.posix -local env = require("env.raw.posix") - -ffi.cdef([[ - ssize_t readlink(const char* path, char* buf, size_t bufsiz); -]]) - -function env.execPath() - local buf = ffi.new("char[?]", 4096) - local len = ffi.C.readlink("/proc/self/exe", buf, 4096) - if len == -1 then - return nil - end - - return ffi.string(buf, len) -end - -return env diff --git a/packages/env/src/raw/macos.lua b/packages/env/src/raw/macos.lua deleted file mode 100644 index d02d04e9..00000000 --- a/packages/env/src/raw/macos.lua +++ /dev/null @@ -1,20 +0,0 @@ -local ffi = require("ffi") - ----@class env.raw.macos: env.raw.posix -local env = require("env.raw.posix") - -ffi.cdef([[ - int _NSGetExecutablePath(char* buf, uint32_t* bufsize); -]]) - -function env.execPath() - local size = ffi.new("uint32_t[1]", 4096) - local buf = ffi.new("char[?]", size[0]) - if ffi.C._NSGetExecutablePath(buf, size) ~= 0 then - return nil - end - - return ffi.string(buf) -end - -return env diff --git a/packages/env/src/raw/posix.lua b/packages/env/src/raw/posix.lua deleted file mode 100644 index bafe8111..00000000 --- a/packages/env/src/raw/posix.lua +++ /dev/null @@ -1,48 +0,0 @@ -local ffi = require("ffi") - -ffi.cdef([[ - char* getenv(const char* name); - int setenv(const char* name, const char* value, int overwrite); - char* getcwd(char* buf, size_t size); - int chdir(const char* path); -]]) - ----@class env.raw.posix -local env = {} - ----@param name string -function env.var(name) ---@return string? - local v = ffi.C.getenv(name) - if v == nil then - return nil - end - - return ffi.string(v) -end - ----@param name string ----@param value string -function env.set(name, value) ---@return boolean - return ffi.C.setenv(name, value, 1) == 0 -end - -function env.tmpdir() - return env.var("TMPDIR") or "/tmp" -end - -function env.cwd() - local buf = ffi.new("char[?]", 4096) - - local result = ffi.C.getcwd(buf, 4096) - if result == nil then - return nil - end - - return ffi.string(buf) -end - -function env.chdir(dir) ---@return boolean - return ffi.C.chdir(dir) == 0 -end - -return env diff --git a/packages/env/src/raw/windows.lua b/packages/env/src/raw/windows.lua deleted file mode 100644 index 2325d4c3..00000000 --- a/packages/env/src/raw/windows.lua +++ /dev/null @@ -1,85 +0,0 @@ -local ffi = require("ffi") - -ffi.cdef([[ - typedef void* HANDLE; - typedef uint32_t DWORD; - typedef uint16_t WORD; - typedef unsigned char BYTE; - typedef int BOOL; - - DWORD GetEnvironmentVariableA(const char* lpName, char* lpBuffer, DWORD nSize); - BOOL SetEnvironmentVariableA(const char* lpName, const char* lpValue); - DWORD GetCurrentDirectoryA(DWORD nBufferLength, char* lpBuffer); - BOOL SetCurrentDirectoryA(const char* lpPathName); - DWORD GetModuleFileNameA(void* hModule, char* lpFilename, DWORD nSize); - - int _putenv_s(const char* name, const char* value); -]]) - -local kernel32 = ffi.load("kernel32") - ----@class env.raw.windows -local env = {} - ----@param name string -function env.var(name) ---@return string? - local bufSize = 1024 - local buf = ffi.new("char[?]", bufSize) - local len = kernel32.GetEnvironmentVariableA(name, buf, bufSize) - - if len == 0 then - return nil - end - - if len > bufSize then - bufSize = len - buf = ffi.new("char[?]", bufSize) - len = kernel32.GetEnvironmentVariableA(name, buf, bufSize) - if len == 0 then - return nil - end - end - - return ffi.string(buf, len) -end - ----@param name string ----@param value string? -function env.set(name, value) ---@return boolean - local result = kernel32.SetEnvironmentVariableA(name, value) - -- Also update the CRT environment so os.getenv() stays in sync - ffi.C._putenv_s(name, value or "") - return result ~= 0 -end - -function env.tmpdir() - return env.var("TEMP") or env.var("TMP") or "C:\\Windows\\Temp" -end - -function env.cwd() - local buf = ffi.new("char[?]", 4096) - local len = kernel32.GetCurrentDirectoryA(4096, buf) - - if len == 0 then - return nil - end - - return ffi.string(buf, len) -end - -function env.chdir(dir) ---@return boolean - return kernel32.SetCurrentDirectoryA(dir) ~= 0 -end - -function env.execPath() - local buf = ffi.new("char[?]", 4096) - local len = kernel32.GetModuleFileNameA(nil, buf, 4096) - - if len == 0 then - return nil - end - - return ffi.string(buf, len) -end - -return env diff --git a/packages/fs/lde.json b/packages/fs/lde.json deleted file mode 100644 index 96f0df7b..00000000 --- a/packages/fs/lde.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "fs", - "version": "0.1.0", - "dependencies": {}, - "devDependencies": { - "lde-test": { "path": "../lde-test" }, - "fs": { "path": "../fs" }, - "env": { "path": "../env" }, - "path": { "path": "../path" } - } -} diff --git a/packages/fs/src/init.lua b/packages/fs/src/init.lua deleted file mode 100644 index a6c72bb4..00000000 --- a/packages/fs/src/init.lua +++ /dev/null @@ -1,237 +0,0 @@ -local path = require("path") - ----@class fs.Stat ----@field size number # Size in bytes ----@field accessTime number ----@field modifyTime number ----@field type fs.Stat.Type? ----@field mode number? # Permission bits (Unix only) - ----@alias fs.Stat.Type fs.DirEntry.Type ----@alias fs.DirEntry.Type "file" | "dir" | "symlink" | "unknown" - ----@class fs.DirEntry ----@field name string ----@field type fs.DirEntry.Type - ----@alias fs.WatchEvent "create" | "modify" | "delete" | "rename" - ----@class fs.Watcher ----@field poll fun() ----@field wait fun() ----@field close fun() - ----@class fs.raw ----@field exists fun(p: string): boolean ----@field isdir fun(p: string): boolean ----@field islink fun(p: string): boolean ----@field isfile fun(p: string): boolean ----@field readdir fun(p: string): (fun(): fs.DirEntry?)? ----@field mkdir fun(p: string): boolean ----@field mklink fun(src: string, dest: string): boolean ----@field rmlink fun(p: string): boolean ----@field stat fun(p: string): fs.Stat? ----@field lstat fun(p: string): fs.Stat? ----@field watch fun(p: string, callback: fun(event: fs.WatchEvent, name: string), opts: { recursive: boolean? }?): fs.Watcher? - -local rawfs ---@type fs.raw -if jit.os == "Windows" then - rawfs = require("fs.raw.windows") -elseif jit.os == "Linux" then - rawfs = require("fs.raw.linux") -elseif jit.os == "OSX" then - rawfs = require("fs.raw.macos") -else - error("Unsupported OS: " .. jit.os) -end - ----@class fs: fs.raw -local fs = {} - -for k, v in pairs(rawfs) do - fs[k] = v -end - ----@param p string ----@return string|nil -function fs.read(p) - local file = io.open(p, "rb") - if not file then - return nil - end - - local content = file:read("*a") - file:close() - - return content -end - ----@param p string ----@param content string ----@return boolean -function fs.write(p, content) - local file = io.open(p, "wb") - if not file then - return false - end - - file:write(content) - file:close() - - return true -end - ----@param src string ----@param dest string -function fs.copy(src, dest) - if fs.isfile(src) then - local content = fs.read(src) - if not content then return false end - fs.write(dest, content) - return true - end - - local iter = fs.readdir(src) - if not iter then return false end - if not fs.isdir(dest) and not fs.mkdir(dest) then return false end - - for entry in iter do - local srcPath = path.join(src, entry.name) - local destPath = path.join(dest, entry.name) - - local r = fs.copy(srcPath, destPath) - if not r then - return false - end - end - - return true -end - ----@param old string ----@param new string -function fs.move(old, new) - -- Fast path: os.rename works for both files and dirs on same device - if os.rename(old, new) then - return true - end - - -- Fallback to copy+delete for cross-device moves - if not fs.copy(old, new) then return false, "Failed to copy" end - local ok = fs.isdir(old) and fs.rmdir(old) or fs.delete(old) - if not ok then return false, "Failed to delete" end - - return true -end - ----@param p string -function fs.delete(p) - return os.remove(p) ~= nil -end - ---- Recursively removes a directory and all its contents. ----@param dir string ----@return boolean -function fs.rmdir(dir) - if not fs.exists(dir) then return false end - - -- Symlinks/junctions: remove the link itself without recursing into the target. - -- On Windows, junctions require RemoveDirectoryA (not DeleteFileA/os.remove). - if fs.islink(dir) then - return fs.rmlink(dir) - end - - local iter = fs.readdir(dir) - if not iter then return false end - - for entry in iter do - local full = path.join(dir, entry.name) - if entry.type == "symlink" then - fs.rmlink(full) - elseif entry.type == "dir" then - fs.rmdir(full) - else - os.remove(full) - end - end - - return fs.rmlink(dir) -end - -local sep = string.sub(package.config, 1, 1) - ----@param glob string -function fs.globToPattern(glob) - local pattern = glob - :gsub("([%^%$%(%)%%%.%[%]%+%-])", "%%%1") - :gsub("%*%*", "\001") - :gsub("%*", "[^/\\]*") - :gsub("%?", "[^/\\]") - :gsub("\001", ".*") - - return "^" .. pattern .. "$" -end - ----@param cwd string ----@param glob string ----@param opts { absolute: boolean, followSymlinks: boolean }? ----@return string[] -function fs.scan(cwd, glob, opts) - if not fs.isdir(cwd) then - error("not a directory: '" .. cwd .. "'") - end - - local absolute = opts and opts.absolute or false - local followSymlinks = opts and opts.followSymlinks or false - - local pattern = fs.globToPattern(glob) - local entries = {} - - local function dir(p) - local dirIter = fs.readdir(p) - if not dirIter then - return - end - - for entry in dirIter do - local entryPath = p .. sep .. entry.name - local entryType = entry.type - - -- d_type can be DT_UNKNOWN on some filesystems; fall back to lstat - if entryType == "unknown" then - local s = fs.lstat(entryPath) - entryType = s and s.type or "unknown" - end - - -- Resolve symlinks when followSymlinks is set - if entryType == "symlink" and followSymlinks then - local s = fs.stat(entryPath) - entryType = s and s.type or "unknown" - end - - if entryType == "dir" then - dir(entryPath) - elseif entryType == "file" then - if string.find(entryPath, pattern) then - if absolute then - entries[#entries + 1] = entryPath - else - entries[#entries + 1] = path.relative(cwd, entryPath) - end - end - end - end - end - - dir(cwd) - return entries -end - ----@param dir string -function fs.mkdirAll(dir) - if fs.isdir(dir) then return end - fs.mkdirAll(path.dirname(dir)) - fs.mkdir(dir) -end - -return fs diff --git a/packages/fs/src/raw/linux.lua b/packages/fs/src/raw/linux.lua deleted file mode 100644 index fcf2bb76..00000000 --- a/packages/fs/src/raw/linux.lua +++ /dev/null @@ -1,213 +0,0 @@ -local ffi = require("ffi") - -if jit.arch == "x64" then - ffi.cdef([[ - struct stat { - unsigned long st_dev; - unsigned long st_ino; - unsigned long st_nlink; - unsigned int st_mode; - unsigned int st_uid; - unsigned int st_gid; - unsigned int __pad0; - unsigned long st_rdev; - long st_size; - long st_blksize; - long st_blocks; - unsigned long st_atime; - unsigned long st_atime_nsec; - unsigned long st_mtime; - unsigned long st_mtime_nsec; - unsigned long st_ctime; - unsigned long st_ctime_nsec; - long __unused[3]; - }; - ]]) -elseif jit.arch == "arm64" then - ffi.cdef([[ - struct stat { - unsigned long st_dev; - unsigned long st_ino; - unsigned int st_mode; - unsigned int st_nlink; - unsigned int st_uid; - unsigned int st_gid; - unsigned long st_rdev; - unsigned long __pad1; - long st_size; - int st_blksize; - int __pad2; - long st_blocks; - long st_atime; - unsigned long st_atime_nsec; - long st_mtime; - unsigned long st_mtime_nsec; - long st_ctime; - unsigned long st_ctime_nsec; - unsigned int __unused[2]; - }; - ]]) -else - error("Unsupported architecture: " .. jit.arch) -end - -ffi.cdef([[ - struct dirent { - unsigned long d_ino; - unsigned long d_off; - unsigned short d_reclen; - unsigned char d_type; - char d_name[256]; - }; -]]) - -pcall(ffi.cdef, [[ - int inotify_init1(int flags); - int inotify_add_watch(int fd, const char* pathname, uint32_t mask); - int inotify_rm_watch(int fd, int wd); - long read(int fd, void* buf, size_t count); - int close(int fd); -]]) - -local IN_CREATE = 0x00000100 -local IN_DELETE = 0x00000200 -local IN_MODIFY = 0x00000002 -local IN_MOVED_FROM = 0x00000040 -local IN_MOVED_TO = 0x00000080 -local IN_NONBLOCK = 0x800 - -pcall(ffi.cdef, [[ - int fcntl(int fd, int cmd, ...); -]]) -local F_GETFL = 3 -local F_SETFL = 4 -local O_NONBLOCK = 0x800 - ----@class fs.raw.linux: fs.raw.posix -local fs = require("fs.raw.posix")(function(s, modeToStatType) - return { - size = s.st_size, - modifyTime = s.st_mtime, - accessTime = s.st_atime, - type = modeToStatType[bit.band(s.st_mode, 0xF000)], - mode = bit.band(s.st_mode, 0x1FF) - } -end) - ----@alias fs.WatchEvent "create" | "modify" | "delete" | "rename" - ----@class fs.Watcher ----@field close fun() ----@field poll fun() ----@field wait fun() - ---- Watch a path for changes. Calls callback(event, name) for each change. ---- Returns a watcher with :poll() (non-blocking), :wait() (blocking), and :close(). ----@param p string ----@param callback fun(event: fs.WatchEvent, name: string) ----@param opts { recursive: boolean? }? ----@return fs.Watcher? -function fs.watch(p, callback, opts) - local recursive = opts and opts.recursive or false - - local ifd = ffi.C.inotify_init1(IN_NONBLOCK) - if ifd < 0 then return nil end - - local mask = bit.bor(IN_CREATE, IN_DELETE, IN_MODIFY, IN_MOVED_FROM, IN_MOVED_TO) - - -- wd -> absolute dir path, for resolving event names in recursive mode - local wdPaths = {} ---@type table - - local function addWatch(dir) - local wd = ffi.C.inotify_add_watch(ifd, dir, mask) - if wd >= 0 then wdPaths[tonumber(wd)] = dir end - return wd - end - - if addWatch(p) < 0 then - ffi.C.close(ifd) - return nil - end - - if recursive then - local function walkDirs(dir) - local iter = fs.readdir(dir) - if not iter then return end - for entry in iter do - if entry.type == "dir" then - local sub = dir .. "/" .. entry.name - addWatch(sub) - walkDirs(sub) - end - end - end - walkDirs(p) - end - - local bufSize = 4096 - local buf = ffi.new("uint8_t[?]", bufSize) - - local function drain() - local n = ffi.C.read(ifd, buf, bufSize) - if n <= 0 then return end - - local i = 0 - while i < n do - local ptr = buf + i - local wd = ffi.cast("int32_t*", ptr)[0] - local evMask = ffi.cast("uint32_t*", ptr + 4)[0] - local nameLen = ffi.cast("uint32_t*", ptr + 12)[0] - local name = nameLen > 0 and ffi.string(ptr + 16) or "" - - -- In recursive mode, prefix name with the relative subdir path - if recursive and wdPaths[tonumber(wd)] and wdPaths[tonumber(wd)] ~= p then - local rel = string.sub(wdPaths[tonumber(wd)], #p + 2) - name = rel .. "/" .. name - end - - local event ---@type fs.WatchEvent - if bit.band(evMask, IN_CREATE) ~= 0 then - event = "create" - -- Watch newly created subdirectories - if recursive and bit.band(evMask, 0x40000000) ~= 0 then -- IN_ISDIR - local newDir = wdPaths[tonumber(wd)] .. "/" .. (nameLen > 0 and ffi.string(ptr + 16) or "") - addWatch(newDir) - end - elseif bit.band(evMask, IN_DELETE) ~= 0 then - event = "delete" - elseif bit.band(evMask, IN_MODIFY) ~= 0 then - event = "modify" - elseif bit.band(evMask, bit.bor(IN_MOVED_FROM, IN_MOVED_TO)) ~= 0 then - event = "rename" - end - - if event then callback(event, name) end - i = i + 16 + nameLen - end - end - - ---@type fs.Watcher - local watcher = {} - - function watcher.poll() - drain() - end - - function watcher.wait() - local flags = ffi.C.fcntl(ifd, F_GETFL) - ffi.C.fcntl(ifd, F_SETFL, bit.band(flags, bit.bnot(O_NONBLOCK))) - drain() - ffi.C.fcntl(ifd, F_SETFL, flags) - end - - function watcher.close() - for wd in pairs(wdPaths) do - ffi.C.inotify_rm_watch(ifd, wd) - end - ffi.C.close(ifd) - end - - return watcher -end - -return fs diff --git a/packages/fs/src/raw/macos.lua b/packages/fs/src/raw/macos.lua deleted file mode 100644 index 59cab97c..00000000 --- a/packages/fs/src/raw/macos.lua +++ /dev/null @@ -1,324 +0,0 @@ -local ffi = require("ffi") - -ffi.cdef([[typedef uint64_t ino_t;]]) - -ffi.cdef([[ - struct timespec { - long tv_sec; - long tv_nsec; - }; -]]) - -if jit.arch == "arm64" then - ffi.cdef([[ - struct stat { - int32_t st_dev; - uint16_t st_mode; - uint16_t st_nlink; - ino_t st_ino; - uint32_t st_uid; - uint32_t st_gid; - int32_t st_rdev; - struct timespec st_atimespec; - struct timespec st_mtimespec; - struct timespec st_ctimespec; - struct timespec st_birthtimespec; - int64_t st_size; - int64_t st_blocks; - int32_t st_blksize; - uint32_t st_flags; - uint32_t st_gen; - int32_t st_lspare; - int64_t st_qspare[2]; - }; - - struct dirent { - ino_t d_ino; - uint64_t d_seekoff; - uint16_t d_reclen; - uint16_t d_namlen; - uint8_t d_type; - char d_name[1024]; - }; - ]]) -else - -- x86-64 macOS: plain `stat` uses old 32-bit inode layout; must use stat$INODE64 - ffi.cdef([[ - struct stat { - int32_t st_dev; - uint16_t st_mode; - uint16_t st_nlink; - ino_t st_ino; - uint32_t st_uid; - uint32_t st_gid; - int32_t st_rdev; - int32_t st_rdev_pad; - struct timespec st_atimespec; - struct timespec st_mtimespec; - struct timespec st_ctimespec; - struct timespec st_birthtimespec; - int64_t st_size; - int64_t st_blocks; - int32_t st_blksize; - uint32_t st_flags; - uint32_t st_gen; - int32_t st_lspare; - int64_t st_qspare[2]; - }; - int stat(const char* pathname, struct stat* statbuf) asm("stat$INODE64"); - int lstat(const char* pathname, struct stat* statbuf) asm("lstat$INODE64"); - - struct dirent { - uint32_t d_ino; - uint16_t d_reclen; - uint8_t d_type; - uint8_t d_namlen; - char d_name[1024]; - }; - ]]) -end - -pcall(ffi.cdef, [[ - int kqueue(void); - typedef int64_t intptr_t; - typedef uint64_t uintptr_t; - - struct kevent { - uintptr_t ident; - int16_t filter; - uint16_t flags; - uint32_t fflags; - intptr_t data; - void* udata; - }; - - struct timespec_kq { - long tv_sec; - long tv_nsec; - }; - - int kevent(int kq, const struct kevent* changelist, int nchanges, - struct kevent* eventlist, int nevents, const struct timespec_kq* timeout); - - int open(const char* path, int oflag, ...); - int close(int fd); -]]) - -local O_EVTONLY = 0x8000 -local EVFILT_VNODE = -4 -local EV_ADD = 0x0001 -local EV_ENABLE = 0x0004 -local EV_CLEAR = 0x0020 -local NOTE_WRITE = 0x00000002 -local NOTE_DELETE = 0x00000001 -local NOTE_RENAME = 0x00000020 -local NOTE_ATTRIB = 0x00000008 - ----@class fs.raw.macos: fs.raw.posix -local fs = require("fs.raw.posix")(function(s, modeToStatType) - return { - size = s.st_size, - modifyTime = s.st_mtimespec.tv_sec, - accessTime = s.st_atimespec.tv_sec, - type = modeToStatType[bit.band(s.st_mode, 0xF000)], - mode = bit.band(s.st_mode, 0x1FF) - } -end) - ----@alias fs.WatchEvent "create" | "modify" | "delete" | "rename" - ----@class fs.Watcher ----@field close fun() ----@field poll fun() ----@field wait fun() - ---- Watch a path for changes. Calls callback(event, name) for each change. ---- Returns a watcher with :poll() (non-blocking), :wait() (blocking), and :close(). ----@param p string ----@param callback fun(event: fs.WatchEvent, name: string) ----@param opts { recursive: boolean? }? ----@return fs.Watcher? -function fs.watch(p, callback, opts) - local recursive = opts and opts.recursive or false - - local kq = ffi.C.kqueue() - if kq < 0 then return nil end - - local isDir = fs.isdir(p) - - local change = ffi.new("struct kevent[1]") - local function register(fd) - change[0].ident = fd - change[0].filter = EVFILT_VNODE - change[0].flags = bit.bor(EV_ADD, EV_ENABLE, EV_CLEAR) - change[0].fflags = bit.bor(NOTE_WRITE, NOTE_DELETE, NOTE_RENAME, NOTE_ATTRIB) - change[0].data = 0 - change[0].udata = nil - ffi.C.kevent(kq, change, 1, nil, 0, nil) - end - - local dirfd = ffi.C.open(p, O_EVTONLY) - if dirfd < 0 then - ffi.C.close(kq); return nil - end - register(dirfd) - - -- fd -> relative path (from p) for all watched entries - local filefds = {} ---@type table fd -> relative path - -- fd -> absolute dir path for watched subdirs (recursive mode) - local subdirfds = {} ---@type table fd -> absolute dir path - -- dir absolute path -> snapshot of children names - local dirSnaps = {} ---@type table> - - local function watchEntry(absPath, relPath, isDirectory) - local fd = ffi.C.open(absPath, O_EVTONLY) - if fd < 0 then return end - register(fd) - if isDirectory then - subdirfds[tonumber(fd)] = absPath - else - filefds[tonumber(fd)] = relPath - end - end - - local function snapDir(absDir) - local snap = {} - local iter = fs.readdir(absDir) - if iter then for entry in iter do snap[entry.name] = true end end - return snap - end - - local function walkDir(absDir, relBase) - local snap = snapDir(absDir) - dirSnaps[absDir] = snap - for name in pairs(snap) do - local absChild = absDir .. "/" .. name - local relChild = relBase ~= "" and (relBase .. "/" .. name) or name - local childIsDir = fs.isdir(absChild) - watchEntry(absChild, relChild, childIsDir) - if recursive and childIsDir then - walkDir(absChild, relChild) - end - end - end - - local prev ---@type table? - if isDir then - prev = snapDir(p) - dirSnaps[p] = prev - for name in pairs(prev) do - local absChild = p .. "/" .. name - local childIsDir = fs.isdir(absChild) - watchEntry(absChild, name, childIsDir) - if recursive and childIsDir then - walkDir(absChild, name) - end - end - end - - local events = ffi.new("struct kevent[16]") - local zero = ffi.new("struct timespec_kq[1]", { { 0, 0 } }) - - local function process(n) - for i = 0, n - 1 do - local ident = tonumber(events[i].ident) - local ff = events[i].fflags - - if ident == tonumber(dirfd) then - if isDir and bit.band(ff, NOTE_WRITE) ~= 0 then - local curr = snapDir(p) - for name in pairs(curr) do - if not prev[name] then - local absChild = p .. "/" .. name - local childIsDir = fs.isdir(absChild) - callback("create", name) - watchEntry(absChild, name, childIsDir) - if recursive and childIsDir then walkDir(absChild, name) end - end - end - for name in pairs(prev) do - if not curr[name] then callback("delete", name) end - end - prev = curr - dirSnaps[p] = curr - end - if bit.band(ff, NOTE_DELETE) ~= 0 then - callback("delete", p) - elseif bit.band(ff, NOTE_RENAME) ~= 0 then - callback("rename", p) - end - elseif subdirfds[ident] then - -- Event on a watched subdir (recursive mode) - local absDir = subdirfds[ident] - local relDir = string.sub(absDir, #p + 2) - if bit.band(ff, NOTE_WRITE) ~= 0 then - local oldSnap = dirSnaps[absDir] or {} - local curr = snapDir(absDir) - for name in pairs(curr) do - if not oldSnap[name] then - local absChild = absDir .. "/" .. name - local relChild = relDir .. "/" .. name - local childIsDir = fs.isdir(absChild) - callback("create", relChild) - watchEntry(absChild, relChild, childIsDir) - if childIsDir then walkDir(absChild, relChild) end - end - end - for name in pairs(oldSnap) do - if not curr[name] then callback("delete", relDir .. "/" .. name) end - end - dirSnaps[absDir] = curr - end - if bit.band(ff, NOTE_DELETE) ~= 0 then - callback("delete", relDir) - elseif bit.band(ff, NOTE_RENAME) ~= 0 then - callback("rename", relDir) - end - else - local relPath = filefds[ident] - if relPath then - if bit.band(ff, NOTE_WRITE) ~= 0 or bit.band(ff, NOTE_ATTRIB) ~= 0 then - callback("modify", relPath) - end - if bit.band(ff, NOTE_DELETE) ~= 0 or bit.band(ff, NOTE_RENAME) ~= 0 then - ffi.C.close(ident) - filefds[ident] = nil - end - elseif not isDir then - if bit.band(ff, NOTE_WRITE) ~= 0 or bit.band(ff, NOTE_ATTRIB) ~= 0 then - callback("modify", p) - end - if bit.band(ff, NOTE_DELETE) ~= 0 then - callback("delete", p) - elseif bit.band(ff, NOTE_RENAME) ~= 0 then - callback("rename", p) - end - end - end - end - end - - ---@type fs.Watcher - local watcher = {} - - function watcher.poll() - local n = ffi.C.kevent(kq, nil, 0, events, 16, zero) - process(n) - end - - function watcher.wait() - local n = ffi.C.kevent(kq, nil, 0, events, 16, nil) - process(n) - end - - function watcher.close() - for fd in pairs(filefds) do ffi.C.close(fd) end - for fd in pairs(subdirfds) do ffi.C.close(fd) end - ffi.C.close(dirfd) - ffi.C.close(kq) - end - - return watcher -end - -return fs diff --git a/packages/fs/src/raw/posix.lua b/packages/fs/src/raw/posix.lua deleted file mode 100644 index 5c9d34ad..00000000 --- a/packages/fs/src/raw/posix.lua +++ /dev/null @@ -1,142 +0,0 @@ -local ffi = require("ffi") - -ffi.cdef([[ - typedef struct __dirstream DIR; - DIR* opendir(const char* name); - int closedir(DIR* dirp); - int mkdir(const char* pathname, unsigned int mode); - int symlink(const char* target, const char* linkpath); - int chmod(const char* pathname, unsigned int mode); -]]) - ----@type table -local dTypeToEntryType = { - [0] = "unknown", - [4] = "dir", - [8] = "file", - [10] = "symlink" -} - ----@type table -local modeToStatType = { - [0x4000] = "dir", - [0x8000] = "file", - [0xA000] = "symlink" -} - ---- Call after defining struct dirent and struct stat in ffi. ----@param rawToCrossStat fun(s: ffi.cdata*, modeToStatType: table): fs.Stat ----@return fs.raw.posix -return function(rawToCrossStat) - ffi.cdef([[ - struct dirent* readdir(DIR* dirp); - int stat(const char* pathname, struct stat* statbuf); - int lstat(const char* pathname, struct stat* statbuf); - ]]) - - ---@class fs.raw.posix: fs.raw - local fs = {} - - local newStat = ffi.typeof("struct stat") - - local function rawStat(p) - local buf = newStat() - if ffi.C.stat(p, buf) ~= 0 then return nil end - return buf - end - - local function rawLstat(p) - local buf = newStat() - if ffi.C.lstat(p, buf) ~= 0 then return nil end - return buf - end - - ---@param p string - ---@return (fun(): fs.DirEntry?)? - function fs.readdir(p) - local dir = ffi.C.opendir(p) - if dir == nil then return nil end - - return function() - while true do - local entry = ffi.C.readdir(dir) - if entry == nil then - ffi.C.closedir(dir) - return nil - end - - local name = ffi.string(entry.d_name) - if name ~= "." and name ~= ".." then - return { - name = name, - type = dTypeToEntryType[entry.d_type] or "unknown" - } - end - end - end - end - - ---@param p string - function fs.exists(p) - return rawStat(p) ~= nil - end - - ---@param p string - function fs.stat(p) - local s = rawStat(p) - if s == nil then return nil end - return rawToCrossStat(s, modeToStatType) - end - - ---@param p string - function fs.lstat(p) - local s = rawLstat(p) - if s == nil then return nil end - return rawToCrossStat(s, modeToStatType) - end - - ---@param p string - function fs.isdir(p) - local s = rawStat(p) - if s == nil then return false end - return bit.band(s.st_mode, 0x4000) ~= 0 - end - - ---@param p string - function fs.isfile(p) - local s = rawStat(p) - if s == nil then return false end - return bit.band(s.st_mode, 0x8000) ~= 0 - end - - ---@param p string - function fs.islink(p) - local s = rawLstat(p) - if s == nil then return false end - return bit.band(s.st_mode, 0xA000) ~= 0 - end - - ---@param p string - function fs.mkdir(p) - return ffi.C.mkdir(p, 511) == 0 - end - - ---@param src string - ---@param dest string - function fs.mklink(src, dest) - return ffi.C.symlink(src, dest) == 0 - end - - ---@param p string - function fs.rmlink(p) - return os.remove(p) ~= nil - end - - ---@param p string - ---@param mode number - function fs.chmod(p, mode) - return ffi.C.chmod(p, mode) == 0 - end - - return fs -end diff --git a/packages/fs/src/raw/windows.lua b/packages/fs/src/raw/windows.lua deleted file mode 100644 index 8bd51e95..00000000 --- a/packages/fs/src/raw/windows.lua +++ /dev/null @@ -1,586 +0,0 @@ -local ffi = require("ffi") - -ffi.cdef([[ - typedef void* HANDLE; - typedef uint32_t DWORD; - typedef uint16_t WORD; - typedef unsigned char BYTE; - typedef int BOOL; - typedef unsigned short WCHAR; - - typedef struct { - DWORD dwLowDateTime; - DWORD dwHighDateTime; - } FILETIME; - - typedef struct { - DWORD dwFileAttributes; - FILETIME ftCreationTime; - FILETIME ftLastAccessTime; - FILETIME ftLastWriteTime; - DWORD nFileSizeHigh; - DWORD nFileSizeLow; - DWORD dwReserved0; - DWORD dwReserved1; - char cFileName[260]; - char cAlternateFileName[14]; - } WIN32_FIND_DATAA; - - HANDLE FindFirstFileA(const char* lpFileName, WIN32_FIND_DATAA* lpFindFileData); - BOOL FindNextFileA(HANDLE hFindFile, WIN32_FIND_DATAA* lpFindFileData); - BOOL FindClose(HANDLE hFindFile); - BOOL CreateDirectoryA(const char* lpPathName, void* lpSecurityAttributes); - BOOL CreateSymbolicLinkA(const char* lpSymlinkFileName, const char* lpTargetFileName, DWORD dwFlags); - DWORD GetFileAttributesA(const char* lpFileName); - - typedef struct { - DWORD dwFileAttributes; - FILETIME ftCreationTime; - FILETIME ftLastAccessTime; - FILETIME ftLastWriteTime; - DWORD nFileSizeHigh; - DWORD nFileSizeLow; - } WIN32_FILE_ATTRIBUTE_DATA; - - BOOL GetFileAttributesExA(const char* lpFileName, int fInfoLevelClass, WIN32_FILE_ATTRIBUTE_DATA* lpFileInformation); - - HANDLE CreateFileA( - const char* lpFileName, - DWORD dwDesiredAccess, - DWORD dwShareMode, - void* lpSecurityAttributes, - DWORD dwCreationDisposition, - DWORD dwFlagsAndAttributes, - HANDLE hTemplateFile - ); - - BOOL DeviceIoControl( - HANDLE hDevice, - DWORD dwIoControlCode, - void* lpInBuffer, - DWORD nInBufferSize, - void* lpOutBuffer, - DWORD nOutBufferSize, - DWORD* lpBytesReturned, - void* lpOverlapped - ); - - BOOL CloseHandle(HANDLE hObject); - - DWORD GetFullPathNameA( - const char* lpFileName, - DWORD nBufferLength, - char* lpBuffer, - char** lpFilePart - ); - - BOOL RemoveDirectoryA(const char* lpPathName); - BOOL DeleteFileA(const char* lpFileName); - BOOL CreateHardLinkA(const char* lpFileName, const char* lpExistingFileName, void* lpSecurityAttributes); - - HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort, - uintptr_t CompletionKey, DWORD NumberOfConcurrentThreads); - BOOL ReadDirectoryChangesW( - HANDLE hDirectory, - void* lpBuffer, - DWORD nBufferLength, - BOOL bWatchSubtree, - DWORD dwNotifyFilter, - DWORD* lpBytesReturned, - void* lpOverlapped, - void* lpCompletionRoutine - ); - BOOL GetOverlappedResult(HANDLE hFile, void* lpOverlapped, DWORD* lpNumberOfBytesTransferred, BOOL bWait); - BOOL HasOverlappedIoCompleted(void* lpOverlapped); -]]) - -local kernel32 = ffi.load("kernel32") - -local INVALID_HANDLE_VALUE = ffi.cast("HANDLE", -1) -local INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF -local FILE_ATTRIBUTE_DIRECTORY = 0x10 -local FILE_ATTRIBUTE_REPARSE_POINT = 0x400 - ----@class fs.raw.windows: fs.raw -local fs = {} - ----@param p string ----@return (fun(): fs.DirEntry?)? -function fs.readdir(p) - local searchPath = p .. "\\*" - - ---@type { cFileName: string, dwFileAttributes: number } - local findData = ffi.new("WIN32_FIND_DATAA") - - local handle = kernel32.FindFirstFileA(searchPath, findData) - if handle == INVALID_HANDLE_VALUE then - return nil - end - - local first = true - - return function() - while true do - local hasNext - if first then - first = false - hasNext = true - else - hasNext = kernel32.FindNextFileA(handle, findData) ~= 0 - end - - if not hasNext then - kernel32.FindClose(handle) - return nil - end - - local name = ffi.string(findData.cFileName) - if name ~= "." and name ~= ".." then - local isDir = bit.band(findData.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY) ~= 0 - local isLink = bit.band(findData.dwFileAttributes, FILE_ATTRIBUTE_REPARSE_POINT) ~= 0 - - local entryType - if isLink then - entryType = "symlink" - elseif isDir then - entryType = "dir" - else - entryType = "file" - end - - return { - name = name, - type = entryType - } - end - end - end -end - ----@param p string ----@return number? -local function getFileAttrs(p) - local attrs = kernel32.GetFileAttributesA(p) - if attrs == INVALID_FILE_ATTRIBUTES then - return nil - end - return attrs -end - ----@param p string ----@return boolean -function fs.exists(p) - return getFileAttrs(p) ~= nil -end - ----@param p string -function fs.isdir(p) - local attrs = getFileAttrs(p) - if attrs == nil then - return false - end - - return bit.band(attrs, FILE_ATTRIBUTE_DIRECTORY) ~= 0 -end - ----@param p string -function fs.mkdir(p) - return kernel32.CreateDirectoryA(p, nil) ~= 0 -end - -local GENERIC_WRITE = 0x40000000 -local OPEN_EXISTING = 3 -local FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 -local FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 -local FSCTL_SET_REPARSE_POINT = 0x000900A4 -local IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003 - ---- Resolves a path to an absolute path using Win32 GetFullPathNameA. ----@param p string ----@return string? -local function getFullPath(p) - local buf = ffi.new("char[?]", 1024) - local len = kernel32.GetFullPathNameA(p, 1024, buf, nil) - if len == 0 or len >= 1024 then - return nil - end - return ffi.string(buf, len) -end - ---- Creates an NTFS junction point (directory only). ---- Junctions do not require elevated privileges, unlike symlinks. ----@param src string # Target directory (must be absolute or will be resolved) ----@param dest string # Junction path to create ----@return boolean -local function createJunction(src, dest) - -- Junctions require an absolute target path - local absTarget = getFullPath(src) - if not absTarget then - return false - end - - -- Create the junction directory - if kernel32.CreateDirectoryA(dest, nil) == 0 then - return false - end - - -- Build the NT path: \??\C:\path\to\target - local ntTarget = "\\??\\" .. absTarget - - -- Encode the target as UTF-16LE - local ntTargetW = {} ---@type string[] - for i = 1, #ntTarget do - ntTargetW[#ntTargetW + 1] = string.sub(ntTarget, i, i) .. "\0" - end - local targetBytes = table.concat(ntTargetW) - local targetByteLen = #targetBytes - - -- Build REPARSE_DATA_BUFFER for mount point (junction) - -- Layout: - -- DWORD ReparseTag - -- WORD ReparseDataLength - -- WORD Reserved - -- WORD SubstituteNameOffset - -- WORD SubstituteNameLength - -- WORD PrintNameOffset - -- WORD PrintNameLength - -- WCHAR PathBuffer[...] (SubstituteName + NUL + PrintName + NUL) - local pathBufSize = targetByteLen + 2 + 2 -- substitute name + NUL + print name (empty) + NUL - local reparseDataLen = 8 + pathBufSize -- 4 WORDs (8 bytes) + path buffer - local totalSize = 8 + reparseDataLen -- header (tag + length + reserved) + data - - local buf = ffi.new("uint8_t[?]", totalSize) - local ptr = ffi.cast("uint8_t*", buf) - - -- ReparseTag (DWORD) - ffi.cast("uint32_t*", ptr)[0] = IO_REPARSE_TAG_MOUNT_POINT - -- ReparseDataLength (WORD) - ffi.cast("uint16_t*", ptr + 4)[0] = reparseDataLen - -- Reserved (WORD) - ffi.cast("uint16_t*", ptr + 6)[0] = 0 - -- SubstituteNameOffset (WORD) - ffi.cast("uint16_t*", ptr + 8)[0] = 0 - -- SubstituteNameLength (WORD) - without null terminator - ffi.cast("uint16_t*", ptr + 10)[0] = targetByteLen - -- PrintNameOffset (WORD) - after substitute name + null terminator - ffi.cast("uint16_t*", ptr + 12)[0] = targetByteLen + 2 - -- PrintNameLength (WORD) - empty print name - ffi.cast("uint16_t*", ptr + 14)[0] = 0 - - -- PathBuffer: substitute name - ffi.copy(ptr + 16, targetBytes, targetByteLen) - -- Null terminator for substitute name (2 bytes) - ffi.cast("uint16_t*", ptr + 16 + targetByteLen)[0] = 0 - -- Null terminator for print name (2 bytes) - ffi.cast("uint16_t*", ptr + 16 + targetByteLen + 2)[0] = 0 - - -- Open the junction directory with reparse point access - local handle = kernel32.CreateFileA( - dest, - GENERIC_WRITE, - 0, - nil, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS + FILE_FLAG_OPEN_REPARSE_POINT, - nil - ) - - if handle == INVALID_HANDLE_VALUE then - kernel32.RemoveDirectoryA(dest) - return false - end - - local bytesReturned = ffi.new("DWORD[1]") - local ok = kernel32.DeviceIoControl( - handle, - FSCTL_SET_REPARSE_POINT, - buf, - totalSize, - nil, - 0, - bytesReturned, - nil - ) - - kernel32.CloseHandle(handle) - - if ok == 0 then - kernel32.RemoveDirectoryA(dest) - return false - end - - return true -end - ---- Removes a symlink or junction without following it. ----@param p string ----@return boolean -function fs.rmlink(p) - local attrs = getFileAttrs(p) - if attrs ~= nil and bit.band(attrs, FILE_ATTRIBUTE_DIRECTORY) ~= 0 then - return kernel32.RemoveDirectoryA(p) ~= 0 - end - return kernel32.DeleteFileA(p) ~= 0 -end - ----@param src string ----@param dest string -function fs.mklink(src, dest) - if fs.isdir(src) then - return createJunction(src, dest) - end - - if kernel32.CreateSymbolicLinkA(dest, src, 0x2) ~= 0 then - return true - end - return kernel32.CreateHardLinkA(dest, src, nil) ~= 0 -end - ----@param p string -function fs.islink(p) - local attrs = getFileAttrs(p) - if attrs == nil then - return false - end - - return bit.band(attrs, FILE_ATTRIBUTE_REPARSE_POINT) ~= 0 -end - ----@param p string -function fs.isfile(p) - local attrs = getFileAttrs(p) - if attrs == nil then - return false - end - - return bit.band(attrs, FILE_ATTRIBUTE_DIRECTORY) == 0 and bit.band(attrs, FILE_ATTRIBUTE_REPARSE_POINT) == 0 -end - --- FILETIME is 100ns intervals since 1601-01-01. Unix epoch is 1970-01-01. --- Difference: 11644473600 seconds = 116444736000000000 in 100ns units. -local EPOCH_DIFF = 116444736000000000ULL - ----@param ft { dwLowDateTime: number, dwHighDateTime: number } -local function filetimeToUnix(ft) - local ticks = ffi.cast("uint64_t", ft.dwHighDateTime) * 0x100000000ULL + ft.dwLowDateTime - return tonumber((ticks - EPOCH_DIFF) / 10000000ULL) -end - ----@param attrs number ----@return fs.Stat.Type -local function attrsToType(attrs) - if bit.band(attrs, FILE_ATTRIBUTE_REPARSE_POINT) ~= 0 then - return "symlink" - elseif bit.band(attrs, FILE_ATTRIBUTE_DIRECTORY) ~= 0 then - return "dir" - else - return "file" - end -end - ----@class fs.raw.windows.Stat ----@field dwFileAttributes number ----@field ftLastAccessTime { dwLowDateTime: number, dwHighDateTime: number } ----@field ftLastWriteTime { dwLowDateTime: number, dwHighDateTime: number } ----@field nFileSizeHigh number ----@field nFileSizeLow number - ----@type fun(): fs.raw.windows.Stat ----@diagnostic disable-next-line: assign-type-mismatch -local newFileAttrData = ffi.typeof("WIN32_FILE_ATTRIBUTE_DATA") - ----@param s fs.raw.windows.Stat -local function fileSize(s) - return tonumber(s.nFileSizeHigh) * 0x100000000 + tonumber(s.nFileSizeLow) -end - ----@param s fs.raw.windows.Stat ----@param type fs.Stat.Type ----@return fs.Stat -local function rawToCrossStat(s, type) - return { - size = fileSize(s), - accessTime = filetimeToUnix(s.ftLastAccessTime), - modifyTime = filetimeToUnix(s.ftLastWriteTime), - type = type - } -end - ----@param p string ----@return fs.Stat? -function fs.stat(p) - local data = newFileAttrData() - if kernel32.GetFileAttributesExA(p, 0, data) == 0 then - return nil - end - - local type = bit.band(data.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY) ~= 0 and "dir" or "file" - return rawToCrossStat(data, type) -end - ----@param p string ----@return fs.Stat? -function fs.lstat(p) - local data = newFileAttrData() - if kernel32.GetFileAttributesExA(p, 0, data) == 0 then - return nil - end - - return rawToCrossStat(data, attrsToType(data.dwFileAttributes)) -end - ----@alias fs.WatchEvent "create" | "modify" | "delete" | "rename" - ----@class fs.Watcher ----@field close fun() ----@field poll fun() - -local FILE_LIST_DIRECTORY = 0x0001 -local FILE_SHARE_READ = 0x00000001 -local FILE_SHARE_WRITE = 0x00000002 -local FILE_SHARE_DELETE = 0x00000004 -local OPEN_EXISTING_W = 3 -local FILE_FLAG_BACKUP_SEMANTICS_W = 0x02000000 -local FILE_FLAG_OVERLAPPED = 0x40000000 - -local FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001 -local FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002 -local FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010 -local FILE_NOTIFY_CHANGE_SIZE = 0x00000008 - -local FILE_ACTION_ADDED = 1 -local FILE_ACTION_REMOVED = 2 -local FILE_ACTION_MODIFIED = 3 -local FILE_ACTION_RENAMED_OLD = 4 -local FILE_ACTION_RENAMED_NEW = 5 - -ffi.cdef([[ - typedef struct { - uintptr_t Internal; - uintptr_t InternalHigh; - DWORD Offset; - DWORD OffsetHigh; - HANDLE hEvent; - } OVERLAPPED_W; - - HANDLE CreateEventA(void* lpEventAttributes, BOOL bManualReset, BOOL bInitialState, const char* lpName); - DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds); -]]) - -local WAIT_OBJECT_0 = 0 -local WAIT_TIMEOUT = 0x102 - ---- Watch a directory for changes. Calls callback(event, name) for each change. ---- Returns a watcher with :poll() (non-blocking), :wait() (blocking), and :close(). ----@param p string ----@param callback fun(event: fs.WatchEvent, name: string) ----@param opts { recursive: boolean? }? ----@return fs.Watcher? -function fs.watch(p, callback, opts) - local recursive = opts and opts.recursive or false - - local handle = kernel32.CreateFileA( - p, - FILE_LIST_DIRECTORY, - bit.bor(FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE), - nil, - OPEN_EXISTING_W, - bit.bor(FILE_FLAG_BACKUP_SEMANTICS_W, FILE_FLAG_OVERLAPPED), - nil - ) - if handle == INVALID_HANDLE_VALUE then return nil end - - local event = kernel32.CreateEventA(nil, 1, 0, nil) -- manual reset, initially unsignaled - if event == nil then - kernel32.CloseHandle(handle); return nil - end - - local bufSize = 4096 - local buf = ffi.new("uint8_t[?]", bufSize) - local overlapped = ffi.new("OVERLAPPED_W[1]") - overlapped[0].hEvent = event - local bytesReturned = ffi.new("DWORD[1]") - - local notifyFilter = bit.bor( - FILE_NOTIFY_CHANGE_FILE_NAME, - FILE_NOTIFY_CHANGE_DIR_NAME, - FILE_NOTIFY_CHANGE_LAST_WRITE, - FILE_NOTIFY_CHANGE_SIZE - ) - - local function issueRead() - ffi.fill(overlapped, ffi.sizeof("OVERLAPPED_W")) - overlapped[0].hEvent = event - kernel32.ReadDirectoryChangesW(handle, buf, bufSize, recursive and 1 or 0, notifyFilter, bytesReturned, - overlapped, nil) - end - - issueRead() - - local INFINITE = 0xFFFFFFFF - - local function drain() - local transferred = ffi.new("DWORD[1]") - if kernel32.GetOverlappedResult(handle, overlapped, transferred, 0) == 0 then - issueRead(); return - end - - local n = tonumber(transferred[0]) - if not n or n == 0 then - issueRead(); return - end - - -- FILE_NOTIFY_INFORMATION: NextEntryOffset(4), Action(4), FileNameLength(4), FileName[...] - local i = 0 - while i < n do - local ptr = buf + i - local nextOff = ffi.cast("uint32_t*", ptr)[0] - local action = ffi.cast("uint32_t*", ptr + 4)[0] - local nameLen = ffi.cast("uint32_t*", ptr + 8)[0] - local name = "" - for j = 0, nameLen / 2 - 1 do - local ch = ffi.cast("uint16_t*", ptr + 12)[j] - name = name .. string.char(ch < 128 and ch or 63) - end - - local ev ---@type fs.WatchEvent - if action == FILE_ACTION_ADDED then - ev = "create" - elseif action == FILE_ACTION_REMOVED then - ev = "delete" - elseif action == FILE_ACTION_MODIFIED then - ev = "modify" - elseif action == FILE_ACTION_RENAMED_OLD or action == FILE_ACTION_RENAMED_NEW then - ev = "rename" - end - - if ev then callback(ev, name) end - if nextOff == 0 then break end - i = i + nextOff - end - - issueRead() - end - - ---@type fs.Watcher - local watcher = {} - - function watcher.poll() - if kernel32.WaitForSingleObject(event, 0) ~= WAIT_OBJECT_0 then return end - drain() - end - - function watcher.wait() - kernel32.WaitForSingleObject(event, INFINITE) - drain() - end - - function watcher.close() - kernel32.CloseHandle(event) - kernel32.CloseHandle(handle) - end - - return watcher -end - -return fs diff --git a/packages/fs/tests/fs.test.lua b/packages/fs/tests/fs.test.lua deleted file mode 100644 index 3996b7b3..00000000 --- a/packages/fs/tests/fs.test.lua +++ /dev/null @@ -1,699 +0,0 @@ -local test = require("lde-test") -local fs = require("fs") -local env = require("env") -local path = require("path") - -local tmpBase = path.join(env.tmpdir(), "lde-fs-pkg-tests") -fs.rmdir(tmpBase) -fs.mkdir(tmpBase) - --- helpers -local function tmp(name) - return path.join(tmpBase, name) -end - --- --- exists / isfile / isdir --- - -test.it("exists returns false for missing path", function() - test.falsy(fs.exists(tmp("no-such-file"))) -end) - -test.it("write creates a file and exists returns true", function() - local p = tmp("hello.txt") - test.truthy(fs.write(p, "hello")) - test.truthy(fs.exists(p)) - test.truthy(fs.isfile(p)) - test.falsy(fs.isdir(p)) -end) - -test.it("read returns written content", function() - local p = tmp("read-test.txt") - fs.write(p, "content123") - test.equal(fs.read(p), "content123") -end) - -test.it("read returns nil for missing file", function() - test.falsy(fs.read(tmp("missing.txt"))) -end) - --- --- mkdir / isdir --- - -test.it("mkdir creates a directory", function() - local d = tmp("mydir") - test.truthy(fs.mkdir(d)) - test.truthy(fs.isdir(d)) - test.falsy(fs.isfile(d)) -end) - --- --- stat --- - -test.it("stat returns size and modifyTime for a file", function() - local p = tmp("stat-test.txt") - fs.write(p, "abcde") - local s = fs.stat(p) - test.truthy(s) - test.equal(s.size, 5) - test.truthy(s.modifyTime) - test.equal(s.type, "file") -end) - -test.it("stat returns type=dir for a directory", function() - local d = tmp("stat-dir") - fs.mkdir(d) - local s = fs.stat(d) - test.truthy(s) - test.equal(s.type, "dir") -end) - -test.it("stat returns nil for missing path", function() - test.falsy(fs.stat(tmp("nope"))) -end) - --- --- delete --- - -test.it("delete removes a file", function() - local p = tmp("del-me.txt") - fs.write(p, "bye") - test.truthy(fs.delete(p)) - test.falsy(fs.exists(p)) -end) - --- --- rmdir --- - -test.it("rmdir removes a directory recursively", function() - local d = tmp("rmdir-test") - fs.mkdir(d) - fs.write(path.join(d, "a.txt"), "a") - fs.mkdir(path.join(d, "sub")) - fs.write(path.join(d, "sub", "b.txt"), "b") - test.truthy(fs.rmdir(d)) - test.falsy(fs.exists(d)) -end) - --- --- copy --- - -test.it("copy copies a file", function() - local src = tmp("copy-src.txt") - local dst = tmp("copy-dst.txt") - fs.write(src, "copied!") - test.truthy(fs.copy(src, dst)) - test.equal(fs.read(dst), "copied!") -end) - -test.it("copy copies a directory recursively", function() - local src = tmp("copy-dir-src") - local dst = tmp("copy-dir-dst") - fs.mkdir(src) - fs.write(path.join(src, "f.txt"), "hi") - fs.mkdir(path.join(src, "sub")) - fs.write(path.join(src, "sub", "g.txt"), "there") - test.truthy(fs.copy(src, dst)) - test.equal(fs.read(path.join(dst, "f.txt")), "hi") - test.equal(fs.read(path.join(dst, "sub", "g.txt")), "there") -end) - --- --- move --- - -test.it("move renames a file", function() - local src = tmp("move-src.txt") - local dst = tmp("move-dst.txt") - fs.write(src, "moved") - test.truthy(fs.move(src, dst)) - test.falsy(fs.exists(src)) - test.equal(fs.read(dst), "moved") -end) - -test.it("move removes source directory after moving", function() - local src = tmp("move-dir-src") - local dst = tmp("move-dir-dst") - fs.mkdir(src) - fs.write(path.join(src, "file.txt"), "content") - test.truthy(fs.move(src, dst)) - test.falsy(fs.exists(src)) - test.equal(fs.read(path.join(dst, "file.txt")), "content") -end) - --- --- readdir --- - -test.it("readdir iterates directory entries", function() - local d = tmp("readdir-test") - fs.mkdir(d) - fs.write(path.join(d, "one.txt"), "") - fs.write(path.join(d, "two.txt"), "") - - local names = {} - for entry in fs.readdir(d) do - names[#names + 1] = entry.name - end - table.sort(names) - test.equal(#names, 2) - test.equal(names[1], "one.txt") - test.equal(names[2], "two.txt") -end) - -test.it("readdir returns nil for missing directory", function() - test.falsy(fs.readdir(tmp("no-dir"))) -end) - --- --- symlinks --- - -test.it("mklink creates a symlink and islink detects it", function() - local target = tmp("link-target.txt") - local link = tmp("link-itself") - fs.write(target, "target") - test.truthy(fs.mklink(target, link)) - -- On Windows, file symlinks fall back to hard links when Developer Mode is - -- disabled. Hard links are not reparse points, so islink returns false. - if jit.os ~= "Windows" then - test.truthy(fs.islink(link)) - else - test.truthy(fs.exists(link)) - end -end) - -test.it("rmlink removes a symlink", function() - local target = tmp("rmlink-target.txt") - local link = tmp("rmlink-link") - fs.write(target, "t") - fs.mklink(target, link) - test.truthy(fs.rmlink(link)) - test.falsy(fs.exists(link)) -end) - --- --- scan --- - -test.it("scan finds files matching glob", function() - local d = tmp("scan-test") - fs.mkdir(d) - fs.mkdir(path.join(d, "sub")) - fs.write(path.join(d, "a.lua"), "") - fs.write(path.join(d, "b.txt"), "") - fs.write(path.join(d, "sub", "c.lua"), "") - - local results = fs.scan(d, "**.lua") - test.equal(#results, 2) -end) - --- --- watch --- - -test.it("watch returns a watcher for an existing directory", function() - local d = tmp("watch-test") - fs.mkdir(d) - local w = fs.watch(d, function() end) - test.truthy(w) - w.close() -end) - -test.it("watch detects file creation via poll", function() - local d = tmp("watch-create") - fs.mkdir(d) - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end) - test.truthy(w) - - fs.write(path.join(d, "new.txt"), "hello") - - -- Give the OS a moment to register the event, then poll - local deadline = os.clock() + 1 - while #events == 0 and os.clock() < deadline do - w.poll() - end - - w.close() - test.truthy(#events > 0) - test.equal(events[1].event, "create") -end) - -test.it("watch detects file modification via poll", function() - local d = tmp("watch-modify") - fs.mkdir(d) - local p = path.join(d, "mod.txt") - fs.write(p, "v1") - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end) - test.truthy(w) - - fs.write(p, "v2") - - local deadline = os.clock() + 1 - while #events == 0 and os.clock() < deadline do - w.poll() - end - - w.close() - test.truthy(#events > 0) -end) - -test.it("watch detects file deletion via poll", function() - local d = tmp("watch-delete") - fs.mkdir(d) - local p = path.join(d, "gone.txt") - fs.write(p, "bye") - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end) - test.truthy(w) - - fs.delete(p) - - local deadline = os.clock() + 1 - while #events == 0 and os.clock() < deadline do - w.poll() - end - - w.close() - test.truthy(#events > 0) - test.equal(events[1].event, "delete") -end) - -test.it("wait blocks until file creation", function() - local d = tmp("wait-create") - fs.mkdir(d) - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end) - test.truthy(w) - - -- Write in a coroutine so wait() can block the main thread - local co = coroutine.create(function() - fs.write(path.join(d, "new.txt"), "hello") - end) - - -- Issue the write before wait() so the event is queued - coroutine.resume(co) - w.wait() - - w.close() - test.truthy(#events > 0) - test.equal(events[1].event, "create") -end) - -test.it("wait blocks until file modification", function() - local d = tmp("wait-modify") - fs.mkdir(d) - local p = path.join(d, "mod.txt") - fs.write(p, "v1") - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end) - test.truthy(w) - - fs.write(p, "v2") - w.wait() - - w.close() - test.truthy(#events > 0) -end) - -test.it("wait blocks until file deletion", function() - local d = tmp("wait-delete") - fs.mkdir(d) - local p = path.join(d, "gone.txt") - fs.write(p, "bye") - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end) - test.truthy(w) - - fs.delete(p) - w.wait() - - w.close() - test.truthy(#events > 0) - test.equal(events[1].event, "delete") -end) - --- --- watch recursive --- - -test.it("watch recursive detects file creation in subdirectory via poll", function() - local d = tmp("watch-rec-create") - local sub = path.join(d, "sub") - fs.mkdir(d) - fs.mkdir(sub) - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end, { recursive = true }) - test.truthy(w) - - fs.write(path.join(sub, "deep.txt"), "hello") - - local deadline = os.clock() + 1 - while #events == 0 and os.clock() < deadline do - w.poll() - end - - w.close() - test.truthy(#events > 0) - test.equal(events[1].event, "create") -end) - -test.it("watch recursive detects file modification in subdirectory via poll", function() - local d = tmp("watch-rec-modify") - local sub = path.join(d, "sub") - fs.mkdir(d) - fs.mkdir(sub) - local p = path.join(sub, "mod.txt") - fs.write(p, "v1") - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end, { recursive = true }) - test.truthy(w) - - fs.write(p, "v2") - - local deadline = os.clock() + 1 - while #events == 0 and os.clock() < deadline do - w.poll() - end - - w.close() - test.truthy(#events > 0) -end) - -test.it("watch recursive detects creation in newly created subdirectory", function() - local d = tmp("watch-rec-newdir") - fs.mkdir(d) - - local events = {} - local w = fs.watch(d, function(event, name) - events[#events + 1] = { event = event, name = name } - end, { recursive = true }) - test.truthy(w) - - local sub = path.join(d, "newdir") - fs.mkdir(sub) - - -- Block on the mkdir event so the watcher can register the new subdir - w.wait() - - local before = #events - fs.write(path.join(sub, "file.txt"), "hi") - - local deadline = os.clock() + 1 - while #events == before and os.clock() < deadline do - w.poll() - end - - w.close() - test.truthy(#events > before) -end) - - --- --- rmdir edge cases --- - -test.it("rmdir returns false for non-existent directory", function() - test.falsy(fs.rmdir(tmp("rmdir-missing"))) -end) - -test.it("rmdir removes an empty directory", function() - local d = tmp("rmdir-empty") - fs.mkdir(d) - test.truthy(fs.rmdir(d)) - test.falsy(fs.exists(d)) -end) - -test.it("rmdir removes deeply nested directories", function() - local d = tmp("rmdir-deep") - local deep = path.join(d, "a", "b", "c") - -- mkdir only creates one level, so build manually - fs.mkdir(d) - fs.mkdir(path.join(d, "a")) - fs.mkdir(path.join(d, "a", "b")) - fs.mkdir(deep) - fs.write(path.join(deep, "leaf.txt"), "x") - test.truthy(fs.rmdir(d)) - test.falsy(fs.exists(d)) -end) - -test.it("rmdir on a symlink to a directory removes only the link", function() - local target = tmp("rmdir-link-target") - local link = tmp("rmdir-link-itself") - fs.mkdir(target) - fs.mklink(target, link) - test.truthy(fs.rmdir(link)) - test.falsy(fs.exists(link)) - test.truthy(fs.exists(target)) -- target must survive - fs.rmdir(target) -end) - --- --- delete edge cases --- - -test.it("delete returns false for non-existent file", function() - test.falsy(fs.delete(tmp("delete-missing.txt"))) -end) - --- --- mkdirAll --- - -test.it("mkdirAll creates nested directories", function() - local d = tmp("mkdirall-nested/a/b/c") - fs.mkdirAll(d) - test.truthy(fs.isdir(d)) -end) - -test.it("mkdirAll is idempotent on an existing directory", function() - local d = tmp("mkdirall-exist") - fs.mkdir(d) - fs.mkdirAll(d) -- should not error - test.truthy(fs.isdir(d)) -end) - -test.it("mkdirAll creates a single missing directory", function() - local d = tmp("mkdirall-single") - fs.mkdirAll(d) - test.truthy(fs.isdir(d)) -end) - --- --- mkdir edge cases --- - -test.it("mkdir is idempotent on an existing directory", function() - local d = tmp("mkdir-idempotent") - fs.mkdir(d) - -- second call should not error and directory still exists - fs.mkdir(d) - test.truthy(fs.isdir(d)) -end) - --- --- write / read edge cases --- - -test.it("write overwrites existing file content", function() - local p = tmp("overwrite.txt") - fs.write(p, "first") - fs.write(p, "second") - test.equal(fs.read(p), "second") -end) - -test.it("write handles empty string content", function() - local p = tmp("empty-write.txt") - test.truthy(fs.write(p, "")) - test.equal(fs.read(p), "") -end) - -test.it("write handles binary / multi-line content", function() - local p = tmp("binary.txt") - local content = "line1\nline2\nline3" - fs.write(p, content) - test.equal(fs.read(p), content) -end) - --- --- copy edge cases --- - -test.it("copy returns false for missing source", function() - test.falsy(fs.copy(tmp("copy-no-src.txt"), tmp("copy-no-dst.txt"))) -end) - -test.it("copy overwrites an existing destination file", function() - local src = tmp("copy-over-src.txt") - local dst = tmp("copy-over-dst.txt") - fs.write(src, "new") - fs.write(dst, "old") - test.truthy(fs.copy(src, dst)) - test.equal(fs.read(dst), "new") -end) - --- --- move edge cases --- - -test.it("move overwrites an existing destination file", function() - local src = tmp("move-over-src.txt") - local dst = tmp("move-over-dst.txt") - fs.write(src, "winner") - fs.write(dst, "loser") - test.truthy(fs.move(src, dst)) - test.falsy(fs.exists(src)) - test.equal(fs.read(dst), "winner") -end) - --- --- stat / lstat edge cases --- - -test.it("lstat on a symlink returns type=symlink", function() - local target = tmp("lstat-target.txt") - local link = tmp("lstat-link") - fs.write(target, "t") - fs.mklink(target, link) - local s = fs.lstat(link) - test.truthy(s) - test.equal(s.type, "symlink") -end) - -test.it("stat on a symlink follows it and returns type=file", function() - local target = tmp("stat-link-target.txt") - local link = tmp("stat-link-itself") - fs.write(target, "t") - fs.mklink(target, link) - local s = fs.stat(link) - test.truthy(s) - test.equal(s.type, "file") -end) - --- --- readdir entry types --- - -test.it("readdir reports correct entry types", function() - local d = tmp("readdir-types") - local sub = path.join(d, "subdir") - local file = path.join(d, "file.txt") - local target = path.join(d, "link-target.txt") - local link = path.join(d, "link") - fs.mkdir(d) - fs.mkdir(sub) - fs.write(file, "x") - fs.write(target, "t") - fs.mklink(target, link) - - local types = {} - for entry in fs.readdir(d) do - types[entry.name] = entry.type - end - - test.equal(types["subdir"], "dir") - test.equal(types["file.txt"], "file") - -- symlink type may be "symlink" or resolved depending on OS; just check it exists - test.truthy(types["link"]) -end) - --- --- scan edge cases --- - -test.it("scan returns empty table when no files match", function() - local d = tmp("scan-nomatch") - fs.mkdir(d) - fs.write(path.join(d, "a.txt"), "") - local results = fs.scan(d, "**.lua") - test.equal(#results, 0) -end) - -test.it("scan with absolute option returns absolute paths", function() - local d = tmp("scan-absolute") - fs.mkdir(d) - fs.write(path.join(d, "x.lua"), "") - local results = fs.scan(d, "**.lua", { absolute = true }) - test.equal(#results, 1) - -- absolute path must start with the base dir - test.truthy(results[1]:sub(1, #d) == d) -end) - -test.it("scan finds files in nested directories with ** glob", function() - local d = tmp("scan-nested") - fs.mkdir(d) - fs.mkdir(path.join(d, "a")) - fs.mkdir(path.join(d, "a", "b")) - fs.write(path.join(d, "root.lua"), "") - fs.write(path.join(d, "a", "mid.lua"), "") - fs.write(path.join(d, "a", "b", "deep.lua"), "") - local results = fs.scan(d, "**.lua") - test.equal(#results, 3) -end) - -test.it("scan errors on a non-directory path", function() - local p = tmp("scan-notdir.txt") - fs.write(p, "x") - local ok = pcall(fs.scan, p, "**") - test.falsy(ok) -end) - --- --- globToPattern --- - -test.it("globToPattern matches exact filename", function() - local pat = fs.globToPattern("foo.lua") - test.truthy(string.find("foo.lua", pat)) - test.falsy(string.find("bar.lua", pat)) -end) - -test.it("globToPattern * does not cross path separators", function() - local pat = fs.globToPattern("*.lua") - test.truthy(string.find("hello.lua", pat)) - test.falsy(string.find("a/hello.lua", pat)) -end) - -test.it("globToPattern ** crosses path separators", function() - local pat = fs.globToPattern("**.lua") - test.truthy(string.find("hello.lua", pat)) - test.truthy(string.find("a/b/hello.lua", pat)) -end) - -test.it("globToPattern ? matches single non-separator character", function() - local pat = fs.globToPattern("fo?.lua") - test.truthy(string.find("foo.lua", pat)) - test.falsy(string.find("fo.lua", pat)) -end) diff --git a/packages/json/benchmarks/lde.json b/packages/json/benchmarks/lde.json deleted file mode 100644 index ef7dee91..00000000 --- a/packages/json/benchmarks/lde.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "json-benchmarks", - "version": "0.1.0", - "dependencies": { - "json": { "path": "../" }, - "ansi": { "path": "../../ansi" }, - "process2": { "path": "../../process2" }, - "lua-cjson": { "luarocks": "lua-cjson" } - } -} diff --git a/packages/json/benchmarks/src/init.lua b/packages/json/benchmarks/src/init.lua deleted file mode 100644 index 792a5578..00000000 --- a/packages/json/benchmarks/src/init.lua +++ /dev/null @@ -1,117 +0,0 @@ -local ffi = require("ffi") -local ansi = require("ansi") -local json = require("json") -local cjson = require("cjson") - --- ── timer ───────────────────────────────────────────────────────────────────── - -local now -if ffi.os == "Windows" then - ffi.cdef [[ - typedef union { struct { uint32_t lo, hi; }; uint64_t val; } LARGE_INTEGER; - int QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount); - int QueryPerformanceFrequency(LARGE_INTEGER *lpFrequency); - ]] - local freq = ffi.new("LARGE_INTEGER") - ffi.C.QueryPerformanceFrequency(freq) - local f = tonumber(freq.val) - now = function() - local t = ffi.new("LARGE_INTEGER") - ffi.C.QueryPerformanceCounter(t) - return tonumber(t.val) * 1e9 / f - end -else - ffi.cdef [[ typedef struct { long tv_sec; long tv_nsec; } timespec; - int clock_gettime(int clk_id, timespec *tp); ]] - now = function() - local t = ffi.new("timespec") - ffi.C.clock_gettime(1, t) - return tonumber(t.tv_sec) * 1e9 + tonumber(t.tv_nsec) - end -end - --- ── bench helper ────────────────────────────────────────────────────────────── - -local function bench(label, fn, iters) - iters = iters or 1000 - -- warmup - for _ = 1, math.max(1, math.floor(iters / 10)) do fn() end - local t0 = now() - for _ = 1, iters do fn() end - local ns = (now() - t0) / iters - ansi.printf(" {gray}%-40s{reset} {bold}%8.2f ns/op{reset} {gray}(%d iters){reset}", - label, ns, iters) -end - --- ── fixtures ────────────────────────────────────────────────────────────────── - -local SMALL = '{"name":"Alice","age":30,"active":true}' - -local MEDIUM = json.encode({ - users = (function() - local t = {} - for i = 1, 20 do - t[i] = { id = i, name = "user" .. i, score = i * 1.5, active = i % 2 == 0 } - end - return t - end)() -}) - -local LARGE = json.encode((function() - local t = {} - for i = 1, 500 do - t[i] = { id = i, name = "item" .. i, value = i * 3.14, tags = { "a", "b", "c" } } - end - return t -end)()) - -local JSON5_SRC = [[{ - // application config - name: 'myapp', - version: '1.0.0', - /* feature flags */ - features: { - darkMode: true, - beta: false, - }, - ports: [8080, 8443,], -}]] - -local SMALL_T = json.decode(SMALL) -local MEDIUM_T = json.decode(MEDIUM) -local LARGE_T = json.decode(LARGE) -local JSON5_T = json.decode(JSON5_SRC) - --- ── run ─────────────────────────────────────────────────────────────────────── - -ansi.printf("\n{bold}json decode{reset}") -bench("small object (~40 B)", function() json.decode(SMALL) end, 5000) -bench("medium array (~20 objs)", function() json.decode(MEDIUM) end, 500) -bench("large array (~500 objs)", function() json.decode(LARGE) end, 20) -bench("json5 with comments", function() json.decode(JSON5_SRC) end, 2000) - -ansi.printf("\n{bold}json encode{reset}") -bench("small object", function() json.encode(SMALL_T) end, 5000) -bench("medium array", function() json.encode(MEDIUM_T) end, 500) -bench("large array", function() json.encode(LARGE_T) end, 20) -bench("json5 round-trip", function() json.encode(JSON5_T) end, 2000) - -ansi.printf("\n{bold}json round-trip (decode + encode){reset}") -bench("small", function() json.encode(json.decode(SMALL)) end, 5000) -bench("medium", function() json.encode(json.decode(MEDIUM)) end, 500) -bench("large", function() json.encode(json.decode(LARGE)) end, 20) - -ansi.printf("\n{bold}json decodeDocument only (zero-alloc){reset}") -bench("small", function() json.decodeDocument(SMALL) end, 5000) -bench("medium", function() json.decodeDocument(MEDIUM) end, 500) -bench("large", function() json.decodeDocument(LARGE) end, 20) - -ansi.printf("\n{bold}cjson decode{reset}") -bench("small object (~40 B)", function() cjson.decode(SMALL) end, 5000) -bench("medium array (~20 objs)", function() cjson.decode(MEDIUM) end, 500) -bench("large array (~500 objs)", function() cjson.decode(LARGE) end, 20) - -ansi.printf("\n{bold}cjson encode{reset}") -bench("small object", function() cjson.encode(cjson.decode(SMALL)) end, 5000) -bench("medium array", function() cjson.encode(cjson.decode(MEDIUM)) end, 500) -bench("large array", function() cjson.encode(cjson.decode(LARGE)) end, 20) diff --git a/packages/json/lde.json b/packages/json/lde.json deleted file mode 100644 index e159f0bd..00000000 --- a/packages/json/lde.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "json", - "version": "0.1.0" -} diff --git a/packages/json/src/init.lua b/packages/json/src/init.lua deleted file mode 100644 index 2968bdda..00000000 --- a/packages/json/src/init.lua +++ /dev/null @@ -1,922 +0,0 @@ -local json = {} -local ffi = require("ffi") -local strbuf = require("string.buffer") - -ffi.cdef [[ - void* memchr(const void* s, int c, size_t n); - - /* 16-byte token. All decoded JSON lives in a flat json_tok array. - No Lua table allocations for the parsed document. */ - typedef struct { - uint8_t type; /* TY_* constants below */ - uint8_t flags; /* string: 1=has_escapes */ - uint16_t pad; - uint32_t next; /* next sibling index (0 = none) */ - union { - struct { uint32_t str_off; uint32_t str_len; }; - double num; - struct { uint32_t child; uint32_t count; }; - }; - } json_tok; - - typedef struct { uint32_t start; uint32_t count; } json_keyslice; -]] - -local C = ffi.C -local cast = ffi.cast -local u8p = "const uint8_t*" - --- token type constants -local TY_NULL = 0 -local TY_FALSE = 1 -local TY_TRUE = 2 -local TY_INT = 3 -local TY_FLOAT = 4 -local TY_STRING = 5 -local TY_ARRAY = 6 -local TY_OBJECT = 7 - --- ── LuaCATS types ───────────────────────────────────────────────────────────── - ----@alias json.Primitive string | number | boolean | nil ----@alias json.Value json.Primitive | json.Object | json.Array | table ----@alias json.Object table ----@alias json.Array json.Value[] ----@alias json.KeyStyle "ident" | "single" | "double" ----@alias json.StringStyle "single" | "double" - ----@class json.KeyMeta ----@field keyStyle json.KeyStyle ----@field before string | nil ----@field between string | nil ----@field afterColon string | nil ----@field afterValue string | nil ----@field valueStyle json.StringStyle | nil - ----@class json.TableMeta ----@field __trailingComma boolean ----@field __closingTrivia string | nil ----@field [string] json.KeyMeta ----@field [integer] json.KeyMeta - ----@class json.Token : ffi.cdata* ----@field type integer ----@field flags integer ----@field next integer ----@field str_off integer ----@field str_len integer ----@field num number ----@field child integer ----@field count integer - ----@class json.KeySlice : ffi.cdata* ----@field start integer ----@field count integer - ----@class json.Doc ----@field toks json.Token[] token arena ----@field src string original source string ----@field ntoks integer number of tokens used - --- ── token arena (per-decode, grown as needed) ───────────────────────────────── - -local TOK_INIT = 4096 -local tok_ct = ffi.typeof("json_tok") -local tok_arr_ct = ffi.typeof("json_tok[?]") -local ks_ct = ffi.typeof("json_keyslice") - ----@type ffi.cdata* json_tok[?] -local tok_arena = ffi.new(tok_arr_ct, TOK_INIT) -local tok_cap = TOK_INIT -local tok_top = 0 -- next free slot - -local function tok_alloc() - local idx = tok_top - tok_top = idx + 1 - if tok_top > tok_cap then - local newcap = tok_cap * 2 - local newarr = ffi.new(tok_arr_ct, newcap) - ffi.copy(newarr, tok_arena, tok_cap * ffi.sizeof(tok_ct)) - tok_arena = newarr - tok_cap = newcap - end - return idx -end - --- ── key arena (append-only) ─────────────────────────────────────────────────── --- Stores ordered key strings for decoded objects (replaces per-object Lua table). --- Never reset: slices from old decodes remain valid as long as the decoded object lives. - ----@type string[] -local key_arena = {} -local key_arena_top = 0 - -local ks_ct = ffi.typeof("json_keyslice") - -local function newKeySlice() - local s = ks_ct() - s.start = key_arena_top - s.count = 0 - return s -end - -local function pushKey(slice, key) - local idx = key_arena_top + 1 - key_arena[idx] = key - key_arena_top = idx - slice.count = slice.count + 1 -end - --- ── weak stores (for encode-side metadata) ──────────────────────────────────── - ----@type table -local keyStore = setmetatable({}, { __mode = "k" }) ----@type table -local metaStore = setmetatable({}, { __mode = "k" }) - --- ── encode ──────────────────────────────────────────────────────────────────── - ----@type table -local dq_esc = { - ['"'] = '\\"', - ['\\'] = '\\\\', - ['\b'] = '\\b', - ['\f'] = '\\f', - ['\n'] = '\\n', - ['\r'] = '\\r', - ['\t'] = '\\t' -} ----@type table -local sq_esc = { - ["'"] = "\\'", - ['\\'] = '\\\\', - ['\b'] = '\\b', - ['\f'] = '\\f', - ['\n'] = '\\n', - ['\r'] = '\\r', - ['\t'] = '\\t' -} -local function dq_replace(c) return dq_esc[c] or string.format("\\u%04x", string.byte(c)) end -local function sq_replace(c) return sq_esc[c] or string.format("\\u%04x", string.byte(c)) end - -local dq_needs = ffi.new("uint8_t[256]") -for i = 0, 31 do dq_needs[i] = 1 end -dq_needs[34] = 1; dq_needs[92] = 1 - -local sq_needs = ffi.new("uint8_t[256]") -for i = 0, 31 do sq_needs[i] = 1 end -sq_needs[39] = 1; sq_needs[92] = 1 - ----@param tape string.buffer ----@param s string ----@param style json.StringStyle | nil -local function putString(tape, s, style) - local len = #s - local p = cast(u8p, s) - if style == "single" then - tape:put("'") - local clean = true - for i = 0, len - 1 do - if sq_needs[p[i]] == 1 then - clean = false; break - end - end - if clean then tape:put(s) else tape:put((string.gsub(s, "[%z\1-\31'\\]", sq_replace))) end - tape:put("'") - else - tape:put('"') - local clean = true - for i = 0, len - 1 do - if dq_needs[p[i]] == 1 then - clean = false; break - end - end - if clean then tape:put(s) else tape:put((string.gsub(s, '[%z\1-\31"\\]', dq_replace))) end - tape:put('"') - end -end - ----@type fun(tape: string.buffer, v: json.Value, indent: string, level: integer, valueStyle: json.StringStyle | nil) -local putValue -- forward decl - ----@param t table ----@return boolean -local function isArray(t) - if keyStore[t] then return false end - local i = 0 - for _ in pairs(t) do - i = i + 1 - if t[i] == nil then return false end - end - return true -end - ----@param tape string.buffer ----@param t json.Array ----@param indent string ----@param level integer -local function putArray(tape, t, indent, level) - local n = #t - if n == 0 then - tape:put("[]"); return - end - local meta = metaStore[t] - local nextIndent = string.rep(indent, level + 1) - local defaultBefore = "\n" .. nextIndent - local closing = (meta and meta.__closingTrivia) or ("\n" .. string.rep(indent, level)) - tape:put("[") - for i = 1, n do - if i > 1 then tape:put(",") end - local km = meta and meta[i] - tape:put((km and km.before) or defaultBefore) - putValue(tape, t[i], indent, level + 1, km and km.valueStyle) - local av = km and km.afterValue - if av and av ~= "" then tape:put(av) end - end - if meta and meta.__trailingComma then tape:put(",") end - tape:put(closing); tape:put("]") -end - ----@param tape string.buffer ----@param t json.Object ----@param indent string ----@param level integer -local function putObject(tape, t, indent, level) - local ks = keyStore[t] - local meta = metaStore[t] - local nextIndent = string.rep(indent, level + 1) - - if not ks then - local keys = {} - for k in pairs(t) do keys[#keys + 1] = k end - table.sort(keys) - local n = #keys - if n == 0 then - tape:put("{}"); return - end - tape:put("{\n") - for i = 1, n do - if i > 1 then tape:put(",\n") end - tape:put(nextIndent); putString(tape, tostring(keys[i]), nil) - tape:put(": "); putValue(tape, t[keys[i]], indent, level + 1, nil) - end - tape:put("\n"); tape:put(string.rep(indent, level)); tape:put("}") - return - end - - local isSlice = ffi.istype(ks_ct, ks) - local nkeys = isSlice and ks.count or #ks - if nkeys == 0 then - tape:put("{}"); return - end - - if not meta then - tape:put("{\n") - if isSlice then - local base = ks.start - for i = 1, nkeys do - if i > 1 then tape:put(",\n") end - local k = key_arena[base + i] - tape:put(nextIndent); putString(tape, k, nil) - tape:put(": "); putValue(tape, t[k], indent, level + 1, nil) - end - else - for i = 1, nkeys do - if i > 1 then tape:put(",\n") end - local k = ks[i] - tape:put(nextIndent); putString(tape, k, nil) - tape:put(": "); putValue(tape, t[k], indent, level + 1, nil) - end - end - tape:put("\n"); tape:put(string.rep(indent, level)); tape:put("}") - return - end - - tape:put("{") - if isSlice then - local base = ks.start - for i = 1, nkeys do - if i > 1 then tape:put(",") end - local k = key_arena[base + i]; local km = meta[k] - tape:put((km and km.before) or " ") - local ks2 = km and km.keyStyle - if ks2 == "ident" then tape:put(k) else putString(tape, k, ks2) end - tape:put((km and km.between) or ""); tape:put(":") - tape:put((km and km.afterColon) or " ") - putValue(tape, t[k], indent, level + 1, km and km.valueStyle) - local av = km and km.afterValue; if av and av ~= "" then tape:put(av) end - end - else - for i = 1, nkeys do - if i > 1 then tape:put(",") end - local k = ks[i]; local km = meta[k] - tape:put((km and km.before) or " ") - local ks2 = km and km.keyStyle - if ks2 == "ident" then tape:put(k) else putString(tape, k, ks2) end - tape:put((km and km.between) or ""); tape:put(":") - tape:put((km and km.afterColon) or " ") - putValue(tape, t[k], indent, level + 1, km and km.valueStyle) - local av = km and km.afterValue; if av and av ~= "" then tape:put(av) end - end - end - if meta.__trailingComma then tape:put(",") end - tape:put(meta.__closingTrivia or " "); tape:put("}") -end - -local floor = math.floor -local huge = math.huge - -putValue = function(tape, v, indent, level, valueStyle) - local t = type(v) - if t == "nil" or v == json.null then - tape:put("null") - elseif t == "boolean" then - tape:put(v and "true" or "false") - elseif t == "number" then - if v ~= v then - tape:put("NaN") - elseif v == huge then - tape:put("Infinity") - elseif v == -huge then - tape:put("-Infinity") - else - tape:put(tostring(v)) - end - elseif t == "string" then - putString(tape, v, valueStyle) - elseif t == "table" then - if isArray(v) then - putArray(tape, v, indent, level) - else - putObject(tape, v, indent, level) - end - else - error("unsupported type: " .. t) - end -end - ----@param t table ----@param key string ----@param value json.Value -function json.addField(t, key, value) - t[key] = value - local ks = keyStore[t] - if not ks or ffi.istype(ks_ct, ks) then - local arr = {} - if ks then for i = 1, ks.count do arr[i] = key_arena[ks.start + i] end end - keyStore[t] = arr; ks = arr - end - ks[#ks + 1] = key -end - ----@param t table ----@param key string -function json.removeField(t, key) - t[key] = nil - local ks = keyStore[t] - if not ks or ffi.istype(ks_ct, ks) then return end - for i, k in ipairs(ks) do - if k == key then - table.remove(ks, i); return - end - end -end - ----@param value json.Value ----@return string -function json.encode(value) - local tape = strbuf.new() - putValue(tape, value, "\t", 0, nil) - tape:put("\n") - return tape:tostring() -end - --- ── decoder ─────────────────────────────────────────────────────────────────── - ----@type ffi.cdata* -local src_ptr ----@type integer -local src_len ----@type string -local src_s - -local ws_tab = ffi.new("uint8_t[256]") -ws_tab[32] = 1; ws_tab[9] = 1; ws_tab[10] = 1; ws_tab[13] = 1 - -local ident_tab = ffi.new("uint8_t[256]") -for i = 48, 57 do ident_tab[i] = 1 end -for i = 65, 90 do ident_tab[i] = 1 end -for i = 97, 122 do ident_tab[i] = 1 end -ident_tab[95] = 1; ident_tab[36] = 1 - ----@param pos integer 1-based ----@return integer 1-based -local function skipWS(pos) - local i = pos - 1 - while i < src_len and ws_tab[src_ptr[i]] == 1 do i = i + 1 end - return i + 1 -end - ----@param pos integer 1-based, sitting on '/' ----@param ts integer trivia start ----@return string trivia ----@return integer 1-based after trivia -local function collectComments(pos, ts) - while pos <= src_len do - if src_ptr[pos - 1] ~= 47 then break end - local b1 = src_ptr[pos] - if b1 == 47 then - local nl = C.memchr(src_ptr + pos + 1, 10, src_len - pos - 1) - pos = nl ~= nil and (cast(u8p, nl) - src_ptr + 2) or (src_len + 1) - elseif b1 == 42 then - local p = src_ptr + pos + 1; local rem = src_len - pos - 1; local found = false - while rem > 0 do - local star = C.memchr(p, 42, rem); if star == nil then error("unterminated block comment") end - local sp = cast(u8p, star); local off = sp - src_ptr - if off + 1 < src_len and src_ptr[off + 1] == 47 then - pos = off + 3; found = true; break - end - p = sp + 1; rem = src_len - (off + 1) - end - if not found then error("unterminated block comment") end - else - break - end - pos = skipWS(pos) - end - return string.sub(src_s, ts, pos - 1), pos -end - ----@param pos integer 1-based ----@return string|nil trivia ----@return integer 1-based after trivia -local function collectTrivia(pos) - local npos = skipWS(pos) - if npos <= src_len and src_ptr[npos - 1] == 47 then return collectComments(npos, pos) end - if npos == pos then return nil, npos end - return string.sub(src_s, pos, npos - 1), npos -end - ----@type fun(pos:integer): integer, integer -- returns (tok_idx, new_pos) -local parseValue -- forward decl - ----@type table -local escapeMap = { - [34] = '"', - [39] = "'", - [92] = '\\', - [47] = '/', - [98] = '\b', - [102] = '\f', - [110] = '\n', - [114] = '\r', - [116] = '\t' -} - --- Parse a quoted string into a token. Returns (tok_idx, new_pos). ----@param pos integer 1-based, pointing at opening quote ----@return integer tok_idx ----@return integer new_pos -local function parseString(pos) - local quote = src_ptr[pos - 1] - local i = pos + 1 - local pq = C.memchr(src_ptr + i - 1, quote, src_len - i + 1) - if pq == nil then error("unterminated string") end - local q_off = cast(u8p, pq) - src_ptr - local has_esc = C.memchr(src_ptr + i - 1, 92, q_off - (i - 1)) ~= nil - - local idx = tok_alloc() - local tok = tok_arena[idx] - tok.type = TY_STRING - tok.flags = has_esc and 1 or 0 - tok.next = 0 - tok.str_off = i - 1 -- 0-based offset into src_s - tok.str_len = q_off - (i - 1) - - if not has_esc then - return idx, q_off + 2 - end - - -- advance past the full escaped string - local j = i - while j <= src_len do - local rem = src_len - j + 1; local base = src_ptr + j - 1 - local pbs = C.memchr(base, 92, rem); local pq2 = C.memchr(base, quote, rem) - local bs_off = pbs ~= nil and (cast(u8p, pbs) - src_ptr) or src_len - local q_off2 = pq2 ~= nil and (cast(u8p, pq2) - src_ptr) or src_len - if q_off2 <= bs_off then - tok.str_len = q_off2 - (i - 1) -- store full span including escapes - return idx, q_off2 + 2 - end - local esc = src_ptr[bs_off + 1] - j = esc == 117 and bs_off + 7 or bs_off + 3 - end - error("unterminated string") -end - --- Materialise a string token into a Lua string (allocates only when called). ----@param tok json.Token ----@return string -local function tokToString(tok) - local off = tok.str_off + 1 -- 1-based start of content - if tok.flags == 0 then - return string.sub(src_s, off, off + tok.str_len - 1) - end - -- unescape: walk the raw span, replacing escape sequences - local buf = {} - local i = off - local lim = off + tok.str_len -- exclusive end (past last content byte) - while i < lim do - local rem = lim - i - local base = src_ptr + i - 1 - local pbs = C.memchr(base, 92, rem) -- backslash - local bs_off = pbs ~= nil and (cast(u8p, pbs) - src_ptr) or (lim - 1) - if bs_off >= lim - 1 then - -- no more backslashes, copy remainder - if i <= lim - 1 then buf[#buf + 1] = string.sub(src_s, i, lim - 1) end - break - end - if bs_off > i - 1 then buf[#buf + 1] = string.sub(src_s, i, bs_off) end - local esc = src_ptr[bs_off + 1] - if esc == 117 then - buf[#buf + 1] = string.char(tonumber(string.sub(src_s, bs_off + 2, bs_off + 5), 16)) - i = bs_off + 7 - elseif esc == 10 or esc == 13 then - i = bs_off + 3 - else - buf[#buf + 1] = escapeMap[esc] or string.char(esc); i = bs_off + 3 - end - end - return table.concat(buf) -end - ----@param pos integer 1-based ----@return integer tok_idx ----@return integer new_pos -local function parseIdentifier(pos) - local i = pos - 1 - local b = src_ptr[i] - if not ((b >= 65 and b <= 90) or (b >= 97 and b <= 122) or b == 95 or b == 36) then - error("invalid identifier at pos " .. pos) - end - i = i + 1 - while i < src_len and ident_tab[src_ptr[i]] == 1 do i = i + 1 end - local idx = tok_alloc() - local tok = tok_arena[idx] - tok.type = TY_STRING - tok.flags = 0 - tok.next = 0 - tok.str_off = pos - 1 -- 0-based - tok.str_len = i - (pos - 1) - return idx, i + 1 -end - ----@param pos integer 1-based ----@return integer tok_idx ----@return integer new_pos -local function parseNumber(pos) - local i = pos - 1; local neg = src_ptr[i] == 45 - if neg then i = i + 1 end - local b = src_ptr[i] - local idx = tok_alloc() - local tok = tok_arena[idx] - tok.next = 0 - if b >= 48 and b <= 57 then - if b == 48 then - local b2 = src_ptr[i + 1]; if b2 == 120 or b2 == 88 then goto slow end - end - local n = 0 - while i < src_len do - b = src_ptr[i]; if b < 48 or b > 57 then break end; n = n * 10 + (b - 48); i = i + 1 - end - if b ~= 46 and b ~= 101 and b ~= 69 then - tok.type = TY_INT; tok.num = neg and -n or n; return idx, i + 1 - end - end - ::slow:: - local numStr = string.match(src_s, "^-?0[xX]%x+", pos) - or string.match(src_s, "^[+-]?%d+%.?%d*[eE]?[+-]?%d*", pos) - local sub = string.sub(src_s, pos, pos + 8) - local v - if sub:sub(1, 8) == "Infinity" then - v = huge; numStr = sub:sub(1, 8) - elseif sub:sub(1, 9) == "+Infinity" then - v = huge; numStr = sub:sub(1, 9) - elseif sub:sub(1, 9) == "-Infinity" then - v = -huge; numStr = sub:sub(1, 9) - elseif sub:sub(1, 3) == "NaN" then - v = 0 / 0; numStr = sub:sub(1, 3) - else - v = tonumber(numStr) - end - tok.type = TY_FLOAT; tok.num = v - return idx, pos + #numStr -end - ----@param pos integer 1-based, pointing at '[' ----@return integer tok_idx ----@return integer new_pos -local function parseArray(pos) - local idx = tok_alloc() - local tok = tok_arena[idx] - tok.type = TY_ARRAY - tok.next = 0 - tok.child = 0 - tok.count = 0 - - local trivia, npos = collectTrivia(pos + 1); pos = npos - if src_ptr[pos - 1] == 93 then return idx, pos + 1 end - - local first_child = 0 - local prev_idx = 0 - local count = 0 - - while true do - local ci, npos2 = parseValue(pos); pos = npos2 - count = count + 1 - if first_child == 0 then first_child = ci end - if prev_idx ~= 0 then tok_arena[prev_idx].next = ci end - prev_idx = ci - - local _, npos3 = collectTrivia(pos); pos = npos3 - local c = src_ptr[pos - 1] - if c == 93 then break end - if c ~= 44 then error("expected ',' or ']'") end - pos = pos + 1 - local _, npos4 = collectTrivia(pos); pos = npos4 - if src_ptr[pos - 1] == 93 then break end - end - - tok_arena[idx].child = first_child - tok_arena[idx].count = count - return idx, pos + 1 -end - ----@param pos integer 1-based, pointing at '{' ----@return integer tok_idx ----@return integer new_pos -local function parseObject(pos) - local idx = tok_alloc() - local tok = tok_arena[idx] - tok.type = TY_OBJECT - tok.next = 0 - tok.child = 0 - tok.count = 0 - - local trivia, npos = collectTrivia(pos + 1); pos = npos - if src_ptr[pos - 1] == 125 then return idx, pos + 1 end - - local first_child = 0 - local prev_idx = 0 - local count = 0 - - while true do - -- key - local c = src_ptr[pos - 1] - local ki - if c == 34 or c == 39 then - ki, pos = parseString(pos) - else - ki, pos = parseIdentifier(pos) - end - - local _, npos2 = collectTrivia(pos); pos = npos2 - if src_ptr[pos - 1] ~= 58 then error("expected ':'") end - pos = pos + 1 - local _, npos3 = collectTrivia(pos); pos = npos3 - - -- value - local vi, npos4 = parseValue(pos); pos = npos4 - - -- link: key.next -> value, value.next -> next key (set later) - tok_arena[ki].next = vi - count = count + 1 - if first_child == 0 then first_child = ki end - if prev_idx ~= 0 then tok_arena[prev_idx].next = ki end - prev_idx = vi -- next sibling chain continues from value - - local _, npos5 = collectTrivia(pos); pos = npos5 - c = src_ptr[pos - 1] - if c == 125 then break end - if c ~= 44 then error("expected ',' or '}'") end - pos = pos + 1 - local _, npos6 = collectTrivia(pos); pos = npos6 - if src_ptr[pos - 1] == 125 then break end - end - - tok_arena[idx].child = first_child - tok_arena[idx].count = count - return idx, pos + 1 -end - -parseValue = function(pos) - local _, npos = collectTrivia(pos); pos = npos - local c = src_ptr[pos - 1] - if c == 34 or c == 39 then - return parseString(pos) - elseif c == 123 then - return parseObject(pos) - elseif c == 91 then - return parseArray(pos) - elseif c == 116 then - local idx = tok_alloc(); tok_arena[idx].type = TY_TRUE; tok_arena[idx].next = 0 - return idx, pos + 4 - elseif c == 102 then - local idx = tok_alloc(); tok_arena[idx].type = TY_FALSE; tok_arena[idx].next = 0 - return idx, pos + 5 - elseif c == 110 then - local idx = tok_alloc(); tok_arena[idx].type = TY_NULL; tok_arena[idx].next = 0 - return idx, pos + 4 - else - return parseNumber(pos) - end -end - --- ── public API ──────────────────────────────────────────────────────────────── - -json.null = setmetatable({}, { __tostring = function() return "null" end }) - --- Returns a doc table: { toks=tok_arena, src=src_s, root=root_idx } --- The token arena is reused on the next decodeDocument call, so copy if you need persistence. ----@param s string ----@return json.Doc -function json.decodeDocument(s) - tok_top = 0 - src_s = s - src_len = #s - src_ptr = cast(u8p, s) - local root, _ = parseValue(1) - return { toks = tok_arena, src = s, root = root } -end - --- Materialise a token into a plain Lua value (allocates strings/tables). --- For hot paths prefer json.iter / json.get / json.str / json.num. ----@param doc json.Doc ----@param idx integer token index ----@return json.Value -local function materialise(doc, idx) - local tok = doc.toks[idx] - local ty = tok.type - if ty == TY_NULL then - return json.null - elseif ty == TY_FALSE then - return false - elseif ty == TY_TRUE then - return true - elseif ty == TY_INT or ty == TY_FLOAT then - return tok.num - elseif ty == TY_STRING then - -- re-bind src for tokToString - src_s = doc.src - src_ptr = cast(u8p, src_s) - src_len = #src_s - return tokToString(tok) - elseif ty == TY_ARRAY then - local arr = {} - local ci = tok.child - local i = 0 - while ci ~= 0 do - i = i + 1 - arr[i] = materialise(doc, ci) - ci = doc.toks[ci].next - end - return arr - elseif ty == TY_OBJECT then - local obj = {} - local keys = {} - keyStore[obj] = keys - local ki = tok.child - while ki ~= 0 do - src_s = doc.src; src_ptr = cast(u8p, src_s); src_len = #src_s - local k = tokToString(doc.toks[ki]) - local vi = doc.toks[ki].next - obj[k] = materialise(doc, vi) - keys[#keys + 1] = k - ki = doc.toks[vi].next - end - return obj - end - error("unknown token type " .. ty) -end - --- Iterate children of an array or object token. --- For arrays: yields (index, child_tok_idx) --- For objects: yields (key_string, value_tok_idx) ----@param doc json.Doc ----@param idx integer token index of array or object ----@return fun(): (string|integer|nil), integer|nil -function json.iter(doc, idx) - local tok = doc.toks[idx] - local ty = tok.type - if ty == TY_ARRAY then - local ci = tok.child - local i = 0 - return function() - if ci == 0 then return nil end - i = i + 1 - local cur = ci - ci = doc.toks[ci].next - return i, cur - end - elseif ty == TY_OBJECT then - local ki = tok.child - src_s = doc.src; src_ptr = cast(u8p, src_s); src_len = #src_s - return function() - if ki == 0 then return nil end - local k = tokToString(doc.toks[ki]) - local vi = doc.toks[ki].next - ki = doc.toks[vi].next - return k, vi - end - end - return function() return nil end -end - --- Get a child token by key (object) or index (array). Returns token index or nil. ----@param doc json.Doc ----@param idx integer ----@param key string | integer ----@return integer | nil -function json.get(doc, idx, key) - local tok = doc.toks[idx] - local ty = tok.type - if ty == TY_ARRAY then - local ci = tok.child; local i = 0 - while ci ~= 0 do - i = i + 1 - if i == key then return ci end - ci = doc.toks[ci].next - end - elseif ty == TY_OBJECT then - src_s = doc.src; src_ptr = cast(u8p, src_s); src_len = #src_s - local ki = tok.child - while ki ~= 0 do - if tokToString(doc.toks[ki]) == key then return doc.toks[ki].next end - ki = doc.toks[doc.toks[ki].next].next - end - end - return nil -end - --- Get the Lua string value of a string token. ----@param doc json.Doc ----@param idx integer ----@return string -function json.str(doc, idx) - src_s = doc.src; src_ptr = cast(u8p, src_s); src_len = #src_s - return tokToString(doc.toks[idx]) -end - --- Get the numeric value of a number token. ----@param doc json.Doc ----@param idx integer ----@return number -function json.num(doc, idx) - return doc.toks[idx].num -end - --- Get the type name of a token. ----@param doc json.Doc ----@param idx integer ----@return "null"|"boolean"|"number"|"string"|"array"|"object" -function json.type(doc, idx) - local ty = doc.toks[idx].type - if ty == TY_NULL then - return "null" - elseif ty == TY_FALSE or ty == TY_TRUE then - return "boolean" - elseif ty == TY_INT or ty == TY_FLOAT then - return "number" - elseif ty == TY_STRING then - return "string" - elseif ty == TY_ARRAY then - return "array" - else - return "object" - end -end - --- Materialise the full document into Lua tables (old behaviour, allocates). ----@param doc json.Doc ----@param doc json.Doc ----@return json.Value -function json.materialise(doc) - return materialise(doc, doc.root) -end - --- Decode JSON string into plain Lua tables (allocating). ----@param s string ----@return json.Value -function json.decode(s) - local doc = json.decodeDocument(s) - return materialise(doc, doc.root) -end - --- Encode a json.Doc back to a JSON string (materialises then encodes). ----@param doc json.Doc ----@return string -function json.encodeDocument(doc) - return json.encode(materialise(doc, doc.root)) -end - -return json diff --git a/packages/json/tests/json.test.lua b/packages/json/tests/json.test.lua deleted file mode 100644 index 2ad28a64..00000000 --- a/packages/json/tests/json.test.lua +++ /dev/null @@ -1,219 +0,0 @@ -local test = require("lde-test") -local json = require("json") - --- encode - -test.it("encodes primitives", function() - test.equal(json.encode(nil):gsub("%s", ""), "null") - test.equal(json.encode(true):gsub("%s", ""), "true") - test.equal(json.encode(42):gsub("%s", ""), "42") - test.equal(json.encode("hi"):gsub("%s", ""), '"hi"') -end) - -test.it("encodes array", function() - local s = json.encode({ 1, 2, 3 }) - local t = json.decode(s) - test.equal(t[1], 1); test.equal(t[2], 2); test.equal(t[3], 3) -end) - -test.it("encodes object", function() - local s = json.encode({ a = 1 }) - local t = json.decode(s) - test.equal(t.a, 1) -end) - --- decode – standard JSON - -test.it("decodes null", function() - test.equal(tostring(json.decode("null")), "null") -end) - -test.it("decodes booleans", function() - test.equal(json.decode("true"), true) - test.equal(json.decode("false"), false) -end) - -test.it("decodes numbers", function() - test.equal(json.decode("42"), 42) - test.equal(json.decode("-3.14"), -3.14) - test.equal(json.decode("1e2"), 100) -end) - -test.it("decodes strings", function() - test.equal(json.decode('"hello"'), "hello") - test.equal(json.decode('"line\\nbreak"'), "line\nbreak") -end) - -test.it("decodes nested objects and arrays", function() - local t = json.decode('{"a":[1,2],"b":{"c":true}}') - test.equal(t.a[1], 1); test.equal(t.a[2], 2); test.equal(t.b.c, true) -end) - --- decode – JSON5 - -test.it("json5: single-line comment", function() - test.equal(json.decode('{\n// comment\n"a":1}').a, 1) -end) - -test.it("json5: block comment", function() - test.equal(json.decode('{"a": /* comment */ 1}').a, 1) -end) - -test.it("json5: single-quoted string value", function() - test.equal(json.decode("'hello'"), "hello") -end) - -test.it("json5: single-quoted string key", function() - test.equal(json.decode("{'key': 1}").key, 1) -end) - -test.it("json5: unquoted key", function() - test.equal(json.decode("{foo: 1}").foo, 1) -end) - -test.it("json5: trailing comma in object", function() - test.equal(json.decode('{"a":1,}').a, 1) -end) - -test.it("json5: trailing comma in array", function() - test.equal(#json.decode('[1,2,3,]'), 3) -end) - -test.it("json5: hex number", function() - test.equal(json.decode("0xFF"), 255) -end) - -test.it("json5: Infinity", function() - test.equal(json.decode("Infinity"), math.huge) - test.equal(json.decode("+Infinity"), math.huge) - test.equal(json.decode("-Infinity"), -math.huge) -end) - -test.it("json5: NaN", function() - local n = json.decode("NaN") - test.truthy(n ~= n) -end) - --- order preservation - -test.it("addField preserves insertion order on encode", function() - local t = {} - json.addField(t, "z", 1); json.addField(t, "a", 2); json.addField(t, "m", 3) - local s = json.encode(t) - test.truthy(s:find('"z"') < s:find('"a"') and s:find('"a"') < s:find('"m"')) -end) - -test.it("decode preserves key insertion order", function() - local t = json.decode('{"z":1,"a":2,"m":3}') - local s = json.encode(t) - test.truthy(s:find('"z"') < s:find('"a"') and s:find('"a"') < s:find('"m"')) -end) - -test.it("removeField removes key and preserves order of remaining keys", function() - local t = {} - json.addField(t, "a", 1); json.addField(t, "b", 2); json.addField(t, "c", 3) - json.removeField(t, "b") - local s = json.encode(t) - test.truthy(not s:find('"b"')) - test.truthy(s:find('"a"') < s:find('"c"')) -end) - --- comment/style preservation (only via addField/encode, not materialise) - -test.it("preserves unquoted key style on re-encode via addField", function() - local t = {} - json.addField(t, "foo", 1) - -- addField uses plain string[], encode uses double-quote by default - local out = json.encode(t) - test.truthy(out:find('"foo"')) -end) - -test.it("preserves double-quoted key style on re-encode", function() - test.truthy(json.encode(json.decode('{"baz": 3}')):find('"baz"')) -end) - -test.it("preserves double-quoted string value on re-encode", function() - test.truthy(json.encode(json.decode('{"key": "world"}')):find('"world"')) -end) - --- zero-alloc API - -test.it("json.iter over array yields indices and token indices", function() - local doc = json.decodeDocument('[10,20,30]') - local keys, vals = {}, {} - for i, vi in json.iter(doc, doc.root) do - keys[#keys+1] = i - vals[#vals+1] = json.num(doc, vi) - end - test.equal(#keys, 3) - test.equal(vals[1], 10); test.equal(vals[2], 20); test.equal(vals[3], 30) -end) - -test.it("json.iter over object yields key strings and token indices", function() - local doc = json.decodeDocument('{"x":1,"y":2}') - local keys, vals = {}, {} - for k, vi in json.iter(doc, doc.root) do - keys[#keys+1] = k - vals[k] = json.num(doc, vi) - end - test.equal(vals.x, 1); test.equal(vals.y, 2) -end) - -test.it("json.get retrieves array element by index", function() - local doc = json.decodeDocument('[10,20,30]') - test.equal(json.num(doc, json.get(doc, doc.root, 2)), 20) -end) - -test.it("json.get retrieves object value by key", function() - local doc = json.decodeDocument('{"name":"alice","age":30}') - test.equal(json.str(doc, json.get(doc, doc.root, "name")), "alice") - test.equal(json.num(doc, json.get(doc, doc.root, "age")), 30) -end) - -test.it("json.type returns correct type names", function() - local doc = json.decodeDocument('[null,true,false,42,3.14,"hi",[],{}]') - local types = {} - for _, vi in json.iter(doc, doc.root) do types[#types+1] = json.type(doc, vi) end - test.equal(types[1], "null"); test.equal(types[2], "boolean") - test.equal(types[3], "boolean"); test.equal(types[4], "number") - test.equal(types[5], "number"); test.equal(types[6], "string") - test.equal(types[7], "array"); test.equal(types[8], "object") -end) - -test.it("json.str handles escaped strings", function() - local doc = json.decodeDocument('"hello\\nworld"') - test.equal(json.str(doc, doc.root), "hello\nworld") -end) - --- regression: keys from a decoded object must survive any number of subsequent decodes --- (key_arena was being reset to 0 on each decodeDocument, corrupting prior slices) -test.it("encode preserves keys after a subsequent decode clobbers the key arena", function() - local config = json.decode('{"name":"myproject","version":"1.0.0","dependencies":{}}') - -- A second decode used to reset key_arena_top to 0, overwriting arena slots with new keys - json.decode('{"arch":null,"url":null,"luarocks":null}') - json.decode('{"x":1,"y":2,"z":3}') - local out = json.encode(config) - local roundtrip = json.decode(out) - test.equal(roundtrip.name, "myproject") - test.equal(roundtrip.version, "1.0.0") - test.truthy(roundtrip.dependencies) -end) - -test.it("decodeDocument+materialise key slices survive subsequent decodeDocument calls", function() - local doc1 = json.decodeDocument('{"a":1,"b":2}') - local obj1 = json.materialise(doc1) - -- second decodeDocument used to reset key_arena_top, clobbering doc1's slice - local doc2 = json.decodeDocument('{"x":10,"y":20,"z":30}') - local obj2 = json.materialise(doc2) - -- obj1's key order must still be intact - local out1 = json.encode(obj1) - local r1 = json.decode(out1) - test.equal(r1.a, 1) - test.equal(r1.b, 2) - -- obj2 must also be correct - local out2 = json.encode(obj2) - local r2 = json.decode(out2) - test.equal(r2.x, 10) - test.equal(r2.y, 20) - test.equal(r2.z, 30) -end) diff --git a/packages/lde-core/lde.json b/packages/lde-core/lde.json index 165b714e..d59513d4 100644 --- a/packages/lde-core/lde.json +++ b/packages/lde-core/lde.json @@ -2,14 +2,14 @@ "name": "lde-core", "version": "0.1.0", "dependencies": { - "json": { "path": "../json" }, + "json": { "git": "https://github.com/lde-org/json" }, "ansi": { "path": "../ansi" }, "sea": { "path": "../sea" }, - "path": { "path": "../path" }, - "process2": { "path": "../process2" }, + "path": { "git": "https://github.com/lde-org/path" }, + "process": { "git": "https://github.com/lde-org/process" }, "clap": { "path": "../clap" }, - "fs": { "path": "../fs" }, - "env": { "path": "../env" }, + "fs": { "git": "https://github.com/lde-org/fs" }, + "env": { "git": "https://github.com/lde-org/env" }, "util": { "path": "../util" }, "curl-sys": { "git": "https://github.com/lde-org/curl-sys" }, "semver": { "path": "../semver" }, diff --git a/packages/lde-core/src/global/init.lua b/packages/lde-core/src/global/init.lua index 65022d8f..a2934217 100644 --- a/packages/lde-core/src/global/init.lua +++ b/packages/lde-core/src/global/init.lua @@ -2,7 +2,7 @@ local fs = require("fs") local git = require("git") local json = require("json") local path = require("path") -local process = require("process2") +local process = require("process") local semver = require("semver") local lde = require("lde-core") local ansi = require("ansi") diff --git a/packages/lde-core/src/package/init.lua b/packages/lde-core/src/package/init.lua index aebfb7ea..c7bf8120 100644 --- a/packages/lde-core/src/package/init.lua +++ b/packages/lde-core/src/package/init.lua @@ -6,7 +6,7 @@ local fs = require("fs") local env = require("env") local json = require("json") local path = require("path") -local process = require("process2") +local process = require("process") ---@class lde.Package ---@field dir string diff --git a/packages/lde-core/src/package/rockspec.lua b/packages/lde-core/src/package/rockspec.lua index 9cb895aa..f432836a 100644 --- a/packages/lde-core/src/package/rockspec.lua +++ b/packages/lde-core/src/package/rockspec.lua @@ -5,7 +5,7 @@ local lde = require("lde-core") local fs = require("fs") local env = require("env") local path = require("path") -local process = require("process2") +local process = require("process") local util = require("util") local curl = require("curl-sys") diff --git a/packages/lde-core/src/package/run.lua b/packages/lde-core/src/package/run.lua index 18e17718..2cd2fd34 100644 --- a/packages/lde-core/src/package/run.lua +++ b/packages/lde-core/src/package/run.lua @@ -1,7 +1,7 @@ local fs = require("fs") local path = require("path") local ffi = require("ffi") -local process = require("process2") +local process = require("process") local runtime = require("lde-core.runtime") ---@param package lde.Package diff --git a/packages/lde-core/tests/commonrocks.test.lua b/packages/lde-core/tests/commonrocks.test.lua index 5decb6b6..0a11050f 100644 --- a/packages/lde-core/tests/commonrocks.test.lua +++ b/packages/lde-core/tests/commonrocks.test.lua @@ -4,7 +4,6 @@ local fs = require("fs") local env = require("env") local path = require("path") local json = require("json") -local process = require("process2") local tmpBase = path.join(env.tmpdir(), "lde-commonrocks-tests") fs.rmdir(tmpBase) diff --git a/packages/lde-core/tests/compile.test.lua b/packages/lde-core/tests/compile.test.lua index aea5bf85..9dd2ba8b 100644 --- a/packages/lde-core/tests/compile.test.lua +++ b/packages/lde-core/tests/compile.test.lua @@ -5,7 +5,7 @@ local fs = require("fs") local env = require("env") local path = require("path") local json = require("json") -local process = require("process2") +local process = require("process") local tmpBase = path.join(env.tmpdir(), "lde-compile-tests") fs.rmdir(tmpBase) diff --git a/packages/lde/lde.json b/packages/lde/lde.json index b3bf638f..c4f3d225 100644 --- a/packages/lde/lde.json +++ b/packages/lde/lde.json @@ -2,13 +2,13 @@ "name": "lde", "version": "0.1.0", "dependencies": { - "json": { "path": "../json" }, + "json": { "git": "https://github.com/lde-org/json" }, "ansi": { "path": "../ansi" }, - "path": { "path": "../path" }, - "process2": { "path": "../process2" }, + "path": { "git": "https://github.com/lde-org/path" }, + "process": { "git": "https://github.com/lde-org/process" }, "clap": { "path": "../clap" }, - "fs": { "path": "../fs" }, - "env": { "path": "../env" }, + "fs": { "git": "https://github.com/lde-org/fs" }, + "env": { "git": "https://github.com/lde-org/env" }, "curl-sys": { "git": "https://github.com/lde-org/curl-sys" }, "semver": { "path": "../semver" }, "lde-core": { "path": "../lde-core" }, diff --git a/packages/lde/src/commands/compile.lua b/packages/lde/src/commands/compile.lua index d523d8d1..8efa808c 100644 --- a/packages/lde/src/commands/compile.lua +++ b/packages/lde/src/commands/compile.lua @@ -1,6 +1,5 @@ local ansi = require("ansi") local fs = require("fs") -local process = require("process2") local path = require("path") local lde = require("lde-core") diff --git a/packages/lde/src/commands/publish.lua b/packages/lde/src/commands/publish.lua index 8f2c2af2..a06b8a4a 100644 --- a/packages/lde/src/commands/publish.lua +++ b/packages/lde/src/commands/publish.lua @@ -1,7 +1,7 @@ local ansi = require("ansi") local git = require("git") local json = require("json") -local process = require("process2") +local process = require("process") local lde = require("lde-core") diff --git a/packages/lde/src/commands/run.lua b/packages/lde/src/commands/run.lua index b983fdec..e37da927 100644 --- a/packages/lde/src/commands/run.lua +++ b/packages/lde/src/commands/run.lua @@ -1,7 +1,7 @@ local env = require("env") local fs = require("fs") local ansi = require("ansi") -local process = require("process2") +local process = require("process") local lde = require("lde-core") diff --git a/packages/lde/src/commands/uninstall.lua b/packages/lde/src/commands/uninstall.lua index 41d016dd..f2a79dd2 100644 --- a/packages/lde/src/commands/uninstall.lua +++ b/packages/lde/src/commands/uninstall.lua @@ -1,7 +1,6 @@ local ansi = require("ansi") local fs = require("fs") local path = require("path") -local process = require("process2") local lde = require("lde-core") diff --git a/packages/lde/src/init.lua b/packages/lde/src/init.lua index bae234ef..a4b73734 100755 --- a/packages/lde/src/init.lua +++ b/packages/lde/src/init.lua @@ -41,8 +41,8 @@ if os.getenv("BOOTSTRAP") then end local pathPackages = { - "ansi", "clap", "fs", "env", "path", "json", "git", "luarocks", - "process2", "sea", "semver", "util", "lde-core", "lde-test", "rocked", "archive" + "ansi", "clap", "fs", "env", "path", "git", "luarocks", + "sea", "semver", "util", "lde-core", "lde-test", "rocked", "archive" } for _, pkg in ipairs(pathPackages) do diff --git a/packages/lde/src/setup.lua b/packages/lde/src/setup.lua index 4fa879f4..4b03c050 100644 --- a/packages/lde/src/setup.lua +++ b/packages/lde/src/setup.lua @@ -1,7 +1,7 @@ local ansi = require("ansi") local fs = require("fs") local path = require("path") -local process = require("process2") +local process = require("process") local lde = require("lde-core") diff --git a/packages/lde/tests/lib/ldecli.lua b/packages/lde/tests/lib/ldecli.lua index 2f0f8719..8b8d3f04 100644 --- a/packages/lde/tests/lib/ldecli.lua +++ b/packages/lde/tests/lib/ldecli.lua @@ -1,4 +1,4 @@ -local process = require("process2") +local process = require("process") local env = require("env") local ldePath = assert(env.execPath()) diff --git a/packages/path/lde.json b/packages/path/lde.json deleted file mode 100644 index f8a8584d..00000000 --- a/packages/path/lde.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "path", - "version": "0.1.0" -} diff --git a/packages/path/src/init.lua b/packages/path/src/init.lua deleted file mode 100644 index 7f62076d..00000000 --- a/packages/path/src/init.lua +++ /dev/null @@ -1,131 +0,0 @@ -local path = {} - -path.separator = string.sub(package.config, 1, 1) - -local isWindows = path.separator == "\\" - ----@param p string -function path.basename(p) - return string.match(p, "([^/\\]+)$") or "" -end - ----@param p string -function path.extension(p) - return path.basename(p):match("%.([^%.]+)$") or "" -end - ----@param p string -function path.dirname(p) - return p:match("^(.*)[/\\]") or "." -end - -local windowsDriveLetter = "^%a:\\" - ----@param p string -function path.isAbsolute(p) - if string.sub(p, 1, 1) == path.separator then - return true - end - - if isWindows then - return string.match(p, windowsDriveLetter) ~= nil - end -end - ----@param p string -function path.parts(p) - return string.gmatch(p, "[^/\\]+") -end - ----@param p string -function path.root(p) - local root = string.sub(p, 1, 1) - if root == path.separator then - return root - end - - if isWindows then - root = string.match(p, windowsDriveLetter) - if root then - return root - end - end -end - -function path.normalize(p) - local root = path.root(p) -- Root if absolute - local isRelative = root == nil - local parts = {} - - for part in path.parts(p) do - if part == ".." then - if #parts > 0 and parts[#parts] ~= ".." then - table.remove(parts) - elseif isRelative then - parts[#parts + 1] = ".." - end - elseif part ~= "." and part ~= "" then - parts[#parts + 1] = part - end - end - - if #parts == 0 then - return root or "." - else - local result = table.concat(parts, path.separator) - if root and root == path.separator then - return root .. result - else - return result - end - end -end - ----@param base string ----@param relative string -function path.resolve(base, relative) - if path.isAbsolute(relative) then - return path.normalize(relative) - end - - return path.normalize(base .. path.separator .. relative) -end - ----@param ... string -function path.join(...) - return table.concat({ ... }, path.separator) -end - ----@param from string ----@param to string -function path.relative(from, to) - from = path.normalize(from) - to = path.normalize(to) - - local fromParts = {} - for part in path.parts(from) do fromParts[#fromParts + 1] = part end - - local toParts = {} - for part in path.parts(to) do toParts[#toParts + 1] = part end - - local commonLength = 0 - for i = 1, math.min(#fromParts, #toParts) do - if fromParts[i] == toParts[i] then - commonLength = i - else - break - end - end - - local relativeParts = {} - for _ = commonLength + 1, #fromParts do relativeParts[#relativeParts + 1] = ".." end - for i = commonLength + 1, #toParts do relativeParts[#relativeParts + 1] = toParts[i] end - - if #relativeParts == 0 then - return "." - end - - return table.concat(relativeParts, path.separator) -end - -return path diff --git a/packages/process2/.gitignore b/packages/process2/.gitignore deleted file mode 100644 index 8a3d2033..00000000 --- a/packages/process2/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target/ -/lde.lock \ No newline at end of file diff --git a/packages/process2/lde.json b/packages/process2/lde.json deleted file mode 100644 index 66138bca..00000000 --- a/packages/process2/lde.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "process2", - "version": "0.1.0", - "dependencies": {}, - "devDependencies": { - "lde-test": { "path": "../lde-test" } - } -} diff --git a/packages/process2/src/init.lua b/packages/process2/src/init.lua deleted file mode 100644 index 5f6e4cc2..00000000 --- a/packages/process2/src/init.lua +++ /dev/null @@ -1,120 +0,0 @@ -local isWindows = jit.os == "Windows" - ----@class process2.raw -local raw = isWindows - and require("process2.raw.windows") - or require("process2.raw.posix") - ----@alias process2.Stdio "pipe" | "inherit" | "null" - ----@class process2.Options ----@field cwd string? ----@field env table? ----@field stdin string? ----@field stdout process2.Stdio? ----@field stderr process2.Stdio? - ----@class process2.Child ----@field pid number ----@field kill fun(self: process2.Child, force: boolean?) ----@field wait fun(self: process2.Child): number?, string?, string? ----@field poll fun(self: process2.Child): number? - ----@class process2 -local process2 = {} - -if jit.os == "Windows" then - process2.platform = "win32" -elseif jit.os == "Linux" then - process2.platform = "linux" -elseif jit.os == "OSX" then - process2.platform = "darwin" -else - process2.platform = "unix" -end - -local function readOut(r) - if isWindows then - local stdout = r.stdoutHandle and raw.readHandle(r.stdoutHandle) or nil - local stderr = r.stderrHandle and raw.readHandle(r.stderrHandle) or nil - return stdout, stderr - else - if r.stdoutFd and r.stderrFd then - return raw.readFds(r.stdoutFd, r.stderrFd) - end - - local stdout = r.stdoutFd and raw.readFd(r.stdoutFd) or nil - local stderr = r.stderrFd and raw.readFd(r.stderrFd) or nil - return stdout, stderr - end -end - -local function waitHandle(r) - if isWindows then return raw.wait(r.handle) else return raw.wait(r.pid) end -end - -local function pollHandle(r) - if isWindows then return raw.poll(r.handle) else return raw.poll(r.pid) end -end - -local function killHandle(r, force) - if isWindows then raw.kill(r.handle) else raw.kill(r.pid, force) end -end - ---- Spawn a process asynchronously. Returns a Child handle. ----@param name string ----@param args string[]? ----@param opts process2.Options? ----@return process2.Child?, string? -function process2.spawn(name, args, opts) - opts = opts or {} - local result, err = raw.spawn(name, args or {}, { - cwd = opts.cwd, - env = opts.env, - stdin = opts.stdin, - stdout = opts.stdout or "null", - stderr = opts.stderr or "null" - }) - if not result then return nil, err end - - local r = result - ---@type process2.Child - local child = { pid = result.pid } - - function child:kill(force) killHandle(r, force) end - - function child:wait() - local stdout, stderr = readOut(r) - local code = waitHandle(r) - return code, stdout, stderr - end - - function child:poll() return pollHandle(r) end - - return child -end - ---- Execute a process and block until it exits. ----@param name string ----@param args string[]? ----@param opts process2.Options? ----@return number? exitCode ----@return string? stdout ----@return string? stderr -function process2.exec(name, args, opts) - opts = opts or {} - local result, err = raw.spawn(name, args or {}, { - cwd = opts.cwd, - env = opts.env, - stdin = opts.stdin, - stdout = opts.stdout or "pipe", - stderr = opts.stderr or "pipe" - }) - if not result then return nil, nil, err end - - local stdout, stderr = readOut(result) - local code = waitHandle(result) - return code, stdout, stderr -end - -return process2 diff --git a/packages/process2/src/raw/posix.lua b/packages/process2/src/raw/posix.lua deleted file mode 100644 index b0361d2b..00000000 --- a/packages/process2/src/raw/posix.lua +++ /dev/null @@ -1,214 +0,0 @@ -local ffi = require("ffi") -local sb = require("string.buffer") - -ffi.cdef([[ - typedef int pid_t; - pid_t fork(void); - int execvp(const char* file, const char* const argv[]); - pid_t waitpid(pid_t pid, int* status, int options); - int kill(pid_t pid, int sig); - int pipe(int pipefd[2]); - long read(int fd, void* buf, size_t count); - long write(int fd, const void* buf, size_t count); - int close(int fd); - int dup2(int oldfd, int newfd); - int open(const char* path, int flags, ...); - int setenv(const char* name, const char* value, int overwrite); - int chdir(const char* path); - void _exit(int status); - struct pollfd { int fd; short events; short revents; }; - int poll(struct pollfd* fds, unsigned long nfds, int timeout); -]]) - -local WNOHANG = 1 -local SIGTERM = 15 -local SIGKILL = 9 -local O_WRONLY = 1 -local POLLIN = 1 -local POLLHUP = 16 - ----@diagnostic disable: assign-type-mismatch # Ignore incessant ffi type cast annoyance - ----@class process2.ffi.IntBox: ffi.cdata* ----@field [0] number - ----@type fun(): process2.ffi.IntBox -local IntBox = ffi.typeof("int[1]") - ----@class process2.ffi.PipeFds: ffi.cdata* ----@field [0] number ----@field [1] number - ----@type fun(): process2.ffi.PipeFds -local PipeFds = ffi.typeof("int[2]") - ----@type fun(size: number): ffi.cdata* -local PollFds = ffi.typeof("struct pollfd[?]") - ----@class process2.ffi.Argv: ffi.cdata* ----@field [0] string? - ----@type fun(size: number): process2.ffi.Argv -local Argv = ffi.typeof("const char*[?]") - ----@class process2.raw -local M = {} - ----@param status number ----@return number? -local function decodeExit(status) - if bit.band(status, 0x7f) == 0 then - return bit.rshift(bit.band(status, 0xff00), 8) - end - return nil -end - ----@param name string ----@param args string[] ----@return process2.ffi.Argv -local function makeArgv(name, args) - local argv = Argv(#args + 2) - argv[0] = name - for i, a in ipairs(args) do argv[i] = a end - argv[#args + 1] = nil - return argv -end - ---- Spawn a child process. ----@param name string ----@param args string[] ----@param opts { cwd: string?, env: table?, stdin: string?, stdout: "pipe"|"inherit"|"null"?, stderr: "pipe"|"inherit"|"null"? }? ----@return { pid: number, stdoutFd: number?, stderrFd: number? }?, string? -function M.spawn(name, args, opts) - opts = opts or {} - local stdoutMode = opts.stdout or "pipe" - local stderrMode = opts.stderr or "pipe" - local hasStdin = opts.stdin ~= nil - - local pIn = PipeFds() - local pOut = PipeFds() - local pErr = PipeFds() - - if hasStdin and ffi.C.pipe(pIn) ~= 0 then return nil, "pipe() failed" end - if stdoutMode == "pipe" and ffi.C.pipe(pOut) ~= 0 then return nil, "pipe() failed" end - if stderrMode == "pipe" and ffi.C.pipe(pErr) ~= 0 then return nil, "pipe() failed" end - - local pid = ffi.C.fork() - if pid < 0 then return nil, "fork() failed" end - - if pid == 0 then - if hasStdin then - ffi.C.dup2(pIn[0], 0); ffi.C.close(pIn[0]); ffi.C.close(pIn[1]) - end - if stdoutMode == "pipe" then - ffi.C.dup2(pOut[1], 1); ffi.C.close(pOut[0]); ffi.C.close(pOut[1]) - elseif stdoutMode == "null" then - local fd = ffi.C.open("/dev/null", O_WRONLY); ffi.C.dup2(fd, 1); ffi.C.close(fd) - end - if stderrMode == "pipe" then - ffi.C.dup2(pErr[1], 2); ffi.C.close(pErr[0]); ffi.C.close(pErr[1]) - elseif stderrMode == "null" then - local fd = ffi.C.open("/dev/null", O_WRONLY); ffi.C.dup2(fd, 2); ffi.C.close(fd) - end - if opts.cwd then ffi.C.chdir(opts.cwd) end - if opts.env then for k, v in pairs(opts.env) do ffi.C.setenv(k, v, 1) end end - ffi.C.execvp(name, makeArgv(name, args)) - ffi.C._exit(1) - end - - if hasStdin then ffi.C.close(pIn[0]) end - if stdoutMode == "pipe" then ffi.C.close(pOut[1]) end - if stderrMode == "pipe" then ffi.C.close(pErr[1]) end - - if hasStdin then - ffi.C.write(pIn[1], opts.stdin, #opts.stdin) - ffi.C.close(pIn[1]) - end - - return { - pid = tonumber(pid), - stdoutFd = stdoutMode == "pipe" and tonumber(pOut[0]) or nil, - stderrFd = stderrMode == "pipe" and tonumber(pErr[0]) or nil - } -end - ----@param fd number ----@return string -function M.readFd(fd) - local out = sb.new() - while true do - local ptr, len = out:reserve(4096) - local n = ffi.C.read(fd, ptr, len) - if n > 0 then out:commit(n) - else out:commit(0); break - end - end - ffi.C.close(fd) - return out:tostring() -end - ---- Drain two fds concurrently using poll() to avoid deadlock. ----@param outFd number ----@param errFd number ----@return string, string -function M.readFds(outFd, errFd) - local outBuf, errBuf = sb.new(), sb.new() - local fds = PollFds(2) - local outDone, errDone = false, false - while not outDone or not errDone do - fds[0].fd = outDone and -1 or outFd - fds[0].events = POLLIN - fds[1].fd = errDone and -1 or errFd - fds[1].events = POLLIN - ffi.C.poll(fds, 2, -1) - if not outDone then - if bit.band(fds[0].revents, POLLIN) ~= 0 then - local ptr, len = outBuf:reserve(4096) - local n = ffi.C.read(outFd, ptr, len) - if n > 0 then outBuf:commit(n) - else outBuf:commit(0); outDone = true - end - elseif fds[0].revents ~= 0 then - outDone = true - end - end - if not errDone then - if bit.band(fds[1].revents, POLLIN) ~= 0 then - local ptr, len = errBuf:reserve(4096) - local n = ffi.C.read(errFd, ptr, len) - if n > 0 then errBuf:commit(n) - else errBuf:commit(0); errDone = true - end - elseif fds[1].revents ~= 0 then - errDone = true - end - end - end - ffi.C.close(outFd) - ffi.C.close(errFd) - return outBuf:tostring(), errBuf:tostring() -end - ----@param pid number ----@return number? -function M.wait(pid) - local st = IntBox() - ffi.C.waitpid(pid, st, 0) - return decodeExit(st[0]) -end - ----@param pid number ----@return number? -function M.poll(pid) - local st = IntBox() - if ffi.C.waitpid(pid, st, WNOHANG) == 0 then return nil end - return decodeExit(st[0]) -end - ----@param pid number ----@param force boolean? -function M.kill(pid, force) - ffi.C.kill(pid, force and SIGKILL or SIGTERM) -end - -return M diff --git a/packages/process2/src/raw/windows.lua b/packages/process2/src/raw/windows.lua deleted file mode 100644 index 6d8ee047..00000000 --- a/packages/process2/src/raw/windows.lua +++ /dev/null @@ -1,341 +0,0 @@ -local ffi = require("ffi") -local buffer = require("string.buffer") - -ffi.cdef([[ - typedef void* HANDLE; - typedef uint32_t DWORD; - typedef int BOOL; - typedef uint16_t WORD; - typedef char* LPSTR; - - typedef struct { - DWORD nLength; - void* lpSecurityDescriptor; - BOOL bInheritHandle; - } SECURITY_ATTRIBUTES; - - typedef struct { - DWORD cb; - LPSTR lpReserved; - LPSTR lpDesktop; - LPSTR lpTitle; - DWORD dwX, dwY, dwXSize, dwYSize; - DWORD dwXCountChars, dwYCountChars; - DWORD dwFillAttribute; - DWORD dwFlags; - WORD wShowWindow; - WORD cbReserved2; - void* lpReserved2; - HANDLE hStdInput; - HANDLE hStdOutput; - HANDLE hStdError; - } STARTUPINFOA; - - typedef struct { - HANDLE hProcess; - HANDLE hThread; - DWORD dwProcessId; - DWORD dwThreadId; - } PROCESS_INFORMATION; - - BOOL CreateProcessA( - const char* lpApplicationName, - char* lpCommandLine, - void* lpProcessAttributes, - void* lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - void* lpEnvironment, - const char* lpCurrentDirectory, - STARTUPINFOA* lpStartupInfo, - PROCESS_INFORMATION* lpProcessInformation - ); - - BOOL CreatePipe(HANDLE* hReadPipe, HANDLE* hWritePipe, SECURITY_ATTRIBUTES* lpPipeAttributes, DWORD nSize); - BOOL SetHandleInformation(HANDLE hObject, DWORD dwMask, DWORD dwFlags); - BOOL ReadFile(HANDLE hFile, void* lpBuffer, DWORD nNumberOfBytesToRead, DWORD* lpNumberOfBytesRead, void* lpOverlapped); - BOOL WriteFile(HANDLE hFile, const void* lpBuffer, DWORD nNumberOfBytesToWrite, DWORD* lpNumberOfBytesWritten, void* lpOverlapped); - DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds); - BOOL GetExitCodeProcess(HANDLE hProcess, DWORD* lpExitCode); - BOOL TerminateProcess(HANDLE hProcess, DWORD uExitCode); - BOOL CloseHandle(HANDLE hObject); - HANDLE GetStdHandle(DWORD nStdHandle); - HANDLE CreateFileA(const char*, DWORD, DWORD, void*, DWORD, DWORD, HANDLE); - char* GetEnvironmentStringsA(void); - BOOL FreeEnvironmentStringsA(char* penv); -]]) - -local kernel32 = ffi.load("kernel32") - -local STARTF_USESTDHANDLES = 0x00000100 -local HANDLE_FLAG_INHERIT = 0x00000001 -local INFINITE = 0xFFFFFFFF -local STILL_ACTIVE = 259 -local CREATE_NO_WINDOW = 0x08000000 -local STD_INPUT_HANDLE = ffi.cast("DWORD", -10) -local STD_OUTPUT_HANDLE = ffi.cast("DWORD", -11) -local STD_ERROR_HANDLE = ffi.cast("DWORD", -12) -local INVALID_HANDLE_VALUE = ffi.cast("HANDLE", -1) -local GENERIC_READ = 0x80000000 -local GENERIC_WRITE = 0x40000000 -local OPEN_EXISTING = 3 -local FILE_ATTRIBUTE_NORMAL = 0x80 - ----@class process2.ffi.SecurityAttributes: ffi.cdata* ----@field nLength number ----@field lpSecurityDescriptor ffi.cdata* ----@field bInheritHandle number - ----@diagnostic disable: assign-type-mismatch # Ignore incessant ffi type cast annoyance - ----@type fun(): process2.ffi.SecurityAttributes -local SecurityAttributes = ffi.typeof("SECURITY_ATTRIBUTES") - ----@type number -local SecurityAttributesSize = ffi.sizeof("SECURITY_ATTRIBUTES") - ----@class process2.ffi.StartupInfoA: ffi.cdata* ----@field cb number ----@field dwFlags number ----@field hStdInput ffi.cdata* ----@field hStdOutput ffi.cdata* ----@field hStdError ffi.cdata* - ----@type fun(): process2.ffi.StartupInfoA -local StartupInfoA = ffi.typeof("STARTUPINFOA") - ----@type number -local StartupInfoASize = ffi.sizeof("STARTUPINFOA") - ----@class process2.ffi.ProcessInformation: ffi.cdata* ----@field hProcess ffi.cdata* ----@field hThread ffi.cdata* ----@field dwProcessId number ----@field dwThreadId number - ----@type fun(): process2.ffi.ProcessInformation -local ProcessInformation = ffi.typeof("PROCESS_INFORMATION") - ----@class process2.ffi.HandleBox: ffi.cdata* ----@field [0] ffi.cdata* - ----@type fun(): process2.ffi.HandleBox -local HandleBox = ffi.typeof("HANDLE[1]") - ----@class process2.ffi.DwordBox: ffi.cdata* ----@field [0] number - ----@type fun(): process2.ffi.DwordBox -local DwordBox = ffi.typeof("DWORD[1]") - ----@class process2.ffi.CharBuf: ffi.cdata* - ----@type fun(size: number, s: string?): process2.ffi.CharBuf -local CharBuf = ffi.typeof("char[?]") - ----@class process2.raw -local M = {} - ----@param s string ----@return string -local function escapeArg(s) - if not s:find('[ \t\n\v"\\]') and s ~= "" then return s end - local out, i = { '"' }, 1 - while i <= #s do - local c = s:sub(i, i) - if c == "\\" then - local j = i - while j <= #s and s:sub(j, j) == "\\" do j = j + 1 end - local nbs = j - i - if j > #s or s:sub(j, j) == '"' then nbs = nbs * 2 end - out[#out + 1] = string.rep("\\", nbs) - i = j - elseif c == '"' then - out[#out + 1] = '\\"'; i = i + 1 - else - out[#out + 1] = c; i = i + 1 - end - end - out[#out + 1] = '"' - return table.concat(out) -end - ----@param name string ----@param args string[] ----@return string -local function buildCmdLine(name, args) - local parts = { escapeArg(name) } - for _, a in ipairs(args) do parts[#parts + 1] = escapeArg(a) end - return table.concat(parts, " ") -end - ----@param inheritRead boolean ----@param inheritWrite boolean ----@return ffi.cdata*?, ffi.cdata*? -local function makePipe(inheritRead, inheritWrite) - local sa = SecurityAttributes() - sa.nLength = SecurityAttributesSize - sa.bInheritHandle = 1 - - local r, w = HandleBox(), HandleBox() - if kernel32.CreatePipe(r, w, sa, 0) == 0 then return nil, nil end - if not inheritRead then kernel32.SetHandleInformation(r[0], HANDLE_FLAG_INHERIT, 0) end - if not inheritWrite then kernel32.SetHandleInformation(w[0], HANDLE_FLAG_INHERIT, 0) end - - return r[0], w[0] -end - ----@param write boolean ----@return ffi.cdata*? -local function nullHandle(write) - local h = kernel32.CreateFileA("nul", write and GENERIC_WRITE or GENERIC_READ, 0, nil, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, nil) - return h ~= INVALID_HANDLE_VALUE and h or nil -end - ----@param overrides table ----@return string -local function buildEnvBlock(overrides) - -- Start with current environment - local env = {} - local block = kernel32.GetEnvironmentStringsA() - if block ~= nil then - local i = 0 - while true do - local s = ffi.string(block + i) - if #s == 0 then break end - local k, v = s:match("^([^=]+)=(.*)") - if k then env[k:upper()] = { key = k, val = v } end - i = i + #s + 1 - end - kernel32.FreeEnvironmentStringsA(block) - end - -- Apply overrides - for k, v in pairs(overrides) do env[k:upper()] = { key = k, val = v } end - local buf = buffer.new() - for _, entry in pairs(env) do buf:put(entry.key, "=", entry.val, "\0") end - buf:put("\0") - return buf:tostring() -end - ----@param name string ----@param args string[] ----@param opts { cwd: string?, env: table?, stdin: string?, stdout: "pipe"|"inherit"|"null"?, stderr: "pipe"|"inherit"|"null"? }? ----@return { handle: ffi.cdata*, pid: number, stdoutHandle: ffi.cdata*?, stderrHandle: ffi.cdata*? }?, string? -function M.spawn(name, args, opts) - opts = opts or {} - local stdoutMode = opts.stdout or "pipe" - local stderrMode = opts.stderr or "pipe" - local hasStdin = opts.stdin ~= nil - - local si = StartupInfoA() - si.cb = StartupInfoASize - si.dwFlags = STARTF_USESTDHANDLES - - local stdinR, stdinW, stdoutR, stdoutW, stderrR, stderrW - local nullOut, nullErr - - if hasStdin then - stdinR, stdinW = makePipe(true, false) - if not stdinR then return nil, "CreatePipe failed" end - si.hStdInput = stdinR - else - si.hStdInput = kernel32.GetStdHandle(STD_INPUT_HANDLE) - end - - if stdoutMode == "pipe" then - stdoutR, stdoutW = makePipe(false, true) - if not stdoutR then return nil, "CreatePipe failed" end - si.hStdOutput = stdoutW - elseif stdoutMode == "null" then - nullOut = nullHandle(true) - si.hStdOutput = nullOut - else - si.hStdOutput = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) - end - - if stderrMode == "pipe" then - stderrR, stderrW = makePipe(false, true) - if not stderrR then return nil, "CreatePipe failed" end - si.hStdError = stderrW - elseif stderrMode == "null" then - nullErr = nullHandle(true) - si.hStdError = nullErr - else - si.hStdError = kernel32.GetStdHandle(STD_ERROR_HANDLE) - end - - local cmdStr = buildCmdLine(name, args) - local cmdLine = CharBuf(#cmdStr + 1, cmdStr) - local envStr = opts.env and buildEnvBlock(opts.env) or nil - local envBlock = envStr and ffi.cast("void*", envStr) or nil - local pi = ProcessInformation() - - local ok = kernel32.CreateProcessA( - nil, cmdLine, nil, nil, 1, - CREATE_NO_WINDOW, envBlock, opts.cwd or nil, si, pi - ) - - if stdinR then kernel32.CloseHandle(stdinR) end - if stdoutW then kernel32.CloseHandle(stdoutW) end - if stderrW then kernel32.CloseHandle(stderrW) end - if nullOut then kernel32.CloseHandle(nullOut) end - if nullErr then kernel32.CloseHandle(nullErr) end - - if ok == 0 then return nil, "CreateProcess failed" end - - kernel32.CloseHandle(pi.hThread) - - if hasStdin then - local written = DwordBox() - kernel32.WriteFile(stdinW, opts.stdin, #opts.stdin, written, nil) - kernel32.CloseHandle(stdinW) - end - - return { - handle = pi.hProcess, - pid = tonumber(pi.dwProcessId), - stdoutHandle = stdoutR, - stderrHandle = stderrR - } -end - ----@param handle ffi.cdata* ----@return string -function M.readHandle(handle) - local buf = CharBuf(4096) - local read = DwordBox() - local chunks = {} - while kernel32.ReadFile(handle, buf, 4096, read, nil) ~= 0 and read[0] > 0 do - chunks[#chunks + 1] = ffi.string(buf, read[0]) - end - kernel32.CloseHandle(handle) - return table.concat(chunks) -end - ----@param handle ffi.cdata* ----@return number? -function M.wait(handle) - kernel32.WaitForSingleObject(handle, INFINITE) - local code = DwordBox() - kernel32.GetExitCodeProcess(handle, code) - kernel32.CloseHandle(handle) - return tonumber(code[0]) -end - ----@param handle ffi.cdata* ----@return number? -function M.poll(handle) - local code = DwordBox() - kernel32.GetExitCodeProcess(handle, code) - if tonumber(code[0]) == STILL_ACTIVE then return nil end - kernel32.CloseHandle(handle) - return tonumber(code[0]) -end - ----@param handle ffi.cdata* -function M.kill(handle) - kernel32.TerminateProcess(handle, 1) -end - -return M diff --git a/packages/process2/tests/process2.test.lua b/packages/process2/tests/process2.test.lua deleted file mode 100644 index bd35925a..00000000 --- a/packages/process2/tests/process2.test.lua +++ /dev/null @@ -1,182 +0,0 @@ -local test = require("lde-test") -local process = require("process2") - -local isWindows = jit.os == "Windows" -local sh = isWindows and "cmd" or "sh" -local shc = isWindows and "/c" or "-c" - --- --- exec --- - -test.it("exec returns exit code 0 on success", function() - local code = process.exec(sh, { shc, "exit 0" }) - test.equal(code, 0) -end) - -test.it("exec returns non-zero exit code on failure", function() - local code = process.exec(sh, { shc, "exit 1" }) - test.equal(code, 1) -end) - -test.it("exec captures stdout", function() - local cmd = isWindows and "echo hello" or "printf hello" - local code, stdout = process.exec(sh, { shc, cmd }) - test.equal(code, 0) - test.truthy(stdout and stdout:find("hello")) -end) - -test.it("exec captures stderr (merged into stdout on posix)", function() - local cmd = isWindows and "echo err 1>&2" or "printf err >&2" - local code, stdout, stderr = process.exec(sh, { shc, cmd }) - test.equal(code, 0) - -- stderr is captured separately on all platforms - test.truthy(stderr and stderr:find("err")) -end) - -test.it("exec passes stdin", function() - local cmd = isWindows and "more" or "cat" - local code, stdout = process.exec(sh, { shc, cmd }, { stdin = "hello" }) - test.equal(code, 0) - test.truthy(stdout and stdout:find("hello")) -end) - -test.it("exec passes env vars", function() - local cmd = isWindows and "echo %MY_VAR%" or "printf $MY_VAR" - local code, stdout = process.exec(sh, { shc, cmd }, { env = { MY_VAR = "testval" } }) - test.equal(code, 0) - test.truthy(stdout and stdout:find("testval")) -end) - -test.it("exec inherits existing env vars when passing custom env", function() - -- PATH must always be inherited or nothing works - local cmd = isWindows and "echo %PATH%" or "printf $PATH" - local code, stdout = process.exec(sh, { shc, cmd }, { env = { MY_EXTRA = "extra" } }) - test.equal(code, 0) - test.truthy(stdout and #stdout > 0) -end) - -test.it("exec env override replaces specific var without dropping others", function() - local cmd = isWindows and "echo %MY_VAR% %PATH%" or "printf '%s %s' $MY_VAR $PATH" - local code, stdout = process.exec(sh, { shc, cmd }, { env = { MY_VAR = "overridden" } }) - test.equal(code, 0) - -- both the override and inherited PATH should be present - test.truthy(stdout and stdout:find("overridden")) - test.truthy(stdout and #stdout > #"overridden") -- PATH adds more content -end) - -test.it("exec respects cwd", function() - local cmd = isWindows and "cd" or "pwd" - local tmpdir = isWindows and (os.getenv("TEMP") or "C:\\Temp") or "/tmp" - local code, stdout = process.exec(sh, { shc, cmd }, { cwd = tmpdir }) - test.equal(code, 0) - test.truthy(stdout and #stdout > 0) -end) - -test.it("exec handles args with spaces and special chars", function() - -- pass a quoted string through echo; just verify it doesn't crash and exits 0 - local code = process.exec(sh, { shc, isWindows and 'echo "hello world"' or "printf '%s' 'hello world'" }) - test.equal(code, 0) -end) - --- --- spawn (async Child) --- - -test.it("spawn returns a Child with a pid", function() - local child, err = process.spawn(sh, { shc, "exit 0" }) - test.truthy(child, err) - test.truthy(child.pid > 0) - child:wait() -end) - -test.it("spawn Child:wait returns exit code", function() - local child = process.spawn(sh, { shc, "exit 42" }) - test.truthy(child) - local code = child:wait() - test.equal(code, 42) -end) - -test.it("spawn Child:wait captures stdout when piped", function() - local cmd = isWindows and "echo hi" or "printf hi" - local child = process.spawn(sh, { shc, cmd }, { stdout = "pipe" }) - test.truthy(child) - local code, stdout = child:wait() - test.equal(code, 0) - test.truthy(stdout and stdout:find("hi")) -end) - -test.it("spawn Child:kill terminates the process", function() - local cmd = isWindows and "timeout /t 30 /nobreak >nul" or "sleep 30" - local child = process.spawn(sh, { shc, cmd }) - test.truthy(child) - child:kill(true) - child:wait() -- must not hang - test.truthy(true) -end) - -test.it("spawn Child:poll returns nil while running", function() - local cmd = isWindows and "timeout /t 30 /nobreak >nul" or "sleep 30" - local child = process.spawn(sh, { shc, cmd }) - test.truthy(child) - local code = child:poll() - test.falsy(code) -- still running - child:kill(true) - child:wait() -end) - --- --- stdio modes --- - -test.it("exec with stdout=null discards output", function() - local cmd = isWindows and "echo hello" or "printf hello" - local code, stdout = process.exec(sh, { shc, cmd }, { stdout = "null" }) - test.equal(code, 0) - test.falsy(stdout) -end) - -test.it("exec with stderr=null discards stderr", function() - local code, stdout, stderr = process.exec(sh, { shc, "exit 0" }, { stderr = "null" }) - test.equal(code, 0) - test.falsy(stderr) -end) - -test.it("exec with stdout=inherit does not capture stdout", function() - local code, stdout = process.exec(sh, { shc, "exit 0" }, { stdout = "inherit", stderr = "null" }) - test.equal(code, 0) - test.falsy(stdout) -end) - -test.it("exec with stderr=inherit does not capture stderr", function() - local code, stdout, stderr = process.exec(sh, { shc, "exit 0" }, { stdout = "null", stderr = "inherit" }) - test.equal(code, 0) - test.falsy(stderr) -end) - -test.it("spawn with stderr=pipe captures stderr separately", function() - local cmd = isWindows and "echo hello" or "printf hello" - local child = process.spawn(sh, { shc, cmd }, { stdout = "pipe", stderr = "pipe" }) - test.truthy(child) - local code, stdout, stderr = child:wait() - test.equal(code, 0) - test.truthy(stdout and stdout:find("hello")) -end) - --- --- platform --- - -test.it("platform is set to a known value", function() - local known = { win32 = true, linux = true, darwin = true, unix = true } - test.truthy(known[process.platform]) -end) - --- --- exec errors on bad binary --- - -test.it("exec returns non-zero code when binary does not exist", function() - local code = process.exec("__no_such_binary__", {}) - test.truthy(code ~= 0) -end) diff --git a/packages/sea/lde.json b/packages/sea/lde.json index 8cccadf0..3249ec3d 100644 --- a/packages/sea/lde.json +++ b/packages/sea/lde.json @@ -2,10 +2,10 @@ "name": "sea", "version": "0.1.0", "dependencies": { - "process2": { "path": "../process2" }, - "fs": { "path": "../fs" }, - "path": { "path": "../path" }, - "env": { "path": "../env" }, + "process": { "git": "https://github.com/lde-org/process" }, + "fs": { "git": "https://github.com/lde-org/fs" }, + "path": { "git": "https://github.com/lde-org/path" }, + "env": { "git": "https://github.com/lde-org/env" }, "util": { "path": "../util" }, "archive": { "path": "../archive" }, "curl-sys": { "git": "https://github.com/lde-org/curl-sys" } diff --git a/packages/sea/src/init.lua b/packages/sea/src/init.lua index 4cac976d..2e2bc8a0 100644 --- a/packages/sea/src/init.lua +++ b/packages/sea/src/init.lua @@ -1,6 +1,6 @@ local sea = {} -local process = require("process2") +local process = require("process") local path = require("path") local env = require("env") local fs = require("fs") From 67355a963eb46b0996e48ef257e3ac4ea32fcfb7 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sat, 18 Apr 2026 19:13:52 -0700 Subject: [PATCH 02/13] chore: split out archive, ffix --- packages/archive/.gitignore | 2 - packages/archive/lde.json | 16 - packages/archive/src/init.lua | 278 -------------- packages/archive/tests/archive.test.lua | 153 -------- packages/ffix/.gitignore | 2 - packages/ffix/README.md | 13 - packages/ffix/lde.json | 5 - packages/ffix/src/init.lua | 184 ---------- packages/ffix/src/parser.lua | 453 ----------------------- packages/ffix/src/printer.lua | 127 ------- packages/ffix/src/tokenizer.lua | 145 -------- packages/ffix/tests/ffi.test.lua | 168 --------- packages/ffix/tests/parser.test.lua | 461 ------------------------ packages/ffix/tests/printer.test.lua | 254 ------------- packages/ffix/tests/rewrite.test.lua | 112 ------ packages/ffix/tests/tokenizer.test.lua | 114 ------ packages/lde-core/lde.json | 2 +- packages/sea/lde.json | 2 +- 18 files changed, 2 insertions(+), 2489 deletions(-) delete mode 100644 packages/archive/.gitignore delete mode 100644 packages/archive/lde.json delete mode 100644 packages/archive/src/init.lua delete mode 100644 packages/archive/tests/archive.test.lua delete mode 100644 packages/ffix/.gitignore delete mode 100644 packages/ffix/README.md delete mode 100644 packages/ffix/lde.json delete mode 100644 packages/ffix/src/init.lua delete mode 100644 packages/ffix/src/parser.lua delete mode 100644 packages/ffix/src/printer.lua delete mode 100644 packages/ffix/src/tokenizer.lua delete mode 100644 packages/ffix/tests/ffi.test.lua delete mode 100644 packages/ffix/tests/parser.test.lua delete mode 100644 packages/ffix/tests/printer.test.lua delete mode 100644 packages/ffix/tests/rewrite.test.lua delete mode 100644 packages/ffix/tests/tokenizer.test.lua diff --git a/packages/archive/.gitignore b/packages/archive/.gitignore deleted file mode 100644 index 8a3d2033..00000000 --- a/packages/archive/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target/ -/lde.lock \ No newline at end of file diff --git a/packages/archive/lde.json b/packages/archive/lde.json deleted file mode 100644 index 4ba58c83..00000000 --- a/packages/archive/lde.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "archive", - "description": "Archive extraction library. Reads magic bytes to detect zip vs tar.", - "version": "0.1.0", - "dependencies": { - "fs": { "git": "https://github.com/lde-org/fs" }, - "path": { "git": "https://github.com/lde-org/path" }, - "deflate-sys": { "git": "https://github.com/lde-org/deflate-sys" } - }, - "devDependencies": { - "lde-test": { "path": "../lde-test" }, - "fs": { "git": "https://github.com/lde-org/fs" }, - "env": { "git": "https://github.com/lde-org/env" }, - "path": { "git": "https://github.com/lde-org/path" } - } -} diff --git a/packages/archive/src/init.lua b/packages/archive/src/init.lua deleted file mode 100644 index 42e745be..00000000 --- a/packages/archive/src/init.lua +++ /dev/null @@ -1,278 +0,0 @@ ----@diagnostic disable: assign-type-mismatch - -local ffi = require("ffi") -local buf = require("string.buffer") -local deflate = require("deflate-sys") -local fs = require("fs") -local path = require("path") - -ffi.cdef [[ - typedef struct __attribute__((packed)) { - uint32_t sig; uint16_t ver, flags, method, mtime, mdate; - uint32_t crc, compSize, uncompSize; - uint16_t nameLen, extraLen; - } ZipLocal; - - typedef struct __attribute__((packed)) { - uint32_t sig; uint16_t verMade, verNeed, flags, method, mtime, mdate; - uint32_t crc, compSize, uncompSize; - uint16_t nameLen, extraLen, commentLen, disk, iattr; - uint32_t eattr, offset; - } ZipCD; - - typedef struct __attribute__((packed)) { - uint32_t sig; uint16_t disk, diskCd, count, total; - uint32_t cdSize, cdOffset; - uint16_t commentLen; - } ZipEOCD; - - typedef struct __attribute__((packed)) { - char name[100], mode[8], uid[8], gid[8], size[12], mtime[12], - checksum[8], typeflag, linkname[100], magic[6], version[2], - uname[32], gname[32], devmajor[8], devminor[8], prefix[155], pad[12]; - } TarHeader; -]] - ----@class ZipLocal: ffi.cdata* ----@field sig number ----@field ver number ----@field flags number ----@field method number ----@field crc number ----@field compSize number ----@field uncompSize number ----@field nameLen number ----@field extraLen number - ----@class ZipCD: ffi.cdata* ----@field sig number ----@field crc number ----@field compSize number ----@field uncompSize number ----@field nameLen number ----@field extraLen number ----@field commentLen number ----@field method number ----@field offset number - ----@class ZipEOCD: ffi.cdata* ----@field sig number ----@field count number ----@field total number ----@field cdSize number ----@field cdOffset number - ----@class TarHeader: ffi.cdata* ----@field name string ----@field mode string ----@field size string ----@field mtime string ----@field checksum string ----@field typeflag number ----@field magic string ----@field version string - ----@type fun(...): ZipLocal -local ZipLocalT = ffi.typeof("ZipLocal") ----@type fun(...): ZipCD -local ZipCDT = ffi.typeof("ZipCD") ----@type fun(...): ZipEOCD -local ZipEOCDT = ffi.typeof("ZipEOCD") ----@type fun(): TarHeader -local TarHeaderT = ffi.typeof("TarHeader") - -local tarHeaderSize = ffi.sizeof("TarHeader") - ----@param base string ----@param name string ----@param content string -local function writeFile(base, name, content) - local out = path.join(base, name) - fs.mkdirAll(path.dirname(out)) - fs.write(out, content) -end - --- ── ZIP extract ─────────────────────────────────────────────────────────────── - ----@param data string ----@param toPath string ----@param strip boolean -local function zipExtract(data, toPath, strip) - local dptr = ffi.cast("const uint8_t *", data) - local eocdOff = #data - 22 - while eocdOff >= 0 and ffi.cast("ZipEOCD *", dptr + eocdOff).sig ~= 0x06054b50 do - eocdOff = eocdOff - 1 - end - assert(eocdOff >= 0, "ZIP: EOCD not found") - ---@type ZipEOCD - local eocd = ffi.cast("ZipEOCD *", dptr + eocdOff) - local cd = ffi.cast("const uint8_t *", dptr + eocd.cdOffset) - - for _ = 1, eocd.total do - ---@type ZipCD - local e = ffi.cast("ZipCD *", cd) - assert(e.sig == 0x02014b50, "ZIP: bad CD entry") - local name = ffi.string(cd + ffi.sizeof("ZipCD"), e.nameLen) - if strip then name = name:match("^[^/]*/(.+)") or name end - if name:sub(-1) ~= "/" then - ---@type ZipLocal - local lh = ffi.cast("ZipLocal *", dptr + e.offset) - local raw = ffi.string(dptr + e.offset + ffi.sizeof("ZipLocal") + lh.nameLen + lh.extraLen, e.compSize) - local content = e.method == 0 and raw or deflate.deflateDecompress(raw, e.uncompSize) - writeFile(toPath, name, content) - else - fs.mkdir(path.join(toPath, name)) - end - cd = cd + ffi.sizeof("ZipCD") + e.nameLen + e.extraLen + e.commentLen - end -end - --- ── ZIP save ────────────────────────────────────────────────────────────────── - ----@param files table ----@param toPath string -local function zipSave(files, toPath) - local out = buf.new() - local cdBuf = buf.new() - local offset, count = 0, 0 - - for name, content in pairs(files) do - local comp = deflate.deflateCompress(content, 6) - local crc = deflate.crc32(content) - - local lh = ZipLocalT(0x04034b50, 20, 0, 8, 0, 0, crc, #comp, #content, #name, 0) - out:putcdata(lh, ffi.sizeof(lh)); out:put(name, comp) - - local cd = ZipCDT(0x02014b50, 20, 20, 0, 8, 0, 0, crc, #comp, #content, #name, 0, 0, 0, 0, 0, offset) - cdBuf:putcdata(cd, ffi.sizeof(cd)); cdBuf:put(name) - - offset = offset + ffi.sizeof(lh) + #name + #comp - count = count + 1 - end - - local cdStr = cdBuf:tostring() - local eocd = ZipEOCDT(0x06054b50, 0, 0, count, count, #cdStr, offset, 0) - out:put(cdStr); out:putcdata(eocd, ffi.sizeof(eocd)) - return fs.write(toPath, out:tostring()) -end - --- ── TAR extract ─────────────────────────────────────────────────────────────── - ----@param data string ----@param toPath string ----@param strip boolean -local function tarExtract(data, toPath, strip) - local dptr = ffi.cast("const uint8_t *", data) - local pos = 0 - while pos + tarHeaderSize <= #data do - ---@type TarHeader - local h = ffi.cast("TarHeader *", dptr + pos) - if h.name[0] == 0 then break end - local name = ffi.string(h.name) - local size = tonumber(ffi.string(h.size, 11), 8) or 0 - pos = pos + tarHeaderSize - if strip then name = name:match("^[^/]*/(.+)") or name end - if h.typeflag == string.byte("5") or name:sub(-1) == "/" then - fs.mkdir(path.join(toPath, name)) - elseif h.typeflag == string.byte("0") or h.typeflag == 0 then - writeFile(toPath, name, ffi.string(dptr + pos, size)) - end - pos = pos + math.ceil(size / 512) * 512 - end -end - --- ── TAR save ───────────────────────────────────────────────────────────────── - ----@param files table ----@param toPath string -local function tarSave(files, toPath) - local out = buf.new() - for name, content in pairs(files) do - ---@type TarHeader - local h = TarHeaderT() - ffi.copy(h.name, name, math.min(#name, 100)) - ffi.copy(h.mode, "0000644\0", 8) - ffi.copy(h.size, string.format("%011o", #content), 11) - ffi.copy(h.mtime, "00000000000", 11) - ffi.copy(h.magic, "ustar", 5) - ffi.copy(h.version, "00", 2) - h.typeflag = string.byte("0") - local sum = 8 * 32 - local hp = ffi.cast("const uint8_t *", h) - for i = 0, tarHeaderSize - 1 do sum = sum + hp[i] end - ffi.copy(h.checksum, string.format("%06o\0 ", sum), 8) - out:putcdata(h, tarHeaderSize) - out:put(content) - local pad = (512 - (#content % 512)) % 512 - if pad > 0 then out:put(string.rep("\0", pad)) end - end - out:put(string.rep("\0", 1024)) - local tarData = out:tostring() - local final = toPath:match("%.tar%.gz$") and deflate.gzipCompress(tarData) or tarData - return fs.write(toPath, final) -end - --- ── Archive ─────────────────────────────────────────────────────────────────── - ----@class Archive ----@field _source string | table -local Archive = {} -Archive.__index = Archive - ----@class Archive.ExtractOptions ----@field stripComponents boolean? - ---- Create a new Archive. ---- Pass a file path string to decode, or a table of `{ [path] = content }` to encode. ----@param source string | table ----@return Archive -function Archive.new(source) - return setmetatable({ _source = source }, Archive) -end - ---- Extract the archive to the given output directory. ----@param toPath string ----@param opts Archive.ExtractOptions? ----@return boolean ok ----@return string? err -function Archive:extract(toPath, opts) - local src = self._source - if type(src) ~= "string" then return false, "extract() is only valid for file-backed archives" end - local f = io.open(src, "rb") - if not f then return false, "cannot open: " .. src end - local data = f:read("*a"); f:close() - local strip = opts and opts.stripComponents or false - fs.mkdir(toPath) - local ok, err = pcall(function() - if ffi.cast("const uint32_t *", data)[0] == 0x04034b50 then - zipExtract(data, toPath, strip) - else - local raw = data:sub(1, 2) == "\31\139" and deflate.gzipDecompress(data, math.max(#data * 10, 1024 * 1024)) or data - tarExtract(raw, toPath, strip) - end - end) - if not ok then return false, err end - return true -end - ---- Save the in-memory file table to an archive. ---- Infers format from extension: `.zip`, `.tar`, or `.tar.gz`. ----@param toPath string ----@return boolean ok ----@return string? err -function Archive:save(toPath) - local src = self._source - if type(src) ~= "table" then return false, "save() is only valid for table-backed archives" end - local isZip = toPath:match("%.zip$") - local isTar = toPath:match("%.tar") - if not isZip and not isTar then - return false, "cannot determine archive format from path (expected .zip or .tar.gz)" - end - local ok, err = pcall(function() - if isZip then zipSave(src, toPath) else tarSave(src, toPath) end - end) - if not ok then return false, err end - return true -end - -return Archive diff --git a/packages/archive/tests/archive.test.lua b/packages/archive/tests/archive.test.lua deleted file mode 100644 index e7ac8efa..00000000 --- a/packages/archive/tests/archive.test.lua +++ /dev/null @@ -1,153 +0,0 @@ -local test = require("lde-test") -local Archive = require("archive") -local fs = require("fs") -local env = require("env") -local path = require("path") - -local tmpBase = path.join(env.tmpdir(), "lde-archive-tests") -fs.rmdir(tmpBase) -fs.mkdir(tmpBase) - -local function tmp(name) - return path.join(tmpBase, name) -end - --- --- Archive.new --- - -test.it("Archive.new with string returns Archive", function() - local a = Archive.new("/some/path.tar.gz") - test.truthy(a) -end) - -test.it("Archive.new with table returns Archive", function() - local a = Archive.new({ ["hello.txt"] = "hello" }) - test.truthy(a) -end) - -test.it("extract fails when source is a table", function() - local a = Archive.new({ ["hello.txt"] = "hello" }) - local ok, err = a:extract(tmp("out-table")) - test.falsy(ok) - test.truthy(err) -end) - -test.it("save fails when source is a string", function() - local a = Archive.new("/some/path.tar.gz") - local ok, err = a:save(tmp("out.zip")) - test.falsy(ok) - test.truthy(err) -end) - -test.it("save fails for unknown extension", function() - local a = Archive.new({ ["hello.txt"] = "hello" }) - local ok, err = a:save(tmp("out.rar")) - test.falsy(ok) - test.truthy(err) -end) - -test.it("save encodes to .zip and files are extractable", function() - local zipPath = tmp("saved.zip") - local outDir = tmp("out-saved-zip") - fs.mkdir(outDir) - - local a = Archive.new({ ["hello.txt"] = "zip content" }) - local ok = a:save(zipPath) - test.truthy(ok) - test.truthy(fs.exists(zipPath)) - - local b = Archive.new(zipPath) - local ok2 = b:extract(outDir) - test.truthy(ok2) - test.equal(fs.read(path.join(outDir, "hello.txt")), "zip content") -end) - -test.it("save encodes to .tar.gz and files are extractable", function() - local tarPath = tmp("saved.tar.gz") - local outDir = tmp("out-saved-tar") - fs.mkdir(outDir) - - local a = Archive.new({ ["hello.txt"] = "tar content" }) - local ok = a:save(tarPath) - test.truthy(ok) - test.truthy(fs.exists(tarPath)) - - local b = Archive.new(tarPath) - local ok2 = b:extract(outDir) - test.truthy(ok2) - test.equal(fs.read(path.join(outDir, "hello.txt")), "tar content") -end) - -test.it("extracts a .tar archive", function() - local tarPath = tmp("test.tar") - local outDir = tmp("out-tar") - fs.mkdir(outDir) - - local a = Archive.new({ ["hello.txt"] = "tar content" }) - local ok = a:save(tarPath) - test.truthy(ok) - - local b = Archive.new(tarPath) - local ok2 = b:extract(outDir) - test.truthy(ok2) - test.truthy(fs.exists(path.join(outDir, "hello.txt"))) -end) - -test.it("extracts a .zip archive", function() - local zipPath = tmp("test2.zip") - local outDir = tmp("out-zip2") - fs.mkdir(outDir) - - local a = Archive.new({ ["hello.txt"] = "zip content" }) - local ok = a:save(zipPath) - test.truthy(ok) - - local b = Archive.new(zipPath) - local ok2 = b:extract(outDir) - test.truthy(ok2) - test.truthy(fs.exists(path.join(outDir, "hello.txt"))) -end) - -test.it("stripComponents strips top-level dir from zip", function() - local zipPath = tmp("strip.zip") - local outDir = tmp("out-strip-zip") - fs.mkdir(outDir) - - local a = Archive.new({ ["topdir/hello.txt"] = "stripped" }) - a:save(zipPath) - - local b = Archive.new(zipPath) - b:extract(outDir, { stripComponents = true }) - test.equal(fs.read(path.join(outDir, "hello.txt")), "stripped") -end) - --- regression: zips with no explicit directory entries (e.g. .src.rock files) --- must still extract deeply nested files by creating parent dirs recursively -test.it("extracts zip with deeply nested files and no explicit dir entries", function() - local zipPath = tmp("nested.zip") - local outDir = tmp("out-nested") - fs.mkdir(outDir) - - -- save creates file entries only, no dir entries — matches .src.rock behavior - local a = Archive.new({ ["a/b/c/deep.lua"] = "deep content" }) - a:save(zipPath) - - local b = Archive.new(zipPath) - local ok = b:extract(outDir) - test.truthy(ok) - test.equal(fs.read(path.join(outDir, "a/b/c/deep.lua")), "deep content") -end) - -test.it("stripComponents strips top-level dir from tar.gz", function() - local tarPath = tmp("strip.tar.gz") - local outDir = tmp("out-strip-tar") - fs.mkdir(outDir) - - local a = Archive.new({ ["topdir/hello.txt"] = "stripped" }) - a:save(tarPath) - - local b = Archive.new(tarPath) - b:extract(outDir, { stripComponents = true }) - test.equal(fs.read(path.join(outDir, "hello.txt")), "stripped") -end) diff --git a/packages/ffix/.gitignore b/packages/ffix/.gitignore deleted file mode 100644 index 8a3d2033..00000000 --- a/packages/ffix/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target/ -/lde.lock \ No newline at end of file diff --git a/packages/ffix/README.md b/packages/ffix/README.md deleted file mode 100644 index 96330ec4..00000000 --- a/packages/ffix/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# ffix - -This is a namespaced version of the `ffi` library for LuaJIT. - -It works by parsing your C code and individually renaming types and symbols to be namespaced to an `ffix.context()`. - -This solves the issue of ffi redefinition fears that are all too common with a large amount of ffi definitions in LuaJIT. - -## Usage - -``` -lde add ffix --git https://github.com/lde-org/lde -``` diff --git a/packages/ffix/lde.json b/packages/ffix/lde.json deleted file mode 100644 index 70505231..00000000 --- a/packages/ffix/lde.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "ffix", - "version": "0.1.0", - "dependencies": {} -} \ No newline at end of file diff --git a/packages/ffix/src/init.lua b/packages/ffix/src/init.lua deleted file mode 100644 index 0afab0ea..00000000 --- a/packages/ffix/src/init.lua +++ /dev/null @@ -1,184 +0,0 @@ -local ffi = require("ffi") - -local ffix = {} - -local Tokenizer = require("ffix.tokenizer") -local Parser = require("ffix.parser") -local Printer = require("ffix.printer") - ----@class ffix.Context ----@field private pfx string ----@field private names table -- original -> prefixed ----@field C table -- proxy for ffi.C; ctx.C.foo resolves to ffi.C[prefixed_name] -local Context = {} -Context.__index = Context - ----@param t ffix.c.Parser.Type ----@return ffix.c.Parser.Type -function Context:rewriteInlineType(t) - local result = { - qualifiers = t.qualifiers, - inline_kind = t.inline_kind, - inline_tag = t.inline_tag, - inline_attrs = t.inline_attrs, - pointer = t.pointer, - reference = t.reference - } - if t.inline_kind == "enum" then - result.inline_variants = t.inline_variants - else - local fields = {} - for _, f in ipairs(t.inline_fields) do - fields[#fields + 1] = { - type = self:rewriteType(f.type), - name = f.name, - array_size = f.array_size, - attrs = f - .attrs - } - end - result.inline_fields = fields - end - return result -end - ----@param t ffix.c.Parser.Type ----@return ffix.c.Parser.Type -function Context:rewriteType(t) - if t.inline_kind then - return self:rewriteInlineType(t) - end - - local name = t.name - - local kw, base = name:match("^(%a+) ([%a_][%w_]*)$") - if kw == "struct" or kw == "enum" or kw == "union" then - name = kw .. " " .. (self.names[base] or base) - else - name = self.names[name] or name - end - - return { qualifiers = t.qualifiers, name = name, pointer = t.pointer, reference = t.reference } -end - ----@param params ffix.c.Parser.Param[] ----@return ffix.c.Parser.Param[] -function Context:rewriteParams(params) - local out = {} - for _, p in ipairs(params) do - out[#out + 1] = { type = self:rewriteType(p.type), name = p.name } - end - - return out -end - ----@format disable-next ----@private ----@param node ffix.c.Parser.Node ----@return ffix.c.Parser.Node -function Context:rewriteNode(node) - local k = node.kind - local renamed = self.names[node.name] or node.name - - if k == "typedef_alias" then - return { kind = k, name = renamed, type = self:rewriteType(node.type) } - elseif k == "typedef_struct" then - local fields = {} - for _, f in ipairs(node.fields) do - fields[#fields + 1] = { type = self:rewriteType(f.type), name = f.name, array_size = f.array_size, attrs = f.attrs } - end - - return { kind = k, name = renamed, tag = node.tag and (self.names[node.tag] or node.tag), fields = fields, attrs = node.attrs } - elseif k == "typedef_enum" then - return { kind = k, name = renamed, tag = node.tag and (self.names[node.tag] or node.tag), variants = node.variants } - elseif k == "typedef_fnptr" then - return { kind = k, name = renamed, ret = self:rewriteType(node.ret), params = self:rewriteParams(node.params) } - elseif k == "fn_decl" then - return { kind = k, name = renamed, asm_name = node.asm_name or node.name, ret = self:rewriteType(node.ret), params = self:rewriteParams(node.params), attrs = node.attrs } - elseif k == "extern_var" then - return { kind = k, name = renamed, asm_name = node.name, type = self:rewriteType(node.type) } - end - - error("unknown node kind: " .. tostring(node.kind)) -end - ----@param code string -function Context:cdef(code) - local tokens = Tokenizer.new():tokenize(code) - local ok, nodes = Parser.new():parse(tokens) - if not ok then error("ffix: failed to parse cdef block") end - - -- first pass: register all declared names - for _, node in ipairs(nodes) do - if node.name then - self.names[node.name] = self.pfx .. "_" .. node.name - end - end - - -- second pass: rewrite and emit - local rewritten = {} - for _, node in ipairs(nodes) do - rewritten[#rewritten + 1] = self:rewriteNode(node) - end - - ffi.cdef(Printer.new():print(rewritten)) -end - ----@param typename string -function Context:new(typename, ...) - return ffi.new(self.names[typename] or typename, ...) -end - ----@param typename string -function Context:cast(typename, ...) - local base, tail = typename:match("^([%a_][%w_]*)(.*)") - if base and self.names[base] then typename = self.names[base] .. tail end - return ffi.cast(typename, ...) -end - ----@param typename string -function Context:typeof(typename) - return ffi.typeof(self.names[typename] or typename) -end - ----@param typename string -function Context:sizeof(typename) - return ffi.sizeof(self.names[typename] or typename) -end - ----@param lib string -function Context:load(lib) - return ffi.load(lib) -end - ----@param typename string ----@param mt table -function Context:metatype(typename, mt) - return ffi.metatype(self.names[typename] or typename, mt) -end - ----@param typename string -function Context:istype(typename, obj) - return ffi.istype(self.names[typename] or typename, obj) -end - ----@param ctx table -local function generatePrefix(ctx) - return string.format("%d%p", os.clock() * 1e6, ctx) -end - ----@param pfx string? -function ffix.context(pfx) - local ctx = setmetatable({ names = {} }, Context) - ctx.pfx = pfx or generatePrefix(ctx) - - ctx.C = setmetatable({}, { - __index = function(_, k) - return ffi.C[ctx.names[k] or k] - end - }) - - return ctx -end - -return ffix diff --git a/packages/ffix/src/parser.lua b/packages/ffix/src/parser.lua deleted file mode 100644 index f0830af1..00000000 --- a/packages/ffix/src/parser.lua +++ /dev/null @@ -1,453 +0,0 @@ ----@class ffix.c.Parser ----@field private ptr number ----@field private tokens ffix.c.Tokenizer.Token[] -local Parser = {} -Parser.__index = Parser - ----@class ffix.c.Parser.Type ----@field qualifiers string[] ----@field name string? ----@field inline_kind ("struct"|"union"|"enum")? ----@field inline_tag string? ----@field inline_fields ffix.c.Parser.Field[]? ----@field inline_variants ffix.c.Parser.Variant[]? ----@field inline_attrs ffix.c.Attr[]? ----@field pointer number ----@field reference boolean? - ----@class ffix.c.Attr ----@field name string ----@field args string? - ----@class ffix.c.Parser.Field ----@field type ffix.c.Parser.Type ----@field name string? ----@field array_size string? ----@field attrs ffix.c.Attr[]? - ----@class ffix.c.Parser.Variant ----@field name string - ----@class ffix.c.Parser.Param ----@field type ffix.c.Parser.Type ----@field name string? - ----@class ffix.c.Parser.Node.TypedefAlias ----@field kind "typedef_alias" ----@field type ffix.c.Parser.Type ----@field name string - ----@class ffix.c.Parser.Node.TypedefStruct ----@field kind "typedef_struct" ----@field tag string? ----@field fields ffix.c.Parser.Field[] ----@field name string ----@field attrs ffix.c.Attr[]? - ----@class ffix.c.Parser.Node.TypedefEnum ----@field kind "typedef_enum" ----@field tag string? ----@field variants ffix.c.Parser.Variant[] ----@field name string - ----@class ffix.c.Parser.Node.TypedefFnPtr ----@field kind "typedef_fnptr" ----@field ret ffix.c.Parser.Type ----@field name string ----@field params ffix.c.Parser.Param[] - ----@class ffix.c.Parser.Node.FnDecl ----@field kind "fn_decl" ----@field ret ffix.c.Parser.Type ----@field name string ----@field params ffix.c.Parser.Param[] ----@field asm_name string? ----@field attrs ffix.c.Attr[]? - ----@class ffix.c.Parser.Node.ExternVar ----@field kind "extern_var" ----@field type ffix.c.Parser.Type ----@field name string ----@field asm_name string? - ----@alias ffix.c.Parser.Node ---- | ffix.c.Parser.Node.TypedefAlias ---- | ffix.c.Parser.Node.TypedefStruct ---- | ffix.c.Parser.Node.TypedefEnum ---- | ffix.c.Parser.Node.TypedefFnPtr ---- | ffix.c.Parser.Node.FnDecl ---- | ffix.c.Parser.Node.ExternVar - -function Parser.new() - return setmetatable({}, Parser) -end - ----@return ffix.c.Tokenizer.Token? -function Parser:peek() - return self.tokens[self.ptr] -end - ----@return ffix.c.Tokenizer.Token? -function Parser:advance() - local tok = self.tokens[self.ptr] - if tok then self.ptr = self.ptr + 1 end - return tok -end - ----@param variant string ----@return ffix.c.Tokenizer.Token? -function Parser:consume(variant) - local tok = self.tokens[self.ptr] - if tok and tok.variant == variant then - self.ptr = self.ptr + 1 - return tok - end -end - ----@param variant string ----@return ffix.c.Tokenizer.Token -function Parser:expect(variant) - local tok = self:consume(variant) - if not tok then - local got = self.tokens[self.ptr] - error("expected '" .. variant .. "' got '" .. (got and got.variant or "EOF") .. "'") - end - return tok -end - -local type_quals = { const = true, volatile = true, restrict = true, unsigned = true, signed = true, long = true, short = true } -local base_types = { void = true, char = true, int = true, float = true, double = true } - ----@return ffix.c.Parser.Type -function Parser:parseType() - local quals = {} - local name - - while true do - local tok = self:peek() - if not tok then break end - - if type_quals[tok.variant] then - quals[#quals + 1] = tok.variant - self:advance() - elseif base_types[tok.variant] then - name = tok.variant - self:advance() - break - elseif tok.variant == "struct" or tok.variant == "enum" or tok.variant == "union" then - local kw = tok.variant - self:advance() - local tag_tok = self:consume("ident") - if self:peek() and self:peek().variant == "{" then - self:advance() - local inline_fields, inline_variants, inline_attrs - if kw == "enum" then - inline_variants = self:parseVariants() - else - inline_fields = self:parseFields() - inline_attrs = self:parseAttrs() - end - local pointer = 0 - while self:consume("*") do - pointer = pointer + 1 - while true do - local qtok = self:peek() - if qtok and (qtok.variant == "const" or qtok.variant == "volatile" or qtok.variant == "restrict") then - self:advance() - else break end - end - end - local reference = self:consume("&") ~= nil - return { - qualifiers = quals, - inline_kind = kw, - inline_tag = tag_tok and tag_tok.ident, - inline_fields = inline_fields, - inline_variants = inline_variants, - inline_attrs = inline_attrs, - pointer = pointer, - reference = reference or nil, - } - end - if not tag_tok then error("expected tag name or '{' after " .. kw) end - name = kw .. " " .. tag_tok.ident - break - elseif tok.variant == "ident" then - -- if we already have qualifiers (e.g. "unsigned long"), peek at the - -- token after this ident: if it looks like a declaration suffix - -- then this ident is a name not a type, so stop here without consuming - local next = self.tokens[self.ptr + 1] - local next_v = next and next.variant - if #quals > 0 and (next_v == "(" or next_v == ";" or next_v == "," or next_v == ")") then - break - end - - name = tok.ident - self:advance() - break - else - break - end - end - - -- trailing const/volatile after name - while true do - local tok = self:peek() - if tok and type_quals[tok.variant] then - quals[#quals + 1] = tok.variant - self:advance() - else - break - end - end - - if not name then - -- qualifiers only (e.g. "unsigned" as shorthand for "unsigned int") - if #quals > 0 then - name = quals[#quals] - quals[#quals] = nil - else - error("expected type") - end - end - - local pointer = 0 - while self:consume("*") do - pointer = pointer + 1 - -- eat pointer-level qualifiers - while true do - local tok = self:peek() - if tok and (tok.variant == "const" or tok.variant == "volatile" or tok.variant == "restrict") then - self:advance() - else - break - end - end - end - - local reference = self:consume("&") ~= nil - - return { qualifiers = quals, name = name, pointer = pointer, reference = reference or nil } -end - ----@return ffix.c.Parser.Field[] -function Parser:parseFields() - local fields = {} - while not self:consume("}") do - local ftype = self:parseType() - local name_tok - if ftype.inline_kind then - name_tok = self:consume("ident") - else - name_tok = self:expect("ident") - end - local array_size - if self:consume("[") then - local parts = {} - while not self:consume("]") do - local t = self:advance() - if t.variant == "ident" then - parts[#parts + 1] = t.ident - elseif t.variant == "number" then - local n = t.number - parts[#parts + 1] = n == math.floor(n) and tostring(math.floor(n)) or tostring(n) - else - parts[#parts + 1] = t.variant - end - end - array_size = table.concat(parts) - end - local attrs = self:parseAttrs() - self:expect(";") - fields[#fields + 1] = { type = ftype, name = name_tok and name_tok.ident, array_size = array_size, attrs = attrs } - end - return fields -end - ----@return ffix.c.Parser.Variant[] -function Parser:parseVariants() - local variants = {} - while not self:consume("}") do - local name = self:expect("ident") - self:consume(",") - variants[#variants + 1] = { name = name.ident } - end - return variants -end - ----@return ffix.c.Parser.Param[] -function Parser:parseParams() - self:expect("(") - local params = {} - if self:consume(")") then return params end - -- (void) means no params, but (void *) is a real param — peek ahead - if self.tokens[self.ptr] and self.tokens[self.ptr].variant == "void" - and self.tokens[self.ptr + 1] and self.tokens[self.ptr + 1].variant == ")" then - self.ptr = self.ptr + 2 - return params - end - while true do - if self:consume("...") then - self:consume(")") - break - end - local ptype = self:parseType() - local name_tok = self:consume("ident") - params[#params + 1] = { type = ptype, name = name_tok and name_tok.ident } - if self:consume(")") then break end - self:expect(",") - end - return params -end - ----@return string? -function Parser:parseAsmName() - local tok = self:peek() - if tok and tok.variant == "ident" and (tok.ident == "__asm__" or tok.ident == "asm") then - self:advance() - self:expect("(") - local str = self:expect("string") - self:expect(")") - return str.string - end -end - ----@return ffix.c.Attr[]? -function Parser:parseAttrs() - local tok = self:peek() - if not (tok and tok.variant == "ident" and tok.ident == "__attribute__") then return nil end - self:advance() - self:expect("(") - self:expect("(") - local attrs = {} - while true do - if self:consume(")") then break end - local name_tok = self:advance() - local name = name_tok.variant == "ident" and name_tok.ident or name_tok.variant - local args - if self:consume("(") then - local parts = {} - local depth = 0 - while true do - local t = self:peek() - if not t then error("unterminated __attribute__ args") end - if t.variant == ")" then - if depth == 0 then break end - depth = depth - 1 - parts[#parts + 1] = ")" - self:advance() - elseif t.variant == "(" then - depth = depth + 1 - parts[#parts + 1] = "(" - self:advance() - elseif t.variant == "ident" then - parts[#parts + 1] = t.ident - self:advance() - elseif t.variant == "number" then - local n = t.number - parts[#parts + 1] = n == math.floor(n) and tostring(math.floor(n)) or tostring(n) - self:advance() - else - parts[#parts + 1] = t.variant - self:advance() - end - end - args = table.concat(parts) - self:expect(")") - end - attrs[#attrs + 1] = { name = name, args = args } - self:consume(",") - end - self:expect(")") - return attrs -end - ----@return ffix.c.Parser.Node -function Parser:parseDecl() - if self:consume("typedef") then - local kw = self:peek() - - if kw and (kw.variant == "struct" or kw.variant == "union") then - self:advance() - local pre_attrs = self:parseAttrs() - local tag_tok = self:consume("ident") - self:expect("{") - local fields = self:parseFields() - local post_attrs = self:parseAttrs() - local name = self:expect("ident") - self:expect(";") - local attrs - if pre_attrs or post_attrs then - attrs = {} - if pre_attrs then for _, a in ipairs(pre_attrs) do attrs[#attrs + 1] = a end end - if post_attrs then for _, a in ipairs(post_attrs) do attrs[#attrs + 1] = a end end - end - return { kind = "typedef_struct", tag = tag_tok and tag_tok.ident, fields = fields, name = name.ident, attrs = attrs } - end - - if kw and kw.variant == "enum" then - self:advance() - local tag_tok = self:consume("ident") - self:expect("{") - local variants = self:parseVariants() - local name = self:expect("ident") - self:expect(";") - return { kind = "typedef_enum", tag = tag_tok and tag_tok.ident, variants = variants, name = name.ident } - end - - local ret = self:parseType() - - -- function pointer: typedef ret (*name)(params); - if self:consume("(") then - self:expect("*") - local name = self:expect("ident") - self:expect(")") - local params = self:parseParams() - self:expect(";") - return { kind = "typedef_fnptr", ret = ret, name = name.ident, params = params } - end - - local name = self:expect("ident") - self:expect(";") - return { kind = "typedef_alias", type = ret, name = name.ident } - end - - if self:consume("extern") then - local type = self:parseType() - local name = self:expect("ident") - local asm_name = self:parseAsmName() - self:expect(";") - return { kind = "extern_var", type = type, name = name.ident, asm_name = asm_name } - end - - local ret = self:parseType() - local name = self:expect("ident") - local params = self:parseParams() - local asm_name = self:parseAsmName() - local attrs = self:parseAttrs() - self:expect(";") - - return { kind = "fn_decl", ret = ret, name = name.ident, params = params, asm_name = asm_name, attrs = attrs } -end - ----@param tokens ffix.c.Tokenizer.Token[] ----@return boolean, ffix.c.Parser.Node[] -function Parser:parse(tokens) - self.ptr = 1 - self.tokens = tokens - - local nodes = {} - local ok, err = pcall(function() - while self.ptr <= #self.tokens do - nodes[#nodes + 1] = self:parseDecl() - end - end) - - if not ok then - return false, nodes - end - - return true, nodes -end - -return Parser diff --git a/packages/ffix/src/printer.lua b/packages/ffix/src/printer.lua deleted file mode 100644 index ac9d5d97..00000000 --- a/packages/ffix/src/printer.lua +++ /dev/null @@ -1,127 +0,0 @@ ----@class ffix.c.Printer -local Printer = {} -Printer.__index = Printer - -function Printer.new() - return setmetatable({}, Printer) -end - ----@param t ffix.c.Parser.Type ----@return string -function Printer:inlineType(t) - local kw = t.inline_kind - local tag_part = t.inline_tag and (" " .. t.inline_tag) or "" - local attr_str = (t.inline_attrs and #t.inline_attrs > 0) and (" " .. self:attrsStr(t.inline_attrs)) or "" - if kw == "enum" then - local parts = {} - for _, v in ipairs(t.inline_variants) do parts[#parts + 1] = v.name end - return "enum" .. tag_part .. " { " .. table.concat(parts, ", ") .. " }" - else - local parts = {} - for _, f in ipairs(t.inline_fields) do - local arr = f.array_size and ("[" .. f.array_size .. "]") or "" - local fattr = (f.attrs and #f.attrs > 0) and (" " .. self:attrsStr(f.attrs)) or "" - parts[#parts + 1] = self:typedName(f.type, f.name) .. arr .. fattr .. ";" - end - return kw .. tag_part .. attr_str .. " { " .. table.concat(parts, " ") .. " }" - end -end - ----@param t ffix.c.Parser.Type ----@param name string? ----@return string -function Printer:typedName(t, name) - local base - if t.inline_kind then - base = self:inlineType(t) - else - local parts = {} - for _, q in ipairs(t.qualifiers) do parts[#parts + 1] = q end - parts[#parts + 1] = t.name - base = table.concat(parts, " ") - end - local stars = string.rep("*", t.pointer) .. (t.reference and "&" or "") - if t.pointer > 0 or t.reference then - return base .. " " .. stars .. (name or "") - end - return name and (base .. " " .. name) or base -end - ----@param params ffix.c.Parser.Param[] ----@return string -function Printer:paramList(params) - if #params == 0 then return "void" end - local parts = {} - for _, p in ipairs(params) do - parts[#parts + 1] = self:typedName(p.type, p.name) - end - return table.concat(parts, ", ") -end - ----@param attrs ffix.c.Attr[] ----@return string -function Printer:attrsStr(attrs) - local parts = {} - for _, a in ipairs(attrs) do - parts[#parts + 1] = a.args and (a.name .. "(" .. a.args .. ")") or a.name - end - return "__attribute__((" .. table.concat(parts, ", ") .. "))" -end - ----@param node ffix.c.Parser.Node ----@return string -function Printer:node(node) - local k = node.kind - - if k == "typedef_alias" then - return "typedef " .. self:typedName(node.type, node.name) .. ";" - - elseif k == "typedef_struct" then - local attr_str = (node.attrs and #node.attrs > 0) and (" " .. self:attrsStr(node.attrs)) or "" - local lines = { "typedef struct" .. (node.tag and (" " .. node.tag) or "") .. attr_str .. " {" } - for _, f in ipairs(node.fields) do - local arr = f.array_size and ("[" .. f.array_size .. "]") or "" - local fattr = (f.attrs and #f.attrs > 0) and (" " .. self:attrsStr(f.attrs)) or "" - lines[#lines + 1] = "\t" .. self:typedName(f.type, f.name) .. arr .. fattr .. ";" - end - lines[#lines + 1] = "} " .. node.name .. ";" - return table.concat(lines, "\n") - - elseif k == "typedef_enum" then - local lines = { "typedef enum" .. (node.tag and (" " .. node.tag) or "") .. " {" } - for _, v in ipairs(node.variants) do - lines[#lines + 1] = "\t" .. v.name .. "," - end - lines[#lines + 1] = "} " .. node.name .. ";" - return table.concat(lines, "\n") - - elseif k == "typedef_fnptr" then - return "typedef " .. self:typedName(node.ret, "(*" .. node.name .. ")") .. "(" - .. self:paramList(node.params) .. ");" - - elseif k == "fn_decl" then - local s = self:typedName(node.ret, node.name) .. "(" .. self:paramList(node.params) .. ")" - if node.asm_name then s = s .. " __asm__(\"" .. node.asm_name .. "\")" end - if node.attrs and #node.attrs > 0 then s = s .. " " .. self:attrsStr(node.attrs) end - return s .. ";" - - elseif k == "extern_var" then - local s = "extern " .. self:typedName(node.type, node.name) - if node.asm_name then s = s .. " __asm__(\"" .. node.asm_name .. "\")" end - return s .. ";" - end - - error("unknown node kind: " .. tostring(node.kind)) -end - ----@param nodes ffix.c.Parser.Node[] ----@return string -function Printer:print(nodes) - local parts = {} - for _, n in ipairs(nodes) do - parts[#parts + 1] = self:node(n) - end - return table.concat(parts, "\n") -end - -return Printer diff --git a/packages/ffix/src/tokenizer.lua b/packages/ffix/src/tokenizer.lua deleted file mode 100644 index 0c364663..00000000 --- a/packages/ffix/src/tokenizer.lua +++ /dev/null @@ -1,145 +0,0 @@ ----@class ffix.c.Tokenizer ----@field private ptr number ----@field private len number ----@field private src string -local Tokenizer = {} -Tokenizer.__index = Tokenizer - -function Tokenizer.new() - return setmetatable({}, Tokenizer) -end - ----@param pattern string -function Tokenizer:skip(pattern) - local start, finish = string.find(self.src, pattern, self.ptr) - if start then - self.ptr = finish + 1 - return true - end -end - ----@param pattern string ----@return string? -function Tokenizer:consume(pattern) - local start, finish, match = string.find(self.src, pattern, self.ptr) - if start then - self.ptr = finish + 1 - return match or true - end -end - -function Tokenizer:skipWhitespace() - return self:skip("^%s+") -end - -function Tokenizer:skipComments() - return self:skip("^//[^\n]+\n") or self:skip("^#[^\n]+\n") -end - ----@class ffix.c.Tokenizer.Token.Ident ----@field variant "ident" ----@field ident string - ----@class ffix.c.Tokenizer.Token.Number ----@field variant "number" ----@field number number - ----@class ffix.c.Tokenizer.Token.String ----@field variant "string" ----@field number string - ----@class ffix.c.Tokenizer.Token.Special ----@field variant string - ----@alias ffix.c.Tokenizer.Token ---- | ffix.c.Tokenizer.Token.Ident ---- | ffix.c.Tokenizer.Token.String ---- | ffix.c.Tokenizer.Token.Number ---- | ffix.c.Tokenizer.Token.Special - ----@type table -local special = {} - -for _, s in ipairs({ - "typedef", "{", "}", "[", "]", "(", ")", ",", ".", ";", ":", "<", ">", "*", "&", "~", "...", "::", - "struct", "enum", "union", "const", "restrict", "extern", "static", "volatile", - "unsigned", "signed", "void", "char", "short", "int", "long", "float", "double" -}) do - special[s] = true -end - ----@return ffix.c.Tokenizer.Token? -function Tokenizer:next() - local ident = self:consume("^([%a_][%w_]*)") - if ident then - if special[ident] then - return { variant = ident } - end - - return { variant = "ident", ident = ident } - end - - local dec = self:consume("^(%d+%.%d+)") - if dec then - return { variant = "number", number = tonumber(dec) } - end - - local hex = self:consume("^0x([%x]+)") - if hex then - return { variant = "number", number = tonumber(hex, 16) } - end - - local int = self:consume("^(%d+)[uUlL]*") - if int then - return { variant = "number", number = tonumber(int) } - end - - local str = self:consume("^\"([^\"]+)\"") - if str then - return { variant = "string", string = str } - end - - local three = string.sub(self.src, self.ptr, self.ptr + 2) - if special[three] then - self.ptr = self.ptr + 3 - return { variant = three } - end - - local two = string.sub(self.src, self.ptr, self.ptr + 1) - if special[two] then - self.ptr = self.ptr + 2 - return { variant = two } - end - - local one = string.sub(self.src, self.ptr, self.ptr) - if special[one] then - self.ptr = self.ptr + 1 - return { variant = one } - end -end - ----@param src string -function Tokenizer:tokenize(src) - self.ptr = 1 - self.len = #src - self.src = src - - ---@type ffix.c.Tokenizer.Token[] - local tokens = {} - - while true do - while self:skipWhitespace() or self:skipComments() do end - if self.ptr > self.len then break end - - local tok = self:next() - if not tok then - error("Unrecognized character: " .. string.sub(self.src, self.ptr, self.ptr)) - end - - tokens[#tokens + 1] = tok - end - - return tokens -end - -return Tokenizer diff --git a/packages/ffix/tests/ffi.test.lua b/packages/ffix/tests/ffi.test.lua deleted file mode 100644 index 0b68eb0e..00000000 --- a/packages/ffix/tests/ffi.test.lua +++ /dev/null @@ -1,168 +0,0 @@ -local test = require("lde-test") -local ffi = require("ffi") -local ffix = require("ffix") - --- each test gets a unique prefix so cdef doesn't see duplicate type names across runs -local n = 0 -local function ctx() - n = n + 1 - return ffix.context("t" .. n) -end - --- sizeof - -test.it("sizeof resolves prefixed struct", function() - local c = ctx() - c:cdef("typedef struct { int x; int y; } Point;") - test.equal(c:sizeof("Point"), ffi.sizeof("int") * 2) -end) - -test.it("sizeof resolves prefixed alias", function() - local c = ctx() - c:cdef("typedef int MyInt;") - test.equal(c:sizeof("MyInt"), ffi.sizeof("int")) -end) - --- typeof - -test.it("typeof returns the right ctype", function() - local c = ctx() - c:cdef("typedef struct { float x; float y; float z; } Vec3;") - local ct = c:typeof("Vec3") - test.equal(ffi.sizeof(ct), ffi.sizeof("float") * 3) -end) - --- new - -test.it("new creates a zero-initialised struct", function() - local c = ctx() - c:cdef("typedef struct { int a; int b; } Pair;") - local p = c:new("Pair") - test.equal(p.a, 0) - test.equal(p.b, 0) -end) - -test.it("new with initialiser sets fields", function() - local c = ctx() - c:cdef("typedef struct { int x; int y; } Coord;") - local p = c:new("Coord", { x = 3, y = 7 }) - test.equal(p.x, 3) - test.equal(p.y, 7) -end) - -test.it("new field writes survive a read back", function() - local c = ctx() - c:cdef("typedef struct { int val; } Box;") - local b = c:new("Box") - b.val = 99 - test.equal(b.val, 99) -end) - --- cast - -test.it("cast with bare type name works", function() - local c = ctx() - c:cdef("typedef struct { int n; } Wrap;") - local w = c:new("Wrap", { n = 42 }) - local p = c:cast("Wrap *", w) - test.equal(p.n, 42) -end) - -test.it("cast pointer write is visible through original", function() - local c = ctx() - c:cdef("typedef struct { int n; } Cell;") - local cell = c:new("Cell", { n = 1 }) - local ptr = c:cast("Cell *", cell) - ptr.n = 55 - test.equal(cell.n, 55) -end) - --- function resolution via __asm__ - -test.it("declared function resolves to the real symbol via asm", function() - local c = ctx() - c:cdef("unsigned long strlen(const char * s);") - -- rewriter emits: unsigned long tN_strlen(const char *s) __asm__("strlen"); - local pfx_strlen = ffi.C[c.names["strlen"]] - test.equal(tonumber(pfx_strlen("hello")), 5) - test.equal(tonumber(pfx_strlen("")), 0) -end) - -test.it("multiple functions resolve independently", function() - local c = ctx() - c:cdef([[ - unsigned long strlen(const char * s); - int atoi(const char * s); - ]]) - test.equal(tonumber(ffi.C[c.names["strlen"]]("abc")), 3) - test.equal(tonumber(ffi.C[c.names["atoi"]]("123")), 123) -end) - --- ctx.C - -test.it("ctx.C.fn calls through to the real symbol", function() - local c = ctx() - c:cdef("unsigned long strlen(const char * s);") - test.equal(tonumber(c.C.strlen("hello")), 5) - test.equal(tonumber(c.C.strlen("")), 0) -end) - -test.it("ctx.C resolves multiple functions independently", function() - local c = ctx() - c:cdef([[ - unsigned long strlen(const char * s); - int atoi(const char * s); - ]]) - test.equal(tonumber(c.C.strlen("abc")), 3) - test.equal(tonumber(c.C.atoi("42")), 42) -end) - -test.it("ctx.C from different contexts do not collide", function() - local c1 = ctx() - local c2 = ctx() - c1:cdef("unsigned long strlen(const char * s);") - c2:cdef("unsigned long strlen(const char * s);") - test.equal(tonumber(c1.C.strlen("hi")), 2) - test.equal(tonumber(c2.C.strlen("hello")), 5) -end) - --- metatype - -test.it("metatype registers methods accessible on new instances", function() - local c = ctx() - c:cdef("typedef struct { int x; int y; } Point;") - c:metatype("Point", { - __index = { - sum = function(self) return self.x + self.y end, - }, - }) - local p = c:new("Point", { x = 3, y = 4 }) - test.equal(p:sum(), 7) -end) - -test.it("metatype __tostring is called on tostring()", function() - local c = ctx() - c:cdef("typedef struct { int n; } Num;") - c:metatype("Num", { - __tostring = function(self) return "Num(" .. self.n .. ")" end, - }) - local v = c:new("Num", { n = 99 }) - test.equal(tostring(v), "Num(99)") -end) - --- istype - -test.it("istype returns true for matching ctype", function() - local c = ctx() - c:cdef("typedef struct { int x; } Vec;") - local v = c:new("Vec") - test.truthy(c:istype("Vec", v)) -end) - -test.it("istype returns false for non-matching ctype", function() - local c = ctx() - c:cdef("typedef struct { int x; } A;") - c:cdef("typedef struct { int x; } B;") - local a = c:new("A") - test.falsy(c:istype("B", a)) -end) diff --git a/packages/ffix/tests/parser.test.lua b/packages/ffix/tests/parser.test.lua deleted file mode 100644 index 22fe02fc..00000000 --- a/packages/ffix/tests/parser.test.lua +++ /dev/null @@ -1,461 +0,0 @@ -local test = require("lde-test") -local Tokenizer = require("ffix.tokenizer") -local Parser = require("ffix.parser") - -local function parse(src) - local tokens = Tokenizer.new():tokenize(src) - local ok, nodes = Parser.new():parse(tokens) - test.truthy(ok) - return nodes -end - --- typedef alias - -test.it("typedef primitive alias", function() - test.match(parse("typedef int MyInt;"), { - { kind = "typedef_alias", name = "MyInt", type = { name = "int", pointer = 0, qualifiers = {} } }, - }) -end) - -test.it("typedef pointer alias", function() - test.match(parse("typedef char * string_t;"), { - { kind = "typedef_alias", name = "string_t", type = { name = "char", pointer = 1 } }, - }) -end) - -test.it("typedef with qualifier", function() - test.match(parse("typedef const unsigned int uint_t;"), { - { kind = "typedef_alias", name = "uint_t", type = { name = "int", qualifiers = { "const", "unsigned" } } }, - }) -end) - -test.it("typedef double-pointer", function() - test.match(parse("typedef void ** handle_t;"), { - { kind = "typedef_alias", name = "handle_t", type = { name = "void", pointer = 2 } }, - }) -end) - --- typedef struct - -test.it("typedef anonymous struct", function() - test.match(parse("typedef struct { int x; int y; } Point;"), { - { - kind = "typedef_struct", - name = "Point", - tag = nil, - fields = { - { name = "x", type = { name = "int" } }, - { name = "y", type = { name = "int" } }, - }, - }, - }) -end) - -test.it("typedef struct with tag", function() - test.match(parse("typedef struct Node { int val; } Node;"), { - { kind = "typedef_struct", name = "Node", tag = "Node" }, - }) -end) - -test.it("typedef struct with pointer field", function() - test.match(parse("typedef struct { struct Node * next; } Node;"), { - { - kind = "typedef_struct", - fields = { { name = "next", type = { name = "struct Node", pointer = 1 } } }, - }, - }) -end) - -test.it("typedef struct with array field", function() - test.match(parse("typedef struct { char buf[256]; } Buf;"), { - { kind = "typedef_struct", fields = { { name = "buf", type = { name = "char" } } } }, - }) -end) - -test.it("typedef struct with multiple fields of different types", function() - test.match(parse("typedef struct { unsigned int id; const char * name; } Record;"), { - { - kind = "typedef_struct", - name = "Record", - fields = { - { name = "id", type = { name = "int", qualifiers = { "unsigned" } } }, - { name = "name", type = { name = "char", pointer = 1 } }, - }, - }, - }) -end) - --- typedef enum - -test.it("typedef enum", function() - test.match(parse("typedef enum { RED, GREEN, BLUE, } Color;"), { - { - kind = "typedef_enum", - name = "Color", - variants = { { name = "RED" }, { name = "GREEN" }, { name = "BLUE" } }, - }, - }) -end) - -test.it("typedef enum with tag", function() - test.match(parse("typedef enum Dir { UP, DOWN, } Dir;"), { - { kind = "typedef_enum", name = "Dir", tag = "Dir" }, - }) -end) - --- typedef function pointer - -test.it("typedef function pointer no params", function() - test.match(parse("typedef void (*Callback)(void);"), { - { kind = "typedef_fnptr", name = "Callback", ret = { name = "void" }, params = {} }, - }) -end) - -test.it("typedef function pointer with params", function() - test.match(parse("typedef int (*Comparator)(const void * a, const void * b);"), { - { - kind = "typedef_fnptr", - name = "Comparator", - ret = { name = "int" }, - params = { - { type = { name = "void", pointer = 1 } }, - { type = { name = "void", pointer = 1 } }, - }, - }, - }) -end) - -test.it("typedef function pointer returning pointer", function() - test.match(parse("typedef char * (*Getter)(int key);"), { - { - kind = "typedef_fnptr", - name = "Getter", - ret = { name = "char", pointer = 1 }, - params = { { type = { name = "int" } } }, - }, - }) -end) - --- function declarations - -test.it("void function no params", function() - test.match(parse("void init(void);"), { - { kind = "fn_decl", name = "init", ret = { name = "void" }, params = {} }, - }) -end) - -test.it("function with named params", function() - test.match(parse("int add(int a, int b);"), { - { - kind = "fn_decl", - name = "add", - ret = { name = "int" }, - params = { - { name = "a", type = { name = "int" } }, - { name = "b", type = { name = "int" } }, - }, - }, - }) -end) - -test.it("function with unnamed params", function() - test.match(parse("int add(int, int);"), { - { - kind = "fn_decl", - name = "add", - params = { - { name = nil, type = { name = "int" } }, - { name = nil, type = { name = "int" } }, - }, - }, - }) -end) - -test.it("function returning pointer", function() - test.match(parse("char * strdup(const char * s);"), { - { - kind = "fn_decl", - name = "strdup", - ret = { name = "char", pointer = 1 }, - params = { { type = { name = "char", pointer = 1 } } }, - }, - }) -end) - -test.it("variadic function", function() - test.match(parse("int printf(const char * fmt, ...);"), { - { - kind = "fn_decl", - name = "printf", - params = { { type = { name = "char", pointer = 1 } } }, - }, - }) -end) - --- extern variable - -test.it("extern int", function() - test.match(parse("extern int errno;"), { - { kind = "extern_var", name = "errno", type = { name = "int" } }, - }) -end) - -test.it("extern pointer", function() - test.match(parse("extern char * environ;"), { - { kind = "extern_var", name = "environ", type = { name = "char", pointer = 1 } }, - }) -end) - --- multiple declarations - -test.it("parses multiple declarations in sequence", function() - local nodes = parse([[ - typedef int size_t; - extern int errno; - void free(void * ptr); - ]]) - test.equal(#nodes, 3) - test.equal(nodes[1].kind, "typedef_alias") - test.equal(nodes[2].kind, "extern_var") - test.equal(nodes[3].kind, "fn_decl") -end) - --- __asm__ attribute - -test.it("fn_decl with __asm__", function() - test.match(parse("int mylib_add(int a, int b) __asm__(\"add\");"), { - { kind = "fn_decl", name = "mylib_add", asm_name = "add" }, - }) -end) - -test.it("fn_decl with asm (no underscores)", function() - test.match(parse("void mylib_free(void * ptr) asm(\"free\");"), { - { kind = "fn_decl", name = "mylib_free", asm_name = "free" }, - }) -end) - -test.it("extern_var with __asm__", function() - test.match(parse("extern int mylib_errno __asm__(\"errno\");"), { - { kind = "extern_var", name = "mylib_errno", asm_name = "errno" }, - }) -end) - -test.it("fn_decl without __asm__ has nil asm_name", function() - test.match(parse("int add(int a, int b);"), { - { kind = "fn_decl", name = "add", asm_name = nil }, - }) -end) - --- array sizes - -test.it("struct field array size is preserved", function() - test.match(parse("typedef struct { char buf[256]; } Buf;"), { - { kind = "typedef_struct", fields = { { name = "buf", type = { name = "char" }, array_size = "256" } } }, - }) -end) - -test.it("struct field symbolic array size is preserved", function() - test.match(parse("typedef struct { int data[MAX_SIZE]; } S;"), { - { kind = "typedef_struct", fields = { { name = "data", array_size = "MAX_SIZE" } } }, - }) -end) - --- __attribute__ - -test.it("struct field __attribute__((aligned(4)))", function() - test.match(parse("typedef struct { int x __attribute__((aligned(4))); } S;"), { - { kind = "typedef_struct", fields = { - { name = "x", attrs = { { name = "aligned", args = "4" } } }, - } }, - }) -end) - -test.it("struct field __attribute__((packed))", function() - test.match(parse("typedef struct { char c __attribute__((packed)); } S;"), { - { kind = "typedef_struct", fields = { - { name = "c", attrs = { { name = "packed", args = nil } } }, - } }, - }) -end) - -test.it("struct field __attribute__((mode(__word__)))", function() - test.match(parse("typedef struct { int x __attribute__((mode(__word__))); } S;"), { - { kind = "typedef_struct", fields = { - { name = "x", attrs = { { name = "mode", args = "__word__" } } }, - } }, - }) -end) - -test.it("struct field __attribute__((vector_size(16)))", function() - test.match(parse("typedef struct { float v __attribute__((vector_size(16))); } S;"), { - { kind = "typedef_struct", fields = { - { name = "v", attrs = { { name = "vector_size", args = "16" } } }, - } }, - }) -end) - -test.it("struct __attribute__((packed)) before body", function() - test.match(parse("typedef struct __attribute__((packed)) { int x; } S;"), { - { kind = "typedef_struct", attrs = { { name = "packed" } } }, - }) -end) - -test.it("struct __attribute__((packed)) after body", function() - test.match(parse("typedef struct { int x; } __attribute__((packed)) S;"), { - { kind = "typedef_struct", attrs = { { name = "packed" } } }, - }) -end) - -test.it("struct __attribute__((aligned(8))) before body", function() - test.match(parse("typedef struct __attribute__((aligned(8))) { int x; } S;"), { - { kind = "typedef_struct", attrs = { { name = "aligned", args = "8" } } }, - }) -end) - -test.it("fn_decl __attribute__((cdecl))", function() - test.match(parse("void foo(void) __attribute__((cdecl));"), { - { kind = "fn_decl", name = "foo", attrs = { { name = "cdecl" } } }, - }) -end) - -test.it("fn_decl __attribute__((stdcall))", function() - test.match(parse("int bar(int x) __attribute__((stdcall));"), { - { kind = "fn_decl", name = "bar", attrs = { { name = "stdcall" } } }, - }) -end) - -test.it("fn_decl __attribute__((fastcall))", function() - test.match(parse("void baz(void) __attribute__((fastcall));"), { - { kind = "fn_decl", attrs = { { name = "fastcall" } } }, - }) -end) - -test.it("fn_decl __attribute__((thiscall))", function() - test.match(parse("void qux(void) __attribute__((thiscall));"), { - { kind = "fn_decl", attrs = { { name = "thiscall" } } }, - }) -end) - -test.it("fn_decl with __asm__ and __attribute__ preserves both", function() - test.match(parse("void foo(void) __asm__(\"_foo\") __attribute__((cdecl));"), { - { kind = "fn_decl", asm_name = "_foo", attrs = { { name = "cdecl" } } }, - }) -end) - -test.it("field has no attrs when none present", function() - test.match(parse("typedef struct { int x; } S;"), { - { kind = "typedef_struct", fields = { { name = "x", attrs = nil, array_size = nil } } }, - }) -end) - --- reference types - -test.it("typedef reference alias", function() - test.match(parse("typedef int & IntRef;"), { - { kind = "typedef_alias", name = "IntRef", type = { name = "int", pointer = 0, reference = true } }, - }) -end) - -test.it("function with reference param", function() - test.match(parse("void swap(int & a, int & b);"), { - { - kind = "fn_decl", - name = "swap", - params = { - { name = "a", type = { name = "int", reference = true } }, - { name = "b", type = { name = "int", reference = true } }, - }, - }, - }) -end) - -test.it("function returning reference", function() - test.match(parse("int & at(int idx);"), { - { kind = "fn_decl", name = "at", ret = { name = "int", reference = true } }, - }) -end) - -test.it("non-reference type has nil reference field", function() - test.match(parse("typedef int MyInt;"), { - { kind = "typedef_alias", name = "MyInt", type = { reference = nil } }, - }) -end) - --- anonymous / inline struct, union, enum - -test.it("anonymous union field inside struct", function() - test.match(parse("typedef struct { union { int a; float b; }; int c; } Foo;"), { - { - kind = "typedef_struct", - name = "Foo", - fields = { - { name = nil, type = { inline_kind = "union", inline_tag = nil, inline_fields = { - { name = "a", type = { name = "int" } }, - { name = "b", type = { name = "float" } }, - } } }, - { name = "c", type = { name = "int" } }, - }, - }, - }) -end) - -test.it("named inline struct field", function() - test.match(parse("typedef struct { struct { int x; int y; } pos; } Entity;"), { - { - kind = "typedef_struct", - name = "Entity", - fields = { - { name = "pos", type = { inline_kind = "struct", inline_tag = nil, inline_fields = { - { name = "x", type = { name = "int" } }, - { name = "y", type = { name = "int" } }, - } } }, - }, - }, - }) -end) - -test.it("tagged inline union field", function() - test.match(parse("typedef struct { union Val { int i; float f; } val; } S;"), { - { - kind = "typedef_struct", - fields = { - { name = "val", type = { inline_kind = "union", inline_tag = "Val" } }, - }, - }, - }) -end) - -test.it("anonymous struct inside union", function() - test.match(parse("typedef union { struct { int x; int y; }; long long flat; } Vec2;"), { - { - kind = "typedef_struct", - name = "Vec2", - fields = { - { name = nil, type = { inline_kind = "struct" } }, - { name = "flat", type = { name = "long" } }, - }, - }, - }) -end) - -test.it("nested anonymous unions", function() - test.match(parse("typedef struct { union { struct { int x; int y; }; int arr[2]; }; } S;"), { - { - kind = "typedef_struct", - fields = { - { name = nil, type = { inline_kind = "union", inline_fields = { - { name = nil, type = { inline_kind = "struct" } }, - { name = "arr", type = { name = "int" }, array_size = "2" }, - } } }, - }, - }, - }) -end) - --- error handling - -test.it("returns false on invalid input", function() - local tokens = Tokenizer.new():tokenize("int;") - local ok, nodes = Parser.new():parse(tokens) - test.falsy(ok) -end) diff --git a/packages/ffix/tests/printer.test.lua b/packages/ffix/tests/printer.test.lua deleted file mode 100644 index 5d50f71c..00000000 --- a/packages/ffix/tests/printer.test.lua +++ /dev/null @@ -1,254 +0,0 @@ -local test = require("lde-test") -local Tokenizer = require("ffix.tokenizer") -local Parser = require("ffix.parser") -local Printer = require("ffix.printer") - -local function roundtrip(src) - local tokens = Tokenizer.new():tokenize(src) - local ok, nodes = Parser.new():parse(tokens) - test.truthy(ok) - return Printer.new():print(nodes) -end - -test.it("typedef alias", function() - test.equal(roundtrip("typedef int MyInt;"), "typedef int MyInt;") -end) - -test.it("typedef pointer alias", function() - test.equal(roundtrip("typedef char * string_t;"), "typedef char *string_t;") -end) - -test.it("typedef qualified alias", function() - test.equal(roundtrip("typedef const unsigned int uint_t;"), "typedef const unsigned int uint_t;") -end) - -test.it("typedef double-pointer alias", function() - test.equal(roundtrip("typedef void ** handle_t;"), "typedef void **handle_t;") -end) - -test.it("typedef struct anonymous", function() - test.equal(roundtrip("typedef struct { int x; int y; } Point;"), [[ -typedef struct { - int x; - int y; -} Point;]]) -end) - -test.it("typedef struct with tag", function() - test.equal(roundtrip("typedef struct Node { int val; } Node;"), [[ -typedef struct Node { - int val; -} Node;]]) -end) - -test.it("typedef struct pointer field", function() - test.equal(roundtrip("typedef struct { struct Node * next; } Node;"), [[ -typedef struct { - struct Node *next; -} Node;]]) -end) - -test.it("typedef enum", function() - test.equal(roundtrip("typedef enum { RED, GREEN, BLUE, } Color;"), [[ -typedef enum { - RED, - GREEN, - BLUE, -} Color;]]) -end) - -test.it("typedef enum with tag", function() - test.equal(roundtrip("typedef enum Dir { UP, DOWN, } Dir;"), [[ -typedef enum Dir { - UP, - DOWN, -} Dir;]]) -end) - -test.it("typedef function pointer no params", function() - test.equal(roundtrip("typedef void (*Callback)(void);"), "typedef void (*Callback)(void);") -end) - -test.it("typedef function pointer with params", function() - test.equal( - roundtrip("typedef int (*Comparator)(const void * a, const void * b);"), - "typedef int (*Comparator)(const void *a, const void *b);" - ) -end) - -test.it("typedef function pointer returning pointer", function() - test.equal(roundtrip("typedef char * (*Getter)(int key);"), "typedef char *(*Getter)(int key);") -end) - -test.it("function declaration no params", function() - test.equal(roundtrip("void init(void);"), "void init(void);") -end) - -test.it("function declaration named params", function() - test.equal(roundtrip("int add(int a, int b);"), "int add(int a, int b);") -end) - -test.it("function declaration unnamed params", function() - test.equal(roundtrip("int add(int, int);"), "int add(int, int);") -end) - -test.it("function returning pointer", function() - test.equal(roundtrip("char * strdup(const char * s);"), "char *strdup(const char *s);") -end) - -test.it("function with void pointer param", function() - test.equal(roundtrip("void free(void * ptr);"), "void free(void *ptr);") -end) - -test.it("extern variable", function() - test.equal(roundtrip("extern int errno;"), "extern int errno;") -end) - -test.it("extern pointer", function() - test.equal(roundtrip("extern char * environ;"), "extern char *environ;") -end) - -test.it("fn_decl with __asm__ roundtrips", function() - test.equal( - roundtrip("int mylib_add(int a, int b) __asm__(\"add\");"), - "int mylib_add(int a, int b) __asm__(\"add\");" - ) -end) - -test.it("extern_var with __asm__ roundtrips", function() - test.equal( - roundtrip("extern int mylib_errno __asm__(\"errno\");"), - "extern int mylib_errno __asm__(\"errno\");" - ) -end) - -test.it("multiple nodes", function() - test.equal(roundtrip("typedef int size_t;\nextern int errno;"), "typedef int size_t;\nextern int errno;") -end) - --- array sizes - -test.it("struct field with array size roundtrips", function() - test.equal(roundtrip("typedef struct { char buf[256]; } Buf;"), [[ -typedef struct { - char buf[256]; -} Buf;]]) -end) - -test.it("struct field with symbolic array size roundtrips", function() - test.equal(roundtrip("typedef struct { int data[MAX_SIZE]; } S;"), [[ -typedef struct { - int data[MAX_SIZE]; -} S;]]) -end) - --- __attribute__ - -test.it("struct __attribute__((packed)) before body roundtrips", function() - test.equal(roundtrip("typedef struct __attribute__((packed)) { int x; } S;"), [[ -typedef struct __attribute__((packed)) { - int x; -} S;]]) -end) - -test.it("struct __attribute__((packed)) after body normalises to before body", function() - test.equal(roundtrip("typedef struct { int x; } __attribute__((packed)) S;"), [[ -typedef struct __attribute__((packed)) { - int x; -} S;]]) -end) - -test.it("struct __attribute__((aligned(8))) roundtrips", function() - test.equal(roundtrip("typedef struct __attribute__((aligned(8))) { int x; } S;"), [[ -typedef struct __attribute__((aligned(8))) { - int x; -} S;]]) -end) - -test.it("field __attribute__((aligned(4))) roundtrips", function() - test.equal(roundtrip("typedef struct { int x __attribute__((aligned(4))); } S;"), [[ -typedef struct { - int x __attribute__((aligned(4))); -} S;]]) -end) - -test.it("field __attribute__((mode(__word__))) roundtrips", function() - test.equal(roundtrip("typedef struct { int x __attribute__((mode(__word__))); } S;"), [[ -typedef struct { - int x __attribute__((mode(__word__))); -} S;]]) -end) - -test.it("field __attribute__((vector_size(16))) roundtrips", function() - test.equal(roundtrip("typedef struct { float v __attribute__((vector_size(16))); } S;"), [[ -typedef struct { - float v __attribute__((vector_size(16))); -} S;]]) -end) - -test.it("field with array size and __attribute__ roundtrips", function() - test.equal(roundtrip("typedef struct { int arr[4] __attribute__((aligned(16))); } S;"), [[ -typedef struct { - int arr[4] __attribute__((aligned(16))); -} S;]]) -end) - -test.it("fn_decl __attribute__((cdecl)) roundtrips", function() - test.equal(roundtrip("void foo(void) __attribute__((cdecl));"), "void foo(void) __attribute__((cdecl));") -end) - -test.it("fn_decl __attribute__((stdcall)) roundtrips", function() - test.equal(roundtrip("int bar(int x) __attribute__((stdcall));"), "int bar(int x) __attribute__((stdcall));") -end) - -test.it("fn_decl __asm__ and __attribute__ roundtrips", function() - test.equal( - roundtrip("void foo(void) __asm__(\"_foo\") __attribute__((cdecl));"), - "void foo(void) __asm__(\"_foo\") __attribute__((cdecl));" - ) -end) - --- reference types - -test.it("typedef reference alias roundtrips", function() - test.equal(roundtrip("typedef int & IntRef;"), "typedef int &IntRef;") -end) - -test.it("function with reference params roundtrips", function() - test.equal(roundtrip("void swap(int & a, int & b);"), "void swap(int &a, int &b);") -end) - -test.it("function returning reference roundtrips", function() - test.equal(roundtrip("int & at(int idx);"), "int &at(int idx);") -end) - --- anonymous / inline struct, union, enum - -test.it("anonymous union field inside struct", function() - test.equal(roundtrip("typedef struct { union { int a; float b; }; int c; } Foo;"), [[ -typedef struct { - union { int a; float b; }; - int c; -} Foo;]]) -end) - -test.it("named inline struct field", function() - test.equal(roundtrip("typedef struct { struct { int x; int y; } pos; } Entity;"), [[ -typedef struct { - struct { int x; int y; } pos; -} Entity;]]) -end) - -test.it("tagged inline union field", function() - test.equal(roundtrip("typedef struct { union Val { int i; float f; } val; } S;"), [[ -typedef struct { - union Val { int i; float f; } val; -} S;]]) -end) - -test.it("nested anonymous union inside struct", function() - test.equal(roundtrip("typedef struct { union { struct { int x; int y; }; int arr[2]; }; } S;"), [[ -typedef struct { - union { struct { int x; int y; }; int arr[2]; }; -} S;]]) -end) diff --git a/packages/ffix/tests/rewrite.test.lua b/packages/ffix/tests/rewrite.test.lua deleted file mode 100644 index 9266ac5d..00000000 --- a/packages/ffix/tests/rewrite.test.lua +++ /dev/null @@ -1,112 +0,0 @@ -local test = require("lde-test") -local ffix = require("ffix") -local Tokenizer = require("ffix.tokenizer") -local Parser = require("ffix.parser") -local Printer = require("ffix.printer") - --- parse + rewrite + print without calling ffi.cdef -local function rewrite(pfx, src) - local tokens = Tokenizer.new():tokenize(src) - local ok, nodes = Parser.new():parse(tokens) - test.truthy(ok) - - local ctx = ffix.context(pfx) - for _, node in ipairs(nodes) do - if node.name then ctx.names[node.name] = pfx .. "_" .. node.name end - end - - local out = {} - for _, node in ipairs(nodes) do - out[#out + 1] = ctx:rewriteNode(node) - end - return Printer.new():print(out) -end - -test.it("typedef alias is prefixed", function() - test.equal(rewrite("mylib", "typedef int MyInt;"), "typedef int mylib_MyInt;") -end) - -test.it("typedef alias referencing another type is rewritten", function() - test.equal( - rewrite("mylib", "typedef int MyInt;\ntypedef MyInt MyOtherInt;"), - "typedef int mylib_MyInt;\ntypedef mylib_MyInt mylib_MyOtherInt;" - ) -end) - -test.it("typedef struct fields with user types are rewritten", function() - test.equal( - rewrite("mylib", "typedef struct { int x; } Point;\ntypedef struct { Point * origin; } Rect;"), - "typedef struct {\n\tint x;\n} mylib_Point;\ntypedef struct {\n\tmylib_Point *origin;\n} mylib_Rect;" - ) -end) - -test.it("typedef struct with tag rewrites tag too", function() - test.equal( - rewrite("mylib", "typedef struct Node { int val; } Node;"), - "typedef struct mylib_Node {\n\tint val;\n} mylib_Node;" - ) -end) - -test.it("typedef enum is prefixed", function() - test.equal( - rewrite("mylib", "typedef enum { A, B, } Color;"), - "typedef enum {\n\tA,\n\tB,\n} mylib_Color;" - ) -end) - -test.it("typedef fnptr with user type param is rewritten", function() - test.equal( - rewrite("mylib", "typedef struct { int x; } Point;\ntypedef int (*Callback)(Point * p);"), - "typedef struct {\n\tint x;\n} mylib_Point;\ntypedef int (*mylib_Callback)(mylib_Point *p);" - ) -end) - -test.it("fn_decl gets prefixed name and asm attribute", function() - test.equal(rewrite("mylib", "int add(int a, int b);"), "int mylib_add(int a, int b) __asm__(\"add\");") -end) - -test.it("fn_decl with user type params rewrites param types", function() - test.equal( - rewrite("mylib", "typedef struct { int x; } Point;\nvoid transform(Point * p);"), - "typedef struct {\n\tint x;\n} mylib_Point;\nvoid mylib_transform(mylib_Point *p) __asm__(\"transform\");" - ) -end) - -test.it("fn_decl preserves existing __asm__ as the asm target", function() - test.equal( - rewrite("mylib", "int mylib_add(int a, int b) __asm__(\"add\");"), - "int mylib_mylib_add(int a, int b) __asm__(\"add\");" - ) -end) - -test.it("extern_var gets prefixed name and asm attribute", function() - test.equal(rewrite("mylib", "extern int errno_val;"), "extern int mylib_errno_val __asm__(\"errno_val\");") -end) - -test.it("extern_var with pointer type is rewritten", function() - test.equal( - rewrite("mylib", "extern char * global_buf;"), - "extern char *mylib_global_buf __asm__(\"global_buf\");" - ) -end) - -test.it("struct field reference to tagged struct is rewritten", function() - test.equal( - rewrite("mylib", "typedef struct Node { struct Node * next; } Node;"), - "typedef struct mylib_Node {\n\tstruct mylib_Node *next;\n} mylib_Node;" - ) -end) - -test.it("anonymous union field inside struct is preserved through rewrite", function() - test.equal( - rewrite("mylib", "typedef struct { union { int a; float b; }; int c; } Foo;"), - "typedef struct {\n\tunion { int a; float b; };\n\tint c;\n} mylib_Foo;" - ) -end) - -test.it("named inline struct field with user type is rewritten", function() - test.equal( - rewrite("mylib", "typedef int MyInt;\ntypedef struct { struct { MyInt x; MyInt y; } pos; } Entity;"), - "typedef int mylib_MyInt;\ntypedef struct {\n\tstruct { mylib_MyInt x; mylib_MyInt y; } pos;\n} mylib_Entity;" - ) -end) diff --git a/packages/ffix/tests/tokenizer.test.lua b/packages/ffix/tests/tokenizer.test.lua deleted file mode 100644 index 51adae2b..00000000 --- a/packages/ffix/tests/tokenizer.test.lua +++ /dev/null @@ -1,114 +0,0 @@ -local test = require("lde-test") -local Tokenizer = require("ffix.tokenizer") - -local function tok(src) - return Tokenizer.new():tokenize(src) -end - --- idents and keywords - -test.it("tokenizes a plain ident", function() - test.match(tok("myVar"), { { variant = "ident", ident = "myVar" } }) -end) - -test.it("tokenizes underscore ident", function() - test.match(tok("_size_t"), { { variant = "ident", ident = "_size_t" } }) -end) - -test.it("keywords produce their variant directly", function() - for _, kw in ipairs({ "typedef", "struct", "enum", "union", "const", "extern", - "unsigned", "signed", "void", "char", "short", "int", "long", "float", "double", - "static", "volatile", "restrict" }) do - test.match(tok(kw), { { variant = kw } }) - end -end) - --- numbers - -test.it("tokenizes decimal integer", function() - test.match(tok("42"), { { variant = "number", number = 42 } }) -end) - -test.it("tokenizes integer with suffix", function() - test.match(tok("100u"), { { variant = "number", number = 100 } }) -end) - -test.it("tokenizes hex number", function() - test.match(tok("0xff"), { { variant = "number", number = 255 } }) -end) - -test.it("tokenizes float", function() - test.match(tok("3.14"), { { variant = "number", number = 3.14 } }) -end) - --- strings - -test.it("tokenizes a double-quoted string", function() - test.match(tok('"hello world"'), { { variant = "string", string = "hello world" } }) -end) - --- specials - -test.it("tokenizes single-char specials", function() - for _, s in ipairs({ "{", "}", "[", "]", "(", ")", ",", ".", ";", ":", "<", ">", "*", "&", "~" }) do - test.match(tok(s), { { variant = s } }) - end -end) - -test.it("tokenizes :: as one token", function() - test.match(tok("::"), { { variant = "::" } }) -end) - -test.it("tokenizes ... as one token", function() - test.match(tok("..."), { { variant = "..." } }) -end) - --- whitespace and comments - -test.it("skips whitespace", function() - test.match(tok(" int "), { { variant = "int" } }) -end) - -test.it("skips // comments", function() - test.match(tok("// comment\nint"), { { variant = "int" } }) -end) - -test.it("skips # comments", function() - test.match(tok("# preprocessor\nchar"), { { variant = "char" } }) -end) - --- multi-token sequences - -test.it("tokenizes typedef sequence", function() - test.match(tok("typedef int MyInt;"), { - { variant = "typedef" }, - { variant = "int" }, - { variant = "ident", ident = "MyInt" }, - { variant = ";" }, - }) -end) - -test.it("tokenizes pointer with qualifiers", function() - test.match(tok("const char * restrict"), { - { variant = "const" }, - { variant = "char" }, - { variant = "*" }, - { variant = "restrict" }, - }) -end) - -test.it("tokenizes a function signature", function() - test.match(tok("void foo(int x, char *y);"), { - { variant = "void" }, - { variant = "ident", ident = "foo" }, - { variant = "(" }, - { variant = "int" }, - { variant = "ident", ident = "x" }, - { variant = "," }, - { variant = "char" }, - { variant = "*" }, - { variant = "ident", ident = "y" }, - { variant = ")" }, - { variant = ";" }, - }) -end) diff --git a/packages/lde-core/lde.json b/packages/lde-core/lde.json index d59513d4..7aeca0c1 100644 --- a/packages/lde-core/lde.json +++ b/packages/lde-core/lde.json @@ -17,6 +17,6 @@ "git": { "path": "../git" }, "rocked": { "path": "../rocked" }, "luarocks": { "path": "../luarocks" }, - "archive": { "path": "../archive" } + "archive": { "git": "https://github.com/lde-org/archive" } } } diff --git a/packages/sea/lde.json b/packages/sea/lde.json index 3249ec3d..1b928067 100644 --- a/packages/sea/lde.json +++ b/packages/sea/lde.json @@ -7,7 +7,7 @@ "path": { "git": "https://github.com/lde-org/path" }, "env": { "git": "https://github.com/lde-org/env" }, "util": { "path": "../util" }, - "archive": { "path": "../archive" }, + "archive": { "git": "https://github.com/lde-org/archive" }, "curl-sys": { "git": "https://github.com/lde-org/curl-sys" } } } From acd436c663b4b01fb4b8e7e26e774cf20dd9e4c0 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sat, 18 Apr 2026 19:19:38 -0700 Subject: [PATCH 03/13] test: remove unnecessary macos deps Used to use automake/autoconf, now using ninja+cmake for all, which comes with gha macos runner. --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e053948..716c6fa6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,10 +74,6 @@ jobs: shell: pwsh run: echo "SEA_CC=aarch64-w64-mingw32-clang" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - name: Install macOS build deps - if: startsWith(matrix.os, 'macos') - run: brew install autoconf automake libtool - - uses: lde-org/setup-lde@master with: version: nightly From eeb4722d4c88306776c02c939fa33a8207f0214f Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sat, 18 Apr 2026 19:38:16 -0700 Subject: [PATCH 04/13] chore: update devcontainer --- .devcontainer/Dockerfile | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f2ede5f1..cfac01af 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,21 @@ FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive + RUN apt-get update && apt-get install -y \ - luajit \ - libluajit-5.1-dev \ build-essential \ - git + clang \ + cmake \ + ninja-build \ + git \ + curl \ + libcurl4-openssl-dev \ + libssl-dev \ + zlib1g-dev \ + libgit2-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://github.com/lde-org/lde/releases/download/nightly/lde-linux-x86-64 \ + -o /usr/local/bin/lde \ + && chmod +x /usr/local/bin/lde \ + && lde --setup From 0bbad397a9f27d494fd079562e67bcb15d54fc24 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sat, 18 Apr 2026 19:53:13 -0700 Subject: [PATCH 05/13] chore: remove unnecessary devcontainer deps Ubuntu image comes with openssl and the rest are unnecessary. --- .devcontainer/Dockerfile | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cfac01af..5be6672d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,15 +4,9 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ build-essential \ - clang \ cmake \ ninja-build \ - git \ curl \ - libcurl4-openssl-dev \ - libssl-dev \ - zlib1g-dev \ - libgit2-dev \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://github.com/lde-org/lde/releases/download/nightly/lde-linux-x86-64 \ From 8f8062c97cc433c804a57c0041a841b033e80668 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sat, 18 Apr 2026 23:59:48 -0700 Subject: [PATCH 06/13] fix(sea): avoid double free on android Basically atexit would try to free libraries that were already freed by bionic linker. At least thats my understanding of the situation. Lets hope this fixes android. --- packages/sea/src/init.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/sea/src/init.lua b/packages/sea/src/init.lua index 2e2bc8a0..34c2d314 100644 --- a/packages/sea/src/init.lua +++ b/packages/sea/src/init.lua @@ -289,7 +289,7 @@ char lde_tmpdir[4096]; ) } - local stdintInclude = hasLibs and "#include \n#include \n#include " or "" + local stdintInclude = hasLibs and "#include \n#include \n#include " or "#include " -- lde_loadlib_loader: a C closure that calls package.loadlib(upvalue1, "*"). -- Only emitted when there are shared libs to avoid dead-code warnings. @@ -362,11 +362,21 @@ int main(int argc, char** argv) { if (result != LUA_OK) { fprintf(stderr, "%s\n", lua_tostring(L, -1)); lua_close(L); +#ifdef __ANDROID__ + fflush(NULL); + _exit(1); +#else return 1; +#endif } lua_close(L); +#ifdef __ANDROID__ + fflush(NULL); + _exit(0); +#else return 0; +#endif } ]] From 3ecf243497c0680368ff7180e9aeb4441f52b141 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sun, 19 Apr 2026 01:42:16 -0700 Subject: [PATCH 07/13] build(bootstrap): support git dependencies and running build.lua files --- packages/lde/src/init.lua | 109 +++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/packages/lde/src/init.lua b/packages/lde/src/init.lua index a4b73734..39026da3 100755 --- a/packages/lde/src/init.lua +++ b/packages/lde/src/init.lua @@ -16,61 +16,106 @@ if os.getenv("BOOTSTRAP") then end local isWindows = separator == '\\' + + if not baseDir:match("^/") and not baseDir:match("^%a:[/\\]") then + local cwd = isWindows and io.popen("cd"):read("*l") or io.popen("pwd"):read("*l") + baseDir = cwd .. separator .. baseDir + end + local ldeModulesDir = join(baseDir, "target") local function exists(path) local ok, _, code = os.rename(path, path) - if not ok then - if code == 13 then -- permission denied but exists - return true - end - - return false + return code == 13 -- permission denied means it exists end - return true end - if not exists(ldeModulesDir) then - if isWindows then - os.execute('mkdir "' .. ldeModulesDir .. '"') - else - os.execute('mkdir -p "' .. ldeModulesDir .. '"') + local function mkdir(dir) + if not exists(dir) then + if isWindows then + os.execute('mkdir "' .. dir .. '"') + else + os.execute('mkdir -p "' .. dir .. '"') + end end end + -- Semantics of src differ between Windows and Unix symlinks: Windows needs + -- an absolute path for junction points, Unix prefers relative for portability. + local function mklink(src, dest, absSrc) + if not exists(dest) then + if isWindows then + os.execute('mklink /J "' .. dest .. '" "' .. absSrc .. '"') + else + os.execute("ln -sf '" .. src .. "' '" .. dest .. "'") + end + end + end + + mkdir(ldeModulesDir) + local pathPackages = { - "ansi", "clap", "fs", "env", "path", "git", "luarocks", - "sea", "semver", "util", "lde-core", "lde-test", "rocked", "archive" + "ansi", "clap", "git", "luarocks", "readline", + "sea", "semver", "util", "lde-core", "lde-test", "rocked" } for _, pkg in ipairs(pathPackages) do - -- Semantics of the 'src' differ between windows and linux symlinks - local relSrcPath = join("..", "..", pkg, "src") - local absSrcPath = join(baseDir, "..", pkg, "src") + mklink( + join("..", "..", pkg, "src"), + join(ldeModulesDir, pkg), + join(baseDir, "..", pkg, "src") + ) + end + + local tmpBase = os.getenv("TEMP") or os.getenv("TMPDIR") or "/tmp" + local tmpLDEDir = join(tmpBase, "lde") + + ---@type { name: string, url: string }[] + local gitPackages = { + { name = "fs", url = "https://github.com/lde-org/fs" }, + { name = "env", url = "https://github.com/lde-org/env" }, + { name = "process", url = "https://github.com/lde-org/process" }, + { name = "path", url = "https://github.com/lde-org/path" }, + { name = "archive", url = "https://github.com/lde-org/archive" }, + { name = "git", url = "https://github.com/lde-org/git" }, + { name = "json", url = "https://github.com/lde-org/json" }, + { name = "ffix", url = "https://github.com/lde-org/ffix" }, + { name = "curl-sys", url = "https://github.com/lde-org/curl-sys" }, + { name = "git2-sys", url = "https://github.com/lde-org/git2-sys" }, + { name = "deflate-sys", url = "https://github.com/lde-org/deflate-sys" } + } + + mkdir(tmpLDEDir) - local moduleDistPath = join(ldeModulesDir, pkg) + for _, pkg in ipairs(gitPackages) do + local moduleDistPath = join(ldeModulesDir, pkg.name) if not exists(moduleDistPath) then - if isWindows then - os.execute('mklink /J "' .. moduleDistPath .. '" "' .. absSrcPath .. '"') + local cloneDir = join(tmpLDEDir, pkg.name) + if not exists(cloneDir) then + os.execute('git clone --depth 1 --recurse-submodules --shallow-submodules "' .. + pkg.url .. '" "' .. cloneDir .. '"') + end + + local buildScript = join(cloneDir, "build.lua") + if exists(buildScript) then + if isWindows then + os.execute('xcopy /E /I /Y "' .. join(cloneDir, "src") .. '" "' .. moduleDistPath .. '"') + os.execute('cd /d "' .. + cloneDir .. '" && set LDE_OUTPUT_DIR=' .. moduleDistPath .. ' && luajit "' .. buildScript .. '"') + else + os.execute('cp -r "' .. join(cloneDir, "src") .. '/." "' .. moduleDistPath .. '"') + os.execute('cd "' .. + cloneDir .. '" && LDE_OUTPUT_DIR="' .. moduleDistPath .. '" luajit "' .. buildScript .. '"') + end else - os.execute("ln -sf '" .. relSrcPath .. "' '" .. moduleDistPath .. "'") + mklink(join(cloneDir, "src"), moduleDistPath, join(cloneDir, "src")) end end end - local moduleDistPath = join(ldeModulesDir, "lde") - if not exists(moduleDistPath) then - local relSrcPath = join("..", "src") - local absSrcPath = join(baseDir, "src") - - if isWindows then - os.execute('mklink /J "' .. moduleDistPath .. '" "' .. absSrcPath .. '"') - else - os.execute("ln -sf '" .. relSrcPath .. "' '" .. moduleDistPath .. "'") - end - end + mklink(join("..", "src"), join(ldeModulesDir, "lde"), join(baseDir, "src")) end local ansi = require("ansi") From 002695acdea5411d30e46c2d10166ea6b8352804 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sun, 19 Apr 2026 01:46:23 -0700 Subject: [PATCH 08/13] build(bootstrap): add luajit to PATH --- .github/workflows/bootstrap.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index 1e9a700f..968005e5 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -60,6 +60,15 @@ jobs: curl -L -o luajit.tar.gz https://github.com/lde-org/lj-dist/releases/download/latest/${{ matrix.artifact }}.tar.gz tar -xzf luajit.tar.gz + - name: Add LuaJIT to PATH (Unix) + if: runner.os != 'Windows' + run: echo "$GITHUB_WORKSPACE/${{ matrix.artifact }}" >> $GITHUB_PATH + + - name: Add LuaJIT to PATH (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: echo "$env:GITHUB_WORKSPACE\${{ matrix.artifact }}" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install llvm-mingw (Windows ARM64) if: matrix.os == 'windows-11-arm' shell: pwsh From b8f538a0d085a61affa87ff212e5663d174b10c0 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sun, 19 Apr 2026 01:50:41 -0700 Subject: [PATCH 09/13] fix(sea): fix compile error --- packages/sea/src/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sea/src/init.lua b/packages/sea/src/init.lua index 34c2d314..e3638708 100644 --- a/packages/sea/src/init.lua +++ b/packages/sea/src/init.lua @@ -289,7 +289,7 @@ char lde_tmpdir[4096]; ) } - local stdintInclude = hasLibs and "#include \n#include \n#include " or "#include " + local stdintInclude = (hasLibs and "#include \n#include \n#include \n" or "") .. "#ifdef __ANDROID__\n#include \n#endif\n" -- lde_loadlib_loader: a C closure that calls package.loadlib(upvalue1, "*"). -- Only emitted when there are shared libs to avoid dead-code warnings. From 94fd440ee87c6542e5bc44a639428886f38dcf89 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sun, 19 Apr 2026 02:03:56 -0700 Subject: [PATCH 10/13] build(bootstrap): use aarch64 runner and get android ndk --- .github/workflows/bootstrap.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index 968005e5..059ef769 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -36,8 +36,8 @@ jobs: # artifact: luajit-macos-x86-64 # outfile: lde-macos-x86-64 # sea_cc: gcc - - os: ubuntu-22.04 - artifact: luajit-linux-x86-64-gnu + - os: ubuntu-22.04-arm + artifact: luajit-linux-aarch64-gnu outfile: lde-android-aarch64 android: true @@ -86,10 +86,27 @@ jobs: $env:SEA_CC="${{ matrix.sea_cc }}" & "$env:GITHUB_WORKSPACE/${{ matrix.artifact }}/luajit.exe" ./src/init.lua compile --outfile ${{ matrix.outfile }}.exe + - name: Cache Android NDK + if: matrix.android + id: cache-android-ndk + uses: actions/cache@v5 + with: + path: android-ndk-r29 + key: android-ndk-r29-aarch64 + + - name: Setup Android NDK + if: matrix.android + run: | + if [[ "${{ steps.cache-android-ndk.outputs.cache-hit }}" != "true" ]]; then + wget -q https://github.com/lzhiyong/termux-ndk/releases/download/android-ndk/android-ndk-r29-aarch64.7z + 7z x android-ndk-r29-aarch64.7z -o. > /dev/null + fi + echo "ANDROID_NDK_ROOT=$PWD/android-ndk-r29" >> $GITHUB_ENV + - name: Set up Android NDK compiler if: matrix.android run: | - echo "SEA_CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang" >> $GITHUB_ENV + echo "SEA_CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-aarch64/bin/aarch64-linux-android21-clang" >> $GITHUB_ENV - name: Build lde (Unix) if: runner.os != 'Windows' From eaa5bfc8f7e88ae57699784358f81b665ebafe01 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sun, 19 Apr 2026 02:12:15 -0700 Subject: [PATCH 11/13] test(android): add LD_LIBRARY_PATH explicitly --- .github/workflows/nightly.yml | 1 + .github/workflows/test.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 666eefcb..be354e92 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -152,6 +152,7 @@ jobs: docker run --rm --privileged --platform linux/arm64 \ -v ${{ github.workspace }}:/workspace \ -w /workspace \ + -e LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib \ termux-android:latest \ bash -c "/workspace/lde --setup && cd /workspace/packages/lde && /workspace/lde compile --outfile /workspace/packages/lde/${{ matrix.outfile }} && cd /workspace && /workspace/packages/lde/${{ matrix.outfile }} test" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 716c6fa6..0d7e0c10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -141,5 +141,6 @@ jobs: docker run --rm --privileged --platform linux/arm64 \ -v ${{ github.workspace }}:/workspace \ -w /workspace \ + -e LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib \ termux-android:latest \ bash -c "/workspace/lde --setup && cd /workspace/packages/lde && /workspace/lde compile --outfile /workspace/packages/lde/${{ env.OUTFILE }} && cd /workspace && /workspace/packages/lde/${{ env.OUTFILE }} test" From da81c9fbb7d299e103a02e3f241698e1e57ca73f Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sun, 19 Apr 2026 02:28:36 -0700 Subject: [PATCH 12/13] build(bootstrap): build in termux --- .github/workflows/bootstrap.yml | 108 +++++++------------------------- 1 file changed, 23 insertions(+), 85 deletions(-) diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index 059ef769..fa6fe247 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -13,31 +13,18 @@ jobs: matrix: include: # - os: ubuntu-22.04 - # artifact: luajit-linux-x86-64-gnu # outfile: lde-linux-x86-64 - # sea_cc: gcc # - os: ubuntu-22.04-arm - # artifact: luajit-linux-aarch64-gnu # outfile: lde-linux-aarch64 - # sea_cc: gcc # - os: windows-latest - # artifact: luajit-windows-x86-64-gnu # outfile: lde-windows-x86-64 - # sea_cc: gcc # - os: windows-11-arm - # artifact: luajit-windows-aarch64-gnu # outfile: lde-windows-aarch64 - # sea_cc: clang # - os: macos-15 - # artifact: luajit-macos-aarch64 # outfile: lde-macos-aarch64 - # sea_cc: gcc # - os: macos-15-intel - # artifact: luajit-macos-x86-64 # outfile: lde-macos-x86-64 - # sea_cc: gcc - os: ubuntu-22.04-arm - artifact: luajit-linux-aarch64-gnu outfile: lde-android-aarch64 android: true @@ -47,89 +34,40 @@ jobs: - name: Checkout code uses: actions/checkout@v6 - - name: Cache LuaJIT - id: cache-luajit - uses: actions/cache@v5 - with: - path: ${{ matrix.artifact }} - key: ${{ matrix.artifact }}-latest - - - name: Download LuaJIT - if: steps.cache-luajit.outputs.cache-hit != 'true' - run: | - curl -L -o luajit.tar.gz https://github.com/lde-org/lj-dist/releases/download/latest/${{ matrix.artifact }}.tar.gz - tar -xzf luajit.tar.gz - - - name: Add LuaJIT to PATH (Unix) - if: runner.os != 'Windows' - run: echo "$GITHUB_WORKSPACE/${{ matrix.artifact }}" >> $GITHUB_PATH - - - name: Add LuaJIT to PATH (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: echo "$env:GITHUB_WORKSPACE\${{ matrix.artifact }}" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Install llvm-mingw (Windows ARM64) - if: matrix.os == 'windows-11-arm' - shell: pwsh - run: | - curl -L -o llvm-mingw.zip https://github.com/mstorsjo/llvm-mingw/releases/download/20260311/llvm-mingw-20260311-ucrt-aarch64.zip - Expand-Archive -Path llvm-mingw.zip -DestinationPath . - echo "$env:GITHUB_WORKSPACE\llvm-mingw-20260311-ucrt-aarch64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Build lde (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - cd packages/lde - $env:BOOTSTRAP=1 - $env:SEA_CC="${{ matrix.sea_cc }}" - & "$env:GITHUB_WORKSPACE/${{ matrix.artifact }}/luajit.exe" ./src/init.lua compile --outfile ${{ matrix.outfile }}.exe + - name: Fix workspace permissions (Android) + if: matrix.android + run: chmod -R a+rw ${{ github.workspace }} - - name: Cache Android NDK + - name: Cache test image (Android) if: matrix.android - id: cache-android-ndk + id: cache-android-image uses: actions/cache@v5 with: - path: android-ndk-r29 - key: android-ndk-r29-aarch64 + path: /tmp/termux-android.tar + key: termux-android-v3 - - name: Setup Android NDK + - name: Build lde (Android) if: matrix.android run: | - if [[ "${{ steps.cache-android-ndk.outputs.cache-hit }}" != "true" ]]; then - wget -q https://github.com/lzhiyong/termux-ndk/releases/download/android-ndk/android-ndk-r29-aarch64.7z - 7z x android-ndk-r29-aarch64.7z -o. > /dev/null + if [[ "${{ steps.cache-android-image.outputs.cache-hit }}" != "true" ]]; then + docker run --privileged --platform linux/arm64 \ + --name termux-build termux/termux-docker:aarch64 \ + bash -c "apt update && apt install -y luajit clang cmake ninja openssl git" + docker commit termux-build termux-android:latest + docker rm termux-build + docker save termux-android:latest -o /tmp/termux-android.tar fi - echo "ANDROID_NDK_ROOT=$PWD/android-ndk-r29" >> $GITHUB_ENV - - name: Set up Android NDK compiler - if: matrix.android - run: | - echo "SEA_CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-aarch64/bin/aarch64-linux-android21-clang" >> $GITHUB_ENV + docker load -i /tmp/termux-android.tar - - name: Build lde (Unix) - if: runner.os != 'Windows' - run: | - cd packages/lde - BOOTSTRAP=1 SEA_CC="${SEA_CC:-${{ matrix.sea_cc }}}" $GITHUB_WORKSPACE/${{ matrix.artifact }}/luajit ./src/init.lua compile --outfile ${{ matrix.outfile }} - - - name: Upload nightly release asset (Windows) - if: runner.os == 'Windows' - uses: softprops/action-gh-release@v2 - with: - tag_name: nightly - name: Nightly Build - prerelease: true - body: | - Automated nightly build from master. - Commit: ${{ github.sha }} - Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - files: packages/lde/${{ matrix.outfile }}.exe - fail_on_unmatched_files: true + docker run --rm --privileged --platform linux/arm64 \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace \ + -e LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib \ + termux-android:latest \ + bash -c "cd /workspace/packages/lde && BOOTSTRAP=1 luajit /workspace/packages/lde/src/init.lua compile --outfile /workspace/packages/lde/${{ matrix.outfile }}" - - name: Upload nightly release asset (Unix) - if: runner.os != 'Windows' + - name: Upload nightly release asset uses: softprops/action-gh-release@v2 with: tag_name: nightly From 2b91d06480acbf3455db7d295dada09fe83662a0 Mon Sep 17 00:00:00 2001 From: David Cruz Date: Sun, 19 Apr 2026 02:35:40 -0700 Subject: [PATCH 13/13] build(android): i don't think this is needed --- .github/workflows/bootstrap.yml | 1 - .github/workflows/nightly.yml | 1 - .github/workflows/test.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index fa6fe247..72d4ba06 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -63,7 +63,6 @@ jobs: docker run --rm --privileged --platform linux/arm64 \ -v ${{ github.workspace }}:/workspace \ -w /workspace \ - -e LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib \ termux-android:latest \ bash -c "cd /workspace/packages/lde && BOOTSTRAP=1 luajit /workspace/packages/lde/src/init.lua compile --outfile /workspace/packages/lde/${{ matrix.outfile }}" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index be354e92..666eefcb 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -152,7 +152,6 @@ jobs: docker run --rm --privileged --platform linux/arm64 \ -v ${{ github.workspace }}:/workspace \ -w /workspace \ - -e LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib \ termux-android:latest \ bash -c "/workspace/lde --setup && cd /workspace/packages/lde && /workspace/lde compile --outfile /workspace/packages/lde/${{ matrix.outfile }} && cd /workspace && /workspace/packages/lde/${{ matrix.outfile }} test" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d7e0c10..716c6fa6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -141,6 +141,5 @@ jobs: docker run --rm --privileged --platform linux/arm64 \ -v ${{ github.workspace }}:/workspace \ -w /workspace \ - -e LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib \ termux-android:latest \ bash -c "/workspace/lde --setup && cd /workspace/packages/lde && /workspace/lde compile --outfile /workspace/packages/lde/${{ env.OUTFILE }} && cd /workspace && /workspace/packages/lde/${{ env.OUTFILE }} test"