This commit is contained in:
TopchetoEU 2025-05-21 23:20:31 +03:00
commit 71bf0bb188
Signed by: topchetoeu
GPG Key ID: 6531B8583E5F6ED4
18 changed files with 2958 additions and 0 deletions

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
ifeq ($(DEBUG), yes)
dbg_flag := -g
endif
mklua: $(wildcard src/main/*.lua) $(wildcard src/libs/*.c) $(wildcard src/*.lua)
lua src/bootstrap.lua \
--lua-dir "src/main" \
--c-dir "src/libs" \
--entry "init" \
-o "mklua" \
$(dbg_flag)
clean:
rm mklua

23
Readme.md Normal file
View File

@ -0,0 +1,23 @@
# MKLua
A small tool for compiling a C + Lua project to an executable
# **BIG RED LETTERS ALERT!!!**
NOT PRODUCTION READY!!!!! USE AT YOUR OWN RISK!!!!!!!!! WORKS ON LINUX ONLY!!!!!
## How does it work?
This tool assumes you're working on an executable, that may have lua and C files. Each file will correspond to a single module, that can be used using lua's `require`.
You will specify where these files are located, using the `--lua-dir` and `--c-dir` options, for lua and C files respectively. All files in those folders will be included in the final build, and the module names used will be relative to the folders, with the lua require rules respected (`.../init.lua` or `.../init.c` will be the default import of the folder, slashes become dots).
C files will be required by calling the `lua_CFunc` function, by the name of `lualib_open_[name]`, where `name` is the name of the module, with the dots replaced with underscores. This function is expected to return exactly one value, the exported value by the C module.
Finally, a module name must be specified as the entry of the executable. This module should export a function, which will act as the main function of the program.
## How does it really work tho?
The lua files will be compiled by lua's `string.dump` function, with the debug information stripped by default. Then, each bytecode sequence will get loaded into `package.preload`, which is an internal table of lua's `require` mechanism.
C files on the other hand will get linked together and the `lualib_open_...` function will get called inside the auto-generated `int main` function.

30
src/bootstrap.lua Normal file
View File

@ -0,0 +1,30 @@
local function fix_require(argv_0)
local prefix = (argv_0 or arg[0]):match "^(.+)/.-$";
local sep = package.path:match "[/\\]" or "/";
if prefix then
package.path = table.concat {
package.path, ";",
prefix, sep, "?.lua;",
prefix, sep, "?", sep, "init.lua",
};
return prefix, sep;
else
return ".", sep;
end
end
-- Fix package.path
local prefix, sep = fix_require();
package.path = table.concat {
package.path, ";",
prefix, sep, "main", sep, "?.lua;",
prefix, sep, "main", sep, "?", sep, "init.lua",
};
function package.preload.fs()
return require "src.bootstrap_fs";
end
return require "main" (...);

31
src/bootstrap_fs.lua Normal file
View File

@ -0,0 +1,31 @@
-- Duct tape functions to give us temporary access to the FS for the bootstrap build
-- Supports only linux
local fs = {};
function fs.is_file(dir)
return os.execute("test -f " .. ("%q"):format(dir)) or false;
end
function fs.is_dir(dir)
return os.execute("test -d " .. ("%q"):format(dir)) or false;
end
function fs.readdir(dir)
if not fs.is_dir(dir) then
return nil, "not a directory";
end
local res, err = io.popen("ls " .. ("%q/"):format(dir));
if not res then return nil, err end
return res:lines("l");
end
function fs.mkdir(dir, recursive)
if recursive then
assert(os.execute("mkdir -p " .. ("%q"):format(dir)));
else
assert(os.execute("mkdir " .. ("%q"):format(dir)));
end
end
return fs;

5
src/def/exec.d.lua Normal file
View File

@ -0,0 +1,5 @@
--- @meta exec
--- @param settings { [integer]: string, env: { [string]: string }, stdin?: boolean, stdout?: boolean, stderr?: boolean }
--- @return { stdin: file*, stdout: file*, stderr: file*, reap: fun() }
return function (settings) end;

29
src/def/fs.d.lua Normal file
View File

@ -0,0 +1,29 @@
--- @meta fs
local fs = {};
--- @param path string
--- @param recursive? boolean
--- @return true?
--- @return string? err_msg
function fs.mkdir(path, recursive) end
--- @param path string
--- @param recursive? boolean
--- @return (fun(): string?)? iterator
--- @return string? err_msg
function fs.readdir(path, recursive) end
--- @param path string
--- @return true?
--- @return string? err_msg
function fs.is_file(path) end
--- @param path string
--- @return true?
--- @return string? err_msg
function fs.is_dir(path) end
--- @param path string
--- @return { mode: integer, size: integer, uid: integer, gid: integer, ctime: integer, mtime: integer, atime: integer }?
--- @return string? err_msg
function fs.lstat(path) end
return fs;

270
src/libs/exec.c Normal file
View File

@ -0,0 +1,270 @@
#define _GNU_SOURCE
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
static const char *fs_check_path(lua_State *ctx, int i, size_t *psize) {
size_t n;
const char *path = luaL_checklstring(ctx, i, &n);
size_t real_len = strlen(path);
if (n != real_len) {
luaL_error(ctx, "path may not contain \\0");
return NULL;
}
if (psize) *psize = n;
return path;
}
static void free_exec(char **env, size_t env_n) {
for (size_t i = 0; i < env_n; i++) free(env[i]);
if (env) free(env);
}
static int lib_close(lua_State *ctx) {
luaL_Stream *stream = luaL_checkudata(ctx, 1, LUA_FILEHANDLE);
fflush(stream->f);
int stream_fd = fileno(stream->f);
close(stream_fd);
}
static int lib_close_in(lua_State *ctx) {
luaL_Stream *stream = luaL_checkudata(ctx, 1, LUA_FILEHANDLE);
int stream_fd = fileno(stream->f);
dup2(STDIN_FILENO, stream_fd);
}
static int lib_close_out(lua_State *ctx) {
luaL_Stream *stream = luaL_checkudata(ctx, 1, LUA_FILEHANDLE);
int stream_fd = fileno(stream->f);
dup2(STDOUT_FILENO, stream_fd);
}
static int lib_close_err(lua_State *ctx) {
luaL_Stream *stream = luaL_checkudata(ctx, 1, LUA_FILEHANDLE);
int stream_fd = fileno(stream->f);
dup2(STDERR_FILENO, stream_fd);
}
static int lib_reap(lua_State *ctx) {
pid_t pid = lua_tointeger(ctx, lua_upvalueindex(1));
if (pid < 0) return luaL_error(ctx, "Tried to reap child twice");
lua_pushinteger(ctx, -1);
lua_replace(ctx, lua_upvalueindex(1));
int stat;
waitpid(pid, &stat, 0);
if (!WIFEXITED(stat)) {
lua_pushinteger(ctx, -1);
return 1;
}
else {
lua_pushinteger(ctx, WEXITSTATUS(stat));
return 1;
}
}
static void close_pipes(int in[2], int out[2], int err[2]) {
if (in[0] >= 0) close(in[0]);
if (in[1] >= 0) close(in[1]);
if (out[0] >= 0) close(out[0]);
if (out[1] >= 0) close(out[1]);
if (err[0] >= 0) close(err[0]);
if (err[1] >= 0) close(err[1]);
}
static int lib_exec(lua_State *ctx) {
luaL_checktype(ctx, 1, LUA_TTABLE);
size_t n = lua_rawlen(ctx, 1);
if (n <= 0) return luaL_error(ctx, "Table argument must have at least one array element");
const char *args[n + 1];
char **env;
size_t env_cap = 0, env_n = 0;
args[n] = NULL;
for (size_t i = 0; i < n; i++) {
lua_geti(ctx, 1, i + 1);
if (!lua_isstring(ctx, -1)) return luaL_error(ctx, "Table argument must consist only of strings");
args[i] = lua_tostring(ctx, -1);
lua_pop(ctx, 1);
}
if (lua_getfield(ctx, 1, "env")) {
lua_pushnil(ctx);
while (lua_next(ctx, 1)) {
if (lua_type(ctx, -2) == LUA_TSTRING) {
if (!lua_isstring(ctx, -1)) {
free_exec(env, env_n);
return luaL_error(ctx, "Table argument must consist only of strings");
}
size_t prev_cap = env_cap;
while (env_n >= env_cap) {
if (env_cap == 0) env_cap = 16;
else env_cap *= 2;
}
if (prev_cap) env = realloc(env, sizeof *env * env_cap);
else env = malloc(sizeof *env * env_cap);
const char *key = lua_tostring(ctx, -2);
const char *val = lua_tostring(ctx, -1);
char *entry = malloc(strlen(key) + 1 + strlen(val) + 1);
snprintf(entry, sizeof entry, "%s=%s", key, val);
env[env_n++] = entry;
}
lua_pop(ctx, 1);
}
lua_pop(ctx, 1);
}
int stdin_pipe[2] = { -1, -1 };
int stdout_pipe[2] = { -1, -1 };
int stderr_pipe[2] = { -1, -1 };
if (lua_getfield(ctx, 1, "stdin")) {
if (pipe(stdin_pipe) < 0) {
int _ = errno;
close_pipes(stdin_pipe, stdout_pipe, stderr_pipe);
free_exec(env, env_n);
errno = _;
lua_pushnil(ctx);
lua_pushfstring(ctx, "Failed to create stdin pipe: %s", strerror(errno));
return 2;
}
lua_pop(ctx, 1);
}
if (lua_getfield(ctx, 1, "stdout")) {
if (lua_toboolean(ctx, -1)) {
if (pipe(stdout_pipe) < 0) {
int _ = errno;
close_pipes(stdin_pipe, stdout_pipe, stderr_pipe);
free_exec(env, env_n);
errno = _;
lua_pushnil(ctx);
lua_pushfstring(ctx, "Failed to create stdout pipe: %s", strerror(errno));
return 2;
}
}
lua_pop(ctx, 1);
}
if (lua_getfield(ctx, 1, "stderr")) {
if (lua_toboolean(ctx, -1)) {
if (pipe(stderr_pipe) < 0) {
int _ = errno;
close_pipes(stdin_pipe, stdout_pipe, stderr_pipe);
free_exec(env, env_n);
errno = _;
lua_pushnil(ctx);
lua_pushfstring(ctx, "Failed to create stderr pipe: %s", strerror(errno));
return 2;
}
}
lua_pop(ctx, 1);
}
pid_t child = fork();
if (child < 0) {
int _ = errno;
close_pipes(stdin_pipe, stdout_pipe, stderr_pipe);
free_exec(env, env_n);
errno = _;
lua_pushnil(ctx);
lua_pushfstring(ctx, "Failed to fork: %s", strerror(errno));
return 2;
}
else if (child == 0) {
close(stdin_pipe[1]);
close(stdout_pipe[0]);
close(stderr_pipe[0]);
if (stdin_pipe[0] >= 0) dup2(stdin_pipe[0], STDIN_FILENO);
if (stdout_pipe[1] >= 0) dup2(stdout_pipe[1], STDOUT_FILENO);
if (stderr_pipe[1] >= 0) dup2(stderr_pipe[1], STDERR_FILENO);
int res;
if (env_n == 0) {
res = execvp(args[0], (char**)args);
}
else {
if (env_n <= env_cap) {
env = realloc(env, sizeof *env * env_n + 1);
}
env[env_n] = NULL;
res = execvpe(args[0], (char**)args, env);
}
// We can't really log much, can we?
exit(errno);
}
else {
lua_createtable(ctx, 0, 4);
const char *args[n];
if (stdin_pipe[1] >= 0) {
luaL_Stream *stdin_stream = lua_newuserdatauv(ctx, sizeof(luaL_Stream), 0);
stdin_stream->closef = lib_close;
stdin_stream->f = fdopen(stdin_pipe[1], "w");
luaL_getmetatable(ctx, LUA_FILEHANDLE);
lua_setmetatable(ctx, -2);
lua_setfield(ctx, -2, "stdin");
}
if (stdout_pipe[0] >= 0) {
luaL_Stream *stdout_stream = lua_newuserdatauv(ctx, sizeof(luaL_Stream), 0);
stdout_stream->closef = lib_close;
stdout_stream->f = fdopen(stdout_pipe[0], "r");
luaL_getmetatable(ctx, LUA_FILEHANDLE);
lua_setmetatable(ctx, -2);
lua_setfield(ctx, -2, "stdout");
}
if (stderr_pipe[0] >= 0) {
luaL_Stream *stderr_stream = lua_newuserdatauv(ctx, sizeof(luaL_Stream), 0);
stderr_stream->closef = lib_close;
stderr_stream->f = fdopen(stderr_pipe[0], "r");
luaL_getmetatable(ctx, LUA_FILEHANDLE);
lua_setmetatable(ctx, -2);
lua_setfield(ctx, -2, "stderr");
}
lua_pushinteger(ctx, child);
lua_pushcclosure(ctx, lib_reap, 1);
lua_setfield(ctx, -2, "reap");
close(stdin_pipe[0]);
close(stdout_pipe[1]);
close(stderr_pipe[1]);
return 1;
}
}
extern int lualib_open_exec(lua_State *ctx) {
lua_pushcfunction(ctx, lib_exec);
return 1;
}

768
src/libs/fmt.c Normal file
View File

@ -0,0 +1,768 @@
// Similar in concept to Lua's pack and unpack
// Main differences are:
// - Some formats are reinterpreted to mean the same thing across architectures
// - Default endianness is big-endian (but that can easily be changed, of course)
// - Formats are parsed once and used many times, to reduce overhead
// - Polyfills will store packers and unpackers in weak tables
// - Default alignment is no alignment
// - The maximum size of an integer is 64 bits (instead of 128)
#include <lauxlib.h>
#include <lua.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#define FMT_UDATA_NAME "fmt.meta"
typedef enum {
FMT_INT8,
FMT_UINT8,
FMT_INT16,
FMT_UINT16,
FMT_INT32,
FMT_UINT32,
FMT_INT64,
FMT_UINT64,
FMT_FLOAT32,
FMT_FLOAT64,
FMT_STR_FIXED,
FMT_STR8,
FMT_STR16,
FMT_STR32,
FMT_STR64,
FMT_STR_ZERO,
FMT_PADDING,
} fmt_type_t;
typedef enum {
FMT_OK,
FMT_BAD_ARGS,
FMT_INCOMPLETE_OP,
FMT_BAD_OP_ARG,
FMT_BAD_OP,
} fmt_code_t;
typedef struct {
fmt_type_t type;
size_t size, align;
} fmt_segment_t;
typedef struct {
bool little_endian;
size_t max_align;
size_t segments_n;
fmt_segment_t segments[];
} fmt_t;
static int fmt_parse_width(size_t *i, const char *raw, size_t raw_size) {
if (*i < raw_size) {
switch (raw[(*i)++]) {
case '1': return 1;
case '2': return 2;
case '4': return 4;
case '8': return 8;
default:
(*i)--;
return 8;
}
}
else return 8;
}
static fmt_code_t fmt_parse_fmt(lua_State *ctx, const char *raw, size_t raw_size) {
if (raw == NULL || *raw == 0) return FMT_BAD_ARGS;
if (raw_size == SIZE_MAX) raw_size = strlen(raw);
fmt_segment_t segments[raw_size];
size_t segments_n = 0;
bool little_endian;
bool max_align;
size_t i = 0;
bool curr_padding = false;
while (true) {
if (i >= raw_size) break;
fmt_segment_t segment;
switch (raw[i++]) {
case '<': little_endian = true; continue;
case '>':
case '=': little_endian = false; continue;
case '!':
if (i < raw_size && (raw[i] < '0' || raw[i] > '9')) {
max_align = raw[i] - '0';
i++;
}
else max_align = 1;
continue;
case 'b': segment = (fmt_segment_t){ .type = FMT_INT8, .align = 1 }; break;
case 'B': segment = (fmt_segment_t){ .type = FMT_UINT8, .align = 1 }; break;
case 'h': segment = (fmt_segment_t){ .type = FMT_INT16, .align = 2 }; break;
case 'H': segment = (fmt_segment_t){ .type = FMT_UINT16, .align = 2 }; break;
case 'j': segment = (fmt_segment_t){ .type = FMT_INT32, .align = 4 }; break;
case 'J': segment = (fmt_segment_t){ .type = FMT_UINT32, .align = 4 }; break;
case 'l': segment = (fmt_segment_t){ .type = FMT_INT64, .align = 8 }; break;
case 'L':
case 'T': segment = (fmt_segment_t){ .type = FMT_UINT64, .align = 8 }; break;
case 'f': segment = (fmt_segment_t){ .type = FMT_FLOAT32, .align = 4 }; break;
case 'd':
case 'n': segment = (fmt_segment_t){ .type = FMT_FLOAT64, .align = 8 }; break;
case 'i':
switch (fmt_parse_width(&i, raw, raw_size)) {
case 1: segment = (fmt_segment_t){ .type = FMT_INT8, .align = 1 }; break;
case 2: segment = (fmt_segment_t){ .type = FMT_INT16, .align = 2 }; break;
case 4: segment = (fmt_segment_t){ .type = FMT_INT32, .align = 4 }; break;
case 8: segment = (fmt_segment_t){ .type = FMT_INT64, .align = 8 }; break;
}
break;
case 'I':
switch (fmt_parse_width(&i, raw, raw_size)) {
case 1: segment = (fmt_segment_t){ .type = FMT_UINT8, .align = 1 }; break;
case 2: segment = (fmt_segment_t){ .type = FMT_UINT16, .align = 2 }; break;
case 4: segment = (fmt_segment_t){ .type = FMT_UINT32, .align = 4 }; break;
case 8: segment = (fmt_segment_t){ .type = FMT_UINT64, .align = 8 }; break;
}
break;
case 'z': segment = (fmt_segment_t){ .type = FMT_STR_ZERO, .align = 1 }; break;
case 'c':
bool has_size;
size_t str_size;
while (i >= raw_size && raw[i] >= '0' && raw[i] <= '9') {
str_size *= 10;
str_size += raw[i] - '0';
has_size = true;
i++;
}
if (!has_size) return FMT_INCOMPLETE_OP;
segment = (fmt_segment_t){ .type = FMT_STR_FIXED, .size = str_size, .align = 1 };
break;
case 's':
switch (fmt_parse_width(&i, raw, raw_size)) {
case 1: segment = (fmt_segment_t){ .type = FMT_STR8, .align = 1 }; break;
case 2: segment = (fmt_segment_t){ .type = FMT_STR16, .align = 2 }; break;
case 4: segment = (fmt_segment_t){ .type = FMT_STR32, .align = 4 }; break;
case 8: segment = (fmt_segment_t){ .type = FMT_STR64, .align = 8 }; break;
}
break;
case 'x': segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 1, .align = 1 }; break;
case 'X':
curr_padding = true;
continue;
case ' ':
break;
default:
return FMT_BAD_OP;
}
if (curr_padding) {
// TODO: is this correct?
switch (segment.type) {
case FMT_INT8:
case FMT_UINT8:
case FMT_STR8:
segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 1, .align = 1 };
break;
case FMT_INT16:
case FMT_UINT16:
case FMT_STR16:
segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 2, .align = 2 };
break;
case FMT_INT32:
case FMT_UINT32:
case FMT_FLOAT32:
case FMT_STR32:
segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 4, .align = 4 };
break;
case FMT_INT64:
case FMT_UINT64:
case FMT_FLOAT64:
case FMT_STR64:
segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 8, .align = 8 };
break;
case FMT_STR_FIXED:
segment = (fmt_segment_t){ .type = FMT_PADDING, .size = segment.size, .align = segment.size };
break;
default:
return FMT_BAD_OP_ARG;
}
}
segments[segments_n++] = segment;
continue;
}
if (curr_padding) return FMT_INCOMPLETE_OP;
for (size_t i = 0; i < segments_n; i++) {
// TODO: this might not be correct...
if (segments[i].align > max_align) segments[i].align = 8;
if (segments[i].align > max_align) segments[i].align = 4;
if (segments[i].align > max_align) segments[i].align = 2;
if (segments[i].align > max_align) segments[i].align = 1;
}
fmt_t *res = lua_newuserdata(ctx, sizeof *res + sizeof *segments * segments_n);
luaL_getmetatable(ctx, FMT_UDATA_NAME);
lua_setmetatable(ctx, -2);
res->little_endian = little_endian;
res->max_align = max_align;
res->segments_n = segments_n;
memcpy(res->segments, segments, sizeof *segments * segments_n);
return FMT_OK;
}
typedef struct {
char *arr;
size_t cap, n;
} write_buff_t;
static void write_buff_append(write_buff_t *buff, const char *data, size_t size) {
size_t new_cap = buff->cap;
while (new_cap < buff->n + size) {
if (new_cap == 0) new_cap = 16;
else new_cap *= 2;
}
if (new_cap != buff->cap) {
if (buff->cap == 0) {
buff->arr = malloc(new_cap);
}
else {
buff->arr = realloc(buff->arr, new_cap);
}
buff->cap = new_cap;
}
if (data == NULL) {
memset(buff->arr + buff->n, 0, size);
}
else {
memcpy(buff->arr + buff->n, data, size);
}
buff->n += size;
}
static void write_buff_fit(write_buff_t *buff) {
if (buff->arr == NULL) return;
if (buff->n == 0) {
free(buff->arr);
buff->cap = 0;
}
else {
buff->cap = buff->n;
buff->arr = realloc(buff->arr, buff->n);
}
}
static bool fmt_read_uint8(const uint8_t *raw, size_t *i, size_t size, uint8_t *res) {
if ((*i) + 1 > size) return false;
*res = raw[(*i)++];
return true;
}
static bool fmt_read_int8(const uint8_t *raw, size_t *i, size_t size, int8_t *res) {
return fmt_read_uint8(raw, i, size, (uint8_t*)res);
}
static bool fmt_read_uint16(const uint8_t *raw, size_t *i, size_t size, uint16_t *res, bool little_endian) {
if ((*i) + 2 > size) return false;
uint16_t a = raw[(*i)++];
uint16_t b = raw[(*i)++];
if (little_endian) *res = a | b << 8;
else *res = a << 8 | b;
return true;
}
static bool fmt_read_int16(const uint8_t *raw, size_t *i, size_t size, int16_t *res, bool little_endian) {
return fmt_read_uint16(raw, i, size, (uint16_t*)res, little_endian);
}
static bool fmt_read_uint32(const uint8_t *raw, size_t *i, size_t size, uint32_t *res, bool little_endian) {
if ((*i) + 4 > size) return false;
uint8_t a = raw[(*i)++];
uint8_t b = raw[(*i)++];
uint8_t c = raw[(*i)++];
uint8_t d = raw[(*i)++];
if (little_endian) *res = a | b << 8 | c << 16 | d << 24;
else *res = a << 24 | b << 16 | c << 8 | d;
return true;
}
static bool fmt_read_int32(const uint8_t *raw, size_t *i, size_t size, int32_t *res, bool little_endian) {
return fmt_read_uint32(raw, i, size, (uint32_t*)res, little_endian);
}
static bool fmt_read_uint64(const uint8_t *raw, size_t *i, size_t size, uint64_t *res, bool little_endian) {
if ((*i) + 8 > size) return false;
uint64_t a = raw[(*i)++];
uint64_t b = raw[(*i)++];
uint64_t c = raw[(*i)++];
uint64_t d = raw[(*i)++];
uint64_t e = raw[(*i)++];
uint64_t f = raw[(*i)++];
uint64_t g = raw[(*i)++];
uint64_t h = raw[(*i)++];
if (little_endian) *res = a | b << 8 | c << 16 | d << 24 | e << 32 | f << 40 | g << 48 | h << 56;
else *res = a << 56 | b << 48 | c << 40 | d << 32 | e << 24 | f << 16 | g << 8 | h;
return true;
}
static bool fmt_read_int64(const uint8_t *raw, size_t *i, size_t size, int64_t *res, bool little_endian) {
return fmt_read_uint64(raw, i, size, (uint64_t*)res, little_endian);
}
static bool fmt_read_float32(const uint8_t *raw, size_t *i, size_t size, float *res, bool little_endian) {
if ((*i) + 4 > size) return false;
uint8_t a = raw[(*i)++];
uint8_t b = raw[(*i)++];
uint8_t c = raw[(*i)++];
uint8_t d = raw[(*i)++];
// TODO: is this portable enough?
if (little_endian) *res = *(float*)(uint8_t[]) { a, b, c, d };
else *res = *(float*)(uint8_t[]) { d, c, b, a };
return true;
}
static bool fmt_read_float64(const uint8_t *raw, size_t *i, size_t size, double *res, bool little_endian) {
if ((*i) + 8 > size) return false;
uint8_t a = raw[(*i)++];
uint8_t b = raw[(*i)++];
uint8_t c = raw[(*i)++];
uint8_t d = raw[(*i)++];
uint8_t e = raw[(*i)++];
uint8_t f = raw[(*i)++];
uint8_t g = raw[(*i)++];
uint8_t h = raw[(*i)++];
// TODO: is this portable enough?
if (little_endian) *res = *(float*)(uint8_t[]) { a, b, c, d, e, f, g, h };
else *res = *(float*)(uint8_t[]) { h, g, f, e, d, c, b, a };
return true;
}
static bool fmt_read_string(lua_State *ctx, fmt_segment_t segment, const uint8_t *raw, size_t *i, size_t size, bool little_endian) {
uint64_t len;
switch (segment.type) {
case FMT_STR8: {
uint8_t len8;
if (!fmt_read_uint8(raw, i, size, &len8)) return false;
break;
}
case FMT_STR16: {
uint16_t len16;
if (!fmt_read_uint16(raw, i, size, &len16, little_endian)) return false;
len = len16;
break;
}
case FMT_STR32: {
uint32_t len32;
if (!fmt_read_uint32(raw, i, size, &len32, little_endian)) return false;
len = len32;
break;
}
case FMT_STR64:
if (!fmt_read_uint64(raw, i, size, &len, little_endian)) return false;
break;
case FMT_STR_FIXED:
len = segment.size;
break;
case FMT_STR_ZERO:
len = strnlen((const char*)(raw + *i), size - *i);
if (len >= size - *i) return false;
break;
default:
return false;
}
fprintf(stderr, "%lu %lu", len, *i);
if ((*i) + len > size) return false;
char data[len];
memcpy(data, raw + (*i), len);
lua_pushlstring(ctx, data, len);
return true;
}
static void fmt_write_uint8(write_buff_t *buff, uint8_t val) {
write_buff_append(buff, (char[]){ val }, 1);
}
static void fmt_write_int8(write_buff_t *buff, int8_t val) {
return fmt_write_uint8(buff, val);
}
static void fmt_write_uint16(write_buff_t *buff, uint16_t val, bool little_endian) {
uint8_t a = val & 0xFF;
uint8_t b = val >> 8 & 0xFF;
if (little_endian) write_buff_append(buff, (char[]){ a, b }, 2);
else write_buff_append(buff, (char[]){ b, a }, 2);
}
static void fmt_write_int16(write_buff_t *buff, uint16_t val, bool little_endian) {
fmt_write_uint16(buff, val, little_endian);
}
static void fmt_write_uint32(write_buff_t *buff, uint32_t val, bool little_endian) {
uint8_t a = val & 0xFF;
uint8_t b = val >> 8 & 0xFF;
uint8_t c = val >> 16 & 0xFF;
uint8_t d = val >> 24 & 0xFF;
if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d }, 4);
else write_buff_append(buff, (char[]){ d, c, b, a }, 4);
}
static void fmt_write_int32(write_buff_t *buff, uint32_t val, bool little_endian) {
fmt_write_uint32(buff, val, little_endian);
}
static void fmt_write_uint64(write_buff_t *buff, uint64_t val, bool little_endian) {
uint8_t a = val & 0xFF;
uint8_t b = val >> 8 & 0xFF;
uint8_t c = val >> 16 & 0xFF;
uint8_t d = val >> 24 & 0xFF;
uint8_t e = val >> 32 & 0xFF;
uint8_t f = val >> 40 & 0xFF;
uint8_t g = val >> 48 & 0xFF;
uint8_t h = val >> 56 & 0xFF;
if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d, e, f, g, h }, 8);
else write_buff_append(buff, (char[]){ h, g, f, e, d, c, b, a }, 8);
}
static void fmt_write_int64(write_buff_t *buff, uint64_t val, bool little_endian) {
fmt_write_uint64(buff, val, little_endian);
}
static void fmt_write_float32(write_buff_t *buff, float val, bool little_endian) {
uint32_t ival = *(uint32_t*)&val;
uint8_t a = ival & 0xFF;
uint8_t b = ival >> 8 & 0xFF;
uint8_t c = ival >> 16 & 0xFF;
uint8_t d = ival >> 24 & 0xFF;
if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d }, 4);
else write_buff_append(buff, (char[]){ d, c, b, a }, 4);
}
static void fmt_write_float64(write_buff_t *buff, double val, bool little_endian) {
uint64_t ival = *(uint64_t*)&val;
uint8_t a = ival & 0xFF;
uint8_t b = ival >> 8 & 0xFF;
uint8_t c = ival >> 16 & 0xFF;
uint8_t d = ival >> 24 & 0xFF;
uint8_t e = ival >> 32 & 0xFF;
uint8_t f = ival >> 40 & 0xFF;
uint8_t g = ival >> 48 & 0xFF;
uint8_t h = ival >> 56 & 0xFF;
if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d, e, f, g, h }, 8);
else write_buff_append(buff, (char[]){ h, g, f, e, d, c, b, a }, 8);
}
static bool fmt_write_string(write_buff_t *buff, fmt_segment_t segment, const char *str, size_t len, bool little_endian) {
switch (segment.type) {
case FMT_STR8:
if (len > 256) return false;
fmt_write_uint8(buff, (uint8_t)len);
write_buff_append(buff, str, len);
return true;
case FMT_STR16:
if (len > 0x10000) return false;
fmt_write_uint16(buff, (uint16_t)len, little_endian);
write_buff_append(buff, str, len);
return true;
case FMT_STR32:
if (len > 0x100000000) return false;
fmt_write_uint32(buff, (uint32_t)len, little_endian);
write_buff_append(buff, str, len);
return true;
case FMT_STR64:
fmt_write_uint64(buff, len, little_endian);
write_buff_append(buff, str, len);
return true;
case FMT_STR_FIXED:
if (len > segment.size) return false;
write_buff_append(buff, str, len);
write_buff_append(buff, NULL, segment.size - len);
return true;
case FMT_STR_ZERO:
if (strlen(str) != len) return false;
write_buff_append(buff, str, len);
write_buff_append(buff, (char[]) { 0x00 }, 1);
return true;
default:
return false;
}
}
static int lib_fmt_pack(lua_State *ctx) {
fmt_t *fmt = luaL_checkudata(ctx, 1, FMT_UDATA_NAME);
size_t arg_i = 2;
write_buff_t buff = { .arr = NULL, .n = 0, .cap = 0 };
for (size_t i = 0; i < fmt->segments_n; i++) {
fmt_segment_t segment = fmt->segments[i];
size_t n_aligned = -((-buff.n / segment.align) * segment.align);
size_t align_padding = n_aligned - buff.n;
write_buff_append(&buff, NULL, align_padding);
switch (segment.type) {
case FMT_INT8:
fmt_write_int8(&buff, luaL_checkinteger(ctx, arg_i++));
break;
case FMT_UINT8:
fmt_write_uint8(&buff, luaL_checkinteger(ctx, arg_i++));
break;
case FMT_INT16:
fmt_write_int16(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
break;
case FMT_UINT16:
fmt_write_uint16(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
break;
case FMT_INT32:
fmt_write_int32(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
break;
case FMT_UINT32:
fmt_write_uint32(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
break;
case FMT_INT64:
fmt_write_int64(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
break;
case FMT_UINT64:
fmt_write_uint64(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
break;
case FMT_FLOAT32:
fmt_write_float32(&buff, (float)luaL_checknumber(ctx, arg_i++), fmt->little_endian);
break;
case FMT_FLOAT64:
fmt_write_float64(&buff, (double)luaL_checknumber(ctx, arg_i++), fmt->little_endian);
break;
case FMT_STR_FIXED:
case FMT_STR8:
case FMT_STR16:
case FMT_STR32:
case FMT_STR64:
case FMT_STR_ZERO: {
size_t size;
const char *str = luaL_checklstring(ctx, arg_i++, &size);
if (!fmt_write_string(&buff, segment, str, size, fmt->little_endian)) {
luaL_error(ctx, "invalid string at %d", arg_i);
}
break;
}
case FMT_PADDING:
// TODO: might need to remove this later...
write_buff_append(&buff, NULL, segment.size);
}
}
write_buff_fit(&buff);
lua_pushlstring(ctx, buff.arr, buff.n);
free(buff.arr);
return 1;
}
static int lib_fmt_unpack(lua_State *ctx) {
fmt_t *fmt = luaL_checkudata(ctx, 1, FMT_UDATA_NAME);
size_t raw_size;
const uint8_t *raw = (const uint8_t*)luaL_checklstring(ctx, 2, &raw_size);
size_t read_i = 0;
size_t res_n = 0;
if (lua_isinteger(ctx, 3)) {
read_i = lua_tointeger(ctx, 3) - 1;
}
write_buff_t buff = { .arr = NULL, .n = 0, .cap = 0 };
for (size_t i = 0; i < fmt->segments_n; i++) {
fmt_segment_t segment = fmt->segments[i];
size_t n_aligned = -((-buff.n / segment.align) * segment.align);
size_t align_padding = n_aligned - buff.n;
read_i += align_padding;
switch (segment.type) {
case FMT_INT8: {
int8_t res;
if (!fmt_read_int8(raw, &read_i, raw_size, &res)) luaL_error(ctx, "couldn't read int8");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_UINT8: {
uint8_t res;
if (!fmt_read_uint8(raw, &read_i, raw_size, &res)) luaL_error(ctx, "couldn't read uint8");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_INT16: {
int16_t res;
if (!fmt_read_int16(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read int16");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_UINT16: {
uint16_t res;
if (!fmt_read_uint16(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read uint16");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_INT32: {
int32_t res;
if (!fmt_read_int32(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read int32");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_UINT32: {
uint32_t res;
if (!fmt_read_uint32(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read uint32");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_INT64: {
int64_t res;
if (!fmt_read_int64(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read int64");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_UINT64: {
uint64_t res;
if (!fmt_read_uint64(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read uint64");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_FLOAT32: {
float res;
if (!fmt_read_float32(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read float32");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_FLOAT64: {
double res;
if (!fmt_read_float64(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read float64");
lua_pushinteger(ctx, res);
res_n++;
break;
}
case FMT_STR_FIXED:
case FMT_STR8:
case FMT_STR16:
case FMT_STR32:
case FMT_STR64:
case FMT_STR_ZERO: {
if (!fmt_read_string(ctx, segment, raw, &read_i, raw_size, fmt->little_endian)) luaL_error(ctx, "couldn't read string");
res_n++;
break;
}
case FMT_PADDING:
read_i += segment.size;
break;
}
}
lua_pushinteger(ctx, read_i + 1);
return res_n + 1;
}
static int lib_fmt_new(lua_State *ctx) {
size_t raw_size;
const char *raw = luaL_checklstring(ctx, 1, &raw_size);
fmt_code_t code = fmt_parse_fmt(ctx, raw, raw_size);
if (code != FMT_OK) {
switch (code) {
case FMT_BAD_ARGS: return luaL_error(ctx, "illegal C arguments");
case FMT_INCOMPLETE_OP: return luaL_error(ctx, "incomplete operand in format string");
case FMT_BAD_OP_ARG: return luaL_error(ctx, "bad operand argument in format string");
case FMT_BAD_OP: return luaL_error(ctx, "bad operand in format string");
default: return luaL_error(ctx, "unknown error while parsing format string");
}
}
return 1;
}
static void fmt_init_meta(lua_State *ctx) {
luaL_newmetatable(ctx, FMT_UDATA_NAME);
luaL_newlib(ctx, ((luaL_Reg[]) {
{ "pack", lib_fmt_pack },
{ "unpack", lib_fmt_unpack },
{ NULL, NULL }
}));
lua_setfield(ctx, -2, "__index");
lua_pushboolean(ctx, false);
lua_setfield(ctx, -2, "__meta");
lua_pop(ctx, 1);
}
extern int lualib_open_fmt(lua_State *ctx) {
fmt_init_meta(ctx);
luaL_newlib(ctx, ((luaL_Reg[]) {
{ "new", lib_fmt_new },
{ "pack", lib_fmt_pack },
{ "unpack", lib_fmt_unpack },
{ NULL, NULL },
}));
return 1;
}

168
src/libs/fs.c Normal file
View File

@ -0,0 +1,168 @@
#define _GNU_SOURCE
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/dir.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
static const char *fs_check_path(lua_State *ctx, int i, size_t *psize) {
size_t n;
const char *path = luaL_checklstring(ctx, i, &n);
size_t real_len = strlen(path);
if (n != real_len) {
luaL_error(ctx, "path may not contain \\0");
return NULL;
}
if (psize) *psize = n;
return path;
}
static int readdir_next(lua_State *ctx) {
DIR *dir = lua_touserdata(ctx, lua_upvalueindex(1));
while (true) {
struct dirent *entry = readdir(dir);
if (!entry) {
lua_pushnil(ctx);
return 1;
}
if (!strcmp(entry->d_name, ".")) continue;
if (!strcmp(entry->d_name, "..")) continue;
lua_pushstring(ctx, entry->d_name);
return 1;
}
}
static int lib_fs_mkdir(lua_State *ctx) {
size_t path_size;
const char *ro_path = fs_check_path(ctx, 1, &path_size);
bool recursive = lua_toboolean(ctx, 2);
if (recursive) {
char path[path_size + 1];
memcpy(path, ro_path, path_size + 1);
for (size_t i = 0; i <= path_size ; i++) {
if (path[i] == '/' || path[i] == '\0') {
if (i == 0) continue;
path[i] = '\0';
if (mkdir(path, 0755) < 0 && errno != EEXIST) {
lua_pushnil(ctx);
lua_pushfstring(ctx, "can't make directory: %s", strerror(errno));
return 2;
}
if (i < path_size) path[i] = '/';
}
}
}
else {
if (mkdir(ro_path, 0755) < 0) {
lua_pushnil(ctx);
lua_pushfstring(ctx, "can't make directory: %s", strerror(errno));
return 2;
}
}
lua_pushboolean(ctx, true);
return 1;
}
static int lib_fs_readdir(lua_State *ctx) {
const char *path = fs_check_path(ctx, 1, NULL);
DIR *dir = opendir(path);
if (!dir) {
lua_pushnil(ctx);
lua_pushfstring(ctx, "failed to open directory: %s", strerror(errno));
return 2;
}
lua_pushlightuserdata(ctx, dir);
lua_pushcclosure(ctx, readdir_next, 1);
return 1;
}
static int lib_fs_is_file(lua_State *ctx) {
const char *path = fs_check_path(ctx, 1, NULL);
struct stat stat;
if (lstat(path, &stat) < 0) {
lua_pushnil(ctx);
lua_pushfstring(ctx, "failed to open directory: %s", strerror(errno));
return 2;
}
lua_pushboolean(ctx, S_ISREG(stat.st_mode));
return 1;
}
static int lib_fs_is_dir(lua_State *ctx) {
const char *path = fs_check_path(ctx, 1, NULL);
struct stat stat;
if (lstat(path, &stat) < 0) {
lua_pushnil(ctx);
lua_pushfstring(ctx, "failed to open directory: %s", strerror(errno));
return 2;
}
lua_pushboolean(ctx, S_ISDIR(stat.st_mode));
return 1;
}
static int lib_fs_lstat(lua_State *ctx) {
const char *path = fs_check_path(ctx, 1, NULL);
struct stat stat;
if (lstat(path, &stat) < 0) {
lua_pushnil(ctx);
lua_pushfstring(ctx, "failed to stat: %s", strerror(errno));
return 2;
}
lua_createtable(ctx, 0, 7);
lua_pushinteger(ctx, stat.st_mode);
lua_setfield(ctx, -2, "mode");
lua_pushinteger(ctx, stat.st_size);
lua_setfield(ctx, -2, "size");
lua_pushinteger(ctx, stat.st_uid);
lua_setfield(ctx, -2, "uid");
lua_pushinteger(ctx, stat.st_gid);
lua_setfield(ctx, -2, "gid");
lua_pushinteger(ctx, stat.st_ctim.tv_sec);
lua_setfield(ctx, -2, "ctime");
lua_pushinteger(ctx, stat.st_mtim.tv_sec);
lua_setfield(ctx, -2, "mtime");
lua_pushinteger(ctx, stat.st_atim.tv_sec);
lua_setfield(ctx, -2, "atime");
return 1;
}
extern int lualib_open_fs(lua_State *ctx) {
luaL_newlib(ctx, ((luaL_Reg[]) {
{ "mkdir", lib_fs_mkdir },
{ "readdir", lib_fs_readdir },
{ "is_file", lib_fs_is_file },
{ "is_dir", lib_fs_is_dir },
{ NULL, NULL },
}));
return 1;
}

149
src/main/args.lua Normal file
View File

@ -0,0 +1,149 @@
local args = {};
--- @alias consumer (fun(next: fun(): string?)) | string
--- @alias consumers { [string]: consumer, [1]: consumer }
--- @param consumers consumers
--- @param name string
local function get_consumer(consumers, name)
local consumer = nil;
local path = {};
local n = 0;
while true do
local curr = consumer or name;
if path[curr] then
local path_arr = {};
for k, v in next, path do
path_arr[v] = k;
end
error("Alias to '" .. curr .. "' is recursive: " .. table.concat(path_arr, " -> "));
end
consumer = consumers[curr];
if consumer == nil then
local prefix;
if n == 0 then
prefix = "Unknown flag";
else
prefix = "Unknown alias";
end
if #curr == 1 then
error(prefix .. " '-" .. curr .. "'");
else
error(prefix .. " '--" .. curr .. "'");
end
elseif type(consumer) == "function" then
return consumer;
end
path[curr] = n;
n = n + 1;
end
end
local digest_next;
--- @param consumers consumers
--- @param name string
--- @param next fun(): string?
local function digest_arg(consumers, name, next)
get_consumer(consumers, name)(next);
return digest_next(consumers, next);
end
--- @param consumers consumers
--- @param next fun(): string?
--- @param only_rest? boolean
local function digest_rest(consumers, next, only_rest)
if not consumers[1] then
local arg = next();
if not arg then return nil end
error("Invalid argument '" .. arg .. "'");
end
local consumed = false;
local function our_consumer()
consumed = true;
return next();
end
consumers[1](our_consumer);
if not consumed then
local arg = next();
if not arg then return nil end
error("Invalid argument '" .. arg .. "'");
end
if only_rest then
return digest_rest(consumers, next, true);
else
return digest_next(consumers, next);
end
end
--- @param consumers consumers
--- @param next fun(): string?
function digest_next(consumers, next)
local arg = next();
if not arg then return nil end
if arg == "--" then
return digest_rest(consumers, next, true);
end
local name, val;
name, val = arg:match "^%-%-(.-)=(.+)$";
if name then
local function our_next()
our_next = next;
return val;
end
return digest_arg(consumers, name, function () return our_next() end);
end
name = arg:match "^%-%-(.-)$";
if name then
return digest_arg(consumers, name, next);
end
name = arg:match "^%-(.-)$";
if name then
for c in name:gmatch "." do
digest_arg(consumers, c, next);
end
return false;
end
local function our_next()
our_next = next;
return arg;
end
return digest_rest(consumers, function () return our_next() end, false);
end
--- @param consumers consumers
--- @returns fun(...: string)
function args.parser(consumers)
return function (...)
local args = { ... };
local i = 0;
local function next()
i = i + 1;
return args[i];
end
return digest_next(consumers, next);
end
end
return args;

430
src/main/init.lua Normal file
View File

@ -0,0 +1,430 @@
local recurse = require "recurse";
local fs = require "fs";
local help_msg = [[
mklua by TopchetoEU
A tool to compile a simple application, consisting of lua and C files, as well
as some assets to one executable.
[file].lua - Shorthand for --lua-file [file].lua
[file].c - Shorthand for --c-file [file].c
--lua-dir [dir] - Specifies a folder, in which to look for lua files
--lua-file [file]
--lua-file [basename]:[file] - Includes a single lua file
--c-dir [dir] - Specifies a folder, in which to look for C files
--c-file [file]
--c-file [basename]:[file] - Includes a single C file
--asset-dir [dir] - Specifies a folder, in which to look for assets
--asset-file [file]
--asset-file [basename]:[file] - Specifies a folder, in which to look for assets
--cc-args - Arguments, which to pass to the C compiler
--entry - The module, which to use as an entry
--output - The file, to which to compile
--debug - Emits debug symbols for lua and C
--dump - Prints out the generated C entry file and exists
--help - shows this message
A mklua "project" consists of modules and assets, which are searched in their
respective directories.
Modules consist of lua and C modules. Lua modules will be searched for in the
directories, specified with "--lua-dir", and will be put in "package.preload"
(the name of the preloaded module is in the lua format, relative to the folder,
in which the file was found). The same goes with the C modules, with the
exception that a C module must declare a lua_CFunction of the name
"lualib_open_path_to_my_module" (for a C module that is called
'path.to.my.module'). That function's return value will be used to initialize
the module's value.
Assets on the other hand will be searched in the same way modules are, but will
be put in a module, called "assets". The value of this module will be a map of
the asset filenames, relative to the folder they were found in, mapped to their
content, as string.
The entry is a module, that is expected to return a function. That function will
be called with the CLI args, when the program is run.
]];
local c_format = [[
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
%s
char bytecode[] = %s;
void load_libraries(lua_State *ctx) {
luaL_openlibs(ctx);
%s
}
static int print_error(lua_State *ctx) {
luaL_traceback(ctx, ctx, "unhandled error: ", 1);
const char *str = lua_tostring(ctx, -1);
fprintf(stderr, "%%s\n", str);
return 0;
}
int main(int argc, const char **argv) {
lua_State *ctx = luaL_newstate();
load_libraries(ctx);
lua_pushcfunction(ctx, print_error);
int err_handler = lua_gettop(ctx);
if (luaL_loadbuffer(ctx, bytecode, sizeof bytecode - 1, "<main>")) {
fprintf(stderr, "lua bytecode error: %%s", lua_tostring(ctx, -1));
lua_close(ctx);
return 1;
}
for (int i = 1; i < argc; i++) {
lua_pushstring(ctx, argv[i]);
}
if (lua_pcall(ctx, argc - 1, 0, err_handler)) {
lua_close(ctx);
return 1;
}
else {
lua_close(ctx);
return 0;
}
}
]];
local assets_load_func = [[
static int get_assets(lua_State *ctx) {
lua_pushvalue(ctx, lua_upvalueindex(1));
return 1;
}
]];
local asset_init_format = [[
lua_createtable(ctx, 0, 0);
lua_pushvalue(ctx, -1);
luaL_getsubtable(ctx, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
lua_pushvalue(ctx, -2);
lua_setfield(ctx, -2, "assets");
lua_pop(ctx, 2);
]];
local asset_add_format = [[
lua_pushlstring(ctx, %s, %d);
lua_setfield(ctx, -2, %q);
]];
local entry_invoke_format = [[
return xpcall(require %q, function (err)
print("unhandled error: " .. tostring(err));
local i = 2;
local prefix = " ";
while true do
local info = debug.getinfo(i, "Snl");
if info == nil then break end
local name = info.name;
local location = info.short_src;
if location == "[C]" then
location = "at <internal>";
elseif string.find(location, "[string", 1, true) then
location = "at <string>";
else
location = "at " .. location;
end
if info.currentline > 0 then
location = location .. ":" .. info.currentline;
end
if name ~= nil then
print(prefix .. location .. " in " .. name);
else
print(prefix .. location);
end
i = i + 1;
end
end, ...);
]];
--- @generic T
--- @param arr T[]
--- @return fun(): T?
local function iterate(arr)
local i = 0;
local n = #arr;
return function ()
if arr == nil then
return nil;
end
i = i + 1;
if i > n then
--- @diagnostic disable-next-line: cast-local-type
arr = nil;
return nil;
else
return arr[i];
end
end
end
local function get_module_name(f, prefix)
return f
-- Remove prefix slashes
:gsub("^[/\\]+", "")
-- Replace slashes with dots
:gsub("[/\\]", ".")
-- Remove .init suffix
:gsub("%.init$", "");
end
local function c_escape(str)
return "\"" .. str:gsub(".", function (c)
local b = string.byte(c);
if c == "\n" then
return "\\n";
elseif c == "\\" then
return "\\\\";
elseif c == "\"" then
return "\\\"";
elseif b >= 32 and b <= 126 then
return c;
else
return ("\\%.3o"):format(b);
end
end) .. "\"";
end
return function (...)
--- @type string[]
local cc_args = {};
local lua_files = {};
local c_files = {};
local asset_files = {};
local has_assets = false;
local strip = true;
local dump = false;
local entry;
local out_path;
require "args".parser {
function (arg)
local path = assert(arg());
local basename = path:match "^(.+)%.lua$";
if basename then
lua_files[basename] = path;
return;
end
basename = path:match "^(.+)%.c$";
if basename then
c_files[basename] = path;
return;
end
if path:match "^%-" then
error("Unknown option '" .. path .. "'");
else
error("'" .. path .. "' is neither a .c or a .lua file. Use --asset-file if you want to add an asset");
end
end,
help = function ()
print(help_msg);
os.exit();
end,
["lua-dir"] = function (arg)
local dir = assert(arg());
for path in recurse(dir) do
local basename = path:match "^(.+)%.lua$";
if basename and fs.is_file(path) then
lua_files[basename:sub(#dir + 1)] = path;
end
end
end,
["lua-file"] = function (arg)
local val = assert(arg());
local basename, realname = val:match "^(.+):(.+)$";
if basename then
lua_files[basename] = realname;
return;
end
basename = val:match "$(.+)%.[^%.]+";
if basename then
lua_files[basename] = val;
return;
end
lua_files[val] = val;
end,
["c-dir"] = function (arg)
local dir = assert(arg());
for path in recurse(dir) do
local basename = path:match "^(.+)%.c$";
if basename and fs.is_file(path) then
c_files[basename:sub(#dir + 1)] = path;
end
end
end,
["c-file"] = function (arg)
local val = assert(arg());
if not fs.is_file(val) then
error ""
end
local basename, realname = val:match "^(.+):(.+)$";
if basename then
c_files[basename] = realname;
return;
end
basename = val:match "$(.+)%.[^%.]+";
if basename then
c_files[basename] = val;
return;
end
c_files[val] = val;
end,
["asset-dir"] = function (arg)
local dir = assert(arg());
for path in recurse(dir) do
if fs.is_file(path) then
asset_files[path:sub(#dir + 1)] = path;
end
end
has_assets = true;
end,
["asset-file"] = function (arg)
local val = assert(arg());
local basename, realname = val:match "^(.+):(.+)$";
if basename then
c_files[basename] = realname;
else
c_files[val] = val;
end
has_assets = true;
end,
["cc-arg"] = function (arg)
table.insert(cc_args, assert(arg()));
end,
entry = function (arg)
entry = assert(arg());
end,
debug = function ()
strip = false;
end,
dump = function ()
dump = true;
end,
output = function (arg)
if out_path then
error "--output may be specified once";
end
out_path = assert(arg());
end,
o = "output",
e = "entry",
g = "debug",
h = "help",
} (...);
if not out_path then
error "no output file specified; aborting...";
end
if not entry then
error "no entry specified; aborting...";
end
local lib_parts = {};
local extern_parts = {};
local lua_parts = {};
local files = {};
for basename, path in pairs(lua_files) do
local module_name = get_module_name(basename);
-- We do the separate bytecode to keep line numbers and file names
local bytecode = string.dump(assert(load(io.lines(path, 4096), "@" .. path, "t")), strip);
table.insert(lua_parts, ("package.preload[%q] = load(%q, %q, 'b');"):format(module_name, bytecode, path));
end
table.insert(lua_parts, entry_invoke_format:format(entry));
if has_assets then
table.insert(extern_parts, assets_load_func);
table.insert(lib_parts, asset_init_format);
end
for basename, path in pairs(asset_files) do
local f = assert(io.open(path, "r"));
local content = assert(f:read "*a");
assert(f:close());
table.insert(lib_parts, asset_add_format:format(content, #content, basename));
end
for basename, path in pairs(c_files) do
local module_name = get_module_name(basename);
local func_name = "lualib_open_" .. module_name:gsub("%.", "_");
table.insert(extern_parts, ("extern int %s(lua_State *ctx);"):format(func_name));
table.insert(lib_parts, ("luaL_requiref(ctx, %q, %s, false);"):format(module_name, func_name));
table.insert(files, path);
end
local bytecode = string.dump(assert(load(iterate(lua_parts), "@<entry>", "t")), true);
local file = c_format:format(table.concat(extern_parts, "\n"), c_escape(bytecode), table.concat(lib_parts, "\n\t"));
if dump then
io.stdout:write(file);
return;
end
local f = assert(io.popen(table.concat {
"cc -llua -x c - ",
table.concat(files, " "),
" ",
strip and "" or "-g ",
"-o ", out_path, " ",
table.concat(cc_args, " ")
}, "w"));
assert(f:write(file));
assert(f:close());
end

394
src/main/init.lua.old Normal file
View File

@ -0,0 +1,394 @@
local recurse = require "recurse";
local help_msg = [[
mklua by TopchetoEU
A tool to compile a simple application, consisting of lua and C files, as well
as some assets to one executable.
--lua_dir: Specifies a folder, in which to look for lua files
--c_dir: Specifies a folder, in which to look for C files
--assets_dir: Specifies a folder, in which to look for assets
--cc_args: Arguments, which to pass to the C compiler
--entry: The module, which to use as an entry
--output: The file, to which to compile
A mklua "project" consists of modules and assets, which are searched in their
respective directories.
Modules consist of lua and C modules. Lua modules will be searched for in the
directories, specified with "--lua_dir", and will be put in "package.preload"
(the name of the preloaded module is in the lua format, relative to the folder,
in which the file was found). The same goes with the C modules, with the
exception that a C module must declare a lua_CFunction of the name
"lualib_open_path_to_my_module" (for a C module that is called
'path.to.my.module'). That function's return value will be used to initialize
the module's value.
Assets on the other hand will be searched in the same way modules are, but will
be put in a module, called "assets". The value of this module will be a map of
the asset filenames, relative to the folder they were found in, mapped to their
content, as string.
The entry is a module, that is expected to return a function. That function will
be called with the CLI args, when the program is run.
You can create a Makefile, so that you don't have to write out the same command,
using --make. NOTE: This will overwrite any existing Makefile.
]];
local c_format = [[
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
%s
char bytecode[] = %s;
void load_libraries(lua_State *ctx) {
luaL_openlibs(ctx);
%s
}
static int print_error(lua_State *ctx) {
luaL_traceback(ctx, ctx, "unhandled error: ", 1);
const char *str = lua_tostring(ctx, -1);
fprintf(stderr, "%%s\n", str);
return 0;
}
int main(int argc, const char **argv) {
lua_State *ctx = luaL_newstate();
load_libraries(ctx);
lua_pushcfunction(ctx, print_error);
int err_handler = lua_gettop(ctx);
if (luaL_loadbuffer(ctx, bytecode, sizeof bytecode - 1, "<main>")) {
fprintf(stderr, "lua bytecode error: %%s", lua_tostring(ctx, -1));
lua_close(ctx);
return 1;
}
for (int i = 1; i < argc; i++) {
lua_pushstring(ctx, argv[i]);
}
if (lua_pcall(ctx, argc - 1, 0, err_handler)) {
lua_close(ctx);
return 1;
}
else {
lua_close(ctx);
return 0;
}
}
]];
local assets_load_func = [[
static int get_assets(lua_State *ctx) {
lua_pushvalue(ctx, lua_upvalueindex(1));
return 1;
}
]];
local asset_init_format = [[
lua_createtable(ctx, 0, 0);
lua_pushvalue(ctx, -1);
lua_pushcclosure(ctx, get_assets, 1);
luaL_getsubtable(ctx, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
lua_pushvalue(ctx, -2);
lua_setfield(ctx, -2, "assets");
lua_pop(ctx, 2);
]];
local asset_add_format = [[
lua_pushlstring(ctx, %s, %d);
lua_setfield(ctx, -2, %q);
]];
local entry_invoke_format = [[
return xpcall(require %q, function (err)
print("unhandled error: " .. tostring(err));
local i = 2;
local prefix = " ";
while true do
local info = debug.getinfo(i, "Snl");
if info == nil then break end
local name = info.name;
local location = info.short_src;
if location == "[C]" then
location = "at <internal>";
elseif string.find(location, "[string", 1, true) then
location = "at <string>";
else
location = "at " .. location;
end
if info.currentline > 0 then
location = location .. ":" .. info.currentline;
end
if name ~= nil then
print(prefix .. location .. " in " .. name);
else
print(prefix .. location);
end
i = i + 1;
end
end, ...);
]];
local makefile_format = [[
ifeq ($(DEBUG), yes)
dbg_flag := -g
endif
%s:
%s \
%s \
$(dbg_flag)
clean:
rm %s
]];
--- @generic T
--- @param arr T[]
--- @return fun(): T?
local function iterate(arr)
local i = 0;
local n = #arr;
return function ()
if arr == nil then
return nil;
end
i = i + 1;
if i > n then
--- @diagnostic disable-next-line: cast-local-type
arr = nil;
return nil;
else
return arr[i];
end
end
end
local function get_module_name(f, prefix)
return f
-- Remove currently iterated dir prefix
:sub(#prefix + 1)
-- Remove prefix slashes
:gsub("^[/\\]+", "")
-- Replace slashes with dots
:gsub("[/\\]", ".")
-- Remove .init suffix
:gsub("%.init$", "");
end
local function c_escape(str)
return "\"" .. str:gsub(".", function (c)
local b = string.byte(c);
if c == "\n" then
return "\\n";
elseif c == "\\" then
return "\\\\";
elseif c == "\"" then
return "\\\"";
elseif b >= 32 and b <= 126 then
return c;
else
return ("\\%.3o"):format(b);
end
end) .. "\"";
end
return function (...)
--- @type string[]
local cc_args = {};
local lua_dirs = {};
local c_dirs = {};
local assets_dirs = {};
local strip = true;
local dump = false;
local make = false;
local make_exec = "mklua";
local entry;
local out_path;
require "args"({
help = function ()
print(help_msg);
os.exit();
end,
lua_dir = function (arg)
table.insert(lua_dirs, arg);
return true;
end,
c_dir = function (arg)
table.insert(c_dirs, arg);
return true;
end,
assets_dir = function (arg)
table.insert(assets_dirs, arg);
return true;
end,
cc_args = function (arg)
table.insert(cc_args, arg);
return true;
end,
entry = function (arg)
entry = arg;
return true;
end,
debug = function ()
strip = false;
return false;
end,
dump = function ()
dump = true;
return false;
end,
make = function ()
make = true;
return false;
end,
make_exe = function (arg)
make_exec = arg;
return true;
end,
output = function (arg)
if out_path then
error "--output may be specified once";
end
out_path = arg;
return true;
end,
o = "output",
g = "debug",
h = "help",
}, ...);
if not out_path then
error "no output file specified; aborting...";
end
if not entry then
error "no entry specified; aborting...";
end
if make then
local args = {
("--entry %q"):format(entry),
("--output %q"):format(out_path),
};
for dir in iterate(lua_dirs) do
table.insert(args, ("--lua_dir %q"):format(dir));
end
for dir in iterate(c_dirs) do
table.insert(args, ("--c_dir %q"):format(dir));
end
for dir in iterate(assets_dirs) do
table.insert(args, ("--assets_dir %q"):format(dir));
end
local make_out = assert(io.open("Makefile", "w"));
make_out:write(makefile_format:format(out_path, make_exec, table.concat(args, " \\\n\t\t"), out_path));
return;
end
local lib_parts = {};
local extern_parts = {};
local lua_parts = {};
local files = {};
for lua_dir in iterate(lua_dirs) do
for path in recurse(lua_dir, fs.readdir) do
local basename = path:match "(.+)%.lua$";
if basename and fs.is_file(path) then
local module_name = get_module_name(basename, lua_dir);
-- We do the separate bytecode to keep line numbers and file names
local bytecode = string.dump(assert(load(io.lines(path, 4096), "@" .. path, "t")), strip);
table.insert(lua_parts, ("package.preload[%q] = load(%q, %q, 'b');"):format(module_name, bytecode, path));
end
end
end
table.insert(lua_parts, entry_invoke_format:format(entry));
if #assets_dirs > 0 then
table.insert(extern_parts, assets_load_func);
table.insert(lib_parts, asset_init_format);
end
for assets_dir in iterate(assets_dirs) do
for path in recurse(assets_dir, fs.readdir) do
if fs.is_file(path) then
local content = c_escape(assert(io.lines(path, "*a"))());
table.insert(lib_parts, asset_add_format:format(content, #content, path:sub(#assets_dir + 2)));
end
end
end
for c_dir in iterate(c_dirs) do
for path in recurse(c_dir, fs.readdir) do
local basename = path:match "(.+)%.c$";
if basename and fs.is_file(path) then
local module_name = get_module_name(basename, c_dir);
local func_name = "lualib_open_" .. module_name:gsub("%.", "_");
table.insert(extern_parts, ("extern int %s(lua_State *ctx);"):format(func_name));
table.insert(lib_parts, ("luaL_requiref(ctx, %q, %s, false);"):format(module_name, func_name));
table.insert(files, path);
end
end
end
local bytecode = string.dump(assert(load(iterate(lua_parts), "@<entry>", "t")), true);
local file = c_format:format(table.concat(extern_parts, "\n"), c_escape(bytecode), table.concat(lib_parts, "\n\t"));
if dump then
local out_f = assert(io.open(out_path, "w"));
out_f:write(file);
out_f:close();
return;
end
local f = assert(io.popen(table.concat {
"cc -llua -x c - ",
table.concat(files, " "),
" ",
strip and "-g " or "",
"-o ", out_path, " ",
table.concat(cc_args, " ")
}, "w"));
assert(f:write(file));
assert(f:close());
end

77
src/main/mod/c.lua Normal file
View File

@ -0,0 +1,77 @@
local utils = require "utils";
local c = {};
local function c_escape(str)
return "\"" .. str:gsub(".", function (c)
local b = string.byte(c);
if c == "\n" then
return "\\n";
elseif c == "\\" then
return "\\\\";
elseif c == "\"" then
return "\\\"";
elseif b >= 32 and b <= 126 then
return c;
else
return ("\\%.3o"):format(b);
end
end) .. "\"";
end
--- Executes a GCC-like C compiler
--- @param settings { inputs: string[], output: string, flags?: string[], cc?: string }
--- @return task
function c.compile(settings)
local cc = settings.cc or "cc";
local inputs = settings.inputs;
local output = settings.output;
local flags = settings.flags or {};
return function ()
if utils.check_files(inputs, { output }) then return end
local rest = {};
table.move(inputs, 1, #inputs, 1, rest);
table.move(flags, 1, #flags, #rest + 1, rest);
return utils.simple_exec(cc, "-o", output, table.unpack(rest));
end
end
--- Creates a C file, defining the specified constant that will contain a string representation of the input file
--- @param settings { input: string, output: string, const_name: string }
--- @return task
function c.asset(settings)
local input = settings.input;
local output = settings.output;
local const_name = settings.const_name;
return function ()
if utils.check_files({ input }, { output }) then return end
local out_f = assert(io.open(output, "w"));
assert(out_f:write "#pragma once\n");
assert(out_f:write "unsigned char \n");
assert(out_f:write(const_name));
assert(out_f:write " = ");
local has_any = false;
for line in assert(io.lines(input, 128)) do
has_any = true;
assert(out_f:write "\n\t");
assert(out_f:write(c_escape(line)));
end
if not has_any then
assert(out_f:write "\"\"");
end
assert(out_f:write ";\n");
assert(out_f:close());
end
end
return c;

301
src/main/mod/core.lua Normal file
View File

@ -0,0 +1,301 @@
local fs = require "fs";
local core = {};
--- @alias task fun(...)
--- @type { [string]: task }
local tasks = {};
--- Runs a task of the given name
--- @param name string
function core.run(name, ...)
local task = tasks[name];
if not task then
error("No task '" .. name "'");
end
return task(...);
end
--- Creates a task of the given name
--- @param name string
--- @param task task
function core.task(name, task)
tasks[name] = task;
end
--- Converts a file1/file2/**/??/*.txt pattern to a function that can match it
--- % is the escape character, and can escape itself
local function mk_pattern(wildcard)
if type(wildcard) == "function" then
return wildcard;
end
local parts = {};
local prev = nil;
for c in wildcard:gmatch "." do
if prev == "%" then
table.insert(parts, "%" .. c);
elseif prev == "*" then
if c == "*" then
table.insert(parts, "%" .. ".*");
else
table.insert(parts, "%" .. "[^/]*");
table.insert(parts, c);
end
elseif c ~= "*" then
table.insert(parts, c);
end
prev = c;
end
if prev == "*" then
table.insert(parts, "%" .. "[^/]*");
end
local pattern = table.concat(parts);
return function (text)
return string.match(text, pattern) ~= nil;
end;
end
--- Returns an iterator of file paths inside the 'root' dir.
--- If 'pattern' is specified, it is going to be tested for each node in the 'root' dir,
--- BEFORE rerooting
--- If absolute_reroot or relative_reroot are specified, the output's absolute and relative paths
--- will be prefixed with the specified directories, AFTER the pattern check has been performed
--- @param settings string | { root: string, pattern?: string }
--- @return fun(): string?
function core.files(settings)
if type(settings) == "function" then
return settings;
elseif type(settings) == "string" then
return core.files { root = settings };
end
local root = settings.root or error "no root specified";
local pattern = settings.pattern and mk_pattern(settings.pattern);
local recurser, recurse_state = require "recurse" (root);
return function ()
for el in recurser, recurse_state do
local relative = el:sub(#root + 1);
if not pattern or pattern(relative) then
return root .. relative;
end
end
return nil;
end
end
return core;
-- --- @diagnostic disable: cast-local-type
-- --- @alias pattern fun(text: string): boolean
-- local exec = require "exec";
-- local fs = require "fs";
-- local core = {};
-- --- Converts a file1/file2/**/??/*.txt pattern to a function that can match it
-- --- % is the escape character, and can escape itself
-- function core.pattern(wildcard)
-- if type(wildcard) == "function" then
-- return wildcard;
-- end
-- local parts = {};
-- local prev = nil;
-- for c in wildcard:gmatch "." do
-- if prev == "%" then
-- table.insert(parts, "%" .. c);
-- elseif prev == "*" then
-- if c == "*" then
-- table.insert(parts, "%" .. ".*");
-- else
-- table.insert(parts, "%" .. "[^/]*");
-- table.insert(parts, c);
-- end
-- elseif c ~= "*" then
-- table.insert(parts, c);
-- end
-- prev = c;
-- end
-- if prev == "*" then
-- table.insert(parts, "%" .. "[^/]*");
-- end
-- local pattern = table.concat(parts);
-- return function (text)
-- return string.match(text, pattern) ~= nil;
-- end;
-- end
-- function core.combine(arr)
-- local arr_i = 1;
-- local curr = arr[1];
-- return function ()
-- while true do
-- if arr == nil or arr_i > #arr then
-- arr = nil;
-- return nil;
-- end
-- local res = curr();
-- if res ~= nil then return res end
-- arr_i = arr_i + 1;
-- curr = arr[arr_i];
-- end
-- end
-- end
-- --- Creates a generic transformer that independently converts each file to one or more files
-- --- The passed function is called once with a settings object. The function it
-- --- returns will be called upon each transformation
-- function core.create_transformer(func, all)
-- local function finish_call(func, ...)
-- local res = func(...);
-- if type(res) == "string" then
-- res = { res };
-- end
-- if type(res) == "table" then
-- local i = 0;
-- local arr = res;
-- res = function ()
-- if not arr then return nil end
-- i = i + 1;
-- if i > #arr then return nil end
-- return arr[i];
-- end
-- end
-- if type(res) == "function" then
-- return res;
-- else
-- error "Invalid return from transformer function";
-- end
-- end
-- return function (settings)
-- local input = core.files(settings.input);
-- local output = settings.output or error "no output specified";
-- local res_func = func(settings);
-- if all then
-- return function ()
-- if not input then return nil end
-- local a, b = input, output;
-- input, output = nil, nil;
-- return finish_call(res_func, a, b);
-- end
-- else
-- return function ()
-- if not input then return nil end
-- local curr = input();
-- if not curr then
-- input, output = nil, nil;
-- return nil;
-- else
-- return finish_call(res_func, input, output);
-- end
-- end
-- end
-- end
-- end
-- --- A generic cc a.o b.o c.o -o res adapter
-- core.obj = core.create_transformer(function (settings)
-- local cc = settings.cc or "cc";
-- return function (input, output)
-- local rest = {};
-- for el in input do table.insert(rest, el) end
-- table.move(settings.flags, 1, #settings.flags, #rest + 1, rest);
-- simple_exec(cc, "-o", output, table.unpack(rest));
-- end
-- end, true);
-- --- A generic luac adapter
-- core.lua = core.create_transformer(function (settings)
-- local lua = settings.lua or "lua";
-- return function (input, output)
-- simple_exec(lua,
-- input.abs,
-- "-o", output.abs .. "/" .. input.rel .. ".o",
-- table.unpack(settings.flags)
-- );
-- end
-- end);
-- --- A generic luac adapter
-- core.lua_bundle = core.create_transformer(function (settings)
-- local entries = {};
-- if settings.entry then
-- table.insert(entries, settings.entry);
-- end
-- if settings.entries then
-- table.move(settings.entries, 1, #settings.entries, #entries + 1, entries);
-- end
-- if #entries == 0 then
-- error "No entries have been specified";
-- end
-- return function (input, output)
-- local f = assert(io.open(output.abs, "w"));
-- for path in input do
-- local mod_name = get_module_name(path.rel);
-- local bytecode = string.dump(assert(load(io.lines(path.abs, 4096), "@" .. path.abs, "t")), settings.strip);
-- f:write(("package.preload[%q] = load(%q, nil, \"b\");"):format(mod_name, bytecode));
-- end
-- for entry in entries do
-- f:write(("require %q (...);"):format(entry));
-- end
-- end
-- end, true);
-- --- Generates a C file, containing a binary blob
-- core.c_blob = core.create_transformer(function (settings)
-- return function (input, output)
-- local f = assert(io.open(output.abs, "w"));
-- f:write("unsigned char %s[] = ");
-- f:write(c_escape())
-- for path in input do
-- local mod_name = get_module_name(path.rel);
-- local bytecode = string.dump(assert(load(io.lines(path.abs, 4096), "@" .. path.abs, "t")), settings.strip);
-- f:write(("package.preload[%q] = load(%q, nil, \"b\");"):format(mod_name, bytecode));
-- end
-- end
-- end);
-- --- Generates a lua file, containing a binary blob
-- core.lua_blob = core.create_transformer(function (settings)
-- return function (input, output)
-- end
-- end);
-- return core;

6
src/main/mod/glob.lua Normal file
View File

@ -0,0 +1,6 @@
local core = require "mod.core";
return {
run = core.run,
task = core.task,
};

115
src/main/mod/lua.lua Normal file
View File

@ -0,0 +1,115 @@
local utils = require "utils";
local lua = {};
local function get_module_name(path)
return path
-- Remove extension
:gsub("%.[^%.]-$", "")
-- Remove prefix slashes
:gsub("^[/\\]+", "")
-- Replace slashes with dots
:gsub("[/\\]", ".")
-- Remove .init suffix
:gsub("%.init$", "");
end
local function compile(input, output, filename, strip, exe)
filename = filename or input;
if not exe then
local func = assert(load(assert(io.lines(input, 4096)), filename, "t"));
local bytecode = string.dump(func, strip);
local out_f = assert(io.open(output, "w"));
assert(out_f:write(bytecode));
assert(out_f:close());
else
utils.simple_exec(exe, "-e", ([[
local func = assert(load(assert(io.lines(%q, 4096)), %q, "t"));
local bytecode = string.dump(func, %s);
local out_f = assert(io.open(%q, "w"));
assert(out_f:write(bytecode));
assert(out_f:close());
]]):format(input, filename, strip and "true" or "false", output));
end
end
--- Compiles the given lua file to bytecode
--- @param settings { input: string, output: string, filename?: string, strip?: boolean, exe?: string }
--- @return task
function lua.compile(settings)
local input = settings.input;
local output = settings.output;
local filename = "@" .. (settings.filename or settings.input);
local strip = settings.strip or false;
local exe = settings.exe;
return function ()
if utils.check_files({ input }, { output }) then return end
compile(input, output, filename, strip, exe);
end
end
--- Bundles multiple lua files into a single one
--- @param settings { inputs: string[] | (fun(): string?), relative_to?: string, output: string, entry: string }
--- @return task
function lua.bundle(settings)
local inputs = utils.to_array(settings.inputs);
local output = settings.output;
local entry = settings.entry;
local relative_to = settings.relative_to or "";
return function ()
local out_f = assert(io.open(output, "w"));
for i = 1, #inputs do
local path = inputs[i]:sub(#relative_to);
local name = get_module_name(path);
local in_f = assert(io.open(inputs[i], "r"));
local data = assert(in_f:read "*a");
in_f:close();
assert(out_f:write(("package.preload[%q] = load(%q, \"bt\", %q);\n"):format(name, data, path)));
end
assert(out_f:write(("require %q (...);\n"):format(entry)));
out_f:close();
end
end
--- Generates a single C file that exposes the given C libraries to lua
--- @param settings { inputs: string[] | (fun(): string?), relative_to?: string, output: string, func_name?: string }
--- @return task
function lua.c_libs(settings)
local inputs = utils.to_array(settings.inputs);
local output = settings.output;
local func_name = settings.func_name or "mklua_init_libs";
local relative_to = settings.relative_to or "";
return function ()
local out_f = assert(io.open(output, "w"));
assert(out_f:write("#include <lua.h>\n"));
assert(out_f:write("#include <lualib.h>\n"));
assert(out_f:write("#include <lauxlib.h>\n"));
assert(out_f:write("\n"));
for i = 1, #inputs do
local path = inputs[i]:sub(#relative_to);
local name = get_module_name(path);
local in_f = assert(io.open(inputs[i], "r"));
local data = assert(in_f:read "*a");
in_f:close();
assert(out_f:write(("package.preload[%q] = load(%q, \"bt\", %q);\n"):format(name, data, path)));
end
assert(out_f:write(("require %q (...);\n"):format("")));
out_f:close();
end
end
return lua;

38
src/main/recurse.lua Normal file
View File

@ -0,0 +1,38 @@
--- @class recurse_state
--- @field stack ({ prefix: string, data: any, next: fun(): string? } | string)[]
--- @field readdir fun(dir: string): (fun(state: any): string?)
--- @param state recurse_state
--- @return string?
local function recurser(state)
if not state then
return nil;
end
while true do
local top = table.remove(state.stack);
if not top then
return nil;
elseif type(top) == "string" then
local res = state.readdir(top);
if res then
table.insert(state.stack, { next = res, prefix = top .. "/" });
end
return top;
elseif type(top) == "table" then
local res = top.next(top.data);
if res then
table.insert(state.stack, top.prefix .. res);
table.insert(state.stack, top);
end
else
error "invalid stack state";
end
end
end
return function (dir, readdir)
return recurser, { stack = { dir }, readdir = readdir or require "fs".readdir };
end

110
src/main/utils.lua Normal file
View File

@ -0,0 +1,110 @@
local exec = require "exec";
local fs = require "fs";
local utils = {};
utils.exec = exec;
function utils.simple_exec(program, ...)
local child = assert(exec { program, ... });
child.stdin:close(io.stdin);
child.stdout:close(io.stdout);
child.stderr:close(io.stderr);
local code = assert(child.reap());
if code ~= 0 then
error(("'%s' failed with exit code %d"):format(program, code));
end
end
function utils.ensure_dir(path)
local dirname = path:match "^(.+)/[^/]-$";
if dirname then
assert(fs.mkdir(dirname, true));
end
end
--- @generic T
--- @param val T[] | (fun(): T?)
function utils.iterate(val)
if type(val) == "function" then
return val;
else
local i = 0;
return function ()
if not val then
return nil;
end
i = i + 1;
if i > #val then
--- @diagnostic disable-next-line: cast-local-type
val = nil;
return nil;
end
return val[i];
end
end
end
--- @generic T
--- @param val T[] | (fun(): T?)
--- @return T[]
function utils.to_array(val)
local res = {};
for el in utils.iterate(val) do
table.insert(res, el);
end
return res;
end
--- Checks if all the output files are present, and if input files are
--- specified, whether or not they are older than all the output files.
---
--- Returns true if all checks hold
---
--- This function can be used to prevent redundant builds
---
--- @param outputs string[] | (fun(): string?)
--- @param inputs? string[] | (fun(): string?)
--- @return boolean
function utils.check_files(inputs, outputs)
local in_n = 0;
local out_n = 0;
local in_mtime, out_mtime;
local out_missing = false;
if inputs then
for el in utils.iterate(inputs) do
local stat = fs.lstat(el);
if not stat then error("Input '" .. el "' not present") end
in_n = in_n + 1;
if not in_mtime or in_mtime < stat.mtime then
in_mtime = stat.mtime;
end
end
end
for el in utils.iterate(outputs) do
out_n = out_n + 1;
local stat = fs.lstat(el);
if not stat then
out_missing = true;
elseif not out_mtime or out_mtime > stat.mtime then
out_mtime = stat.mtime;
end
end
if out_missing then
return false;
elseif in_mtime and out_mtime then
return in_mtime < out_mtime;
else
return true;
end
end
return utils;