From 8ce76353963a58e5ca0758f240ccb62056126ceb Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 14 Oct 2022 15:21:34 -0400 Subject: Reorganized things. --- README.md | 51 +- SPEC.md | 209 ++++++++ build.sh | 6 +- lpm.c | 575 --------------------- lpm.lua | 1612 ----------------------------------------------------------- src/lpm.c | 575 +++++++++++++++++++++ src/lpm.lua | 1612 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 2431 insertions(+), 2209 deletions(-) create mode 100644 SPEC.md delete mode 100644 lpm.c delete mode 100644 lpm.lua create mode 100644 src/lpm.c create mode 100644 src/lpm.lua diff --git a/README.md b/README.md index 7373727..3a8d9e5 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,40 @@ # Lite XL Plugin Manager -My attempt at a lite-xl plugin manager. A standalone binary that provides an easy way of installing, and uninstalling plugins from lite-xl. +A standalone binary that provides an easy way of installing, and uninstalling +plugins from lite-xl, as well as different version fo lite-xl. -Can be used by a package manager plugin that works from inside the editor and calls this. +Can be used by a package manager plugin that works from inside the editor +and calls this binary. Releases forthcoming, should be available on Windows, Mac, Linux and FreeBSD. -Also contains a plugin_manager.lua plugin to integrate the binary with lite in the form of an easy-to-use GUI. +Also contains a plugin_manager.lua plugin to integrate the binary with lite in +the form of an easy-to-use GUI. + +By default, `lpm` will automatically consume the specification in the `latest` +branch of this repository. + +## Specification + +For details about the `manifest.json` files that `lpm` consumes, +[see here](SPEC.md). ## Quickstart -If you have a C compiler, and `git`, and want to compile from scratch, you can do: +If you have a C compiler, and `git`, and want to compile from scratch, +you can do: ``` -git clone git@github.com:adamharrison/lite-xl-plugin-manager.git --shallow-submodules \ - --recurse-submodules && cd lite-xl-plugin-manager && ./build.sh && ./lpm +git clone git@github.com:adamharrison/lite-xl-plugin-manager.git \ + --shallow-submodules --recurse-submodules && cd lite-xl-plugin-manager &&\ + ./build.sh && ./lpm ```` -If you want to build it quickly, and have the right modules installed, you can do: +If you want to build it quickly, and have the right modules installed, you can +do: ``` -./build.sh -lz -lssl -lgit2 -lcurl -lcrypto -llua +./build.sh -larchive -llzma -lz -lssl -lgit2 -lz -lcurl -lcrypto -llua ``` CI is enabled on this repository, so you can grab Windows and Linux builds from the @@ -30,9 +44,11 @@ You can get a feel for how to use `lpm` by typing `./lpm --help`. ## Supporting Libraries -Unlike lite, due to the precense of the beast of a library that is OpenSSL, I've made no attempt to limit the amount -of libraries being linked in here, I'm only ensuring that everything can be linked statically as much as possible. As -seen with the `lib` folder, the following external libraries are used to build `lpm`: +Unlike lite, due to the precense of the beast of a library that is OpenSSL, +I've made no attempt to limit the amount of libraries being linked in here, +I'm only ensuring that everything can be linked statically as much as possible. +As seen with the `lib` folder, the following external libraries are used to +build `lpm`: * lua (core program written in) * OpenSSL (https/SSL support) @@ -47,24 +63,20 @@ seen with the `lib` folder, the following external libraries are used to build ` To make pre-fab lite builds, you can easily use `lpm` in CI. If you had a linux build container, you could do something like: ```sh - curl https://github.com/adamharrison/lite-xl-plugin-manager/releases/download/v0.1/lpm.x86_64-linux > lpm export LITE_USERDIR=lite-xl/data && export LPM_CACHE=/tmp/cache ./lpm add https://github.com/adamharrison/lite-xl-plugin-manager && ./lpm install plugin_manager lsp - ``` ## Usage ```sh - -lpm update && lpm install aligncarets +lpm install aligncarets lpm uninstall aligncarets +``` -lpm add https://github.com/adamharrison/lite-xl-plugin-manager.git -lpm install plugin_manager -lpm rm https://github.com/adamharrison/lite-xl-plugin-manager.git - +```sh +lpm --help ``` ## Building @@ -80,3 +92,4 @@ lpm rm https://github.com/adamharrison/lite-xl-plugin-manager.git ``` CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-gcc-ar WINDRES=x86_64-w64-mingw32-windres LZMA_CONFIGURE="--host=x86_64-w64-mingw32" ARCHIVE_CONFIGURE="-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_SYSTEM_NAME=Windows" CURL_CONFIGURE="-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_SYSTEM_NAME=Windows" GIT2_CONFIGURE="-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DBUILD_CLAR=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_SYSTEM_NAME=Windows -DDLLTOOL=x86_64-w64-mingw32-dlltool" SSL_CONFIGURE=mingw ./build.sh -DLPM_VERSION='"'$VERSION-x86_64-windows-`git rev-parse --short HEAD`'"' ``` + diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..e48991d --- /dev/null +++ b/SPEC.md @@ -0,0 +1,209 @@ +# Manifest Specification + +A lite-xl manifest is a JSON file contains three different keys: + +* Remotes +* Plugins +* Lite-XLs + +## Remotes + +A simple array of string repository identifiers. A repository identifier takes +the form of a git remote url, i.e. `:`. An example would be: + +`https://github.com/adamharrison/lite-xl-plugin-manager.git:latest` + +## Plugins + +Plugins are the primary objects specified in this specification. A plugin +consists of a series of metadata, the path to the plugin in this repository, +or its location on a remote repository, or a publically accessible URL,and a +set of files to be downloaded with the plugin (usually releases, but can be +data files, or fonts, or anything else). + +Plugins can specify optionally specify a type, which determines where they're +installed. Currently two types are supported: + +* `library` +* `plugin` + +Plugins are classified into two categories. `singleton` plugins, and `complex` +plugins. Plugins are listed a `singleton` if and only if they consist of exactly +one file, have an empty or absent `files` specification, and do not specify +a `remote`. Singleton plugins consist of exactly one `.lua` file, named after +the plugin. Complex plugins are contained within a folder, and have an +`init.lua` or `init.so` file that loads other components within it. + +The vast majority of plugins are `singleton` plugins. + +### Metadata + +* `name`: The name of the plugin. +* `version`: The plugin's semantic version. +* `description`: An english-language description of the plugin. +* `mod_version`: The mod_version this plugin is compatible with. +* `provides`: An optional array of strings that are a shorthand of functionality + this plugin provides. Can be used as a dependency. +* `dependencies`: Optionally a hash of dependencies required, or optional + for this plugin. +* `tags`: Optional freeform tags that may describe attributes of the plugin. + +### Dependencies + +Depedencies are specified in an object, with the key being the name of the +plugin depended upon, or a `provides` alias. + +Dependency values are an object which contain the following keys: + +* `version`: A version specifier. (see below). +* `optional`: A boolean that determines whether the dependency is optional. + +### Remote Repository + +If a plugin likes, it can specify a particular repository, pinned at a specific +commit to be used as a source for its data. In that case, the package manager +must download the repository, and interpret the manifest file found there. + +### Files + +Files are objects that contain at least two keys, `url`, and `checksum`. They +can also optionally contain the `arch` and `path` keys. + +* `url` represents the URL to grab the particular file from. +* `checksum` is the sha256hex checksum for the file. +* `arch` is the lite-xl/clang architecture tuple that the file is relevant for. + if omitted, file is to be assumed to be valid for all arhcitectures. +* `path` is the location to install this file inside the plugin's directory. + +If a file is an archive, of either `.zip` or `.tar.gz`, it will automatically +be extracted inside the plugin's directory. + +## Lite-XLs + +Lite-XLs represent different version of lite-xl that are registered in this +repository. Lite-XLs has the following metadata, as well as a `files` array. + +* `version`: A version specifier. Must take the form of x.x(.x)(-suffix). + Suffixes can be used to denote different flavours of lite-xl. +* `mod_version`: The modversion the binary corresponds to. + +### Files + +The files array is identical to that of the `files` array under `plugins`. +Conventionally, there should be a single file per architecture that is a +`.tar.gz` or `.zip` containing all necessary files for `lite-xl` to run. + +## Version Specifiers + +When asking for a version, all fields can use inequality operators to specify +the version to be asked for. As an example, `>=0.1` can be used to specify +that any version greater than `0.1` can be used. + +## Example File + +```yaml +{ + "plugins": [ # The plugins array contains a list of all plugins registered on this repository. + { + "name": "plugin_manager", # Unique name, used to reference the plugin. + "version": "0.1", # Semantic version. + "description": "A GUI interface to the Adam's lite plugin manager.", # English description of the plugin. + "path": "plugins/plugin_manager", # The path to the plugin in this repository. + "mod_version": 3, # The mod_version this plugin corresponds to. + "provides": [ # A list of small strings that represent functionalities this plugin provides. + "plugin-manager" + ], + "files": [ # A list of files (usually binaries) this plugin requires to function. + { + "url": "https://github.com/adamharrison/lite-xl-plugin-manager/releases/download/v0.1/lpm.x86_64-linux", # A publically accessible to download from. + "arch": "x86_64-linux", # The lite-xl/clang target tuple that represents the architecture this file is for. + "checksum": "d27f03c850bacdf808436722cd16e2d7649683e017fe6267934eeeedbcd21096" # the sha256hex checksum that corresponds to this file. + }, + { + "url": "https://github.com/adamharrison/lite-xl-plugin-manager/releases/download/v0.1/lpm.x86_64-windows.exe", + "arch": "x86_64-windows", + "checksum": "2ed993ed4376e1840b0824d7619f2d3447891d3aa234459378fcf9387c4e4680" + } + ], + "dependencies": { + "json": {} # Depeneds on `json`, can be a plugin's name or one of its `provides`. + } + }, + { + "name": "json", + "version": "1.0", + "description": "JSON support plugin, provides encoding/decoding.", + "type": "library", + "path": "plugins/json.lua", + "provides": [ + "json" + ] + }, + { + "tags": ["language"], + "description": "Syntax for .gitignore, .dockerignore and some other `.*ignore` files", + "version": "1.0", + "mod_version": 3, + "remote": "https://github.com/anthonyaxenov/lite-xl-ignore-syntax:2ed993ed4376e1840b0824d7619f2d3447891d3aa234459378fcf9387c4e4680", # The remote to be used for this plugin. + "name": "language_ignore" + }, + { + "description": "Provides a GUI to manage core and plugin settings, bindings and select color theme. Depends on widget.", + "dependencies": { + "toolbarview": { "version": ">=1.0" }, + "widget": { "version": ">=1.0" } + }, + "version": "1.0", + "mod_version": "3", + "path": "plugins/settings.lua", + "name": "settings" + }, + { + "description": "Syntax for Kaitai struct files", + "url": "https://raw.githubusercontent.com/whiteh0le/lite-plugins/main/plugins/language_ksy.lua?raw=1", # URL directly to the singleton plugin file. + "name": "language_ksy", + "version": "1.0", + "mod_version": 3, + "checksum": "08a9f8635b09a98cec9dfca8bb65f24fd7b6585c7e8308773e7ddff9a3e5a60f", # Checksum for this particular URL. + } + ], + "lite-xls": [ # An array of lite-xl releases. + { + "version": "2.1-simplified", # The version, followed by a release suffix defining the release flavour. The only releases that are permitted to not have suffixes are official relases. + "mod_version": 3, # The mod_version this release corresponds to. + "files": [ # Identical to `files` under `plugins`, although these are usually simply archives to be extracted. + { + "arch": "x86_64-linux", + "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-linux.tar.gz", + "checksum": "b5087bd03fb491c9424485ba5cb16fe3bb0a6473fdc801704e43f82cdf960448" + }, + { + "arch": "x86_64-windows", + "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-windows.zip", + "checksum": "f12cc1c172299dd25575ae1b7473599a21431f9c4e14e73b271ff1429913275d" + } + ] + }, + { + "version": "2.1-simplified-enhanced", + "mod_version": 3, + "files": [ + { + "arch": "x86_64-linux", + "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-linux-enhanced.tar.gz", + "checksum": "4625c7aac70a2834ef5ce5ba501af2d72d203441303e56147dcf8bcc4b889e40" + }, + { + "arch": "x86_64-windows", + "url": "https://github.com/adamharrison/lite-xl-simplified/releases/download/v2.1/lite-xl-2.1.0-simplified-x86_64-windows-enhanced.zip", + "checksum": "5ac009e3d5a5c99ca7fbd4f6b5bd4e25612909bf59c0925eddb41fe294ce28a4" + } + ] + } + ], + "remotes": [ # A list of remote specifiers. The plugin manager will pull these in and add them as additional repositories if specified to do so with a flag. + "https://github.com/lite-xl/lite-xl-plugins.git:2.1", + "https://github.com/adamharrison/lite-xl-simplified.git:v2.1" + ] +} +``` diff --git a/build.sh b/build.sh index 6ea19a1..3085215 100755 --- a/build.sh +++ b/build.sh @@ -5,7 +5,7 @@ : ${BIN=lpm} : ${JOBS=4} -SRCS="*.c" +SRCS="src/*.c" LDFLAGS="$LDFLAGS -lm -pthread -static-libgcc" [[ "$@" == "clean" ]] && rm -rf lib/libgit2/build lib/zlib/build lib/openssl/build lib/curl/build lib/libarchive/build-tmp lib/liblzma/build lib/prefix $BIN *.exe && exit 0 @@ -29,7 +29,7 @@ if [[ "$@" != *"-lcurl"* ]]; then [ ! -e "lib/curl/build" ] && cd lib/curl && mkdir build && cd build && cmake .. -G="Unix Makefiles" $CURL_CONFIGURE -DCURL_USE_LIBPSL=OFF -DCURL_DISABLE_LDAPS=ON -DUSE_OPENSSL=ON -DCURL_DISABLE_LDAP=ON -DCMAKE_INSTALL_PREFIX=`pwd`/../../prefix -DUSE_LIBIDN2=OFF -DENABLE_UNICODE=OFF -DBUILD_CURL_EXE=OFF -DCURL_USE_LIBSSH2=OFF -DOPENSSL_ROOT_DIR=`pwd`/../../prefix -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_LIBDIR=lib && $MAKE -j $JOBS && $MAKE install && cd ../../../ LDFLAGS="-Llib/curl/build -Llib/prefix/lib -l:libcurl.a $LDFLAGS" && CFLAGS="$CFLAGS -Ilib/prefix/include -DCURL_STATICLIB" fi -if [[ "$@" != *"-lzma"* ]]; then +if [[ "$@" != *"-llzma"* ]]; then [ ! -e "lib/liblzma/build" ] && cd lib/liblzma && mkdir build && cd build && ../configure $LZMA_CONFIGURE --prefix=`pwd`/../../prefix && $MAKE -j $JOBS && $MAKE install && cd ../../../ LDFLAGS="-Llib/liblzma/build -Llib/prefix/lib -l:liblzma.a $LDFLAGS" && CFLAGS="$CFLAGS -Ilib/prefix/include" fi @@ -41,7 +41,7 @@ fi [[ "$@" != *"-llua"* ]] && CFLAGS="$CFLAGS -Ilib/lua -DMAKE_LIB=1" && SRCS="$SRCS lib/lua/onelua.c" # Build the pre-packaged lua file into the executbale. -xxd -i lpm.lua > lpm.lua.c +xxd -i src/lpm.lua > src/lpm.lua.c [[ $OSTYPE != 'msys'* && $CC != *'mingw'* && $CC != "emcc" ]] && LDFLAGS=" $LDFLAGS -ldl -pthread" [[ $OSTYPE == 'msys'* || $CC == *'mingw'* ]] && LDFLAGS="$LDFLAGS -lbcrypt -lws2_32 -lz -lwinhttp -lole32 -lcrypt32 -lrpcrt4" diff --git a/lpm.c b/lpm.c deleted file mode 100644 index c8584a5..0000000 --- a/lpm.c +++ /dev/null @@ -1,575 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#ifdef _WIN32 - #include - #include - #include - #include -#else - #define MAX_PATH PATH_MAX -#endif - -static char hex_digits[] = "0123456789abcdef"; -static int lpm_hash(lua_State* L) { - size_t len; - const char* data = luaL_checklstring(L, 1, &len); - const char* type = luaL_optstring(L, 2, "string"); - unsigned char buffer[EVP_MAX_MD_SIZE]; - EVP_MD_CTX* c = EVP_MD_CTX_new(); - EVP_MD_CTX_init(c); - EVP_DigestInit_ex(c, EVP_sha256(), NULL); - if (strcmp(type, "file") == 0) { - FILE* file = fopen(data, "rb"); - if (!file) { - EVP_DigestFinal(c, buffer, NULL); - return luaL_error(L, "can't open %s", data); - } - while (1) { - unsigned char chunk[4096]; - size_t bytes = fread(chunk, 1, sizeof(chunk), file); - EVP_DigestUpdate(c, chunk, bytes); - if (bytes < 4096) - break; - } - fclose(file); - } else { - EVP_DigestUpdate(c, data, len); - } - int digest_length; - EVP_DigestFinal(c, buffer, &digest_length); - EVP_MD_CTX_free(c); - char hex_buffer[EVP_MAX_MD_SIZE * 2]; - for (size_t i = 0; i < digest_length; ++i) { - hex_buffer[i*2+0] = hex_digits[buffer[i] >> 4]; - hex_buffer[i*2+1] = hex_digits[buffer[i] & 0xF]; - } - lua_pushlstring(L, hex_buffer, digest_length * 2); - hex_buffer[digest_length*2]=0; - return 1; -} - -int lpm_symlink(lua_State* L) { - #ifndef _WIN32 - if (symlink(luaL_checkstring(L, 1), luaL_checkstring(L, 2))) - return luaL_error(L, "can't create symlink %s: %s", luaL_checkstring(L, 2), strerror(errno)); - return 0; - #else - return luaL_error(L, "can't create symbolic link %s: your operating system sucks", luaL_checkstring(L, 2)); - #endif -} - -int lpm_chmod(lua_State* L) { - if (chmod(luaL_checkstring(L, 1), luaL_checkinteger(L, 2))) - return luaL_error(L, "can't chmod %s: %s", luaL_checkstring(L, 1), strerror(errno)); - return 0; -} - -/** BEGIN STOLEN LITE CODE **/ -#if _WIN32 -static LPWSTR utfconv_utf8towc(const char *str) { - LPWSTR output; - int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); - if (len == 0) - return NULL; - output = (LPWSTR) malloc(sizeof(WCHAR) * len); - if (output == NULL) - return NULL; - len = MultiByteToWideChar(CP_UTF8, 0, str, -1, output, len); - if (len == 0) { - free(output); - return NULL; - } - return output; -} - -static char *utfconv_wctoutf8(LPCWSTR str) { - char *output; - int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL); - if (len == 0) - return NULL; - output = (char *) malloc(sizeof(char) * len); - if (output == NULL) - return NULL; - len = WideCharToMultiByte(CP_UTF8, 0, str, -1, output, len, NULL, NULL); - if (len == 0) { - free(output); - return NULL; - } - return output; -} -#endif - -static int lpm_ls(lua_State *L) { - const char *path = luaL_checkstring(L, 1); - -#ifdef _WIN32 - lua_settop(L, 1); - lua_pushstring(L, path[0] == 0 || strchr("\\/", path[strlen(path) - 1]) != NULL ? "*" : "/*"); - lua_concat(L, 2); - path = lua_tostring(L, -1); - - LPWSTR wpath = utfconv_utf8towc(path); - if (wpath == NULL) - return luaL_error(L, "can't ls %s: invalid utf8 character conversion", path); - - WIN32_FIND_DATAW fd; - HANDLE find_handle = FindFirstFileExW(wpath, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, 0); - free(wpath); - if (find_handle == INVALID_HANDLE_VALUE) - return luaL_error(L, "can't ls %s: %d", path, GetLastError()); - char mbpath[MAX_PATH * 4]; // utf-8 spans 4 bytes at most - int len, i = 1; - lua_newtable(L); - - do - { - if (wcscmp(fd.cFileName, L".") == 0) { continue; } - if (wcscmp(fd.cFileName, L"..") == 0) { continue; } - - len = WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, mbpath, MAX_PATH * 4, NULL, NULL); - if (len == 0) { break; } - lua_pushlstring(L, mbpath, len - 1); // len includes \0 - lua_rawseti(L, -2, i++); - } while (FindNextFileW(find_handle, &fd)); - - int err = GetLastError(); - FindClose(find_handle); - if (err != ERROR_NO_MORE_FILES) - return luaL_error(L, "can't ls %s: %d", path, GetLastError()); - return 1; -#else - DIR *dir = opendir(path); - if (!dir) - return luaL_error(L, "can't ls %s: %d", path, strerror(errno)); - lua_newtable(L); - int i = 1; - struct dirent *entry; - while ( (entry = readdir(dir)) ) { - if (strcmp(entry->d_name, "." ) == 0) { continue; } - if (strcmp(entry->d_name, "..") == 0) { continue; } - lua_pushstring(L, entry->d_name); - lua_rawseti(L, -2, i); - i++; - } - closedir(dir); - return 1; -#endif -} - -static int lpm_rmdir(lua_State *L) { - const char *path = luaL_checkstring(L, 1); -#ifdef _WIN32 - LPWSTR wpath = utfconv_utf8towc(path); - int deleted = RemoveDirectoryW(wpath); - free(wpath); - if (!deleted) - return luaL_error(L, "can't rmdir %s: %d", path, GetLastError()); -#else - if (remove(path)) - return luaL_error(L, "can't rmdir %s: %s", path, strerror(errno)); -#endif - return 0; -} - -static int lpm_mkdir(lua_State *L) { - const char *path = luaL_checkstring(L, 1); -#ifdef _WIN32 - LPWSTR wpath = utfconv_utf8towc(path); - if (wpath == NULL) - return luaL_error(L, "can't mkdir %s: invalid utf8 character conversion", path); - int err = _wmkdir(wpath); - free(wpath); -#else - int err = mkdir(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); -#endif - if (err < 0) - return luaL_error(L, "can't mkdir %s: %s", path, strerror(errno)); - return 0; -} - -static int lpm_stat(lua_State *L) { - const char *path = luaL_checkstring(L, 1); - lua_newtable(L); -#ifdef _WIN32 - #define realpath(x, y) _wfullpath(y, x, MAX_PATH) - struct _stat s; - LPWSTR wpath = utfconv_utf8towc(path); - if (wpath == NULL) - return luaL_error(L, "can't stat %s: invalid utf8 character conversion", path); - int err = _wstat(wpath, &s); - LPWSTR wfullpath = realpath(wpath, NULL); - free(wpath); - if (!wfullpath) return 0; - char *abs_path = utfconv_wctoutf8(wfullpath); - free(wfullpath); -#else - struct stat s; - int err = lstat(path, &s); - char *abs_path = realpath(path, NULL); -#endif - if (err || !abs_path) { - lua_pushnil(L); - lua_pushstring(L, strerror(errno)); - return 2; - } - lua_pushstring(L, abs_path); lua_setfield(L, -2, "abs_path"); - lua_pushvalue(L, 1); lua_setfield(L, -2, "path"); - -#if __linux__ - if (S_ISLNK(s.st_mode)) { - char buffer[PATH_MAX]; - ssize_t len = readlink(path, buffer, sizeof(buffer)); - if (len < 0) - return 0; - lua_pushlstring(L, buffer, len); - } else - lua_pushnil(L); - lua_setfield(L, -2, "symlink"); - if (S_ISLNK(s.st_mode)) - err = stat(path, &s); - if (err) - return 1; -#endif - lua_pushinteger(L, s.st_mtime); lua_setfield(L, -2, "modified"); - lua_pushinteger(L, s.st_size); lua_setfield(L, -2, "size"); - if (S_ISREG(s.st_mode)) { - lua_pushstring(L, "file"); - } else if (S_ISDIR(s.st_mode)) { - lua_pushstring(L, "dir"); - } else { - lua_pushnil(L); - } - lua_setfield(L, -2, "type"); - return 1; -} -/** END STOLEN LITE CODE **/ - -static const char* git_error_last_string() { - const git_error* last_error = git_error_last(); - return last_error->message; -} - -static int git_get_id(git_oid* commit_id, git_repository* repository, const char* name) { - int length = strlen(name); - int is_hex = length == 40; - for (int i = 0; is_hex && i < length; ++i) - is_hex = isxdigit(name[i]); - if (!is_hex) - return git_reference_name_to_id(commit_id, repository, name); - return git_oid_fromstr(commit_id, name); -} - -static git_repository* luaL_checkgitrepo(lua_State* L, int index) { - const char* path = luaL_checkstring(L, index); - git_repository* repository; - if (git_repository_open(&repository, path)) - return (void*)(long long)luaL_error(L, "git open error: %s", git_error_last_string()); - return repository; -} - - -static git_commit* git_retrieve_commit(git_repository* repository, const char* commit_name) { - git_oid commit_id; - git_commit* commit; - if (git_get_id(&commit_id, repository, commit_name) || git_commit_lookup(&commit, repository, &commit_id)) - return NULL; - return commit; -} - - -static int lpm_reset(lua_State* L) { - git_repository* repository = luaL_checkgitrepo(L, 1); - const char* commit_name = luaL_checkstring(L, 2); - const char* type = luaL_checkstring(L, 3); - git_commit* commit = git_retrieve_commit(repository, commit_name); - if (!commit) { - git_repository_free(repository); - return luaL_error(L, "git retrieve commit error: %s", git_error_last_string()); - } - git_reset_t reset_type = GIT_RESET_SOFT; - if (strcmp(type, "mixed") == 0) - reset_type = GIT_RESET_MIXED; - else if (strcmp(type, "hard") == 0) - reset_type = GIT_RESET_HARD; - int result = git_reset(repository, (git_object*)commit, reset_type, NULL); - git_commit_free(commit); - git_repository_free(repository); - if (result) - return luaL_error(L, "git reset error: %s", git_error_last_string()); - return 0; -} - - -static int lpm_init(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - const char* url = luaL_checkstring(L, 2); - git_repository* repository; - if (git_repository_init(&repository, path, 0) != 0) - return luaL_error(L, "git init error: %s", git_error_last_string()); - git_remote* remote; - if (git_remote_create(&remote, repository, "origin", url)) { - git_repository_free(repository); - return luaL_error(L, "git remote add error: %s", git_error_last_string()); - } - git_remote_free(remote); - git_repository_free(repository); - return 0; -} - - -static int lpm_fetch(lua_State* L) { - git_repository* repository = luaL_checkgitrepo(L, 1); - git_remote* remote; - if (git_remote_lookup(&remote, repository, "origin")) { - git_repository_free(repository); - return luaL_error(L, "git remote fetch error: %s", git_error_last_string()); - } - git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; - if (git_remote_fetch(remote, NULL, &fetch_opts, NULL)) { - git_remote_free(remote); - git_repository_free(repository); - return luaL_error(L, "git remote fetch error: %s", git_error_last_string()); - } - git_remote_free(remote); - git_repository_free(repository); - return 0; -} - - -static CURL *curl; -static int lpm_certs(lua_State* L) { - const char* type = luaL_checkstring(L, 1); - const char* path = luaL_checkstring(L, 2); - if (strcmp(type, "dir") == 0) { - git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, path); - curl_easy_setopt(curl, CURLOPT_CAINFO, path); - } else { - git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, path, NULL); - curl_easy_setopt(curl, CURLOPT_CAPATH, path); - } - return 0; -} - -static int lpm_extract(lua_State* L) { - const char* src = luaL_checkstring(L, 1); - const char* dst = luaL_optstring(L, 2, "."); - - char error_buffer[1024] = {0}; - struct archive_entry *entry; - const void *buff; - int flags = 0; - int r; - size_t size; -#if ARCHIVE_VERSION_NUMBER >= 3000000 - int64_t offset; -#else - off_t offset; -#endif - struct archive *ar = archive_read_new(); - struct archive *aw = archive_write_disk_new(); - archive_write_disk_set_options(aw, flags); - archive_read_support_format_tar(ar); - archive_read_support_format_zip(ar); - archive_read_support_filter_gzip(ar); - if ((r = archive_read_open_filename(ar, src, 10240))) { - snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); - goto cleanup; - } - for (;;) { - int r = archive_read_next_header(ar, &entry); - if (r == ARCHIVE_EOF) - break; - if (r != ARCHIVE_OK) { - snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); - goto cleanup; - } - char path[MAX_PATH]; - strcpy(path, dst); strcat(path, "/"); - strncat(path, archive_entry_pathname(entry), sizeof(path) - 3); path[MAX_PATH-1] = 0; - archive_entry_set_pathname(entry, path); - if (archive_write_header(aw, entry) != ARCHIVE_OK) { - snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); - goto cleanup; - } - for (;;) { - int r = archive_read_data_block(ar, &buff, &size, &offset); - if (r == ARCHIVE_EOF) - break; - if (r != ARCHIVE_OK) { - snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); - goto cleanup; - } - if (archive_write_data_block(aw, buff, size, offset) != ARCHIVE_OK) { - snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); - goto cleanup; - } - } - if (archive_write_finish_entry(aw) != ARCHIVE_OK) { - snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); - goto cleanup; - } - } - cleanup: - archive_read_close(ar); - archive_read_free(ar); - archive_write_close(aw); - archive_write_free(aw); - if (error_buffer[0]) - return luaL_error(L, "error extracting archive %s: %s", src, archive_error_string(ar)); - return 0; -} - -static size_t lpm_curl_write_callback(char *ptr, size_t size, size_t nmemb, void *BL) { - luaL_Buffer* B = BL; - luaL_addlstring(B, ptr, size*nmemb); - return size*nmemb; -} - -static int lpm_get(lua_State* L) { - long response_code; - const char* url = luaL_checkstring(L, 1); - const char* path = luaL_optstring(L, 2, NULL); - // curl_easy_reset(curl); - curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - #ifdef _WIN32 - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); - #endif - if (path) { - FILE* file = fopen(path, "wb"); - if (!file) - return luaL_error(L, "error opening file %s: %s", path, strerror(errno)); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); - CURLcode res = curl_easy_perform(curl); - if (res != CURLE_OK) { - fclose(file); - return luaL_error(L, "curl error accessing %s: %s", url, curl_easy_strerror(res)); - } - fclose(file); - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); - if (response_code != 200) - return luaL_error(L, "curl error accessing %s, non-200 response code: %d", url, response_code); - lua_pushnil(L); - lua_newtable(L); - return 2; - } else { - luaL_Buffer B; - luaL_buffinit(L, &B); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, lpm_curl_write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &B); - CURLcode res = curl_easy_perform(curl); - if (res != CURLE_OK) - return luaL_error(L, "curl error accessing %s: %s", url, curl_easy_strerror(res)); - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); - if (response_code != 200) - return luaL_error(L, "curl error accessing %s, non-200 response code: %d", url, response_code); - luaL_pushresult(&B); - lua_newtable(L); - } - return 2; -} - - -static const luaL_Reg system_lib[] = { - { "ls", lpm_ls }, // Returns an array of files. - { "stat", lpm_stat }, // Returns info about a single file. - { "mkdir", lpm_mkdir }, // Makes a directory. - { "rmdir", lpm_rmdir }, // Removes a directory. - { "hash", lpm_hash }, // Returns a hex sha256 hash. - { "symlink", lpm_symlink }, // Creates a symlink. - { "chmod", lpm_chmod }, // Chmod's a file. - { "init", lpm_init }, // Initializes a git repository with the specified remote. - { "fetch", lpm_fetch }, // Updates a git repository with the specified remote. - { "reset", lpm_reset }, // Updates a git repository to the specified commit/hash/branch. - { "get", lpm_get }, // HTTP(s) GET request. - { "extract", lpm_extract }, // Extracts .tar.gz, and .zip files. - { "certs", lpm_certs }, // Sets the SSL certificate chain folder/file. - { NULL, NULL } -}; - - -#ifndef LPM_VERSION - #define LPM_VERSION "unknown" -#endif - - -#ifndef LITE_ARCH_TUPLE - #if __x86_64__ || _WIN64 || __MINGW64__ - #define ARCH_PROCESSOR "x86_64" - #else - #define ARCH_PROCESSOR "x86" - #endif - #if _WIN32 - #define ARCH_PLATFORM "windows" - #elif __linux__ - #define ARCH_PLATFORM "linux" - #elif __APPLE__ - #define ARCH_PLATFORM "darwin" - #else - #error "Please define -DLITE_ARCH_TUPLE." - #endif - #define LITE_ARCH_TUPLE ARCH_PROCESSOR "-" ARCH_PLATFORM -#endif - - -extern const char lpm_lua[]; -extern unsigned int lpm_lua_len; -int main(int argc, char* argv[]) { - curl = curl_easy_init(); - if (!curl) - return -1; - git_libgit2_init(); - lua_State* L = luaL_newstate(); - luaL_openlibs(L); - luaL_newlib(L, system_lib); - lua_setglobal(L, "system"); - lua_newtable(L); - for (int i = 0; i < argc; ++i) { - lua_pushstring(L, argv[i]); - lua_rawseti(L, -2, i+1); - } - lua_setglobal(L, "ARGV"); - lua_pushliteral(L, LPM_VERSION); - lua_setglobal(L, "VERSION"); - #if _WIN32 - lua_pushliteral(L, "windows"); - lua_pushliteral(L, "\\"); - #else - lua_pushliteral(L, "posix"); - lua_pushliteral(L, "/"); - #endif - lua_setglobal(L, "PATHSEP"); - lua_setglobal(L, "PLATFORM"); - lua_pushliteral(L, LITE_ARCH_TUPLE); - lua_setglobal(L, "ARCH"); - #if LPM_LIVE - if (luaL_loadfile(L, "lpm.lua") || lua_pcall(L, 0, 1, 0)) { - #else - if (luaL_loadbuffer(L, lpm_lua, lpm_lua_len, "lpm.lua") || lua_pcall(L, 0, 1, 0)) { - #endif - fprintf(stderr, "internal error when starting the application: %s\n", lua_tostring(L, -1)); - return -1; - } - int status = lua_tointeger(L, -1); - lua_close(L); - git_libgit2_shutdown(); - curl_easy_cleanup(curl); - return status; -} diff --git a/lpm.lua b/lpm.lua deleted file mode 100644 index c0752f0..0000000 --- a/lpm.lua +++ /dev/null @@ -1,1612 +0,0 @@ -setmetatable(_G, { __index = function(t, k) if not rawget(t, k) then error("cannot get undefined global variable: " .. k, 2) end end, __newindex = function(t, k) error("cannot set global variable: " .. k, 2) end }) - --- Begin rxi JSON library. -local json = { _version = "0.1.2" } -local encode -local escape_char_map = { - [ "\\" ] = "\\", - [ "\"" ] = "\"", - [ "\b" ] = "b", - [ "\f" ] = "f", - [ "\n" ] = "n", - [ "\r" ] = "r", - [ "\t" ] = "t", -} - -local escape_char_map_inv = { [ "/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) -end - - -local function encode_nil(val) - return "null" -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - if rawget(val, 1) ~= nil or next(val) == nil then - -- Treat as array -- check keys are valid and it is not sparse - local n = 0 - for k in pairs(val) do - if type(k) ~= "number" then - error("invalid table: mixed or invalid key types") - end - n = n + 1 - end - if n ~= #val then - error("invalid table: sparse array") - end - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types") - end - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function json.encode(val) - return ( encode(val) ) -end - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - local line_count = 1 - local col_count = 1 - for i = 1, idx - 1 do - col_count = col_count + 1 - if str:sub(i, i) == "\n" then - line_count = line_count + 1 - col_count = 1 - end - end - error( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(1, 4), 16 ) - local n2 = tonumber( s:sub(7, 10), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local res = "" - local j = i + 1 - local k = j - - while j <= #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - - elseif x == 92 then -- `\`: Escape - res = res .. str:sub(k, j - 1) - j = j + 1 - local c = str:sub(j, j) - if c == "u" then - local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) - or str:match("^%x%x%x%x", j + 1) - or decode_error(str, j - 1, "invalid unicode escape in string") - res = res .. parse_unicode_escape(hex) - j = j + #hex - else - if not escape_chars[c] then - decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") - end - res = res .. escape_char_map_inv[c] - end - k = j + 1 - - elseif x == 34 then -- `"`: End of string - res = res .. str:sub(k, j - 1) - return res, j + 1 - end - - j = j + 1 - end - - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - res[key] = val - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - local res, idx = parse(str, next_char(str, 1, space_chars, true)) - idx = next_char(str, idx, space_chars, true) - if idx <= #str then - decode_error(str, idx, "trailing garbage") - end - return res -end - --- End JSON library. -local function is_commit_hash(hash) - return #hash == 40 and not hash:find("[^a-z0-9]") -end - - -local common = {} -function common.merge(dst, src) for k, v in pairs(src) do dst[k] = v end return dst end -function common.map(l, p) local t = {} for i, v in ipairs(l) do table.insert(t, p(v)) end return t end -function common.flat_map(l, p) local t = {} for i, v in ipairs(l) do local r = p(v) for k, w in ipairs(r) do table.insert(t, w) end end return t end -function common.concat(t1, t2) local t = {} for i, v in ipairs(t1) do table.insert(t, v) end for i, v in ipairs(t2) do table.insert(t, v) end return t end -function common.grep(l, p) local t = {} for i, v in ipairs(l) do if p(v) then table.insert(t, v) end end return t end -function common.first(l, p) for i, v in ipairs(l) do if p(v) then return v end end end -function common.slice(t, i, l) local n = {} for j = i, l ~= nil and (i - l) or #t do table.insert(n, t[j]) end return n end -function common.join(j, l) local s = "" for i, v in ipairs(l) do if i > 1 then s = s .. j .. v else s = v end end return s end -function common.sort(t, f) table.sort(t, f) return t end -function common.write(path, contents) local f, err = io.open(path, "wb") if not f then error("can't write to " .. path .. ": " .. err) end f:write(contents) f:flush() end -function common.split(splitter, str) - local o = 1 - local res = {} - while true do - local s, e = str:find(splitter, o) - table.insert(res, str:sub(o, s and (s - 1) or #str)) - if not s then break end - o = e + 1 - end - return table.unpack(res) -end - -function common.dirname(path) local s = path:reverse():find("[/\\]") if not s then return path end return path:sub(1, #path - s) end -function common.basename(path) local s = path:reverse():find("[/\\]") if not s then return path end return path:sub(#path - s + 2) end -function common.path(exec) return common.first(common.map({ common.split(":", os.getenv("PATH")) }, function(e) return e .. PATHSEP .. exec end), function(e) return system.stat(e) end) end -function common.rmrf(root) - local info = root and root ~= "" and system.stat(root) - if not info then return end - if info.type == "file" or info.symlink then return os.remove(root) end - for i,v in ipairs(system.ls(root)) do common.rmrf(root .. PATHSEP .. v) end - system.rmdir(root) -end -function common.mkdirp(path) - local stat = system.stat(path) - if stat and stat.type == "dir" then return true end - if stat and stat.type == "file" then error("path " .. path .. " exists") end - local target - for _, dirname in ipairs({ common.split("[/\\]", path) }) do - target = target and target .. PATHSEP .. dirname or dirname - if target ~= "" and not system.stat(target) then system.mkdir(target) end - end -end -function common.copy(src, dst) - local src_stat, dst_stat = system.stat(src), system.stat(dst) - if not src_stat then error("can't find " .. src) end - if dst_stat and dst_stat.type == "dir" then return common.copy(src, dst .. PATHSEP .. common.basename(src)) end - if src_stat.type == "dir" then - common.mkdirp(dst) - for i, file in ipairs(system.ls(src)) do common.copy(src .. PATHSEP .. file, dst .. PATHSEP .. file) end - else - local src_io, err1 = io.open(src, "rb") - if err1 then error("can't open for reading " .. src .. ": " .. err1) end - local dst_io, err2 = io.open(dst, "wb") - if err2 then error("can't open for writing " .. dst .. ": " .. err2) end - while true do - local chunk = src_io:read(64*1024) - if not chunk then break end - dst_io:write(chunk) - end - dst_io:close() - end -end -function common.rename(src, dst) - local _, err = os.rename(src, dst) - if err then error("can't rename file " .. src .. " to " .. dst .. ": " .. err) end -end -function common.reset(path, ref, type) - if is_commit_hash(ref) then - system.reset(path, ref, type) - else - if not pcall(system.reset, path, "refs/tags/" .. ref, type) then system.reset(path, "refs/remotes/origin/" .. ref, type) end - end -end - -local HOME, USERDIR, CACHEDIR, JSON, VERBOSE, MOD_VERSION, QUIET, FORCE, AUTO_PULL_REMOTES, ARCH, ASSUME_YES, NO_INSTALL_OPTIONAL, TMPDIR, repositories, lite_xls, system_bottle - -local Plugin, Repository, LiteXL, Bottle = {}, {}, {}, {} - -local actions, warnings = {}, {} -local function log_action(message) - if JSON then table.insert(actions, message) end - if not QUIET then io.stderr:write(message .. "\n") end -end -local function log_warning(message) - if JSON then table.insert(warnings, message) end - if not QUIET then io.stderr:write("warning: " .. message .. "\n") end -end -local function fatal_warning(message) - if not FORCE then error(message .. "; use --force to override") else log_warning(message) end -end -local function prompt(message) - io.stderr:write(message .. " [Y/n]: ") - if ASSUME_YES then io.stderr:write("Y\n") return true end - local response = io.stdin:read("*line") - return not response:find("%S") or response:find("^%s*[yY]%s*$") -end - -function common.get(source, target, checksum) - if not checksum then return system.get(source, target) end - if not system.stat(CACHEDIR .. PATHSEP .. "files") then common.mkdirp(CACHEDIR .. PATHSEP .. "files") end - local cache_path = CACHEDIR .. PATHSEP .. "files" .. PATHSEP .. checksum - if not system.stat(cache_path) then - system.get(source, cache_path) - if system.hash(cache_path, "file") ~= checksum then fatal_warning("checksum doesn't match for " .. source) end - end - common.copy(cache_path, target) -end - - -local function compare_version(a, b) -- compares semver - if not a or not b then return false end - local _, _, majora, minora, revisiona = tostring(a):find("(%d+)%.?(%d*)%.?(%d*)") - local _, _, majorb, minorb, revisionb = tostring(b):find("(%d+)%.?(%d*)%.?(%d*)") - if majora == nil then error("can't parse version " .. a) end - if majorb == nil then error("can't parse version " .. b) end - majora, minora, revisiona = majora or 0, minora or 0, revisiona or 0 - majorb, minorb, revisionb = majorb or 0, minorb or 0, revisionb or 0 - if majora ~= majorb then return tonumber(majora) < tonumber(majorb) and -1 or 1 end - if minora ~= minorb then return tonumber(minora) < tonumber(minorb) and -1 or 1 end - if revisiona ~= revisionb then return tonumber(revisiona) < tonumber(revisionb) and -1 or 1 end - return 0 -end - -local function match_version(version, pattern) - if not pattern then return true end - if pattern:find("^>=") then return compare_version(version, pattern:sub(3)) >= 0 end - if pattern:find("^<=") then return compare_version(version, pattern:sub(3)) <= 0 end - if pattern:find("^<") then return compare_version(version, pattern:sub(2)) == -1 end - if pattern:find("^>") then return compare_version(version, pattern:sub(2)) == 1 end - if pattern:find("^=") then return compare_version(version, pattern:sub(2)) == 0 end - return version == pattern -end - - - -function Plugin.__index(self, idx) return rawget(self, idx) or Plugin[idx] end -function Plugin.new(repository, metadata) - local type = metadata.type or "plugin" - local folder = metadata.type == "library" and "libraries" or "plugins" - if metadata.path then metadata.path = metadata.path:gsub("/", PATHSEP) end - local self = setmetatable(common.merge({ - repository = repository, - tags = {}, - type = type, - path = nil, - remote = nil, - version = "1.0", - dependencies = {}, - local_path = repository and (repository.local_path .. PATHSEP .. (repository.commit or repository.branch) .. (metadata.path and (PATHSEP .. metadata.path:gsub("^/", "")) or "")), - }, metadata), Plugin) - -- Directory. - self.organization = metadata.organization or (((self.files and #self.files > 0) or self.remote or (not self.path and not self.url)) and "complex" or "singleton") - return self -end - -function Plugin:get_install_path(bottle) - local folder = self.type == "library" and "libraries" or "plugins" - local path = (bottle.local_path and (bottle.local_path .. PATHSEP .. "user") or USERDIR) .. PATHSEP .. folder .. PATHSEP .. (self.path and common.basename(self.path):gsub("%.lua$", "") or self.name) - if self.organization == "singleton" then path = path .. ".lua" end - return path -end - -function Plugin:is_core(bottle) return self.type == "core" end -function Plugin:is_installed(bottle) return self:is_core(bottle) or (bottle.lite_xl:is_compatible(self) and system.stat(self:get_install_path(bottle))) end -function Plugin:is_incompatible(plugin) return self.dependencies[plugin.name] and not match_version(plugin.version, dependencies[plugin.name]) end - -function Plugin:get_compatibilities(bottle) - local compatible_plugins, incompatible_plugins = {}, {} - local installed_plugins = bottle:installed_plugins() - for plugin, v in pairs(self.dependencies) do - local potential_plugins = { bottle:get_plugin(plugin, v.version, { mod_version = bottle.lite_xl.mod_version }) } - for i, potential_plugin in ipairs(potential_plugins) do - local incomaptibilities = common.grep(installed_plugins, function(p) return p:is_incompatible(potential_plugin) end) - if #incomaptibilities == 0 then - if not compatible_plugins[plugin] or - potential_plugin:is_installed(bottle) or - (compare_version(compatible_plugins[plugin].version, potential_plugin.version) and not compatible_plugins[plugin]:is_installed(bottle)) - then - compatible_plugins[plugin] = potential_plugin - end - else - incompatible_plugins[plugin] = incompatibilities - end - end - end - return compatible_plugins, incompatible_plugins -end - - - -function Plugin:install(bottle, installing) - if self:is_installed(bottle) then log_warning("plugin " .. self.name .. " is already installed") return end - local install_path = self:get_install_path(bottle) - local temporary_install_path = TMPDIR .. PATHSEP .. install_path:sub(#USERDIR + 2) - local status, err = pcall(function() - installing = installing or {} - installing[self.name] = true - local compatible, incompatible = self:get_compatibilities(bottle) - for plugin, version in pairs(self.dependencies) do - if incompatible[plugin] then error("can't install " .. self.name .. ": incompatible with " .. incompatible[plugin][1].name .. ":" .. incompatible[plugin][1].version) end - end - for plugin, v in pairs(self.dependencies) do - if not compatible[plugin] then error("can't find dependency " .. plugin .. (v.version and (":" .. v.version) or "")) end - end - for plugin, v in pairs(self.dependencies) do - if not compatible[plugin]:is_core(bottle) and not compatible[plugin]:is_installed(bottle) then - if installing[plugin] then - error("circular dependency detected in " .. self.name .. ": requires " .. plugin .. " but, " .. plugin .. " requires " .. self.name) - end - if not NO_INSTALL_OPTIONAL and (not v.optional or prompt(plugin .. " is an optional dependency of " .. self.name .. ". Should we install it?")) then - compatible[plugin]:install(bottle, installing) - end - end - end - common.mkdirp(common.dirname(temporary_install_path)) - if self.status == "upgradable" then - log_action("Upgrading " .. self.organization .. "plugin located at " .. self.local_path .. " to " .. install_path) - common.rmrf(install_path) - else - log_action("Installing " .. self.organization .. " plugin located at " .. self.local_path .. " to " .. install_path) - end - - if self.organization == "complex" and self.path and system.stat(self.local_path).type ~= "dir" then common.mkdirp(install_path) end - if self.url then - log_action("Downloading file " .. self.url .. "...") - local path = temporary_install_path .. (self.organization == 'complex' and self.path and system.stat(self.local_path).type ~= "dir" and (PATHSEP .. "init.lua") or "") - common.get(self.url, path, self.checksum) - log_action("Downloaded file " .. self.url .. " to " .. path) - if system.hash(path, "file") ~= self.checksum then fatal_warning("checksum doesn't match for " .. path) end - elseif self.remote then - log_action("Cloning repository " .. self.remote .. " into " .. install_path) - common.mkdirp(temporary_install_path) - local _, _, url, branch = self.remote:find("^(.*):(.*)$") - system.init(temporary_install_path, url) - common.reset(temporary_install_path, branch) - else - local path = install_path .. (self.organization == 'complex' and self.path and system.stat(self.local_path).type ~= "dir" and (PATHSEP .. "init.lua") or "") - local temporary_path = temporary_install_path .. (self.organization == 'complex' and self.path and system.stat(self.local_path).type ~= "dir" and (PATHSEP .. "init.lua") or "") - log_action("Copying " .. self.local_path .. " to " .. path) - common.copy(self.local_path, temporary_path) - end - for i,file in ipairs(self.files or {}) do - if not file.arch or file.arch == ARCH then - if not NO_INSTALL_OPTIONAL and (not file.optional or prompt(common.basename(file.url) .. " is an optional dependency of " .. self.name .. ". Should we install it?")) then - if not file.checksum then error("requires a checksum") end - local path = install_path .. PATHSEP .. (file.path or common.basename(file.url)) - local temporary_path = temporary_install_path .. PATHSEP .. (file.path or common.basename(file.url)) - log_action("Downloading file " .. file.url .. "...") - common.get(file.url, temporary_path, file.checksum) - log_action("Downloaded file " .. file.url .. " to " .. path) - if system.hash(temporary_path, "file") ~= file.checksum then fatal_warning("checksum doesn't match for " .. path) end - end - end - end - end) - if not status then - common.rmrf(temporary_install_path) - error(err) - else - common.rmrf(install_path) - common.mkdirp(common.dirname(install_path)) - common.rename(temporary_install_path, install_path) - end -end - -function Plugin:depends_on(plugin) - if self.dependencies[plugin.name] and self.dependencies[plugin.name].optional ~= true then return true end - for i,v in ipairs(plugin.provides or {}) do if self.dependencies[v] and self.dependencies[v].optional ~= true then return true end end - return false -end - -function Plugin:uninstall(bottle) - local install_path = self:get_install_path(bottle) - if self:is_core(bottle) then error("can't uninstall " .. self.name .. " is a core plugin") end - log_action("Uninstalling plugin located at " .. install_path) - local incompatible_plugins = common.grep(bottle:installed_plugins(), function(p) return p:depends_on(self) end) - if #incompatible_plugins == 0 or prompt(self.name .. " is depended upon by " .. common.join(", ", common.map(incompatible_plugins, function(p) return p.name end)) .. ". Remove as well?") then - for i,plugin in ipairs(incompatible_plugins) do - if not plugin:uninstall(bottle) then return false end - end - common.rmrf(install_path) - return true - end - return false -end - - -function Repository.__index(self, idx) return rawget(self, idx) or Repository[idx] end -function Repository.new(hash) - if not hash.remote then error("requires a remote") end - if not hash.remote:find("^https?:") and not hash.remote:find("^file:") then error("only repositories with http and file transports are supported (" .. hash.remote .. ")") end - local self = setmetatable({ - commit = hash.commit, - remote = hash.remote, - branch = hash.branch, - plugins = nil, - lite_xls = {}, - local_path = CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. system.hash(hash.remote), - last_retrieval = nil - }, Repository) - if system.stat(self.local_path) and not self.commit and not self.branch then - -- In the case where we don't have a branch, and don't have a commit, check for the presence of `master` and `main`. - if system.stat(self.local_path .. PATHSEP .. "master") then - self.branch = "master" - elseif system.stat(self.local_path .. PATHSEP .. "main") then - self.branch = "main" - else - error("can't find branch for " .. self.remote) - end - end - return self -end - -function Repository.url(url) - if type(url) == "table" then return url.remote .. ":" .. (url.branch or url.commit) end - local e = url:reverse():find(":") - local s = e and (#url - e + 1) - local remote, branch_or_commit = url:sub(1, s and (s-1) or #url), s and url:sub(s+1) - if remote == "https" or remote == "file" then remote, branch_or_commit = url, nil end - if branch_or_commit and is_commit_hash(branch_or_commit) then - return Repository.new({ remote = remote, commit = branch_or_commit }) - end - return Repository.new({ remote = remote, branch = branch_or_commit }) -end - -function Repository:parse_manifest(already_pulling) - if self.manifest then return self.manifest, self.remotes end - if system.stat(self.local_path) and system.stat(self.local_path .. PATHSEP .. (self.commit or self.branch)) then - self.manifest_path = self.local_path .. PATHSEP .. (self.commit or self.branch) .. PATHSEP .. "manifest.json" - if not system.stat(self.manifest_path) then self:generate_manifest() end - self.manifest = json.decode(io.open(self.manifest_path, "rb"):read("*all")) - self.plugins = {} - self.remotes = {} - for i, metadata in ipairs(self.manifest["plugins"] or {}) do - if metadata.remote then - local _, _, url, branch_or_commit = metadata.remote:find("^(.-):?(.*)?$") - if branch_or_commit and is_commit_hash(branch_or_commit) then - repo = Repository.new({ remote = url, commit = branch_or_commit }) - table.insert(remotes, repo) - table.insert(self.plugins, Plugin.new(self, metadata)) - else - -- log_warning("plugin " .. metadata.name .. " specifies remote as source, but isn't a commit") - end - else - table.insert(self.plugins, Plugin.new(self, metadata)) - end - end - for i, metadata in ipairs(self.manifest["lite-xls"] or {}) do - if metadata.remote then - local _, _, url, branch_or_commit = metadata.remote:find("^(.-):?(.*)?$") - if branch_or_commit and is_commit_hash(branch_or_commit) then - repo = Repository.new({ remote = url, commit = branch_or_commit }) - table.insert(remotes, repo) - table.insert(self.lite_xls, LiteXL.new(self, metadata)) - else - -- log_warning("plugin " .. metadata.name .. " specifies remote as source, but isn't a commit") - end - else - table.insert(self.lite_xls, LiteXL.new(self, metadata)) - end - end - self.remotes = common.map(self.manifest["remotes"] or {}, function(r) return Repository.url(r) end) - end - return self.manifest, self.remotes -end - - --- in the cases where we don't have a manifest, assume generalized structure, take plugins folder, trawl through it, build manifest that way --- assuming each .lua file under the `plugins` folder is a plugin. also parse the README, if present, and see if any of the plugins -function Repository:generate_manifest() - if not self.commit and not self.branch then error("requires an instantiation") end - local path = self.local_path .. PATHSEP .. (self.commit or self.branch) - local plugin_dir = system.stat(path .. PATHSEP .. "plugins") and PATHSEP .. "plugins" .. PATHSEP or PATHSEP - local plugins, plugin_map = {}, {} - if system.stat(path .. PATHSEP .. "README.md") then -- If there's a README, parse it for a table like in our primary repository. - for line in io.lines(path .. PATHSEP .. "README.md") do - local _, _, name, path, description = line:find("^%s*%|%s*%[`([%w_]+)%??.-`%]%((.-)%).-%|%s*(.-)%s*%|%s*$") - if name then - plugin_map[name] = { name = name, description = description, files = {} } - if path:find("^http") then - if path:find("%.lua") then - plugin_map[name].url = path - local file = common.get(path) - plugin_map[name].checksum = system.hash(file) - else - plugin_map[name].remote = path - end - else - plugin_map[name].path = path:gsub("%?.*$", "") - end - end - end - end - for i, file in ipairs(system.ls(path .. plugin_dir)) do - if file:find("%.lua$") then - local plugin = { description = nil, files = {}, name = common.basename(file):gsub("%.lua$", ""), dependencies = {}, mod_version = 3, version = "1.0", tags = {}, path = plugin_dir .. file } - for line in io.lines(path .. plugin_dir .. file) do - local _, _, mod_version = line:find("%-%-.*mod%-version:%s*(%w+)") - if mod_version then plugin.mod_version = mod_version end - local _, _, required_plugin = line:find("require [\"']plugins.([%w_]+)") - if required_plugin then if required_plugin ~= plugin.name then plugin.dependencies[required_plugin] = ">=1.0" end end - end - if plugin_map[plugin.name] then - plugin = common.merge(plugin, plugin_map[plugin.name]) - plugin_map[plugin.name].plugin = plugin - end - table.insert(plugins, plugin) - end - end - for k, v in pairs(plugin_map) do - if not v.plugin then - table.insert(plugins, common.merge({ dependencies = {}, mod_version = self.branch == "master" and 2 or 3, version = "1.0", tags = {} }, v)) - end - end - common.write(path .. PATHSEP .. "manifest.json", json.encode({ plugins = plugins })) -end - -function Repository:add(pull_remotes) - -- If neither specified then pull onto `master`, and check the main branch name, and move if necessary. - if not self.branch and not self.commit then - local path = self.local_path .. PATHSEP .. "master" - common.mkdirp(path) - log_action("Retrieving " .. self.remote .. ":master/main...") - system.init(path, self.remote) - system.fetch(path) - if not pcall(system.reset, path, "refs/remotes/origin/master", "hard") then - if pcall(system.reset, path, "refs/remotes/origin/main", "hard") then - common.rename(path, self.local_path .. PATHSEP .. "main") - self.branch = "main" - else - error("can't find master or main.") - end - else - self.branch = "master" - end - log_action("Retrieved " .. self.remote .. ":master/main.") - else - local path = self.local_path .. PATHSEP .. (self.commit or self.branch) - common.mkdirp(path) - log_action("Retrieving " .. self.remote .. ":" .. (self.commit or self.branch) .. "...") - system.init(path, self.remote) - system.fetch(path) - common.reset(path, self.commit or self.branch, "hard") - log_action("Retrieved " .. self:url() .. "...") - self.manifest = nil - end - local manifest, remotes = self:parse_manifest() - if pull_remotes then -- any remotes we don't have in our listing, call add, and add into the list - for i, remote in ipairs(remotes) do - if not common.first(repositories, function(repo) return repo.remote == remote.remote and repo.branch == remote.branch and repo.commit == remote.commit end) then - remote:add(pull_remotes == "recursive" and "recursive" or false) - table.insert(repositories, remote) - end - end - end - return self -end - - -function Repository:update(pull_remotes) - local manifest, remotes = self:parse_manifest() - if self.branch then - local path = self.local_path .. PATHSEP .. self.branch - system.fetch(path) - common.reset(path, self.branch, "hard") - log_action("Updated " .. self:url()) - self.manifest = nil - manifest, remotes = self:parse_manifest() - end - if pull_remotes then -- any remotes we don't have in our listing, call add, and add into the list - for i, remote in ipairs(remotes) do - if common.first(repositories, function(repo) return repo.remote == remote.remote and repo.branch == remote.branch and repo.commit == remote.comit end) then - remote:add(pull_remotes == "recursive" and "recursive" or false) - table.insert(repositories, remote) - end - end - end -end - - -function Repository:remove() - common.rmrf(self.local_path .. PATHSEP .. (self.commit or self.branch)) - if #system.ls(self.local_path) == 0 then common.rmrf(self.local_path) end -end - - -function LiteXL.__index(t, k) return LiteXL[k] end -function LiteXL.new(repository, metadata) - if not metadata.version then error("lite-xl entry requires a version") end - local self = setmetatable({ - repository = repository, - version = metadata.version, - remote = metadata.remote, - url = metadata.url, - tags = metadata.tags or {}, - mod_version = metadata.mod_version, - path = metadata.path, - files = metadata.files or {} - }, LiteXL) - self.hash = system.hash((repository and repository:url() or "") .. "-" .. metadata.version .. common.join("", common.map(self.files, function(f) return f.checksum end))) - self.local_path = self:is_local() and self.path or (CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. self.version .. PATHSEP .. self.hash) - return self -end - -function LiteXL:get_binary_path() return self.local_path .. PATHSEP .. "lite-xl" end -function LiteXL:get_data_directory() return self.local_path .. PATHSEP .. "data" end -function LiteXL:is_system() return system_bottle and system_bottle.lite_xl == self end -function LiteXL:is_local() return not self.repository and self.path end -function LiteXL:is_compatible(plugin) return compare_version(self.mod_version, plugin.mod_version) == 0 end -function LiteXL:is_installed() return system.stat(self.local_path) end - -function LiteXL:install() - if self:is_installed() then log_warning("lite-xl " .. self.version .. " already installed") return end - common.mkdirp(self.local_path) - if system_bottle.lite_xl == self then -- system lite-xl. We have to copy it because we can't really set the user directory. - local executable, datadir = common.path("lite-xl") - if not executable then error("can't find system lite-xl executable") end - local stat = system.stat(executable) - executable = stat.symlink and stat.symlink or executable - datadir = common.dirname(executable) .. PATHSEP .. "data" - if not system.stat(datadir) then error("can't find system lite-xl data dir") end - common.copy(executable, self.local_path .. PATHSEP .. "lite-xl") - system.chmod(self.local_path .. PATHSEP .. "lite-xl", 448) -- chmod to rwx------- - common.copy(datadir, self.local_path .. PATHSEP .. "data") - elseif self.path and not self.repository then -- local repository - system.symlink(self:get_binary_path(), self.path .. PATHSEP .. "lite_xl") - else - if self.remote then - system.init(self.local_path, self.remote) - common.reset(self.local_path, self.commit or self.branch) - end - for i,file in ipairs(self.files or {}) do - if file.arch and file.arch == ARCH then - if not file.checksum then error("requires a checksum") end - local basename = common.basename(file.url) - local archive = basename:find("%.zip$") or basename:find("%.tar%.gz$") - local path = self.local_path .. PATHSEP .. (archive and basename or "lite-xl") - log_action("Downloading file " .. file.url .. "...") - common.get(file.url, path, file.checksum) - log_action("Downloaded file " .. file.url .. " to " .. path) - if system.hash(path, "file") ~= file.checksum then fatal_warning("checksum doesn't match for " .. path) end - if archive then - log_action("Extracting file " .. basename .. " in " .. self.local_path) - system.extract(path, self.local_path) - end - end - end - end - if not system.stat(self.local_path .. PATHSEP .. "lite-xl") then error("can't find executable for lite-xl " .. self.version) end -end - -function LiteXL:uninstall() - if not system.stat(self.local_path) then error("lite-xl " .. self.version .. " not installed") end - common.rmrf(self.local_path) -end - - -function Bottle.__index(t, k) return Bottle[k] end -function Bottle.new(lite_xl, plugins, is_system) - local self = setmetatable({ - lite_xl = lite_xl, - plugins = plugins, - is_system = is_system - }, Bottle) - if not is_system then - table.sort(self.plugins, function(a, b) return (a.name .. ":" .. a.version) < (b.name .. ":" .. b.version) end) - self.hash = system.hash(lite_xl.version .. " " .. common.join(" ", common.map(self.plugins, function(p) return p.name .. ":" .. p.version end))) - self.local_path = CACHEDIR .. PATHSEP .. "bottles" .. PATHSEP .. self.hash - end - return self -end - -function Bottle:is_constructed() return self.is_system or system.stat(self.local_path) end - -function Bottle:construct() - if self.is_system then error("system bottle cannot be constructed") end - if self:is_constructed() then error("bottle " .. self.hash .. " already constructed") end - if not self.lite_xl:is_installed() then self.lite_xl:install() end - common.mkdirp(self.local_path .. PATHSEP .. "user") - common.copy(self.lite_xl.local_path .. PATHSEP .. "lite-xl", self.local_path .. PATHSEP .. "lite-xl") - system.chmod(self.local_path .. PATHSEP .. "lite-xl", 448) -- chmod to rwx------- - common.copy(self.lite_xl.local_path .. PATHSEP .. "data", self.local_path .. PATHSEP .. "data") - for i,plugin in ipairs(self.plugins) do plugin:install(self) end -end - -function Bottle:destruct() - if self.is_system then error("system bottle cannot be destructed") end - if not self:is_constructed() then error("lite-xl " .. self.version .. " not constructed") end - common.rmrf(self.local_path) -end - -function Bottle:run(args) - args = args or {} - if self.is_system then error("system bottle cannot be run") end - os.execute(self.local_path .. PATHSEP .. "lite-xl", table.unpack(args)) -end - -function Bottle:all_plugins() - local t = common.flat_map(repositories, function(r) return r.plugins end) - local hash = { } - for i, v in ipairs(t) do hash[v.name] = v end - local plugin_paths = { - (self.local_path and (self.local_path .. PATHSEP .. "user") or USERDIR) .. PATHSEP .. "plugins", - self.lite_xl:get_data_directory() .. PATHSEP .. "plugins" - } - for i, plugin_path in ipairs(common.grep(plugin_paths, function(e) return system.stat(e) end)) do - for k, v in ipairs(common.grep(system.ls(plugin_path), function(e) return not hash[e:gsub("%.lua$", "")] end)) do - table.insert(t, Plugin.new(nil, { - name = v:gsub("%.lua$", ""), - type = i == 2 and "core", - organization = (v:find("%.lua$") and "singleton" or "complex"), - mod_version = self.lite_xl.mod_version, - path = "plugins/" .. v, - version = "1.0" - })) - end - end - return t -end - -function Bottle:installed_plugins() - return common.grep(self:all_plugins(), function(p) return p:is_installed(self) end) -end - -function Bottle:get_plugin(name, version, filter) - local candidates = {} - local wildcard = name:find("%*$") - filter = filter or {} - for i,plugin in ipairs(self:all_plugins()) do - if not version and plugin.provides then - for k, provides in ipairs(plugin.provides) do - if provides == name then - table.insert(candidates, plugin) - end - end - end - if (plugin.name == name or (wildcard and plugin.name:find("^" .. name:sub(1, #name - 1)))) and match_version(plugin.version, version) then - if (not filter.mod_version or not plugin.mod_version or tonumber(plugin.mod_version) == tonumber(filter.mod_version)) then - table.insert(candidates, plugin) - end - end - end - return table.unpack(common.sort(candidates, function (a,b) return a.version < b.version end)) -end - - -local function get_repository(url) - if not url then error("requires a repository url") end - local r = Repository.url(url) - for i,v in ipairs(repositories) do - if v.remote == r.remote and v.branch == r.branch and v.commit == r.commit then return i, v end - end - return nil -end - - -local DEFAULT_REPOS -local function lpm_repo_init() - DEFAULT_REPOS = { Repository.url("https://github.com/adamharrison/lite-xl-plugin-manager.git:latest") } - if not system.stat(CACHEDIR .. PATHSEP .. "repos") then - for i, repository in ipairs(DEFAULT_REPOS) do - if not system.stat(repository.local_path) or not system.stat(repository.local_path .. PATHSEP .. (repository.commit or repository.branch)) then - table.insert(repositories, repository:add(true)) - end - end - end -end - - -local function lpm_repo_add(...) - for i, url in ipairs({ ... }) do - local idx, repo = get_repository(url) - if repo then -- if we're alreayd a repo, put this at the head of the resolution list - table.remove(repositories, idx) - else - repo = Repository.url(url):add(AUTO_PULL_REMOTES and "recursive" or false) - end - table.insert(repositories, 1, repo) - repo:update() - end -end - - -local function lpm_repo_rm(...) - for i, url in ipairs({ ... }) do - local idx, repo = get_repository(url) - if not repo then error("cannot find repository " .. url) end - table.remove(repositories, idx) - repo:remove() - end -end - - -local function lpm_repo_update(...) - local t = { ... } - if #t == 0 then table.insert(t, false) end - for i, url in ipairs(t) do - local repo = url and get_repository(url) - for i,v in ipairs(repositories) do - if not repo or v == repo then - v:update(AUTO_PULL_REMOTES and "recursive" or false) - end - end - end -end - -local function get_lite_xl(version) - return common.first(common.concat(lite_xls, common.flat_map(repositories, function(e) return e.lite_xls end)), function(lite_xl) return lite_xl.version == version end) -end - -local function lpm_lite_xl_save() - common.mkdirp(CACHEDIR .. PATHSEP .. "lite_xls") - common.write(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json", - json.encode(common.map(common.grep(lite_xls, function(l) return l:is_local() and not l:is_system() end), function(l) return { version = l.version, mod_version = l.mod_version, path = l.path } end)) - ) -end - -local function lpm_lite_xl_add(version, path) - if not version then error("requires a version") end - if not path then error("requires a path") end - if not system.stat(path .. PATHSEP .. "lite-xl") then error("can't find " .. path .. PATHSEP .. "lite-xl") end - if not system.stat(path .. PATHSEP .. "data") then error("can't find " .. path .. PATHSEP .. "data") end - table.insert(lite_xls, LiteXL.new(nil, { version = version, path = path:gsub(PATHSEP .. "$", ""), mod_version = MOD_VERSION or 3 })) - lpm_lite_xl_save() -end - -local function lpm_lite_xl_rm(version) - if not version then error("requires a version") end - local lite_xl = get_lite_xl(version) or error("can't find lite_xl version " .. version) - lite_xls = common.grep(lite_xls, function(l) return l ~= lite_xl end) - lpm_lite_xl_save() -end - -local function lpm_lite_xl_install(version) - if not version then error("requires a version") end - (get_lite_xl(version) or error("can't find lite-xl version " .. version)):install() -end - - -local function lpm_lite_xl_switch(version, target) - if not version then error("requires a version") end - target = target or common.path("lite-xl") - if not target then error("can't find installed lite-xl. please provide a target to install the symlink explicitly") end - local lite_xl = get_lite_xl(version) or error("can't find lite-xl version " .. version) - if not lite_xl:is_installed() then log_action("Installing lite-xl " .. lite_xl.version) lite_xl:install() end - local stat = system.stat(target) - if stat and stat.symlink then os.remove(target) end - system.symlink(lite_xl:get_binary_path(), target) - if not common.path('lite-xl') then - os.remove(target) - error(target .. " is not on your $PATH; please supply a target that can be found on your $PATH, called `lite-xl`.") - end -end - - -local function lpm_lite_xl_uninstall(version) - (get_lite_xl(version) or error("can't find lite-xl version " .. version)):uninstall() -end - - -local function lpm_lite_xl_list() - local result = { ["lite-xl"] = { } } - local max_version = 0 - for i,lite_xl in ipairs(lite_xls) do - table.insert(result["lite-xl"], { - version = lite_xl.version, - mod_version = lite_xl.mod_version, - tags = lite_xl.tags, - is_system = lite_xl:is_system(), - status = lite_xl:is_installed() and (lite_xl:is_local() and "local" or "installed") or "available", - local_path = lite_xl.local_path - }) - max_version = math.max(max_version, #lite_xl.version) - end - for i,repo in ipairs(repositories) do - if not repo.lite_xls then error("can't find lite-xl for repo " .. repo:url()) end - for j, lite_xl in ipairs(repo.lite_xls) do - table.insert(result["lite-xl"], { - version = lite_xl.version, - mod_version = lite_xl.mod_version, - repository = repo:url(), - tags = lite_xl.tags, - is_system = lite_xl:is_system(), - status = lite_xl:is_installed() and (lite_xl:is_local() and "local" or "installed") or "available", - local_path = lite_xl.local_path - }) - max_version = math.max(max_version, #lite_xl.version) - end - end - if JSON then - io.stdout:write(json.encode(result) .. "\n") - else - if VERBOSE then - for i, lite_xl in ipairs(result["lite-xl"]) do - if i ~= 0 then print("---------------------------") end - print("Version: " .. lite_xl.version) - print("Status: " .. lite_xl.status) - print("Mod-Version: " .. (lite_xl.mod_version or "unknown")) - print("Tags: " .. common.join(", ", lite_xl.tags)) - end - else - max_version = max_version + 2 - print(string.format("%" .. max_version .. "s | %10s | %s", "Version", "Status", "Location")) - print(string.format("%" .. max_version .."s | %10s | %s", "-------", "---------", "---------------------------")) - for i, lite_xl in ipairs(result["lite-xl"]) do - print(string.format("%" .. max_version .. "s | %10s | %s", (lite_xl.is_system and "* " or "") .. lite_xl.version, lite_xl.status, (lite_xl.status ~= "available" and lite_xl.local_path or lite_xl.repository))) - end - end - end -end - -local function lpm_lite_xl_run(version, ...) - if not version then error("requires a version") end - local lite_xl = get_lite_xl(version) or error("can't find lite-xl version " .. version) - local plugins = {} - for i, str in ipairs({ ... }) do - local name, version = common.split(":", str) - local plugin = system_bottle:get_plugin(name, version, { mod_version = lite_xl.mod_version }) - if not plugin then error("can't find plugin " .. str) end - table.insert(plugins, plugin) - end - local bottle = Bottle.new(lite_xl, plugins) - if not bottle:is_constructed() then bottle:construct() end - bottle:run() -end - - -local function lpm_install(...) - for i, identifier in ipairs({ ... }) do - local s = identifier:find(":") - local name, version = (s and identifier:sub(1, s-1) or identifier), (s and identifier:sub(s+1) or nil) - if not name then error('unrecognized identifier ' .. identifier) end - if name == "lite-xl" then - lpm_lite_xl_install(version) - else - local plugins = { system_bottle:get_plugin(name, version, { mod_version = system_bottle.lite_xl.mod_version }) } - if #plugins == 0 then error("can't find plugin " .. name .. " mod-version: " .. (system_bottle.lite_xl.mod_version or 'any')) end - for j,v in ipairs(plugins) do v:install(system_bottle) end - end - end -end - - -local function lpm_plugin_uninstall(...) - for i, name in ipairs({ ... }) do - local plugins = { system_bottle:get_plugin(name) } - if #plugins == 0 then error("can't find plugin " .. name) end - local installed_plugins = common.grep(plugins, function(e) return e:is_installed(system_bottle) end) - if #installed_plugins == 0 then error("plugin " .. name .. " not installed") end - for i, plugin in ipairs(installed_plugins) do plugin:uninstall(system_bottle) end - end -end - -local function lpm_plugin_reinstall(...) for i, name in ipairs({ ... }) do pcall(lpm_plugin_uninstall, name) end lpm_install(...) end - -local function lpm_repo_list() - if JSON then - io.stdout:write(json.encode({ repositories = common.map(repositories, function(repo) return { remote = repo.remote, commit = repo.commit, branch = repo.branch, path = repo.local_path .. PATHSEP .. (repo.commit or repo.branch), remotes = common.map(repo.remotes or {}, function(r) return r:url() end) } end) }) .. "\n") - else - for i, repository in ipairs(repositories) do - local _, remotes = repository:parse_manifest() - if i ~= 0 then print("---------------------------") end - print("Remote : " .. repository:url()) - print("Path : " .. repository.local_path .. PATHSEP .. (repository.commit or repository.branch)) - print("Remotes: " .. json.encode(common.map(repository.remotes or {}, function(r) return r:url() end))) - end - end -end - -local function lpm_plugin_list() - local max_name = 0 - local result = { plugins = { } } - for j,plugin in ipairs(system_bottle:all_plugins()) do - max_name = math.max(max_name, #plugin.name) - local repo = plugin.repository - table.insert(result.plugins, { - name = plugin.name, - status = plugin.repository and (plugin:is_installed(system_bottle) and "installed" or (system_bottle.lite_xl:is_compatible(plugin) and "available" or "incompatible")) or (plugin:is_core(system_bottle) and "core" or "orphan"), - version = "" .. plugin.version, - dependencies = plugin.dependencies, - description = plugin.description, - mod_version = plugin.mod_version, - tags = plugin.tags, - type = plugin.type, - organization = plugin.organization, - repository = repo and repo:url() - }) - end - if JSON then - io.stdout:write(json.encode(result) .. "\n") - else - if not VERBOSE then - print(string.format("%" .. max_name .."s | %10s | %10s | %s", "Name", "Version", "ModVer", "Status")) - print(string.format("%" .. max_name .."s | %10s | %10s | %s", "--------------", "----------", "----------", "-----------")) - end - for i, plugin in ipairs(common.sort(result.plugins, function(a,b) return a.name < b.name end)) do - if VERBOSE then - if i ~= 0 then print("---------------------------") end - print("Name: " .. plugin.name) - print("Version: " .. plugin.version) - print("Status: " .. plugin.status) - print("Type: " .. plugin.type) - print("Orgnization: " .. plugin.organization) - print("Repository: " .. (plugin.repository or "orphan")) - print("Description: " .. (plugin.description or "")) - print("Mod-Version: " .. (plugin.mod_version or "unknown")) - print("Dependencies: " .. json.encode(plugin.dependencies)) - print("Tags: " .. common.join(", ", plugin.tags)) - elseif plugin.status ~= "incompatible" then - print(string.format("%" .. max_name .."s | %10s | %10s | %s", plugin.name, plugin.version, plugin.mod_version, plugin.status)) - end - end - end -end - -local function lpm_describe() - for i,v in ipairs(repositories) do - if #common.grep(DEFAULT_REPOS, function(r) return r:url() == v:url() end) == 0 then - io.stdout:write("lpm add " .. v:url() .. " && ") - end - end - print("lpm run " .. system_bottle.lite_xl.version .. " " .. common.join(" ", common.map(system_bottle:installed_plugins(), function(p) return p.name .. ":" .. p.version end))) -end - -local function lpm_plugin_upgrade() - for i,plugin in ipairs(system_bottle:installed_plugins()) do - local upgrade = common.sort(system_bottle:get_plugin(plugin.name, ">" .. plugin.version), function(a, b) return compare_version(b.version, a.version) end)[1] - if upgrade then upgrade:install(system_bottle) end - end -end - -local function lpm_purge() - -- local path = common.path("lite-xl") - -- if path then - -- local lite_xl = get_lite_xl("system") - -- if lite_xl then - -- os.remove(path) - -- system.symlink(lite_xl:get_binary_path(), target) - -- log_action("Reset lite-xl symlink to system.") - -- end - -- end - log_action("Removed " .. CACHEDIR .. ".") - common.rmrf(CACHEDIR) -end - -local function parse_arguments(arguments, options) - local args = {} - local i = 1 - while i <= #arguments do - local s,e, option, value = arguments[i]:find("%-%-([^=]+)=?(.*)") - if s then - local flag_type = options[option] - if not flag_type then error("unknown flag --" .. option) end - if flag_type == "flag" then - args[option] = true - elseif flag_type == "string" or flag_type == "number" then - if not value then - if i < #arguments then error("option " .. option .. " requires a " .. flag_type) end - value = arguments[i+1] - i = i + 1 - end - if flag_type == "number" and tonumber(flag_type) == nil then error("option " .. option .. " should be a number") end - args[option] = value - end - else - table.insert(args, arguments[i]) - end - i = i + 1 - end - return args -end - -local status = 0 -local function error_handler(err) - local s, e = err:find(":%d+") - local message = e and err:sub(e + 3) or err - if JSON then - if VERBOSE then - io.stderr:write(json.encode({ error = err, actions = actions, warnings = warnings, traceback = debug.traceback() }) .. "\n") - else - io.stderr:write(json.encode({ error = message or err, actions = actions, warnings = warnings }) .. "\n") - end - else - io.stderr:write((not VERBOSE and message or err) .. "\n") - if VERBOSE then io.stderr:write(debug.traceback() .. "\n") end - end - status = -1 -end - -local function run_command(ARGS) - if not ARGS[2]:find("%S") then return - elseif ARGS[2] == "init" then return - elseif ARGS[2] == "repo" and ARGV[3] == "add" then lpm_repo_add(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "repo" and ARGS[3] == "rm" then lpm_repo_rm(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "add" then lpm_repo_add(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "rm" then lpm_repo_rm(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "update" then lpm_repo_update(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "repo" and ARGS[3] == "update" then lpm_repo_update(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "repo" and (#ARGS == 2 or ARGS[3] == "list") then return lpm_repo_list() - elseif ARGS[2] == "plugin" and ARGS[3] == "install" then lpm_install(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "plugin" and ARGS[3] == "uninstall" then lpm_plugin_uninstall(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "plugin" and ARGS[3] == "reinstall" then lpm_plugin_reinstall(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "plugin" and (#ARGS == 2 or ARGS[3] == "list") then return lpm_plugin_list(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "plugin" and ARGS[3] == "upgrade" then return lpm_plugin_upgrade(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "upgrade" then return lpm_plugin_upgrade(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "install" then lpm_install(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "uninstall" then lpm_plugin_uninstall(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "reinstall" then lpm_plugin_reinstall(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "describe" then lpm_describe(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "list" then return lpm_plugin_list(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "lite-xl" and (#ARGS == 2 or ARGS[3] == "list") then return lpm_lite_xl_list(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "lite-xl" and ARGS[3] == "uninstall" then return lpm_lite_xl_uninstall(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "lite-xl" and ARGS[3] == "install" then return lpm_lite_xl_install(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "lite-xl" and ARGS[3] == "switch" then return lpm_lite_xl_switch(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "lite-xl" and ARGS[3] == "run" then return lpm_lite_xl_run(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "lite-xl" and ARGS[3] == "add" then return lpm_lite_xl_add(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "lite-xl" and ARGS[3] == "rm" then return lpm_lite_xl_rm(table.unpack(common.slice(ARGS, 4))) - elseif ARGS[2] == "run" then return lpm_lite_xl_run(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "switch" then return lpm_lite_xl_switch(table.unpack(common.slice(ARGS, 3))) - elseif ARGS[2] == "purge" then lpm_purge() - else error("unknown command: " .. ARGS[2]) end - if JSON then - io.stdout:write(json.encode({ actions = actions, warnings = warnings })) - end -end - - -xpcall(function() - local ARGS = parse_arguments(ARGV, { - json = "flag", userdir = "string", cachedir = "string", version = "flag", verbose = "flag", - quiet = "flag", version = "string", ["mod-version"] = "string", remotes = "flag", help = "flag", - remotes = "flag", ssl_certs = "string", force = "flag", arch = "string", ["assume-yes"] = "flag", - ["install-optional"] = "flag" - }) - if ARGS["version"] then - io.stdout:write(VERSION .. "\n") - return 0 - end - if ARGS["help"] or #ARGS == 1 or ARGS[2] == "help" then - io.stderr:write([[ -Usage: lpm COMMAND [...ARGUMENTS] [--json] [--userdir=directory] - [--cachedir=directory] [--quiet] [--version] [--help] [--remotes] - [--ssl_certs=directory/file] [--force] [--arch=]] .. _G.ARCH .. [[] - [--assume-yes] [--no-install-optional] [--verbose] [--mod-version=3] - -LPM is a package manager for `lite-xl`, written in C (and packed-in lua). - -It's designed to install packages from our central github repository (and -affiliated repositories), directly into your lite-xl user directory. It can -be called independently, for from the lite-xl `plugin_manager` plugin. - -LPM will always use https://github.com/lite-xl/lite-xl-plugins as its base -repository, if none are present, and the cache directory does't exist, -but others can be added, and this base one can be removed. - -It has the following commands: - - lpm init Implicitly called before all commands - if necessary, but can be called - independently to save time later. - lpm repo list List all extant repos. - lpm [repo] add Add a source repository. - [...] - lpm [repo] rm Remove a source repository. - [...] - lpm [repo] update [] Update all/the specified repos. - [...] - lpm [plugin] install Install specific plugins. - [:] If installed, upgrades. - [...:] - lpm [plugin] uninstall Uninstall the specific plugin. - [...] - lpm [plugin] reinstall Uninstall and installs the specific plugin. - [...] - lpm [plugin] list List all/associated plugins. - [...] - lpm [plugin] upgrade Upgrades all installed plugins - to new version if applicable. - lpm [lite-xl] install Installs lite-xl. Infers the - [binary] [datadir] paths on your system if not - supplied. Automatically - switches to be your system default - if path auto inferred. - lpm lite-xl add Adds a local version of lite-xl to - the managed list, allowing it to be - easily bottled. - lpm lite-xl remove Removes a local version of lite-xl - from the managed list. - lpm [lite-xl] switch [] Sets the active version of lite-xl - to be the specified version. Auto-detects - current install of lite-xl; if none found - path can be specifeid. - lpm lite-xl list Lists all installed versions of - lite-xl. - lpm run [...plugins] Sets up a "bottle" to run the specified - lite version, with the specified plugins - and then opens it. - lpm describe [bottle] Describes the bottle specified in the form - of a list of commands, that allow someone - else to run your configuration. - - lpm purge Completely purge all state for LPM. - lpm - Read these commands from stdin in - an interactive print-eval loop. - lpm help Displays this help text. - -Flags have the following effects: - - --json Performs all communication in JSON. - --userdir=directory Sets the lite-xl userdir manually. - If omitted, uses the normal lite-xl logic. - --cachedir=directory Sets the directory to store all repositories. - --tmpdir=directory During install, sets the staging area. - --verbose Spits out more information, including intermediate - steps to install and whatnot. - --quiet Outputs nothing but explicit responses. - --mod-version Sets the mod version of lite-xl to install plugins. - --version Returns version information. - --remotes Automatically adds any specified remotes in the - repository to the end of the resolution list. - This is a potential security risk, so be careful. - --help Displays this help text. - --ssl_certs Sets the SSL certificate store. - --arch Sets the architecture (default: ]] .. _G.ARCH .. [[). - --force Ignores checksum inconsitencies. - Not recommended; security risk. - --assume-yes Ignores any prompts, and automatically answers yes - to all. - --no-install-optional On install, anything marked as optional - won't prompt. -]] - ) - return 0 - end - - VERBOSE = ARGS["verbose"] or false - JSON = ARGS["json"] or os.getenv("LPM_JSON") - QUIET = ARGS["quiet"] or os.getenv("LPM_QUIET") - FORCE = ARGS["force"] - NO_INSTALL_OPTIONAL = ARGS["no-install-optional"] - ARCH = ARGS["arch"] or _G.ARCH - ASSUME_YES = ARGS["assume-yes"] or FORCE - MOD_VERSION = ARGS["mod-version"] or os.getenv("LPM_MODVERSION") - if MOD_VERSION == "any" then MOD_VERSION = nil end - HOME = (os.getenv("USERPROFILE") or os.getenv("HOME")):gsub(PATHSEP .. "$", "") - USERDIR = ARGS["userdir"] or os.getenv("LITE_USERDIR") or (os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. PATHSEP .. "lite-xl") - or (HOME and (HOME .. PATHSEP .. '.config' .. PATHSEP .. 'lite-xl')) - AUTO_PULL_REMOTES = ARGS["remotes"] - if not system.stat(USERDIR) then error("can't find user directory " .. USERDIR) end - CACHEDIR = ARGS["cachedir"] or os.getenv("LPM_CACHE") or USERDIR .. PATHSEP .. "lpm" - TMPDIR = ARGS["tmpdir"] or CACHEDIR .. "/tmp" - - repositories = {} - if ARGS[2] == "purge" then return lpm_purge() end - if ARGS["ssl_certs"] then - local stat = system.stat(ARGS["ssl_certs"]) - if not stat then error("can't find " .. ARGS["ssl_certs"]) end - system.certs(stat.type, ARGS["ssl_certs"]) - elseif not os.getenv("SSL_CERT_DIR") and not os.getenv("SSL_CERT_FILE") then - local paths = { -- https://serverfault.com/questions/62496/ssl-certificate-location-on-unix-linux#comment1155804_62500 - "/etc/ssl/certs/ca-certificates.crt", -- Debian/Ubuntu/Gentoo etc. - "/etc/pki/tls/certs/ca-bundle.crt", -- Fedora/RHEL 6 - "/etc/ssl/ca-bundle.pem", -- OpenSUSE - "/etc/pki/tls/cacert.pem", -- OpenELEC - "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", -- CentOS/RHEL 7 - "/etc/ssl/cert.pem", -- Alpine Linux - "/etc/ssl/certs", -- SLES10/SLES11, https://golang.org/issue/12139 - "/system/etc/security/cacerts", -- Android - "/usr/local/share/certs", -- FreeBSD - "/etc/pki/tls/certs", -- Fedora/RHEL - "/etc/openssl/certs", -- NetBSD - "/var/ssl/certs", -- AIX - } - for i, path in ipairs(paths) do - local stat = system.stat(path) - if stat then - system.certs(stat.type, path) - break - end - end - end - - - -- Base setup; initialize default repos if applicable, read them in. Determine Lite XL system binary if not specified, and pull in a list of all local lite-xl's. - lpm_repo_init() - repositories = {} - for i, remote_hash in ipairs(system.ls(CACHEDIR .. PATHSEP .. "repos")) do - local remote - for j, commit_or_branch in ipairs(system.ls(CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. remote_hash)) do - if system.stat(CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. remote_hash .. PATHSEP .. commit_or_branch .. PATHSEP .. ".git" .. PATHSEP .."config") then - for line in io.lines(CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. remote_hash .. PATHSEP .. commit_or_branch .. PATHSEP .. ".git" .. PATHSEP .."config") do - local s,e = line:find("url = ") - if s then remote = line:sub(e+1) break end - end - if remote then - table.insert(repositories, Repository.url(remote .. ":" .. commit_or_branch)) - repositories[#repositories]:parse_manifest() - end - end - end - end - - lite_xls = {} - if system.stat(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json") then - for i, lite_xl in ipairs(json.decode(io.open(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json", "rb"):read("*all"))) do - table.insert(lite_xls, LiteXL.new(nil, { version = lite_xl.version, mod_version = lite_xl.mod_version, path = lite_xl.path, tags = { "local" } })) - end - end - local lite_xl_binary = common.path("lite-xl") - if lite_xl_binary then - lite_xl_binary = system.stat(lite_xl_binary).symlink or lite_xl_binary - local directory = common.dirname(lite_xl_binary) - local hash = system.hash(lite_xl_binary, "file") - local system_lite_xl = common.first(common.concat(common.flat_map(repositories, function(r) return r.lite_xls end), lite_xls), function(lite_xl) return lite_xl.local_path == directory end) - if not system_lite_xl then - if #common.grep(lite_xls, function(e) return e.version == "system" end) > 0 then error("can't create new system lite, please `lpm rm lite-xl system`, or resolve otherwise") end - system_lite_xl = LiteXL.new(nil, { path = directory, mod_version = 3, version = "system", tags = { "system", "local" } }) - table.insert(lite_xls, system_lite_xl) - lpm_lite_xl_save() - else - table.insert(system_lite_xl.tags, "system") - end - system_bottle = Bottle.new(system_lite_xl, nil, true) - end - if not system_bottle then system_bottle = Bottle.new(nil, nil, true) end - - if ARGS[2] ~= '-' then - run_command(ARGS) - else - while true do - local line = io.stdin:read("*line") - if line == "quit" or line == "exit" then return 0 end - local args = { ARGS[1] } - local s = 1 - while true do - local a,e = line:find("%s+", s) - table.insert(args, line:sub(s, a and (a - 1) or #line)) - if not e then break end - s = e + 1 - end - xpcall(function() - run_command(args) - end, error_handler) - actions, warnings = {}, {} - end - end - -end, error_handler) - - -return status diff --git a/src/lpm.c b/src/lpm.c new file mode 100644 index 0000000..760c372 --- /dev/null +++ b/src/lpm.c @@ -0,0 +1,575 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifdef _WIN32 + #include + #include + #include + #include +#else + #define MAX_PATH PATH_MAX +#endif + +static char hex_digits[] = "0123456789abcdef"; +static int lpm_hash(lua_State* L) { + size_t len; + const char* data = luaL_checklstring(L, 1, &len); + const char* type = luaL_optstring(L, 2, "string"); + unsigned char buffer[EVP_MAX_MD_SIZE]; + EVP_MD_CTX* c = EVP_MD_CTX_new(); + EVP_MD_CTX_init(c); + EVP_DigestInit_ex(c, EVP_sha256(), NULL); + if (strcmp(type, "file") == 0) { + FILE* file = fopen(data, "rb"); + if (!file) { + EVP_DigestFinal(c, buffer, NULL); + return luaL_error(L, "can't open %s", data); + } + while (1) { + unsigned char chunk[4096]; + size_t bytes = fread(chunk, 1, sizeof(chunk), file); + EVP_DigestUpdate(c, chunk, bytes); + if (bytes < 4096) + break; + } + fclose(file); + } else { + EVP_DigestUpdate(c, data, len); + } + int digest_length; + EVP_DigestFinal(c, buffer, &digest_length); + EVP_MD_CTX_free(c); + char hex_buffer[EVP_MAX_MD_SIZE * 2]; + for (size_t i = 0; i < digest_length; ++i) { + hex_buffer[i*2+0] = hex_digits[buffer[i] >> 4]; + hex_buffer[i*2+1] = hex_digits[buffer[i] & 0xF]; + } + lua_pushlstring(L, hex_buffer, digest_length * 2); + hex_buffer[digest_length*2]=0; + return 1; +} + +int lpm_symlink(lua_State* L) { + #ifndef _WIN32 + if (symlink(luaL_checkstring(L, 1), luaL_checkstring(L, 2))) + return luaL_error(L, "can't create symlink %s: %s", luaL_checkstring(L, 2), strerror(errno)); + return 0; + #else + return luaL_error(L, "can't create symbolic link %s: your operating system sucks", luaL_checkstring(L, 2)); + #endif +} + +int lpm_chmod(lua_State* L) { + if (chmod(luaL_checkstring(L, 1), luaL_checkinteger(L, 2))) + return luaL_error(L, "can't chmod %s: %s", luaL_checkstring(L, 1), strerror(errno)); + return 0; +} + +/** BEGIN STOLEN LITE CODE **/ +#if _WIN32 +static LPWSTR utfconv_utf8towc(const char *str) { + LPWSTR output; + int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (len == 0) + return NULL; + output = (LPWSTR) malloc(sizeof(WCHAR) * len); + if (output == NULL) + return NULL; + len = MultiByteToWideChar(CP_UTF8, 0, str, -1, output, len); + if (len == 0) { + free(output); + return NULL; + } + return output; +} + +static char *utfconv_wctoutf8(LPCWSTR str) { + char *output; + int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL); + if (len == 0) + return NULL; + output = (char *) malloc(sizeof(char) * len); + if (output == NULL) + return NULL; + len = WideCharToMultiByte(CP_UTF8, 0, str, -1, output, len, NULL, NULL); + if (len == 0) { + free(output); + return NULL; + } + return output; +} +#endif + +static int lpm_ls(lua_State *L) { + const char *path = luaL_checkstring(L, 1); + +#ifdef _WIN32 + lua_settop(L, 1); + lua_pushstring(L, path[0] == 0 || strchr("\\/", path[strlen(path) - 1]) != NULL ? "*" : "/*"); + lua_concat(L, 2); + path = lua_tostring(L, -1); + + LPWSTR wpath = utfconv_utf8towc(path); + if (wpath == NULL) + return luaL_error(L, "can't ls %s: invalid utf8 character conversion", path); + + WIN32_FIND_DATAW fd; + HANDLE find_handle = FindFirstFileExW(wpath, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, 0); + free(wpath); + if (find_handle == INVALID_HANDLE_VALUE) + return luaL_error(L, "can't ls %s: %d", path, GetLastError()); + char mbpath[MAX_PATH * 4]; // utf-8 spans 4 bytes at most + int len, i = 1; + lua_newtable(L); + + do + { + if (wcscmp(fd.cFileName, L".") == 0) { continue; } + if (wcscmp(fd.cFileName, L"..") == 0) { continue; } + + len = WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, mbpath, MAX_PATH * 4, NULL, NULL); + if (len == 0) { break; } + lua_pushlstring(L, mbpath, len - 1); // len includes \0 + lua_rawseti(L, -2, i++); + } while (FindNextFileW(find_handle, &fd)); + + int err = GetLastError(); + FindClose(find_handle); + if (err != ERROR_NO_MORE_FILES) + return luaL_error(L, "can't ls %s: %d", path, GetLastError()); + return 1; +#else + DIR *dir = opendir(path); + if (!dir) + return luaL_error(L, "can't ls %s: %d", path, strerror(errno)); + lua_newtable(L); + int i = 1; + struct dirent *entry; + while ( (entry = readdir(dir)) ) { + if (strcmp(entry->d_name, "." ) == 0) { continue; } + if (strcmp(entry->d_name, "..") == 0) { continue; } + lua_pushstring(L, entry->d_name); + lua_rawseti(L, -2, i); + i++; + } + closedir(dir); + return 1; +#endif +} + +static int lpm_rmdir(lua_State *L) { + const char *path = luaL_checkstring(L, 1); +#ifdef _WIN32 + LPWSTR wpath = utfconv_utf8towc(path); + int deleted = RemoveDirectoryW(wpath); + free(wpath); + if (!deleted) + return luaL_error(L, "can't rmdir %s: %d", path, GetLastError()); +#else + if (remove(path)) + return luaL_error(L, "can't rmdir %s: %s", path, strerror(errno)); +#endif + return 0; +} + +static int lpm_mkdir(lua_State *L) { + const char *path = luaL_checkstring(L, 1); +#ifdef _WIN32 + LPWSTR wpath = utfconv_utf8towc(path); + if (wpath == NULL) + return luaL_error(L, "can't mkdir %s: invalid utf8 character conversion", path); + int err = _wmkdir(wpath); + free(wpath); +#else + int err = mkdir(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); +#endif + if (err < 0) + return luaL_error(L, "can't mkdir %s: %s", path, strerror(errno)); + return 0; +} + +static int lpm_stat(lua_State *L) { + const char *path = luaL_checkstring(L, 1); + lua_newtable(L); +#ifdef _WIN32 + #define realpath(x, y) _wfullpath(y, x, MAX_PATH) + struct _stat s; + LPWSTR wpath = utfconv_utf8towc(path); + if (wpath == NULL) + return luaL_error(L, "can't stat %s: invalid utf8 character conversion", path); + int err = _wstat(wpath, &s); + LPWSTR wfullpath = realpath(wpath, NULL); + free(wpath); + if (!wfullpath) return 0; + char *abs_path = utfconv_wctoutf8(wfullpath); + free(wfullpath); +#else + struct stat s; + int err = lstat(path, &s); + char *abs_path = realpath(path, NULL); +#endif + if (err || !abs_path) { + lua_pushnil(L); + lua_pushstring(L, strerror(errno)); + return 2; + } + lua_pushstring(L, abs_path); lua_setfield(L, -2, "abs_path"); + lua_pushvalue(L, 1); lua_setfield(L, -2, "path"); + +#if __linux__ + if (S_ISLNK(s.st_mode)) { + char buffer[PATH_MAX]; + ssize_t len = readlink(path, buffer, sizeof(buffer)); + if (len < 0) + return 0; + lua_pushlstring(L, buffer, len); + } else + lua_pushnil(L); + lua_setfield(L, -2, "symlink"); + if (S_ISLNK(s.st_mode)) + err = stat(path, &s); + if (err) + return 1; +#endif + lua_pushinteger(L, s.st_mtime); lua_setfield(L, -2, "modified"); + lua_pushinteger(L, s.st_size); lua_setfield(L, -2, "size"); + if (S_ISREG(s.st_mode)) { + lua_pushstring(L, "file"); + } else if (S_ISDIR(s.st_mode)) { + lua_pushstring(L, "dir"); + } else { + lua_pushnil(L); + } + lua_setfield(L, -2, "type"); + return 1; +} +/** END STOLEN LITE CODE **/ + +static const char* git_error_last_string() { + const git_error* last_error = git_error_last(); + return last_error->message; +} + +static int git_get_id(git_oid* commit_id, git_repository* repository, const char* name) { + int length = strlen(name); + int is_hex = length == 40; + for (int i = 0; is_hex && i < length; ++i) + is_hex = isxdigit(name[i]); + if (!is_hex) + return git_reference_name_to_id(commit_id, repository, name); + return git_oid_fromstr(commit_id, name); +} + +static git_repository* luaL_checkgitrepo(lua_State* L, int index) { + const char* path = luaL_checkstring(L, index); + git_repository* repository; + if (git_repository_open(&repository, path)) + return (void*)(long long)luaL_error(L, "git open error: %s", git_error_last_string()); + return repository; +} + + +static git_commit* git_retrieve_commit(git_repository* repository, const char* commit_name) { + git_oid commit_id; + git_commit* commit; + if (git_get_id(&commit_id, repository, commit_name) || git_commit_lookup(&commit, repository, &commit_id)) + return NULL; + return commit; +} + + +static int lpm_reset(lua_State* L) { + git_repository* repository = luaL_checkgitrepo(L, 1); + const char* commit_name = luaL_checkstring(L, 2); + const char* type = luaL_checkstring(L, 3); + git_commit* commit = git_retrieve_commit(repository, commit_name); + if (!commit) { + git_repository_free(repository); + return luaL_error(L, "git retrieve commit error: %s", git_error_last_string()); + } + git_reset_t reset_type = GIT_RESET_SOFT; + if (strcmp(type, "mixed") == 0) + reset_type = GIT_RESET_MIXED; + else if (strcmp(type, "hard") == 0) + reset_type = GIT_RESET_HARD; + int result = git_reset(repository, (git_object*)commit, reset_type, NULL); + git_commit_free(commit); + git_repository_free(repository); + if (result) + return luaL_error(L, "git reset error: %s", git_error_last_string()); + return 0; +} + + +static int lpm_init(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + const char* url = luaL_checkstring(L, 2); + git_repository* repository; + if (git_repository_init(&repository, path, 0)) + return luaL_error(L, "git init error: %s", git_error_last_string()); + git_remote* remote; + if (git_remote_create(&remote, repository, "origin", url)) { + git_repository_free(repository); + return luaL_error(L, "git remote add error: %s", git_error_last_string()); + } + git_remote_free(remote); + git_repository_free(repository); + return 0; +} + + +static int lpm_fetch(lua_State* L) { + git_repository* repository = luaL_checkgitrepo(L, 1); + git_remote* remote; + if (git_remote_lookup(&remote, repository, "origin")) { + git_repository_free(repository); + return luaL_error(L, "git remote fetch error: %s", git_error_last_string()); + } + git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; + if (git_remote_fetch(remote, NULL, &fetch_opts, NULL)) { + git_remote_free(remote); + git_repository_free(repository); + return luaL_error(L, "git remote fetch error: %s", git_error_last_string()); + } + git_remote_free(remote); + git_repository_free(repository); + return 0; +} + + +static CURL *curl; +static int lpm_certs(lua_State* L) { + const char* type = luaL_checkstring(L, 1); + const char* path = luaL_checkstring(L, 2); + if (strcmp(type, "dir") == 0) { + git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, path); + curl_easy_setopt(curl, CURLOPT_CAINFO, path); + } else { + git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, path, NULL); + curl_easy_setopt(curl, CURLOPT_CAPATH, path); + } + return 0; +} + +static int lpm_extract(lua_State* L) { + const char* src = luaL_checkstring(L, 1); + const char* dst = luaL_optstring(L, 2, "."); + + char error_buffer[1024] = {0}; + struct archive_entry *entry; + const void *buff; + int flags = 0; + int r; + size_t size; +#if ARCHIVE_VERSION_NUMBER >= 3000000 + int64_t offset; +#else + off_t offset; +#endif + struct archive *ar = archive_read_new(); + struct archive *aw = archive_write_disk_new(); + archive_write_disk_set_options(aw, flags); + archive_read_support_format_tar(ar); + archive_read_support_format_zip(ar); + archive_read_support_filter_gzip(ar); + if ((r = archive_read_open_filename(ar, src, 10240))) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); + goto cleanup; + } + for (;;) { + int r = archive_read_next_header(ar, &entry); + if (r == ARCHIVE_EOF) + break; + if (r != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); + goto cleanup; + } + char path[MAX_PATH]; + strcpy(path, dst); strcat(path, "/"); + strncat(path, archive_entry_pathname(entry), sizeof(path) - 3); path[MAX_PATH-1] = 0; + archive_entry_set_pathname(entry, path); + if (archive_write_header(aw, entry) != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); + goto cleanup; + } + for (;;) { + int r = archive_read_data_block(ar, &buff, &size, &offset); + if (r == ARCHIVE_EOF) + break; + if (r != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); + goto cleanup; + } + if (archive_write_data_block(aw, buff, size, offset) != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); + goto cleanup; + } + } + if (archive_write_finish_entry(aw) != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); + goto cleanup; + } + } + cleanup: + archive_read_close(ar); + archive_read_free(ar); + archive_write_close(aw); + archive_write_free(aw); + if (error_buffer[0]) + return luaL_error(L, "error extracting archive %s: %s", src, archive_error_string(ar)); + return 0; +} + +static size_t lpm_curl_write_callback(char *ptr, size_t size, size_t nmemb, void *BL) { + luaL_Buffer* B = BL; + luaL_addlstring(B, ptr, size*nmemb); + return size*nmemb; +} + +static int lpm_get(lua_State* L) { + long response_code; + const char* url = luaL_checkstring(L, 1); + const char* path = luaL_optstring(L, 2, NULL); + // curl_easy_reset(curl); + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + #ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); + #endif + if (path) { + FILE* file = fopen(path, "wb"); + if (!file) + return luaL_error(L, "error opening file %s: %s", path, strerror(errno)); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + fclose(file); + return luaL_error(L, "curl error accessing %s: %s", url, curl_easy_strerror(res)); + } + fclose(file); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 200) + return luaL_error(L, "curl error accessing %s, non-200 response code: %d", url, response_code); + lua_pushnil(L); + lua_newtable(L); + return 2; + } else { + luaL_Buffer B; + luaL_buffinit(L, &B); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, lpm_curl_write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &B); + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) + return luaL_error(L, "curl error accessing %s: %s", url, curl_easy_strerror(res)); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 200) + return luaL_error(L, "curl error accessing %s, non-200 response code: %d", url, response_code); + luaL_pushresult(&B); + lua_newtable(L); + } + return 2; +} + + +static const luaL_Reg system_lib[] = { + { "ls", lpm_ls }, // Returns an array of files. + { "stat", lpm_stat }, // Returns info about a single file. + { "mkdir", lpm_mkdir }, // Makes a directory. + { "rmdir", lpm_rmdir }, // Removes a directory. + { "hash", lpm_hash }, // Returns a hex sha256 hash. + { "symlink", lpm_symlink }, // Creates a symlink. + { "chmod", lpm_chmod }, // Chmod's a file. + { "init", lpm_init }, // Initializes a git repository with the specified remote. + { "fetch", lpm_fetch }, // Updates a git repository with the specified remote. + { "reset", lpm_reset }, // Updates a git repository to the specified commit/hash/branch. + { "get", lpm_get }, // HTTP(s) GET request. + { "extract", lpm_extract }, // Extracts .tar.gz, and .zip files. + { "certs", lpm_certs }, // Sets the SSL certificate chain folder/file. + { NULL, NULL } +}; + + +#ifndef LPM_VERSION + #define LPM_VERSION "unknown" +#endif + + +#ifndef LITE_ARCH_TUPLE + #if __x86_64__ || _WIN64 || __MINGW64__ + #define ARCH_PROCESSOR "x86_64" + #else + #define ARCH_PROCESSOR "x86" + #endif + #if _WIN32 + #define ARCH_PLATFORM "windows" + #elif __linux__ + #define ARCH_PLATFORM "linux" + #elif __APPLE__ + #define ARCH_PLATFORM "darwin" + #else + #error "Please define -DLITE_ARCH_TUPLE." + #endif + #define LITE_ARCH_TUPLE ARCH_PROCESSOR "-" ARCH_PLATFORM +#endif + + +extern const char lpm_lua[]; +extern unsigned int lpm_lua_len; +int main(int argc, char* argv[]) { + curl = curl_easy_init(); + if (!curl) + return -1; + git_libgit2_init(); + lua_State* L = luaL_newstate(); + luaL_openlibs(L); + luaL_newlib(L, system_lib); + lua_setglobal(L, "system"); + lua_newtable(L); + for (int i = 0; i < argc; ++i) { + lua_pushstring(L, argv[i]); + lua_rawseti(L, -2, i+1); + } + lua_setglobal(L, "ARGV"); + lua_pushliteral(L, LPM_VERSION); + lua_setglobal(L, "VERSION"); + #if _WIN32 + lua_pushliteral(L, "windows"); + lua_pushliteral(L, "\\"); + #else + lua_pushliteral(L, "posix"); + lua_pushliteral(L, "/"); + #endif + lua_setglobal(L, "PATHSEP"); + lua_setglobal(L, "PLATFORM"); + lua_pushliteral(L, LITE_ARCH_TUPLE); + lua_setglobal(L, "ARCH"); + #if LPM_LIVE + if (luaL_loadfile(L, "lpm.lua") || lua_pcall(L, 0, 1, 0)) { + #else + if (luaL_loadbuffer(L, lpm_lua, lpm_lua_len, "lpm.lua") || lua_pcall(L, 0, 1, 0)) { + #endif + fprintf(stderr, "internal error when starting the application: %s\n", lua_tostring(L, -1)); + return -1; + } + int status = lua_tointeger(L, -1); + lua_close(L); + git_libgit2_shutdown(); + curl_easy_cleanup(curl); + return status; +} diff --git a/src/lpm.lua b/src/lpm.lua new file mode 100644 index 0000000..c0752f0 --- /dev/null +++ b/src/lpm.lua @@ -0,0 +1,1612 @@ +setmetatable(_G, { __index = function(t, k) if not rawget(t, k) then error("cannot get undefined global variable: " .. k, 2) end end, __newindex = function(t, k) error("cannot set global variable: " .. k, 2) end }) + +-- Begin rxi JSON library. +local json = { _version = "0.1.2" } +local encode +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + +-- End JSON library. +local function is_commit_hash(hash) + return #hash == 40 and not hash:find("[^a-z0-9]") +end + + +local common = {} +function common.merge(dst, src) for k, v in pairs(src) do dst[k] = v end return dst end +function common.map(l, p) local t = {} for i, v in ipairs(l) do table.insert(t, p(v)) end return t end +function common.flat_map(l, p) local t = {} for i, v in ipairs(l) do local r = p(v) for k, w in ipairs(r) do table.insert(t, w) end end return t end +function common.concat(t1, t2) local t = {} for i, v in ipairs(t1) do table.insert(t, v) end for i, v in ipairs(t2) do table.insert(t, v) end return t end +function common.grep(l, p) local t = {} for i, v in ipairs(l) do if p(v) then table.insert(t, v) end end return t end +function common.first(l, p) for i, v in ipairs(l) do if p(v) then return v end end end +function common.slice(t, i, l) local n = {} for j = i, l ~= nil and (i - l) or #t do table.insert(n, t[j]) end return n end +function common.join(j, l) local s = "" for i, v in ipairs(l) do if i > 1 then s = s .. j .. v else s = v end end return s end +function common.sort(t, f) table.sort(t, f) return t end +function common.write(path, contents) local f, err = io.open(path, "wb") if not f then error("can't write to " .. path .. ": " .. err) end f:write(contents) f:flush() end +function common.split(splitter, str) + local o = 1 + local res = {} + while true do + local s, e = str:find(splitter, o) + table.insert(res, str:sub(o, s and (s - 1) or #str)) + if not s then break end + o = e + 1 + end + return table.unpack(res) +end + +function common.dirname(path) local s = path:reverse():find("[/\\]") if not s then return path end return path:sub(1, #path - s) end +function common.basename(path) local s = path:reverse():find("[/\\]") if not s then return path end return path:sub(#path - s + 2) end +function common.path(exec) return common.first(common.map({ common.split(":", os.getenv("PATH")) }, function(e) return e .. PATHSEP .. exec end), function(e) return system.stat(e) end) end +function common.rmrf(root) + local info = root and root ~= "" and system.stat(root) + if not info then return end + if info.type == "file" or info.symlink then return os.remove(root) end + for i,v in ipairs(system.ls(root)) do common.rmrf(root .. PATHSEP .. v) end + system.rmdir(root) +end +function common.mkdirp(path) + local stat = system.stat(path) + if stat and stat.type == "dir" then return true end + if stat and stat.type == "file" then error("path " .. path .. " exists") end + local target + for _, dirname in ipairs({ common.split("[/\\]", path) }) do + target = target and target .. PATHSEP .. dirname or dirname + if target ~= "" and not system.stat(target) then system.mkdir(target) end + end +end +function common.copy(src, dst) + local src_stat, dst_stat = system.stat(src), system.stat(dst) + if not src_stat then error("can't find " .. src) end + if dst_stat and dst_stat.type == "dir" then return common.copy(src, dst .. PATHSEP .. common.basename(src)) end + if src_stat.type == "dir" then + common.mkdirp(dst) + for i, file in ipairs(system.ls(src)) do common.copy(src .. PATHSEP .. file, dst .. PATHSEP .. file) end + else + local src_io, err1 = io.open(src, "rb") + if err1 then error("can't open for reading " .. src .. ": " .. err1) end + local dst_io, err2 = io.open(dst, "wb") + if err2 then error("can't open for writing " .. dst .. ": " .. err2) end + while true do + local chunk = src_io:read(64*1024) + if not chunk then break end + dst_io:write(chunk) + end + dst_io:close() + end +end +function common.rename(src, dst) + local _, err = os.rename(src, dst) + if err then error("can't rename file " .. src .. " to " .. dst .. ": " .. err) end +end +function common.reset(path, ref, type) + if is_commit_hash(ref) then + system.reset(path, ref, type) + else + if not pcall(system.reset, path, "refs/tags/" .. ref, type) then system.reset(path, "refs/remotes/origin/" .. ref, type) end + end +end + +local HOME, USERDIR, CACHEDIR, JSON, VERBOSE, MOD_VERSION, QUIET, FORCE, AUTO_PULL_REMOTES, ARCH, ASSUME_YES, NO_INSTALL_OPTIONAL, TMPDIR, repositories, lite_xls, system_bottle + +local Plugin, Repository, LiteXL, Bottle = {}, {}, {}, {} + +local actions, warnings = {}, {} +local function log_action(message) + if JSON then table.insert(actions, message) end + if not QUIET then io.stderr:write(message .. "\n") end +end +local function log_warning(message) + if JSON then table.insert(warnings, message) end + if not QUIET then io.stderr:write("warning: " .. message .. "\n") end +end +local function fatal_warning(message) + if not FORCE then error(message .. "; use --force to override") else log_warning(message) end +end +local function prompt(message) + io.stderr:write(message .. " [Y/n]: ") + if ASSUME_YES then io.stderr:write("Y\n") return true end + local response = io.stdin:read("*line") + return not response:find("%S") or response:find("^%s*[yY]%s*$") +end + +function common.get(source, target, checksum) + if not checksum then return system.get(source, target) end + if not system.stat(CACHEDIR .. PATHSEP .. "files") then common.mkdirp(CACHEDIR .. PATHSEP .. "files") end + local cache_path = CACHEDIR .. PATHSEP .. "files" .. PATHSEP .. checksum + if not system.stat(cache_path) then + system.get(source, cache_path) + if system.hash(cache_path, "file") ~= checksum then fatal_warning("checksum doesn't match for " .. source) end + end + common.copy(cache_path, target) +end + + +local function compare_version(a, b) -- compares semver + if not a or not b then return false end + local _, _, majora, minora, revisiona = tostring(a):find("(%d+)%.?(%d*)%.?(%d*)") + local _, _, majorb, minorb, revisionb = tostring(b):find("(%d+)%.?(%d*)%.?(%d*)") + if majora == nil then error("can't parse version " .. a) end + if majorb == nil then error("can't parse version " .. b) end + majora, minora, revisiona = majora or 0, minora or 0, revisiona or 0 + majorb, minorb, revisionb = majorb or 0, minorb or 0, revisionb or 0 + if majora ~= majorb then return tonumber(majora) < tonumber(majorb) and -1 or 1 end + if minora ~= minorb then return tonumber(minora) < tonumber(minorb) and -1 or 1 end + if revisiona ~= revisionb then return tonumber(revisiona) < tonumber(revisionb) and -1 or 1 end + return 0 +end + +local function match_version(version, pattern) + if not pattern then return true end + if pattern:find("^>=") then return compare_version(version, pattern:sub(3)) >= 0 end + if pattern:find("^<=") then return compare_version(version, pattern:sub(3)) <= 0 end + if pattern:find("^<") then return compare_version(version, pattern:sub(2)) == -1 end + if pattern:find("^>") then return compare_version(version, pattern:sub(2)) == 1 end + if pattern:find("^=") then return compare_version(version, pattern:sub(2)) == 0 end + return version == pattern +end + + + +function Plugin.__index(self, idx) return rawget(self, idx) or Plugin[idx] end +function Plugin.new(repository, metadata) + local type = metadata.type or "plugin" + local folder = metadata.type == "library" and "libraries" or "plugins" + if metadata.path then metadata.path = metadata.path:gsub("/", PATHSEP) end + local self = setmetatable(common.merge({ + repository = repository, + tags = {}, + type = type, + path = nil, + remote = nil, + version = "1.0", + dependencies = {}, + local_path = repository and (repository.local_path .. PATHSEP .. (repository.commit or repository.branch) .. (metadata.path and (PATHSEP .. metadata.path:gsub("^/", "")) or "")), + }, metadata), Plugin) + -- Directory. + self.organization = metadata.organization or (((self.files and #self.files > 0) or self.remote or (not self.path and not self.url)) and "complex" or "singleton") + return self +end + +function Plugin:get_install_path(bottle) + local folder = self.type == "library" and "libraries" or "plugins" + local path = (bottle.local_path and (bottle.local_path .. PATHSEP .. "user") or USERDIR) .. PATHSEP .. folder .. PATHSEP .. (self.path and common.basename(self.path):gsub("%.lua$", "") or self.name) + if self.organization == "singleton" then path = path .. ".lua" end + return path +end + +function Plugin:is_core(bottle) return self.type == "core" end +function Plugin:is_installed(bottle) return self:is_core(bottle) or (bottle.lite_xl:is_compatible(self) and system.stat(self:get_install_path(bottle))) end +function Plugin:is_incompatible(plugin) return self.dependencies[plugin.name] and not match_version(plugin.version, dependencies[plugin.name]) end + +function Plugin:get_compatibilities(bottle) + local compatible_plugins, incompatible_plugins = {}, {} + local installed_plugins = bottle:installed_plugins() + for plugin, v in pairs(self.dependencies) do + local potential_plugins = { bottle:get_plugin(plugin, v.version, { mod_version = bottle.lite_xl.mod_version }) } + for i, potential_plugin in ipairs(potential_plugins) do + local incomaptibilities = common.grep(installed_plugins, function(p) return p:is_incompatible(potential_plugin) end) + if #incomaptibilities == 0 then + if not compatible_plugins[plugin] or + potential_plugin:is_installed(bottle) or + (compare_version(compatible_plugins[plugin].version, potential_plugin.version) and not compatible_plugins[plugin]:is_installed(bottle)) + then + compatible_plugins[plugin] = potential_plugin + end + else + incompatible_plugins[plugin] = incompatibilities + end + end + end + return compatible_plugins, incompatible_plugins +end + + + +function Plugin:install(bottle, installing) + if self:is_installed(bottle) then log_warning("plugin " .. self.name .. " is already installed") return end + local install_path = self:get_install_path(bottle) + local temporary_install_path = TMPDIR .. PATHSEP .. install_path:sub(#USERDIR + 2) + local status, err = pcall(function() + installing = installing or {} + installing[self.name] = true + local compatible, incompatible = self:get_compatibilities(bottle) + for plugin, version in pairs(self.dependencies) do + if incompatible[plugin] then error("can't install " .. self.name .. ": incompatible with " .. incompatible[plugin][1].name .. ":" .. incompatible[plugin][1].version) end + end + for plugin, v in pairs(self.dependencies) do + if not compatible[plugin] then error("can't find dependency " .. plugin .. (v.version and (":" .. v.version) or "")) end + end + for plugin, v in pairs(self.dependencies) do + if not compatible[plugin]:is_core(bottle) and not compatible[plugin]:is_installed(bottle) then + if installing[plugin] then + error("circular dependency detected in " .. self.name .. ": requires " .. plugin .. " but, " .. plugin .. " requires " .. self.name) + end + if not NO_INSTALL_OPTIONAL and (not v.optional or prompt(plugin .. " is an optional dependency of " .. self.name .. ". Should we install it?")) then + compatible[plugin]:install(bottle, installing) + end + end + end + common.mkdirp(common.dirname(temporary_install_path)) + if self.status == "upgradable" then + log_action("Upgrading " .. self.organization .. "plugin located at " .. self.local_path .. " to " .. install_path) + common.rmrf(install_path) + else + log_action("Installing " .. self.organization .. " plugin located at " .. self.local_path .. " to " .. install_path) + end + + if self.organization == "complex" and self.path and system.stat(self.local_path).type ~= "dir" then common.mkdirp(install_path) end + if self.url then + log_action("Downloading file " .. self.url .. "...") + local path = temporary_install_path .. (self.organization == 'complex' and self.path and system.stat(self.local_path).type ~= "dir" and (PATHSEP .. "init.lua") or "") + common.get(self.url, path, self.checksum) + log_action("Downloaded file " .. self.url .. " to " .. path) + if system.hash(path, "file") ~= self.checksum then fatal_warning("checksum doesn't match for " .. path) end + elseif self.remote then + log_action("Cloning repository " .. self.remote .. " into " .. install_path) + common.mkdirp(temporary_install_path) + local _, _, url, branch = self.remote:find("^(.*):(.*)$") + system.init(temporary_install_path, url) + common.reset(temporary_install_path, branch) + else + local path = install_path .. (self.organization == 'complex' and self.path and system.stat(self.local_path).type ~= "dir" and (PATHSEP .. "init.lua") or "") + local temporary_path = temporary_install_path .. (self.organization == 'complex' and self.path and system.stat(self.local_path).type ~= "dir" and (PATHSEP .. "init.lua") or "") + log_action("Copying " .. self.local_path .. " to " .. path) + common.copy(self.local_path, temporary_path) + end + for i,file in ipairs(self.files or {}) do + if not file.arch or file.arch == ARCH then + if not NO_INSTALL_OPTIONAL and (not file.optional or prompt(common.basename(file.url) .. " is an optional dependency of " .. self.name .. ". Should we install it?")) then + if not file.checksum then error("requires a checksum") end + local path = install_path .. PATHSEP .. (file.path or common.basename(file.url)) + local temporary_path = temporary_install_path .. PATHSEP .. (file.path or common.basename(file.url)) + log_action("Downloading file " .. file.url .. "...") + common.get(file.url, temporary_path, file.checksum) + log_action("Downloaded file " .. file.url .. " to " .. path) + if system.hash(temporary_path, "file") ~= file.checksum then fatal_warning("checksum doesn't match for " .. path) end + end + end + end + end) + if not status then + common.rmrf(temporary_install_path) + error(err) + else + common.rmrf(install_path) + common.mkdirp(common.dirname(install_path)) + common.rename(temporary_install_path, install_path) + end +end + +function Plugin:depends_on(plugin) + if self.dependencies[plugin.name] and self.dependencies[plugin.name].optional ~= true then return true end + for i,v in ipairs(plugin.provides or {}) do if self.dependencies[v] and self.dependencies[v].optional ~= true then return true end end + return false +end + +function Plugin:uninstall(bottle) + local install_path = self:get_install_path(bottle) + if self:is_core(bottle) then error("can't uninstall " .. self.name .. " is a core plugin") end + log_action("Uninstalling plugin located at " .. install_path) + local incompatible_plugins = common.grep(bottle:installed_plugins(), function(p) return p:depends_on(self) end) + if #incompatible_plugins == 0 or prompt(self.name .. " is depended upon by " .. common.join(", ", common.map(incompatible_plugins, function(p) return p.name end)) .. ". Remove as well?") then + for i,plugin in ipairs(incompatible_plugins) do + if not plugin:uninstall(bottle) then return false end + end + common.rmrf(install_path) + return true + end + return false +end + + +function Repository.__index(self, idx) return rawget(self, idx) or Repository[idx] end +function Repository.new(hash) + if not hash.remote then error("requires a remote") end + if not hash.remote:find("^https?:") and not hash.remote:find("^file:") then error("only repositories with http and file transports are supported (" .. hash.remote .. ")") end + local self = setmetatable({ + commit = hash.commit, + remote = hash.remote, + branch = hash.branch, + plugins = nil, + lite_xls = {}, + local_path = CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. system.hash(hash.remote), + last_retrieval = nil + }, Repository) + if system.stat(self.local_path) and not self.commit and not self.branch then + -- In the case where we don't have a branch, and don't have a commit, check for the presence of `master` and `main`. + if system.stat(self.local_path .. PATHSEP .. "master") then + self.branch = "master" + elseif system.stat(self.local_path .. PATHSEP .. "main") then + self.branch = "main" + else + error("can't find branch for " .. self.remote) + end + end + return self +end + +function Repository.url(url) + if type(url) == "table" then return url.remote .. ":" .. (url.branch or url.commit) end + local e = url:reverse():find(":") + local s = e and (#url - e + 1) + local remote, branch_or_commit = url:sub(1, s and (s-1) or #url), s and url:sub(s+1) + if remote == "https" or remote == "file" then remote, branch_or_commit = url, nil end + if branch_or_commit and is_commit_hash(branch_or_commit) then + return Repository.new({ remote = remote, commit = branch_or_commit }) + end + return Repository.new({ remote = remote, branch = branch_or_commit }) +end + +function Repository:parse_manifest(already_pulling) + if self.manifest then return self.manifest, self.remotes end + if system.stat(self.local_path) and system.stat(self.local_path .. PATHSEP .. (self.commit or self.branch)) then + self.manifest_path = self.local_path .. PATHSEP .. (self.commit or self.branch) .. PATHSEP .. "manifest.json" + if not system.stat(self.manifest_path) then self:generate_manifest() end + self.manifest = json.decode(io.open(self.manifest_path, "rb"):read("*all")) + self.plugins = {} + self.remotes = {} + for i, metadata in ipairs(self.manifest["plugins"] or {}) do + if metadata.remote then + local _, _, url, branch_or_commit = metadata.remote:find("^(.-):?(.*)?$") + if branch_or_commit and is_commit_hash(branch_or_commit) then + repo = Repository.new({ remote = url, commit = branch_or_commit }) + table.insert(remotes, repo) + table.insert(self.plugins, Plugin.new(self, metadata)) + else + -- log_warning("plugin " .. metadata.name .. " specifies remote as source, but isn't a commit") + end + else + table.insert(self.plugins, Plugin.new(self, metadata)) + end + end + for i, metadata in ipairs(self.manifest["lite-xls"] or {}) do + if metadata.remote then + local _, _, url, branch_or_commit = metadata.remote:find("^(.-):?(.*)?$") + if branch_or_commit and is_commit_hash(branch_or_commit) then + repo = Repository.new({ remote = url, commit = branch_or_commit }) + table.insert(remotes, repo) + table.insert(self.lite_xls, LiteXL.new(self, metadata)) + else + -- log_warning("plugin " .. metadata.name .. " specifies remote as source, but isn't a commit") + end + else + table.insert(self.lite_xls, LiteXL.new(self, metadata)) + end + end + self.remotes = common.map(self.manifest["remotes"] or {}, function(r) return Repository.url(r) end) + end + return self.manifest, self.remotes +end + + +-- in the cases where we don't have a manifest, assume generalized structure, take plugins folder, trawl through it, build manifest that way +-- assuming each .lua file under the `plugins` folder is a plugin. also parse the README, if present, and see if any of the plugins +function Repository:generate_manifest() + if not self.commit and not self.branch then error("requires an instantiation") end + local path = self.local_path .. PATHSEP .. (self.commit or self.branch) + local plugin_dir = system.stat(path .. PATHSEP .. "plugins") and PATHSEP .. "plugins" .. PATHSEP or PATHSEP + local plugins, plugin_map = {}, {} + if system.stat(path .. PATHSEP .. "README.md") then -- If there's a README, parse it for a table like in our primary repository. + for line in io.lines(path .. PATHSEP .. "README.md") do + local _, _, name, path, description = line:find("^%s*%|%s*%[`([%w_]+)%??.-`%]%((.-)%).-%|%s*(.-)%s*%|%s*$") + if name then + plugin_map[name] = { name = name, description = description, files = {} } + if path:find("^http") then + if path:find("%.lua") then + plugin_map[name].url = path + local file = common.get(path) + plugin_map[name].checksum = system.hash(file) + else + plugin_map[name].remote = path + end + else + plugin_map[name].path = path:gsub("%?.*$", "") + end + end + end + end + for i, file in ipairs(system.ls(path .. plugin_dir)) do + if file:find("%.lua$") then + local plugin = { description = nil, files = {}, name = common.basename(file):gsub("%.lua$", ""), dependencies = {}, mod_version = 3, version = "1.0", tags = {}, path = plugin_dir .. file } + for line in io.lines(path .. plugin_dir .. file) do + local _, _, mod_version = line:find("%-%-.*mod%-version:%s*(%w+)") + if mod_version then plugin.mod_version = mod_version end + local _, _, required_plugin = line:find("require [\"']plugins.([%w_]+)") + if required_plugin then if required_plugin ~= plugin.name then plugin.dependencies[required_plugin] = ">=1.0" end end + end + if plugin_map[plugin.name] then + plugin = common.merge(plugin, plugin_map[plugin.name]) + plugin_map[plugin.name].plugin = plugin + end + table.insert(plugins, plugin) + end + end + for k, v in pairs(plugin_map) do + if not v.plugin then + table.insert(plugins, common.merge({ dependencies = {}, mod_version = self.branch == "master" and 2 or 3, version = "1.0", tags = {} }, v)) + end + end + common.write(path .. PATHSEP .. "manifest.json", json.encode({ plugins = plugins })) +end + +function Repository:add(pull_remotes) + -- If neither specified then pull onto `master`, and check the main branch name, and move if necessary. + if not self.branch and not self.commit then + local path = self.local_path .. PATHSEP .. "master" + common.mkdirp(path) + log_action("Retrieving " .. self.remote .. ":master/main...") + system.init(path, self.remote) + system.fetch(path) + if not pcall(system.reset, path, "refs/remotes/origin/master", "hard") then + if pcall(system.reset, path, "refs/remotes/origin/main", "hard") then + common.rename(path, self.local_path .. PATHSEP .. "main") + self.branch = "main" + else + error("can't find master or main.") + end + else + self.branch = "master" + end + log_action("Retrieved " .. self.remote .. ":master/main.") + else + local path = self.local_path .. PATHSEP .. (self.commit or self.branch) + common.mkdirp(path) + log_action("Retrieving " .. self.remote .. ":" .. (self.commit or self.branch) .. "...") + system.init(path, self.remote) + system.fetch(path) + common.reset(path, self.commit or self.branch, "hard") + log_action("Retrieved " .. self:url() .. "...") + self.manifest = nil + end + local manifest, remotes = self:parse_manifest() + if pull_remotes then -- any remotes we don't have in our listing, call add, and add into the list + for i, remote in ipairs(remotes) do + if not common.first(repositories, function(repo) return repo.remote == remote.remote and repo.branch == remote.branch and repo.commit == remote.commit end) then + remote:add(pull_remotes == "recursive" and "recursive" or false) + table.insert(repositories, remote) + end + end + end + return self +end + + +function Repository:update(pull_remotes) + local manifest, remotes = self:parse_manifest() + if self.branch then + local path = self.local_path .. PATHSEP .. self.branch + system.fetch(path) + common.reset(path, self.branch, "hard") + log_action("Updated " .. self:url()) + self.manifest = nil + manifest, remotes = self:parse_manifest() + end + if pull_remotes then -- any remotes we don't have in our listing, call add, and add into the list + for i, remote in ipairs(remotes) do + if common.first(repositories, function(repo) return repo.remote == remote.remote and repo.branch == remote.branch and repo.commit == remote.comit end) then + remote:add(pull_remotes == "recursive" and "recursive" or false) + table.insert(repositories, remote) + end + end + end +end + + +function Repository:remove() + common.rmrf(self.local_path .. PATHSEP .. (self.commit or self.branch)) + if #system.ls(self.local_path) == 0 then common.rmrf(self.local_path) end +end + + +function LiteXL.__index(t, k) return LiteXL[k] end +function LiteXL.new(repository, metadata) + if not metadata.version then error("lite-xl entry requires a version") end + local self = setmetatable({ + repository = repository, + version = metadata.version, + remote = metadata.remote, + url = metadata.url, + tags = metadata.tags or {}, + mod_version = metadata.mod_version, + path = metadata.path, + files = metadata.files or {} + }, LiteXL) + self.hash = system.hash((repository and repository:url() or "") .. "-" .. metadata.version .. common.join("", common.map(self.files, function(f) return f.checksum end))) + self.local_path = self:is_local() and self.path or (CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. self.version .. PATHSEP .. self.hash) + return self +end + +function LiteXL:get_binary_path() return self.local_path .. PATHSEP .. "lite-xl" end +function LiteXL:get_data_directory() return self.local_path .. PATHSEP .. "data" end +function LiteXL:is_system() return system_bottle and system_bottle.lite_xl == self end +function LiteXL:is_local() return not self.repository and self.path end +function LiteXL:is_compatible(plugin) return compare_version(self.mod_version, plugin.mod_version) == 0 end +function LiteXL:is_installed() return system.stat(self.local_path) end + +function LiteXL:install() + if self:is_installed() then log_warning("lite-xl " .. self.version .. " already installed") return end + common.mkdirp(self.local_path) + if system_bottle.lite_xl == self then -- system lite-xl. We have to copy it because we can't really set the user directory. + local executable, datadir = common.path("lite-xl") + if not executable then error("can't find system lite-xl executable") end + local stat = system.stat(executable) + executable = stat.symlink and stat.symlink or executable + datadir = common.dirname(executable) .. PATHSEP .. "data" + if not system.stat(datadir) then error("can't find system lite-xl data dir") end + common.copy(executable, self.local_path .. PATHSEP .. "lite-xl") + system.chmod(self.local_path .. PATHSEP .. "lite-xl", 448) -- chmod to rwx------- + common.copy(datadir, self.local_path .. PATHSEP .. "data") + elseif self.path and not self.repository then -- local repository + system.symlink(self:get_binary_path(), self.path .. PATHSEP .. "lite_xl") + else + if self.remote then + system.init(self.local_path, self.remote) + common.reset(self.local_path, self.commit or self.branch) + end + for i,file in ipairs(self.files or {}) do + if file.arch and file.arch == ARCH then + if not file.checksum then error("requires a checksum") end + local basename = common.basename(file.url) + local archive = basename:find("%.zip$") or basename:find("%.tar%.gz$") + local path = self.local_path .. PATHSEP .. (archive and basename or "lite-xl") + log_action("Downloading file " .. file.url .. "...") + common.get(file.url, path, file.checksum) + log_action("Downloaded file " .. file.url .. " to " .. path) + if system.hash(path, "file") ~= file.checksum then fatal_warning("checksum doesn't match for " .. path) end + if archive then + log_action("Extracting file " .. basename .. " in " .. self.local_path) + system.extract(path, self.local_path) + end + end + end + end + if not system.stat(self.local_path .. PATHSEP .. "lite-xl") then error("can't find executable for lite-xl " .. self.version) end +end + +function LiteXL:uninstall() + if not system.stat(self.local_path) then error("lite-xl " .. self.version .. " not installed") end + common.rmrf(self.local_path) +end + + +function Bottle.__index(t, k) return Bottle[k] end +function Bottle.new(lite_xl, plugins, is_system) + local self = setmetatable({ + lite_xl = lite_xl, + plugins = plugins, + is_system = is_system + }, Bottle) + if not is_system then + table.sort(self.plugins, function(a, b) return (a.name .. ":" .. a.version) < (b.name .. ":" .. b.version) end) + self.hash = system.hash(lite_xl.version .. " " .. common.join(" ", common.map(self.plugins, function(p) return p.name .. ":" .. p.version end))) + self.local_path = CACHEDIR .. PATHSEP .. "bottles" .. PATHSEP .. self.hash + end + return self +end + +function Bottle:is_constructed() return self.is_system or system.stat(self.local_path) end + +function Bottle:construct() + if self.is_system then error("system bottle cannot be constructed") end + if self:is_constructed() then error("bottle " .. self.hash .. " already constructed") end + if not self.lite_xl:is_installed() then self.lite_xl:install() end + common.mkdirp(self.local_path .. PATHSEP .. "user") + common.copy(self.lite_xl.local_path .. PATHSEP .. "lite-xl", self.local_path .. PATHSEP .. "lite-xl") + system.chmod(self.local_path .. PATHSEP .. "lite-xl", 448) -- chmod to rwx------- + common.copy(self.lite_xl.local_path .. PATHSEP .. "data", self.local_path .. PATHSEP .. "data") + for i,plugin in ipairs(self.plugins) do plugin:install(self) end +end + +function Bottle:destruct() + if self.is_system then error("system bottle cannot be destructed") end + if not self:is_constructed() then error("lite-xl " .. self.version .. " not constructed") end + common.rmrf(self.local_path) +end + +function Bottle:run(args) + args = args or {} + if self.is_system then error("system bottle cannot be run") end + os.execute(self.local_path .. PATHSEP .. "lite-xl", table.unpack(args)) +end + +function Bottle:all_plugins() + local t = common.flat_map(repositories, function(r) return r.plugins end) + local hash = { } + for i, v in ipairs(t) do hash[v.name] = v end + local plugin_paths = { + (self.local_path and (self.local_path .. PATHSEP .. "user") or USERDIR) .. PATHSEP .. "plugins", + self.lite_xl:get_data_directory() .. PATHSEP .. "plugins" + } + for i, plugin_path in ipairs(common.grep(plugin_paths, function(e) return system.stat(e) end)) do + for k, v in ipairs(common.grep(system.ls(plugin_path), function(e) return not hash[e:gsub("%.lua$", "")] end)) do + table.insert(t, Plugin.new(nil, { + name = v:gsub("%.lua$", ""), + type = i == 2 and "core", + organization = (v:find("%.lua$") and "singleton" or "complex"), + mod_version = self.lite_xl.mod_version, + path = "plugins/" .. v, + version = "1.0" + })) + end + end + return t +end + +function Bottle:installed_plugins() + return common.grep(self:all_plugins(), function(p) return p:is_installed(self) end) +end + +function Bottle:get_plugin(name, version, filter) + local candidates = {} + local wildcard = name:find("%*$") + filter = filter or {} + for i,plugin in ipairs(self:all_plugins()) do + if not version and plugin.provides then + for k, provides in ipairs(plugin.provides) do + if provides == name then + table.insert(candidates, plugin) + end + end + end + if (plugin.name == name or (wildcard and plugin.name:find("^" .. name:sub(1, #name - 1)))) and match_version(plugin.version, version) then + if (not filter.mod_version or not plugin.mod_version or tonumber(plugin.mod_version) == tonumber(filter.mod_version)) then + table.insert(candidates, plugin) + end + end + end + return table.unpack(common.sort(candidates, function (a,b) return a.version < b.version end)) +end + + +local function get_repository(url) + if not url then error("requires a repository url") end + local r = Repository.url(url) + for i,v in ipairs(repositories) do + if v.remote == r.remote and v.branch == r.branch and v.commit == r.commit then return i, v end + end + return nil +end + + +local DEFAULT_REPOS +local function lpm_repo_init() + DEFAULT_REPOS = { Repository.url("https://github.com/adamharrison/lite-xl-plugin-manager.git:latest") } + if not system.stat(CACHEDIR .. PATHSEP .. "repos") then + for i, repository in ipairs(DEFAULT_REPOS) do + if not system.stat(repository.local_path) or not system.stat(repository.local_path .. PATHSEP .. (repository.commit or repository.branch)) then + table.insert(repositories, repository:add(true)) + end + end + end +end + + +local function lpm_repo_add(...) + for i, url in ipairs({ ... }) do + local idx, repo = get_repository(url) + if repo then -- if we're alreayd a repo, put this at the head of the resolution list + table.remove(repositories, idx) + else + repo = Repository.url(url):add(AUTO_PULL_REMOTES and "recursive" or false) + end + table.insert(repositories, 1, repo) + repo:update() + end +end + + +local function lpm_repo_rm(...) + for i, url in ipairs({ ... }) do + local idx, repo = get_repository(url) + if not repo then error("cannot find repository " .. url) end + table.remove(repositories, idx) + repo:remove() + end +end + + +local function lpm_repo_update(...) + local t = { ... } + if #t == 0 then table.insert(t, false) end + for i, url in ipairs(t) do + local repo = url and get_repository(url) + for i,v in ipairs(repositories) do + if not repo or v == repo then + v:update(AUTO_PULL_REMOTES and "recursive" or false) + end + end + end +end + +local function get_lite_xl(version) + return common.first(common.concat(lite_xls, common.flat_map(repositories, function(e) return e.lite_xls end)), function(lite_xl) return lite_xl.version == version end) +end + +local function lpm_lite_xl_save() + common.mkdirp(CACHEDIR .. PATHSEP .. "lite_xls") + common.write(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json", + json.encode(common.map(common.grep(lite_xls, function(l) return l:is_local() and not l:is_system() end), function(l) return { version = l.version, mod_version = l.mod_version, path = l.path } end)) + ) +end + +local function lpm_lite_xl_add(version, path) + if not version then error("requires a version") end + if not path then error("requires a path") end + if not system.stat(path .. PATHSEP .. "lite-xl") then error("can't find " .. path .. PATHSEP .. "lite-xl") end + if not system.stat(path .. PATHSEP .. "data") then error("can't find " .. path .. PATHSEP .. "data") end + table.insert(lite_xls, LiteXL.new(nil, { version = version, path = path:gsub(PATHSEP .. "$", ""), mod_version = MOD_VERSION or 3 })) + lpm_lite_xl_save() +end + +local function lpm_lite_xl_rm(version) + if not version then error("requires a version") end + local lite_xl = get_lite_xl(version) or error("can't find lite_xl version " .. version) + lite_xls = common.grep(lite_xls, function(l) return l ~= lite_xl end) + lpm_lite_xl_save() +end + +local function lpm_lite_xl_install(version) + if not version then error("requires a version") end + (get_lite_xl(version) or error("can't find lite-xl version " .. version)):install() +end + + +local function lpm_lite_xl_switch(version, target) + if not version then error("requires a version") end + target = target or common.path("lite-xl") + if not target then error("can't find installed lite-xl. please provide a target to install the symlink explicitly") end + local lite_xl = get_lite_xl(version) or error("can't find lite-xl version " .. version) + if not lite_xl:is_installed() then log_action("Installing lite-xl " .. lite_xl.version) lite_xl:install() end + local stat = system.stat(target) + if stat and stat.symlink then os.remove(target) end + system.symlink(lite_xl:get_binary_path(), target) + if not common.path('lite-xl') then + os.remove(target) + error(target .. " is not on your $PATH; please supply a target that can be found on your $PATH, called `lite-xl`.") + end +end + + +local function lpm_lite_xl_uninstall(version) + (get_lite_xl(version) or error("can't find lite-xl version " .. version)):uninstall() +end + + +local function lpm_lite_xl_list() + local result = { ["lite-xl"] = { } } + local max_version = 0 + for i,lite_xl in ipairs(lite_xls) do + table.insert(result["lite-xl"], { + version = lite_xl.version, + mod_version = lite_xl.mod_version, + tags = lite_xl.tags, + is_system = lite_xl:is_system(), + status = lite_xl:is_installed() and (lite_xl:is_local() and "local" or "installed") or "available", + local_path = lite_xl.local_path + }) + max_version = math.max(max_version, #lite_xl.version) + end + for i,repo in ipairs(repositories) do + if not repo.lite_xls then error("can't find lite-xl for repo " .. repo:url()) end + for j, lite_xl in ipairs(repo.lite_xls) do + table.insert(result["lite-xl"], { + version = lite_xl.version, + mod_version = lite_xl.mod_version, + repository = repo:url(), + tags = lite_xl.tags, + is_system = lite_xl:is_system(), + status = lite_xl:is_installed() and (lite_xl:is_local() and "local" or "installed") or "available", + local_path = lite_xl.local_path + }) + max_version = math.max(max_version, #lite_xl.version) + end + end + if JSON then + io.stdout:write(json.encode(result) .. "\n") + else + if VERBOSE then + for i, lite_xl in ipairs(result["lite-xl"]) do + if i ~= 0 then print("---------------------------") end + print("Version: " .. lite_xl.version) + print("Status: " .. lite_xl.status) + print("Mod-Version: " .. (lite_xl.mod_version or "unknown")) + print("Tags: " .. common.join(", ", lite_xl.tags)) + end + else + max_version = max_version + 2 + print(string.format("%" .. max_version .. "s | %10s | %s", "Version", "Status", "Location")) + print(string.format("%" .. max_version .."s | %10s | %s", "-------", "---------", "---------------------------")) + for i, lite_xl in ipairs(result["lite-xl"]) do + print(string.format("%" .. max_version .. "s | %10s | %s", (lite_xl.is_system and "* " or "") .. lite_xl.version, lite_xl.status, (lite_xl.status ~= "available" and lite_xl.local_path or lite_xl.repository))) + end + end + end +end + +local function lpm_lite_xl_run(version, ...) + if not version then error("requires a version") end + local lite_xl = get_lite_xl(version) or error("can't find lite-xl version " .. version) + local plugins = {} + for i, str in ipairs({ ... }) do + local name, version = common.split(":", str) + local plugin = system_bottle:get_plugin(name, version, { mod_version = lite_xl.mod_version }) + if not plugin then error("can't find plugin " .. str) end + table.insert(plugins, plugin) + end + local bottle = Bottle.new(lite_xl, plugins) + if not bottle:is_constructed() then bottle:construct() end + bottle:run() +end + + +local function lpm_install(...) + for i, identifier in ipairs({ ... }) do + local s = identifier:find(":") + local name, version = (s and identifier:sub(1, s-1) or identifier), (s and identifier:sub(s+1) or nil) + if not name then error('unrecognized identifier ' .. identifier) end + if name == "lite-xl" then + lpm_lite_xl_install(version) + else + local plugins = { system_bottle:get_plugin(name, version, { mod_version = system_bottle.lite_xl.mod_version }) } + if #plugins == 0 then error("can't find plugin " .. name .. " mod-version: " .. (system_bottle.lite_xl.mod_version or 'any')) end + for j,v in ipairs(plugins) do v:install(system_bottle) end + end + end +end + + +local function lpm_plugin_uninstall(...) + for i, name in ipairs({ ... }) do + local plugins = { system_bottle:get_plugin(name) } + if #plugins == 0 then error("can't find plugin " .. name) end + local installed_plugins = common.grep(plugins, function(e) return e:is_installed(system_bottle) end) + if #installed_plugins == 0 then error("plugin " .. name .. " not installed") end + for i, plugin in ipairs(installed_plugins) do plugin:uninstall(system_bottle) end + end +end + +local function lpm_plugin_reinstall(...) for i, name in ipairs({ ... }) do pcall(lpm_plugin_uninstall, name) end lpm_install(...) end + +local function lpm_repo_list() + if JSON then + io.stdout:write(json.encode({ repositories = common.map(repositories, function(repo) return { remote = repo.remote, commit = repo.commit, branch = repo.branch, path = repo.local_path .. PATHSEP .. (repo.commit or repo.branch), remotes = common.map(repo.remotes or {}, function(r) return r:url() end) } end) }) .. "\n") + else + for i, repository in ipairs(repositories) do + local _, remotes = repository:parse_manifest() + if i ~= 0 then print("---------------------------") end + print("Remote : " .. repository:url()) + print("Path : " .. repository.local_path .. PATHSEP .. (repository.commit or repository.branch)) + print("Remotes: " .. json.encode(common.map(repository.remotes or {}, function(r) return r:url() end))) + end + end +end + +local function lpm_plugin_list() + local max_name = 0 + local result = { plugins = { } } + for j,plugin in ipairs(system_bottle:all_plugins()) do + max_name = math.max(max_name, #plugin.name) + local repo = plugin.repository + table.insert(result.plugins, { + name = plugin.name, + status = plugin.repository and (plugin:is_installed(system_bottle) and "installed" or (system_bottle.lite_xl:is_compatible(plugin) and "available" or "incompatible")) or (plugin:is_core(system_bottle) and "core" or "orphan"), + version = "" .. plugin.version, + dependencies = plugin.dependencies, + description = plugin.description, + mod_version = plugin.mod_version, + tags = plugin.tags, + type = plugin.type, + organization = plugin.organization, + repository = repo and repo:url() + }) + end + if JSON then + io.stdout:write(json.encode(result) .. "\n") + else + if not VERBOSE then + print(string.format("%" .. max_name .."s | %10s | %10s | %s", "Name", "Version", "ModVer", "Status")) + print(string.format("%" .. max_name .."s | %10s | %10s | %s", "--------------", "----------", "----------", "-----------")) + end + for i, plugin in ipairs(common.sort(result.plugins, function(a,b) return a.name < b.name end)) do + if VERBOSE then + if i ~= 0 then print("---------------------------") end + print("Name: " .. plugin.name) + print("Version: " .. plugin.version) + print("Status: " .. plugin.status) + print("Type: " .. plugin.type) + print("Orgnization: " .. plugin.organization) + print("Repository: " .. (plugin.repository or "orphan")) + print("Description: " .. (plugin.description or "")) + print("Mod-Version: " .. (plugin.mod_version or "unknown")) + print("Dependencies: " .. json.encode(plugin.dependencies)) + print("Tags: " .. common.join(", ", plugin.tags)) + elseif plugin.status ~= "incompatible" then + print(string.format("%" .. max_name .."s | %10s | %10s | %s", plugin.name, plugin.version, plugin.mod_version, plugin.status)) + end + end + end +end + +local function lpm_describe() + for i,v in ipairs(repositories) do + if #common.grep(DEFAULT_REPOS, function(r) return r:url() == v:url() end) == 0 then + io.stdout:write("lpm add " .. v:url() .. " && ") + end + end + print("lpm run " .. system_bottle.lite_xl.version .. " " .. common.join(" ", common.map(system_bottle:installed_plugins(), function(p) return p.name .. ":" .. p.version end))) +end + +local function lpm_plugin_upgrade() + for i,plugin in ipairs(system_bottle:installed_plugins()) do + local upgrade = common.sort(system_bottle:get_plugin(plugin.name, ">" .. plugin.version), function(a, b) return compare_version(b.version, a.version) end)[1] + if upgrade then upgrade:install(system_bottle) end + end +end + +local function lpm_purge() + -- local path = common.path("lite-xl") + -- if path then + -- local lite_xl = get_lite_xl("system") + -- if lite_xl then + -- os.remove(path) + -- system.symlink(lite_xl:get_binary_path(), target) + -- log_action("Reset lite-xl symlink to system.") + -- end + -- end + log_action("Removed " .. CACHEDIR .. ".") + common.rmrf(CACHEDIR) +end + +local function parse_arguments(arguments, options) + local args = {} + local i = 1 + while i <= #arguments do + local s,e, option, value = arguments[i]:find("%-%-([^=]+)=?(.*)") + if s then + local flag_type = options[option] + if not flag_type then error("unknown flag --" .. option) end + if flag_type == "flag" then + args[option] = true + elseif flag_type == "string" or flag_type == "number" then + if not value then + if i < #arguments then error("option " .. option .. " requires a " .. flag_type) end + value = arguments[i+1] + i = i + 1 + end + if flag_type == "number" and tonumber(flag_type) == nil then error("option " .. option .. " should be a number") end + args[option] = value + end + else + table.insert(args, arguments[i]) + end + i = i + 1 + end + return args +end + +local status = 0 +local function error_handler(err) + local s, e = err:find(":%d+") + local message = e and err:sub(e + 3) or err + if JSON then + if VERBOSE then + io.stderr:write(json.encode({ error = err, actions = actions, warnings = warnings, traceback = debug.traceback() }) .. "\n") + else + io.stderr:write(json.encode({ error = message or err, actions = actions, warnings = warnings }) .. "\n") + end + else + io.stderr:write((not VERBOSE and message or err) .. "\n") + if VERBOSE then io.stderr:write(debug.traceback() .. "\n") end + end + status = -1 +end + +local function run_command(ARGS) + if not ARGS[2]:find("%S") then return + elseif ARGS[2] == "init" then return + elseif ARGS[2] == "repo" and ARGV[3] == "add" then lpm_repo_add(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "repo" and ARGS[3] == "rm" then lpm_repo_rm(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "add" then lpm_repo_add(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "rm" then lpm_repo_rm(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "update" then lpm_repo_update(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "repo" and ARGS[3] == "update" then lpm_repo_update(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "repo" and (#ARGS == 2 or ARGS[3] == "list") then return lpm_repo_list() + elseif ARGS[2] == "plugin" and ARGS[3] == "install" then lpm_install(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "plugin" and ARGS[3] == "uninstall" then lpm_plugin_uninstall(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "plugin" and ARGS[3] == "reinstall" then lpm_plugin_reinstall(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "plugin" and (#ARGS == 2 or ARGS[3] == "list") then return lpm_plugin_list(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "plugin" and ARGS[3] == "upgrade" then return lpm_plugin_upgrade(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "upgrade" then return lpm_plugin_upgrade(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "install" then lpm_install(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "uninstall" then lpm_plugin_uninstall(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "reinstall" then lpm_plugin_reinstall(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "describe" then lpm_describe(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "list" then return lpm_plugin_list(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "lite-xl" and (#ARGS == 2 or ARGS[3] == "list") then return lpm_lite_xl_list(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "lite-xl" and ARGS[3] == "uninstall" then return lpm_lite_xl_uninstall(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "lite-xl" and ARGS[3] == "install" then return lpm_lite_xl_install(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "lite-xl" and ARGS[3] == "switch" then return lpm_lite_xl_switch(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "lite-xl" and ARGS[3] == "run" then return lpm_lite_xl_run(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "lite-xl" and ARGS[3] == "add" then return lpm_lite_xl_add(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "lite-xl" and ARGS[3] == "rm" then return lpm_lite_xl_rm(table.unpack(common.slice(ARGS, 4))) + elseif ARGS[2] == "run" then return lpm_lite_xl_run(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "switch" then return lpm_lite_xl_switch(table.unpack(common.slice(ARGS, 3))) + elseif ARGS[2] == "purge" then lpm_purge() + else error("unknown command: " .. ARGS[2]) end + if JSON then + io.stdout:write(json.encode({ actions = actions, warnings = warnings })) + end +end + + +xpcall(function() + local ARGS = parse_arguments(ARGV, { + json = "flag", userdir = "string", cachedir = "string", version = "flag", verbose = "flag", + quiet = "flag", version = "string", ["mod-version"] = "string", remotes = "flag", help = "flag", + remotes = "flag", ssl_certs = "string", force = "flag", arch = "string", ["assume-yes"] = "flag", + ["install-optional"] = "flag" + }) + if ARGS["version"] then + io.stdout:write(VERSION .. "\n") + return 0 + end + if ARGS["help"] or #ARGS == 1 or ARGS[2] == "help" then + io.stderr:write([[ +Usage: lpm COMMAND [...ARGUMENTS] [--json] [--userdir=directory] + [--cachedir=directory] [--quiet] [--version] [--help] [--remotes] + [--ssl_certs=directory/file] [--force] [--arch=]] .. _G.ARCH .. [[] + [--assume-yes] [--no-install-optional] [--verbose] [--mod-version=3] + +LPM is a package manager for `lite-xl`, written in C (and packed-in lua). + +It's designed to install packages from our central github repository (and +affiliated repositories), directly into your lite-xl user directory. It can +be called independently, for from the lite-xl `plugin_manager` plugin. + +LPM will always use https://github.com/lite-xl/lite-xl-plugins as its base +repository, if none are present, and the cache directory does't exist, +but others can be added, and this base one can be removed. + +It has the following commands: + + lpm init Implicitly called before all commands + if necessary, but can be called + independently to save time later. + lpm repo list List all extant repos. + lpm [repo] add Add a source repository. + [...] + lpm [repo] rm Remove a source repository. + [...] + lpm [repo] update [] Update all/the specified repos. + [...] + lpm [plugin] install Install specific plugins. + [:] If installed, upgrades. + [...:] + lpm [plugin] uninstall Uninstall the specific plugin. + [...] + lpm [plugin] reinstall Uninstall and installs the specific plugin. + [...] + lpm [plugin] list List all/associated plugins. + [...] + lpm [plugin] upgrade Upgrades all installed plugins + to new version if applicable. + lpm [lite-xl] install Installs lite-xl. Infers the + [binary] [datadir] paths on your system if not + supplied. Automatically + switches to be your system default + if path auto inferred. + lpm lite-xl add Adds a local version of lite-xl to + the managed list, allowing it to be + easily bottled. + lpm lite-xl remove Removes a local version of lite-xl + from the managed list. + lpm [lite-xl] switch [] Sets the active version of lite-xl + to be the specified version. Auto-detects + current install of lite-xl; if none found + path can be specifeid. + lpm lite-xl list Lists all installed versions of + lite-xl. + lpm run [...plugins] Sets up a "bottle" to run the specified + lite version, with the specified plugins + and then opens it. + lpm describe [bottle] Describes the bottle specified in the form + of a list of commands, that allow someone + else to run your configuration. + + lpm purge Completely purge all state for LPM. + lpm - Read these commands from stdin in + an interactive print-eval loop. + lpm help Displays this help text. + +Flags have the following effects: + + --json Performs all communication in JSON. + --userdir=directory Sets the lite-xl userdir manually. + If omitted, uses the normal lite-xl logic. + --cachedir=directory Sets the directory to store all repositories. + --tmpdir=directory During install, sets the staging area. + --verbose Spits out more information, including intermediate + steps to install and whatnot. + --quiet Outputs nothing but explicit responses. + --mod-version Sets the mod version of lite-xl to install plugins. + --version Returns version information. + --remotes Automatically adds any specified remotes in the + repository to the end of the resolution list. + This is a potential security risk, so be careful. + --help Displays this help text. + --ssl_certs Sets the SSL certificate store. + --arch Sets the architecture (default: ]] .. _G.ARCH .. [[). + --force Ignores checksum inconsitencies. + Not recommended; security risk. + --assume-yes Ignores any prompts, and automatically answers yes + to all. + --no-install-optional On install, anything marked as optional + won't prompt. +]] + ) + return 0 + end + + VERBOSE = ARGS["verbose"] or false + JSON = ARGS["json"] or os.getenv("LPM_JSON") + QUIET = ARGS["quiet"] or os.getenv("LPM_QUIET") + FORCE = ARGS["force"] + NO_INSTALL_OPTIONAL = ARGS["no-install-optional"] + ARCH = ARGS["arch"] or _G.ARCH + ASSUME_YES = ARGS["assume-yes"] or FORCE + MOD_VERSION = ARGS["mod-version"] or os.getenv("LPM_MODVERSION") + if MOD_VERSION == "any" then MOD_VERSION = nil end + HOME = (os.getenv("USERPROFILE") or os.getenv("HOME")):gsub(PATHSEP .. "$", "") + USERDIR = ARGS["userdir"] or os.getenv("LITE_USERDIR") or (os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. PATHSEP .. "lite-xl") + or (HOME and (HOME .. PATHSEP .. '.config' .. PATHSEP .. 'lite-xl')) + AUTO_PULL_REMOTES = ARGS["remotes"] + if not system.stat(USERDIR) then error("can't find user directory " .. USERDIR) end + CACHEDIR = ARGS["cachedir"] or os.getenv("LPM_CACHE") or USERDIR .. PATHSEP .. "lpm" + TMPDIR = ARGS["tmpdir"] or CACHEDIR .. "/tmp" + + repositories = {} + if ARGS[2] == "purge" then return lpm_purge() end + if ARGS["ssl_certs"] then + local stat = system.stat(ARGS["ssl_certs"]) + if not stat then error("can't find " .. ARGS["ssl_certs"]) end + system.certs(stat.type, ARGS["ssl_certs"]) + elseif not os.getenv("SSL_CERT_DIR") and not os.getenv("SSL_CERT_FILE") then + local paths = { -- https://serverfault.com/questions/62496/ssl-certificate-location-on-unix-linux#comment1155804_62500 + "/etc/ssl/certs/ca-certificates.crt", -- Debian/Ubuntu/Gentoo etc. + "/etc/pki/tls/certs/ca-bundle.crt", -- Fedora/RHEL 6 + "/etc/ssl/ca-bundle.pem", -- OpenSUSE + "/etc/pki/tls/cacert.pem", -- OpenELEC + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", -- CentOS/RHEL 7 + "/etc/ssl/cert.pem", -- Alpine Linux + "/etc/ssl/certs", -- SLES10/SLES11, https://golang.org/issue/12139 + "/system/etc/security/cacerts", -- Android + "/usr/local/share/certs", -- FreeBSD + "/etc/pki/tls/certs", -- Fedora/RHEL + "/etc/openssl/certs", -- NetBSD + "/var/ssl/certs", -- AIX + } + for i, path in ipairs(paths) do + local stat = system.stat(path) + if stat then + system.certs(stat.type, path) + break + end + end + end + + + -- Base setup; initialize default repos if applicable, read them in. Determine Lite XL system binary if not specified, and pull in a list of all local lite-xl's. + lpm_repo_init() + repositories = {} + for i, remote_hash in ipairs(system.ls(CACHEDIR .. PATHSEP .. "repos")) do + local remote + for j, commit_or_branch in ipairs(system.ls(CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. remote_hash)) do + if system.stat(CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. remote_hash .. PATHSEP .. commit_or_branch .. PATHSEP .. ".git" .. PATHSEP .."config") then + for line in io.lines(CACHEDIR .. PATHSEP .. "repos" .. PATHSEP .. remote_hash .. PATHSEP .. commit_or_branch .. PATHSEP .. ".git" .. PATHSEP .."config") do + local s,e = line:find("url = ") + if s then remote = line:sub(e+1) break end + end + if remote then + table.insert(repositories, Repository.url(remote .. ":" .. commit_or_branch)) + repositories[#repositories]:parse_manifest() + end + end + end + end + + lite_xls = {} + if system.stat(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json") then + for i, lite_xl in ipairs(json.decode(io.open(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json", "rb"):read("*all"))) do + table.insert(lite_xls, LiteXL.new(nil, { version = lite_xl.version, mod_version = lite_xl.mod_version, path = lite_xl.path, tags = { "local" } })) + end + end + local lite_xl_binary = common.path("lite-xl") + if lite_xl_binary then + lite_xl_binary = system.stat(lite_xl_binary).symlink or lite_xl_binary + local directory = common.dirname(lite_xl_binary) + local hash = system.hash(lite_xl_binary, "file") + local system_lite_xl = common.first(common.concat(common.flat_map(repositories, function(r) return r.lite_xls end), lite_xls), function(lite_xl) return lite_xl.local_path == directory end) + if not system_lite_xl then + if #common.grep(lite_xls, function(e) return e.version == "system" end) > 0 then error("can't create new system lite, please `lpm rm lite-xl system`, or resolve otherwise") end + system_lite_xl = LiteXL.new(nil, { path = directory, mod_version = 3, version = "system", tags = { "system", "local" } }) + table.insert(lite_xls, system_lite_xl) + lpm_lite_xl_save() + else + table.insert(system_lite_xl.tags, "system") + end + system_bottle = Bottle.new(system_lite_xl, nil, true) + end + if not system_bottle then system_bottle = Bottle.new(nil, nil, true) end + + if ARGS[2] ~= '-' then + run_command(ARGS) + else + while true do + local line = io.stdin:read("*line") + if line == "quit" or line == "exit" then return 0 end + local args = { ARGS[1] } + local s = 1 + while true do + local a,e = line:find("%s+", s) + table.insert(args, line:sub(s, a and (a - 1) or #line)) + if not e then break end + s = e + 1 + end + xpcall(function() + run_command(args) + end, error_handler) + actions, warnings = {}, {} + end + end + +end, error_handler) + + +return status -- cgit v1.2.3