├── .gitignore ├── LICENSE.txt ├── README.md ├── apt.txt ├── examples └── welcome_to_lua.ipynb ├── lupyter ├── __init__.py ├── kernel.py ├── lua_runtime │ └── lua_runtime.c └── parse_docs.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tom Stitt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Lua Kernel for Jupyter 2 | 3 | Lupyter is a Lua kernel built on [ipykernel](https://github.com/ipython/ipykernel). 4 | Tab-completion is available but documentation lookup is still a work in progress. 5 | 6 | ## Installing 7 | 8 | In most cases it's enough to `pip install` with `LUA_DIR` pointing 9 | to your Lua installation but if Lua is installed 10 | in your `PATH` you may not need `LUA_DIR`. You can always add 11 | `-v` after `pip install` if your Lua install isn't found and you 12 | want to see a list of the directories searched. 13 | 14 | ``` 15 | [LUA_DIR=/path/to/your/lua_root] pip install . 16 | ``` 17 | 18 | If your Python is in a weird place the kernelspec might not be 19 | visible to Jupyter. 20 | 21 | You can confirm that the kernelspec was install correctly with: 22 | 23 | ``` 24 | jupyter kernelspec list 25 | ``` 26 | 27 | If the kernelspec isn't in the list check which paths Jupyter can see 28 | and make sure your kernelspec was installed to one of them: 29 | 30 | ``` 31 | jupyter --paths 32 | ``` 33 | 34 | You can manually install to one of those paths with: 35 | 36 | ``` 37 | jupyter kernelspec install data_kernelspec --name lupyter --prefix ... 38 | ``` 39 | -------------------------------------------------------------------------------- /apt.txt: -------------------------------------------------------------------------------- 1 | lua5.3 2 | -------------------------------------------------------------------------------- /examples/welcome_to_lua.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "challenging-massage", 6 | "metadata": {}, 7 | "source": [ 8 | "# Welcome to Lua!\n", 9 | "\n", 10 | "Let's start off with the version you're using:" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "religious-printer", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "print(\"Hello World from \" .. _VERSION .. \"!\")" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "focused-seventh", 26 | "metadata": {}, 27 | "source": [ 28 | "As you saw above strings are concatenated with `..`.\n", 29 | "\n", 30 | "The `_G` `table` is the global table, similar to `globals()` in Python.\n", 31 | "You can print its contents with a `for` loop and the `pairs` function, which gives you a `(key, value)` iterator, similar to `dict.items()` in Python.\n", 32 | "\n", 33 | "Let's try it:" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "early-murder", 40 | "metadata": { 41 | "scrolled": true 42 | }, 43 | "outputs": [], 44 | "source": [ 45 | "for k,v in pairs(_G) do\n", 46 | " print(k .. \": \" .. type(v))\n", 47 | "end" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "id": "configured-peace", 53 | "metadata": {}, 54 | "source": [ 55 | "For more info see [Learn X in Y minutes Lua guide](https://learnxinyminutes.com/docs/lua/). Or help out and add more to this Notebook =]" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "id": "understood-merchant", 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [] 65 | } 66 | ], 67 | "metadata": { 68 | "kernelspec": { 69 | "display_name": "Lua 5.1.5", 70 | "language": "lua", 71 | "name": "lupyter" 72 | }, 73 | "language_info": { 74 | "file_extension": ".lua", 75 | "mimetype": "text/x-lua", 76 | "name": "lua", 77 | "version": "5.1.5" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 5 82 | } 83 | -------------------------------------------------------------------------------- /lupyter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomstitt/lupyter/4762b2994ddcd9997ca5d1a017b50f318eba33f5/lupyter/__init__.py -------------------------------------------------------------------------------- /lupyter/kernel.py: -------------------------------------------------------------------------------- 1 | from ipykernel.kernelbase import Kernel 2 | from .lua_runtime import LuaRuntime 3 | 4 | try: 5 | from .versions import lua_version 6 | except ImportError: 7 | lua_version = "5.1.0" 8 | 9 | class LuaKernel(Kernel): 10 | implementation = "iPython" 11 | implementation_version = "1" 12 | banner = "Lua" 13 | language_info = { 14 | "name": "lua", 15 | "file_extension": ".lua", 16 | "version": lua_version, 17 | "mimetype": "text/x-lua" 18 | } 19 | 20 | def stdout(self, text): 21 | self.send_response(self.iopub_socket, "stream", { 22 | "name": "stdout", 23 | "text": text}) 24 | 25 | def __init__(self, *args, **kwargs): 26 | super().__init__(*args, **kwargs) 27 | self.lua = LuaRuntime(lambda text: self.stdout(text)) 28 | 29 | def do_execute(self, code, silent, store_history=True, user_expression=None, allow_stdin=False): 30 | # TODO: doc lookup 31 | if code.startswith("?") or code.endswith("?"): 32 | pass 33 | else: 34 | self.lua.eval(code) 35 | 36 | return { 37 | "status": "ok", 38 | "execution_count": self.execution_count, 39 | "payload": [], 40 | "user_expression": {} 41 | } 42 | 43 | def do_complete(self, code, cursor_pos): 44 | matches, cursor_start = self.lua.complete(code, cursor_pos) 45 | return { 46 | "status": "ok", 47 | "matches": matches, 48 | "cursor_start": cursor_start, 49 | "cursor_end": cursor_pos, 50 | "metadata": None 51 | } 52 | 53 | 54 | if __name__ == "__main__": 55 | from ipykernel.kernelapp import IPKernelApp 56 | IPKernelApp.launch_instance(kernel_class=LuaKernel) 57 | -------------------------------------------------------------------------------- /lupyter/lua_runtime/lua_runtime.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | // Handle Python version 13 | // PyMODINIT returns a PyObject * in py3 but nothing 14 | // in py2, PyMODINIT_RET handles that 15 | #if PY_MAJOR_VERSION == 3 16 | #define PY3 17 | #define PyMODINIT_RET(x) x 18 | 19 | #elif PY_MAJOR_VERSION == 2 20 | #define PY2 21 | #define PyMODINIT_RET(x) 22 | #define PyString_FromString PyUnicode_FromString 23 | // https://docs.python.org/2.7/extending/newtypes.html 24 | // Note that PyMODINIT_FUNC declares the function as void return type, declares 25 | // any special linkage declarations required by the platform, and for C++ declares 26 | // the function as extern "C". 27 | #ifndef PyMODINIT_FUNC 28 | #define PyMODINIT_FUNC void 29 | #endif 30 | 31 | #endif 32 | 33 | // Handle Lua version 34 | #if LUA_VERSION_NUM < 501 35 | #error "Please use a newer version of Lua =]" 36 | 37 | #elif LUA_VERSION_NUM < 502 38 | void lua_pushglobaltable(lua_State * L) { lua_pushvalue(L, LUA_GLOBALSINDEX); } 39 | 40 | #endif 41 | 42 | 43 | const char * reserved_words[] = { 44 | "and", "break", "do", "else", "elseif", "end", 45 | "false", "for", "function", "if", "in", 46 | "local", "nil", "not", "or", "repeat", "return", 47 | "then", "true", "until", "while" 48 | #if LUA_VERSION_NUM >= 502 49 | , "goto" 50 | #endif 51 | }; 52 | 53 | 54 | int is_identifier(char c) { 55 | return isalpha(c) || isnumber(c) || c == '_'; 56 | } 57 | 58 | 59 | int get_metaindex(lua_State * L) { 60 | if (luaL_getmetafield(L, -1, "__index") == LUA_TNIL /* 0 in Lua51 */) { 61 | lua_pop(L, 1); 62 | return 0; 63 | } 64 | 65 | if (lua_type(L, -1) != LUA_TTABLE) { 66 | lua_pop(L, 2); 67 | return 0; 68 | } 69 | 70 | return 1; 71 | } 72 | 73 | 74 | int get_path(lua_State * L, const char * path, int path_length) { 75 | int offset = 0; 76 | char op = '.'; 77 | lua_pushglobaltable(L); 78 | for (int i = 0; i < path_length; ++i) { 79 | if (path[i] == ':' || path[i] == '.') { 80 | if (lua_type(L, -1) != LUA_TTABLE && get_metaindex(L) == 0) { 81 | return 0; 82 | } 83 | lua_pushlstring(L, path+offset, i-offset); 84 | lua_gettable(L, -2); 85 | lua_replace(L, -2); 86 | op = path[i]; 87 | offset = i+1; 88 | } 89 | } 90 | 91 | if (op == ':' && get_metaindex(L) == 0) { 92 | return 0; 93 | } 94 | 95 | if (lua_type(L, -1) != LUA_TTABLE) { 96 | lua_pop(L, 1); 97 | return 0; 98 | } 99 | 100 | return 1; 101 | } 102 | 103 | 104 | int table_matches(lua_State * L, int table_index, const char * identifier, int identifier_length, PyObject * py_matches) { 105 | int match_count = 0; 106 | lua_pushnil(L); 107 | // fix offset after pushing 108 | table_index = table_index < 0 ? table_index-1 : table_index; 109 | 110 | while (lua_next(L, table_index)) { 111 | if (lua_type(L, -2) == LUA_TSTRING) { 112 | const char * key = lua_tostring(L, -2); 113 | if (strncmp(identifier, key, identifier_length) == 0) { 114 | PyList_Append(py_matches, PyUnicode_FromString(key)); 115 | match_count++; 116 | } 117 | } 118 | lua_pop(L, 1); 119 | } 120 | 121 | return match_count; 122 | } 123 | 124 | 125 | // TODO: this misses tables with __index metafields and indexes using [] 126 | int complete(lua_State * L, const char * code, int cursor_pos, PyObject * py_matches) { 127 | int dot_loc = -1; 128 | int i; 129 | 130 | // cursor is one to the right of the starting char for completion 131 | cursor_pos -= 1; 132 | for (i = cursor_pos; i >= 0; --i) { 133 | if (code[i] == '.') { 134 | // check for string concat 135 | if (i > 0 && code[i-1] == '.') { break; } 136 | if (dot_loc == -1) { dot_loc = i; } 137 | } 138 | else if (code[i] == ':') { 139 | // check for '::', only in >= 5.2 140 | #if LUA_VERSION_NUM >= 502 141 | if (i > 0 && code[i-1] == ':') { break; } 142 | #endif 143 | if (dot_loc == -1) { dot_loc = i; } 144 | // invalid to have a colon after finding a dot/colon 145 | else { return 0; } 146 | } 147 | else if (!is_identifier(code[i])) { 148 | break; 149 | } 150 | } 151 | 152 | // break char is to the left of the start of the identifier 153 | int cursor_start = i+1; 154 | 155 | // don't try to match numbers 156 | if (isnumber(code[cursor_start])) { return 0; } 157 | 158 | int match_count = 0; 159 | if (dot_loc > 0) { 160 | const char * identifier = code+dot_loc+1; 161 | const char * path = code+cursor_start; 162 | int identifier_length = cursor_pos-dot_loc; 163 | int path_length = dot_loc-cursor_start+1; 164 | if (get_path(L, path, path_length) == 0) { 165 | return 0; 166 | } 167 | match_count = table_matches(L, -1, identifier, identifier_length, py_matches); 168 | // cursor_start for fields is just the start of the "basename" 169 | cursor_start = identifier-code; 170 | } 171 | 172 | else { 173 | int identifier_length = cursor_pos-cursor_start+1; 174 | const char * identifier = code+cursor_start; 175 | 176 | // check for global matches 177 | lua_pushglobaltable(L); 178 | match_count = table_matches(L, -1, identifier, identifier_length, py_matches); 179 | 180 | // check for reserved word match 181 | for (unsigned i = 0; i < sizeof(reserved_words)/sizeof(reserved_words[0]); ++i) { 182 | if (strncmp(identifier, reserved_words[i], identifier_length) == 0) { 183 | match_count++; 184 | PyList_Append(py_matches, PyUnicode_FromString(reserved_words[i])); 185 | } 186 | } 187 | } 188 | return cursor_start; 189 | } 190 | 191 | 192 | static void py_print(PyObject * py_print_cbk, const char * str) { 193 | PyObject * args = Py_BuildValue("(s)", str); 194 | // TODO: depricated 195 | PyObject * result = PyEval_CallObject(py_print_cbk, args); 196 | Py_DECREF(args); 197 | Py_DECREF(result); 198 | } 199 | 200 | 201 | void eval_code(lua_State * L, const char * code) { 202 | lua_pushfstring(L, "print(%s)", code); 203 | if (luaL_dostring(L, lua_tostring(L, -1))) { 204 | if (luaL_dostring(L, code)) { 205 | lua_getglobal(L, "print"); 206 | lua_pushvalue(L, -2); 207 | lua_call(L, 1, 0); 208 | } 209 | } 210 | lua_settop(L, 0); 211 | } 212 | 213 | 214 | int print_cb(lua_State * L, PyObject * py_cbk) { 215 | lua_getglobal(L, "tostring"); 216 | for (int i = 1; i <= lua_gettop(L)-1; ++i) { 217 | lua_pushvalue(L, -1); 218 | lua_pushvalue(L, i); 219 | lua_call(L, 1, 1); 220 | const char * tostr = lua_tostring(L, -1); 221 | if (tostr == NULL) { 222 | return luaL_error(L, "'tostring' returned NULL value\n"); 223 | } 224 | if (i > 1) { py_print(py_cbk, "\t"); } 225 | py_print(py_cbk, tostr); 226 | lua_pop(L, 1); 227 | } 228 | py_print(py_cbk, "\n"); 229 | return 0; 230 | } 231 | 232 | 233 | int py_print_lua_cb(lua_State * L) { 234 | PyObject * py_cbk = lua_touserdata(L, lua_upvalueindex(1)); 235 | return print_cb(L, py_cbk); 236 | } 237 | 238 | 239 | typedef struct { 240 | PyObject_HEAD // ';' isn't need, it can cause compiler issues 241 | lua_State * L; 242 | PyObject * py_print_cbk; 243 | } PyLuaRuntime; 244 | 245 | 246 | static PyObject * lua_new(PyTypeObject * type, PyObject * args, PyObject * kwargs) { 247 | PyLuaRuntime * self = (PyLuaRuntime *)type->tp_alloc(type, 0); 248 | if (self != NULL) { 249 | self->L = NULL; 250 | self->py_print_cbk = NULL; 251 | } 252 | 253 | return (PyObject *)self; 254 | } 255 | 256 | 257 | static int lua_init(PyLuaRuntime * self, PyObject * args, PyObject * kwargs) { 258 | PyObject * cbk = NULL; 259 | if (!PyArg_ParseTuple(args, "|O", &cbk)) { 260 | return -1; 261 | } 262 | 263 | if (cbk && !PyCallable_Check(cbk)) { 264 | PyErr_SetString(PyExc_TypeError, "expected callable"); 265 | return -1; 266 | } 267 | 268 | // handle multiple init calls 269 | if (self->L) { 270 | lua_close(self->L); 271 | self->L = NULL; 272 | } 273 | if (self->py_print_cbk) { 274 | Py_XDECREF(self->py_print_cbk); 275 | self->py_print_cbk = NULL; 276 | } 277 | 278 | self->L = luaL_newstate(); 279 | luaL_openlibs(self->L); 280 | // if the callback was given, replace 'print' 281 | // TODO: stdout and stderr as well 282 | if (cbk) { 283 | Py_INCREF(cbk); 284 | lua_pushlightuserdata(self->L, cbk); 285 | lua_pushcclosure(self->L, py_print_lua_cb, 1); 286 | lua_setglobal(self->L, "print"); 287 | } 288 | self->py_print_cbk = cbk; 289 | 290 | return 0; 291 | } 292 | 293 | 294 | static void lua_dealloc(PyLuaRuntime * self) { 295 | if (self->L) { lua_close(self->L); } 296 | Py_XDECREF(self->py_print_cbk); 297 | Py_TYPE(self)->tp_free((PyObject *)self); 298 | } 299 | 300 | 301 | static PyObject * lua_eval(PyLuaRuntime * self, PyObject * args) { 302 | const char * code; 303 | if (!PyArg_ParseTuple(args, "s", &code)) { 304 | return NULL; 305 | } 306 | eval_code(self->L, code); 307 | 308 | return Py_None; 309 | } 310 | 311 | 312 | static PyObject * lua_complete(PyLuaRuntime * self, PyObject * args) { 313 | const char * code; 314 | int cursor_pos = -1; 315 | if (!PyArg_ParseTuple(args, "s|i", &code, &cursor_pos)) { 316 | return NULL; 317 | } 318 | 319 | PyObject * py_matches = PyList_New(0); 320 | if (cursor_pos == -1) { cursor_pos = strlen(code); } 321 | int cursor_start = complete(self->L, code, cursor_pos, py_matches); 322 | PyObject * res = PyTuple_New(2); 323 | PyTuple_SetItem(res, 0, py_matches); 324 | PyTuple_SetItem(res, 1, PyLong_FromLong(cursor_start)); 325 | 326 | return res; 327 | } 328 | 329 | 330 | static PyMethodDef pylua_methods[] = { 331 | {"eval", (PyCFunction)lua_eval, METH_VARARGS, 332 | "$.eval(code) -> None\n\nEvaluate the Lua string."}, 333 | {"complete", (PyCFunction)lua_complete, METH_VARARGS, 334 | "$.complete(code[, cursor_pos]) -> (list, int)\n\n" 335 | "Returns a list of possible matches and a starting cursor position."}, 336 | {NULL} 337 | }; 338 | 339 | 340 | static PyTypeObject PyLuaRuntimeType = { 341 | PyVarObject_HEAD_INIT(NULL, 0) 342 | .tp_name = "lupyter.lua_runime.LuaRuntime", 343 | .tp_doc = "(print_callback: (str) -> None) -> new LuaRuntime", 344 | .tp_basicsize = sizeof(PyLuaRuntime), 345 | .tp_flags = Py_TPFLAGS_DEFAULT, 346 | .tp_new = lua_new, 347 | .tp_init = (initproc)lua_init, 348 | .tp_dealloc = (destructor)lua_dealloc, 349 | .tp_methods = pylua_methods, 350 | }; 351 | 352 | 353 | #define MOD_NAME "lupyter.lua_runtime" 354 | #define MOD_DOC "Lua runtime wrapper for executing and completing Lua blobs" 355 | #ifdef PY3 356 | static PyModuleDef lup_module = { 357 | PyModuleDef_HEAD_INIT, 358 | .m_name = MOD_NAME, 359 | .m_doc = MOD_DOC, 360 | .m_size = -1, 361 | }; 362 | #endif 363 | 364 | 365 | PyMODINIT_FUNC 366 | #if defined (PY3) 367 | PyInit_lua_runtime() 368 | #elif defined (PY2) 369 | initlua_runtime() 370 | #endif 371 | { 372 | if (PyType_Ready(&PyLuaRuntimeType) < 0) { return PyMODINIT_RET(NULL); } 373 | 374 | #if defined (PY3) 375 | PyObject * m = PyModule_Create(&lup_module); 376 | #elif defined (PY2) 377 | PyObject * m = Py_InitModule3(MOD_NAME, NULL, MOD_DOC); 378 | #endif 379 | if (m == NULL) { return PyMODINIT_RET(NULL); } 380 | 381 | Py_INCREF(&PyLuaRuntimeType); 382 | PyModule_AddObject(m, "LuaRuntime", (PyObject *)&PyLuaRuntimeType); 383 | PyMODINIT_RET(return m;) 384 | } 385 | -------------------------------------------------------------------------------- /lupyter/parse_docs.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | import json 4 | 5 | doc_url = "http://www.lua.org/manual/{}/manual.html" 6 | 7 | def parse_from_url(version): 8 | req = requests.get(doc_url.format(version)) 9 | lines = req.text.split('\n') 10 | docs = parse_docs(lines) 11 | 12 | 13 | def parse_from_file(filename): 14 | with open(filename) as f: 15 | lines = f.readlines() 16 | docs = parse_docs(lines) 17 | 18 | 19 | def parse_docs(lines, verbose=False, json_path=None): 20 | pattern = re.compile('.*(

.+

)') 21 | 22 | docs = {} 23 | for i in range(len(lines)): 24 | line = lines[i] 25 | m = pattern.match(line) 26 | if m: 27 | grps = m.groups() 28 | body = "" 29 | for j in range(i+1, len(lines)): 30 | line = lines[j] 31 | if not line.startswith('
'): 32 | body += line 33 | else: 34 | i -= 1 35 | break 36 | docs[grps[1]] = { 37 | "heading": grps[0], 38 | "body": re.sub("(\n)+", "\n", body.strip()) 39 | } 40 | 41 | if verbose: 42 | print("parsed:") 43 | print(" " + "\n ".join(docs.keys())) 44 | 45 | if json_path: 46 | with open(json_path, 'w') as f: 47 | json.dump(docs, f, indent=2) 48 | 49 | return docs 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import os 3 | import sys 4 | import fnmatch 5 | import re 6 | import platform 7 | 8 | distname = "lupyter" 9 | description = "A Lua Jupyter Kernel using ipykernel" 10 | 11 | here = os.path.abspath(os.path.dirname(__file__)) 12 | interface_name = "lua_runtime" 13 | 14 | 15 | def get_lua_release(path): 16 | with open(path) as f: 17 | version = re.findall(r"#define LUA_RELEASE.*\"Lua ([0-9\. ]+)", f.read())[0] 18 | return version 19 | 20 | 21 | def get_kernel_name(name): 22 | return name.lower().replace(" ", "_").replace(".", "_") 23 | 24 | 25 | def find_lua(root): 26 | include_dir = os.path.join(root, "include") 27 | lib_dir = os.path.join(root, "lib") 28 | if not os.path.exists(os.path.join(include_dir, "lua.h")): 29 | return None 30 | if not os.path.exists(os.path.join(lib_dir, "liblua.a")): 31 | return None 32 | version = get_lua_release(os.path.join(include_dir, "lua.h")) 33 | name = "Lua " + version 34 | return {"display_name": name, 35 | "kernel_name": get_kernel_name(name), 36 | "version": version, 37 | "include_dir": include_dir, 38 | "lib_dir": lib_dir, 39 | "lib": "lua"} 40 | 41 | 42 | def find_luajit(root): 43 | include_root = os.path.join(root, "include") 44 | if not os.path.exists(include_root): 45 | return None 46 | for f in os.listdir(include_root): 47 | if fnmatch.fnmatch(f, "luajit*"): 48 | include_dir = os.path.join(include_root, f) 49 | if os.path.isdir(include_dir): 50 | if os.path.exists(os.path.join(include_dir, "lua.h")): 51 | break 52 | else: 53 | return None 54 | 55 | libdir = os.path.join(root, "lib") 56 | if not os.path.exists(libdir): 57 | return None 58 | for f in os.listdir(libdir): 59 | if fnmatch.fnmatch(f, "libluajit*.a"): 60 | lib = f[3:-2] 61 | break 62 | else: 63 | return None 64 | 65 | lua_version = get_lua_release(os.path.join(include_dir, "lua.h")) 66 | name = "LuaJIT" 67 | with open(os.path.join(include_dir, "luajit.h")) as f: 68 | luajit_version = re.findall(r"#define LUAJIT_VERSION.*\"LuaJIT ([0-9\. ]+)", f.read())[0] 69 | name = "LuaJIT " + luajit_version 70 | 71 | return {"display_name": name, 72 | "kernel_name": get_kernel_name(name), 73 | "version": lua_version, 74 | "luajit_version": luajit_version, 75 | "include_dir": include_dir, 76 | "lib_dir": lib_dir, 77 | "lib": lib} 78 | 79 | if "LUA_DIR" in os.environ: 80 | lua_dir = os.environ["LUA_DIR"] 81 | if not os.path.exists(lua_dir): 82 | sys.exit(f"fatal: {LUA_DIR} is not a valid path") 83 | info = find_lua(lua_dir) or find_luajit(lua_dir) 84 | else: 85 | paths = os.environ["PATH"] 86 | paths_tried = set() 87 | for p in paths.split(":"): 88 | p = "/".join(p.split("/")[:-1]) 89 | if p == "": p = "/" 90 | if p in paths_tried: continue 91 | paths_tried.add(p) 92 | print(f"looking for existing lua installation in {p}... ", end="") 93 | info = find_lua(p) or find_luajit(p) 94 | if info is not None: 95 | print("found!") 96 | break 97 | print("unsuccessful") 98 | 99 | if info is None: 100 | sys.exit(f"fatal: unable to find Lua please set LUA_DIR to a valid install root") 101 | else: 102 | print("found lua:") 103 | for k,v in info.items(): 104 | print(f" {k}: {v}") 105 | 106 | with open(os.path.join(distname, "versions.py"), "w") as f: 107 | fstr = f'lua_version = "{info["version"]}"' 108 | if "luajit_version" in info: 109 | fstr += f'\nluajit_version = "{info["luajit_version"]}"' 110 | f.write(fstr) 111 | 112 | if platform.system() == "Windows": 113 | ext_args = dict(libraries=[info["lib"]], 114 | library_dirs=[info["lib_dir"]]) 115 | else: 116 | ext_args = dict(extra_objects=[os.path.join(info["lib_dir"], f"lib{info['lib']}.a")]) 117 | 118 | ext_mod = setuptools.Extension(f"{distname}.{interface_name}", 119 | sources=[f"{distname}/{interface_name}/lua_runtime.c"], 120 | include_dirs=[info["include_dir"]], 121 | **ext_args) 122 | 123 | setup_args = dict(name=distname, 124 | description=description, 125 | url="https://github.com/tomstitt/lupyter", 126 | license="MIT", 127 | version="1.0.0", 128 | packages=setuptools.find_packages(), 129 | ext_modules=[ext_mod], 130 | install_requires=["ipykernel", "jupyter_core"]) 131 | 132 | # Inspired by https://github.com/ipython/ipykernel/blob/master/setup.py 133 | if any(arg.startswith(("bdist", "install")) for arg in sys.argv): 134 | from ipykernel.kernelspec import write_kernel_spec 135 | from jupyter_core.paths import jupyter_path 136 | from glob import glob 137 | import shutil 138 | 139 | kernelspec_dir = "data_kernelspec" 140 | dest = os.path.join(here, kernelspec_dir) 141 | if os.path.exists(dest): 142 | shutil.rmtree(dest) 143 | 144 | argv = [sys.executable, "-m", f"{distname}.kernel", "-f", "{connection_file}"] 145 | write_kernel_spec(dest, overrides={ 146 | "argv": argv, 147 | "display_name": info["display_name"], 148 | "language": "lua" 149 | }) 150 | 151 | # this doesn't even seem to do the right thing, the kernel isn't even always 152 | # installed relative to sys.prefix; for example, homebrew in /opt/homebrew 153 | possible_kernel_dirs = jupyter_path() 154 | search_dir = os.path.join(sys.prefix, "share", "jupyter") 155 | if search_dir not in possible_kernel_dirs: 156 | print(f"The target kernel directory ({search_dir}) will not be picked up by Jupyter, " 157 | f"please manually install {kernelspec_dir} with: ") 158 | print(f"jupyter-kernelspec install {kernelspec_dir} --sys-prefix --name {info['kernel_name']}") 159 | else: 160 | setup_args["data_files"] = [ 161 | ( 162 | # could use info["kernel_name"] for many kernels if Python package 163 | # name also changed 164 | os.path.join("share", "jupyter", "kernels", distname), 165 | glob(os.path.join(kernelspec_dir, "*")) 166 | ) 167 | ] 168 | print(setup_args["data_files"]) 169 | 170 | setuptools.setup(**setup_args) 171 | --------------------------------------------------------------------------------