From efb3aae55908cb88f5a9c900d6563603ab792d6a Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Tue, 20 May 2025 15:22:41 -0400 Subject: Add more advanced configuration options to modules.ini and support automatically downloading and installing from it. --- CHANGES.md | 2 + Makefile | 3 +- docs/libraries.md | 32 +++++++- docs/versions.md | 4 +- examples/README.md | 2 - examples/colorful/README.md | 8 +- examples/colorful/colorful.tm | 2 +- examples/colorful/modules.ini | 2 + examples/coroutines/README.md | 8 +- examples/http-server/connection-queue.tm | 2 +- examples/http-server/http-server.tm | 8 +- examples/http-server/modules.ini | 8 ++ examples/http-server/sample-site/random.tm | 2 +- examples/ini/ini.tm | 2 +- examples/ini/modules.ini | 2 + examples/tomo-install/CHANGES.md | 5 -- examples/tomo-install/tomo-install.tm | 84 -------------------- examples/tomodeps/CHANGES.md | 5 -- examples/tomodeps/tomodeps.tm | 117 ---------------------------- src/compile.c | 19 +++-- src/environment.c | 40 ---------- src/environment.h | 1 - src/modules.c | 121 +++++++++++++++++++++++++++++ src/modules.h | 14 ++++ src/tomo.c | 10 ++- src/typecheck.c | 12 ++- 26 files changed, 229 insertions(+), 286 deletions(-) create mode 100644 examples/colorful/modules.ini create mode 100644 examples/http-server/modules.ini create mode 100644 examples/ini/modules.ini delete mode 100644 examples/tomo-install/CHANGES.md delete mode 100644 examples/tomo-install/tomo-install.tm delete mode 100644 examples/tomodeps/CHANGES.md delete mode 100644 examples/tomodeps/tomodeps.tm create mode 100644 src/modules.c create mode 100644 src/modules.h diff --git a/CHANGES.md b/CHANGES.md index f071f7fa..3e3ac7c9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ - Added a versioning system based on `CHANGES.md` files and `modules.ini` configuration for module aliases. +- When attempting to run a program with a module that is not installed, Tomo + can prompt the user to automatically install it. - Programs can use `--version` as a CLI flag to print a Tomo program's version number and exit. - Added `tomo --prefix` to print the Tomo install prefix. diff --git a/Makefile b/Makefile index e5baef93..c00d9bf4 100644 --- a/Makefile +++ b/Makefile @@ -179,8 +179,7 @@ man/man1/tomo.1: docs/tomo.1.md examples: ./local-tomo -qIL examples/log examples/ini examples/vectors examples/http examples/wrap examples/colorful - ./local-tomo -e examples/game/game.tm examples/http-server/http-server.tm \ - examples/tomodeps/tomodeps.tm examples/tomo-install/tomo-install.tm + ./local-tomo -e examples/game/game.tm examples/http-server/http-server.tm ./local-tomo examples/learnxiny.tm deps: diff --git a/docs/libraries.md b/docs/libraries.md index 37f8ee05..8457b67a 100644 --- a/docs/libraries.md +++ b/docs/libraries.md @@ -171,9 +171,9 @@ path where the library is installed, so if the library `foo` has version `v1.2`, then it will be installed to `~/.local/share/tomo_vX.Y/installed/foo_v1.2/`. When using a library, you must explicitly supply either the exact version in the `use` statement like this: -`use foo_v1.2`, or provide a `modules.ini` file that lists shorthand aliases -for the libraries you're using alongside the files that use them. The syntax -is a simple `alias=full_name` format on each line. Here is an example: +`use foo_v1.2`, or provide a `modules.ini` file that lists version information +and other details about modules being used. For each module, you should provide +a `[modulename]` section with a `version=` field. ```tomo # File: foo.tm @@ -184,10 +184,34 @@ use mylib And the accompanying `modules.ini`: ```ini -mylib=mylib_v1.2 +[mylib] +version=v1.2 ``` The `modules.ini` file must be in the same directory as the source files that use its aliases, so if you want to share a `modules.ini` file across multiple subdirectories, use a symbolic link. +### Module Downloading + +If you want, you can also provide the following options for a module: + +- `git`: a Git URL to clone the repository +- `revision`: if a Git URL is provided, use this revision +- `url`: a URL to download an archive of the library (`.zip`, `.tar`, `.tar.gz`) +- `path`: if the library is provided in a subdirectory of the repository or + archive, list the subdirectory here. + +For example, this is what it would look like to use the `colorful` library that +is distributed with the Tomo compiler in the `examples/colorful` subdirectory: + +```ini +[colorful] +version=v1.0 +git=git@github.com:bruce-hill/tomo +path=examples/colorful +``` + +If this extra information is provided, Tomo will prompt the user to ask if they +want to download and install this module automatically when they run a program +and don't have the necessary module installed. diff --git a/docs/versions.md b/docs/versions.md index db7a1036..c68d1f81 100644 --- a/docs/versions.md +++ b/docs/versions.md @@ -70,8 +70,8 @@ version number will be used to determine its installation location and how it's used in code. You must either explicitly import the library with its version number (e.g. `use foo_v1.2`) or include a `modules.ini` configuration file that maps a shorthand alias to a specific version of a library. For example, if the -`modules.ini` file has `foo=foo_v1.2`, you can put `use foo` to use v1.2 of the -`foo` library (assuming you have it installed). +`modules.ini` file has a `[foo]` section with `version=v1.2`, you can put `use +foo` to use v1.2 of the `foo` library (assuming you have it installed). # Rationale diff --git a/examples/README.md b/examples/README.md index 2b5f81fa..9e9291e5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,8 +8,6 @@ This folder contains some example programs and libraries. style of [learnxinyminutes.com](https://learnxinyminutes.com/). - [game](game/): An example game using raylib. - [http-server](http-server/): A multithreaded HTTP server. -- [tomodeps](tomodeps/): A library for finding Tomo dependencies. -- [tomo-install](tomo-install/): A library for installing Tomo dependencies. - [wrap](wrap/): A command-line program to wrap text. ## Example Libraries diff --git a/examples/colorful/README.md b/examples/colorful/README.md index 04a221e3..faded9b1 100644 --- a/examples/colorful/README.md +++ b/examples/colorful/README.md @@ -54,8 +54,14 @@ colorful [--help] [texts...] [--by-line] [--files ...] `colorful` can also be used as a Tomo library: +```ini +# modules.ini +[colorful] +version=v1.0 +``` + ```tomo -use colorful_v1.0 +use colorful $Colorful" @(blue:Welcome to the @(bold:party)!) diff --git a/examples/colorful/colorful.tm b/examples/colorful/colorful.tm index 270efe8c..9a8bbbba 100644 --- a/examples/colorful/colorful.tm +++ b/examples/colorful/colorful.tm @@ -7,7 +7,7 @@ HELP := " CSI := "\033[" -use patterns_v1.0 +use patterns lang Colorful convert(text:Text -> Colorful) diff --git a/examples/colorful/modules.ini b/examples/colorful/modules.ini new file mode 100644 index 00000000..fb52a859 --- /dev/null +++ b/examples/colorful/modules.ini @@ -0,0 +1,2 @@ +[patterns] +version=v1.0 diff --git a/examples/coroutines/README.md b/examples/coroutines/README.md index 86b5dd00..644c0e07 100644 --- a/examples/coroutines/README.md +++ b/examples/coroutines/README.md @@ -5,8 +5,14 @@ This is a coroutine library built on top of a modified version of ## Example Usage +```ini +# modules.ini +[coroutines] +version=v1.0 +``` + ```tomo -use coroutines_v1.0 +use coroutines func main() co := Coroutine(func() diff --git a/examples/http-server/connection-queue.tm b/examples/http-server/connection-queue.tm index 5cf8bb91..c56069e1 100644 --- a/examples/http-server/connection-queue.tm +++ b/examples/http-server/connection-queue.tm @@ -1,4 +1,4 @@ -use pthreads_v1.0 +use pthreads func _assert_success(name:Text, val:Int32; inline) fail("$name() failed!") if val < 0 diff --git a/examples/http-server/http-server.tm b/examples/http-server/http-server.tm index 717aa733..89141c32 100644 --- a/examples/http-server/http-server.tm +++ b/examples/http-server/http-server.tm @@ -9,9 +9,9 @@ use use use -use commands_v1.0 -use pthreads_v1.0 -use patterns_v1.0 +use commands +use pthreads +use patterns use ./connection-queue.tm @@ -138,7 +138,7 @@ func load_routes(directory:Path -> {Text=RouteEntry}) func main(directory:Path, port=Int32(8080)) say("Serving on port $port") routes := load_routes(directory) - say(" Hosting: $routes") + say("Hosting: $routes") serve(port, func(request:HTTPRequest) if handler := routes[request.path] diff --git a/examples/http-server/modules.ini b/examples/http-server/modules.ini new file mode 100644 index 00000000..171e11d2 --- /dev/null +++ b/examples/http-server/modules.ini @@ -0,0 +1,8 @@ +[pthreads] +version=v1.0 + +[patterns] +version=v1.0 + +[commands] +version=v1.0 diff --git a/examples/http-server/sample-site/random.tm b/examples/http-server/sample-site/random.tm index 75e185b3..153ac2af 100755 --- a/examples/http-server/sample-site/random.tm +++ b/examples/http-server/sample-site/random.tm @@ -1,5 +1,5 @@ #!/bin/env tomo -use random_v1.0 +use random func main() say(" diff --git a/examples/ini/ini.tm b/examples/ini/ini.tm index 9bcfb3a4..4dc27725 100644 --- a/examples/ini/ini.tm +++ b/examples/ini/ini.tm @@ -1,5 +1,5 @@ -use patterns_v1.0 +use patterns _USAGE := " Usage: ini "[section[/key]]" diff --git a/examples/ini/modules.ini b/examples/ini/modules.ini new file mode 100644 index 00000000..fb52a859 --- /dev/null +++ b/examples/ini/modules.ini @@ -0,0 +1,2 @@ +[patterns] +version=v1.0 diff --git a/examples/tomo-install/CHANGES.md b/examples/tomo-install/CHANGES.md deleted file mode 100644 index 42ae752c..00000000 --- a/examples/tomo-install/CHANGES.md +++ /dev/null @@ -1,5 +0,0 @@ -# Version History - -## v1.0 - -Initial version diff --git a/examples/tomo-install/tomo-install.tm b/examples/tomo-install/tomo-install.tm deleted file mode 100644 index e9838859..00000000 --- a/examples/tomo-install/tomo-install.tm +++ /dev/null @@ -1,84 +0,0 @@ -use shell_v1.0 -use patterns_v1.0 - -_USAGE := " - tomo-install file.tm... -" - -_HELP := " - tomo-install: a tool for installing libraries for the Tomo language - Usage: $_USAGE -" - -func find_urls(path:Path -> [Text]) - urls : @[Text] - if path.is_directory() - for f in path.children() - urls.insert_all(find_urls(f)) - else if path.is_file() and path.extension() == ".tm" - for line in path.by_line()! - if captures := line.pattern_captures($Pat/use{space}{url}/) or line.pattern_captures($Pat/{id}{space}:={space}use{space}{url}/) - urls.insert(captures[-1]) - return urls - -func main(paths:[Path]) - if paths.length == 0 - paths = [(./)] - - urls := (++: find_urls(p) for p in paths) or [] - - github_token := (~/.config/tomo/github-token).read() - - (~/.local/share/tomo/installed).create_directory() - (~/.local/share/tomo/lib).create_directory() - - for url in urls - original_url := url - url_without_protocol := url.trim_pattern($Pat"http{0-1 s}://", right=no) - hash := $Shell@(echo -n @url_without_protocol | sha256sum).get_output()!.slice(to=32) - if (~/.local/share/tomo/installed/$hash).is_directory() - say("Already installed: $url") - skip - - alias : Text? - curl_flags := ["-L"] - if github := url_without_protocol.pattern_captures($Pat"github.com/{!/}/{!/}#{..}") - user := github[1] - repo := github[2] - tag := github[3] - url = "https://api.github.com/repos/$user/$repo/tarball/$tag" - alias = "$(repo.without_prefix("tomo-")).$(tag).$(user)" - if github_token - curl_flags ++= ["-H", "Authorization: Bearer $github_token"] - curl_flags ++= [ - "-H", "Accept: application/vnd.github+json", - "-H", "X-GitHub-Api-Version: 2022-11-28", - ] - - (~/.local/share/tomo/downloads/$hash).create_directory() - say($Shell@` - set -euo pipefail - cd ~/.local/share/tomo/downloads/@hash - curl @curl_flags @url | tar xz -C ~/.local/share/tomo/installed --strip-components=1 --one-top-level=@hash - echo @original_url > ~/.local/share/tomo/installed/@hash/source.url - tomo -L ~/.local/share/tomo/installed/@hash - if [ "`uname -s`" = "Darwin" ]; then - ln -f -s ../installed/@hash/lib@hash.dylib ~/.local/share/tomo/lib/lib@hash.dylib - else - ln -f -s ../installed/@hash/lib@hash.so ~/.local/share/tomo/lib/lib@hash.so - fi - `.get_output()!) - - if alias - say($Shell( - set -exuo pipefail - ln -f -s @hash ~/.local/share/tomo/installed/@alias - if [ "`uname -s`" = "Darwin" ]; then - ln -f -s lib@hash.dylib ~/.local/share/tomo/lib/lib@alias.dylib - else - ln -f -s lib@hash.so ~/.local/share/tomo/lib/lib@alias.so - fi - ).get_output()!) - - say("\[1]Installed $url!\[]") - diff --git a/examples/tomodeps/CHANGES.md b/examples/tomodeps/CHANGES.md deleted file mode 100644 index 42ae752c..00000000 --- a/examples/tomodeps/CHANGES.md +++ /dev/null @@ -1,5 +0,0 @@ -# Version History - -## v1.0 - -Initial version diff --git a/examples/tomodeps/tomodeps.tm b/examples/tomodeps/tomodeps.tm deleted file mode 100644 index af7f2573..00000000 --- a/examples/tomodeps/tomodeps.tm +++ /dev/null @@ -1,117 +0,0 @@ -# Show a Tomo dependency graph - -use patterns_v1.0 - -_USAGE := "Usage: tomodeps " - -_HELP := " - tomodeps: Show a file dependency graph for Tomo source files. - $_USAGE -" - -enum Dependency(File(path:Path), Module(name:Text)) - -func _get_file_dependencies(file:Path -> |Dependency|) - if not file.is_file() - say("Could not read file: $file") - return || - - deps : @|Dependency| - if lines := file.by_line() - for line in lines - if line.matches_pattern($Pat/use {..}.tm/) - file_import := Path.from_text(line.replace_pattern($Pat/use {..}/, "@1")).resolved(relative_to=file) - deps.add(Dependency.File(file_import)) - else if line.matches_pattern($Pat/use {id}/) - module_name := line.replace_pattern($Pat/use {..}/, "@1") - deps.add(Dependency.Module(module_name)) - return deps[] - -func _build_dependency_graph(dep:Dependency, dependencies:@{Dependency=|Dependency|}) - return if dependencies.has(dep) - - dependencies[dep] = || # Placeholder - - dep_deps := when dep is File(path) - _get_file_dependencies(path) - is Module(module) - dir := (~/.local/share/tomo/installed/$module) - module_deps : @|Dependency| - visited : @|Path| - unvisited := @|f.resolved() for f in dir.files() if f.extension() == ".tm"| - while unvisited.length > 0 - file := unvisited.items[-1] - unvisited.remove(file) - visited.add(file) - - for file_dep in _get_file_dependencies(file) - when file_dep is File(f) - if not visited.has(f) - unvisited.add(f) - is Module(m) - module_deps.add(file_dep) - module_deps[] - - dependencies[dep] = dep_deps - - for dep2 in dep_deps - _build_dependency_graph(dep2, dependencies) - -func get_dependency_graph(dep:Dependency -> {Dependency=|Dependency|}) - graph : @{Dependency=|Dependency|} - _build_dependency_graph(dep, graph) - return graph - -func _printable_name(dep:Dependency -> Text) - when dep is Module(module) - return "\[34;1]$module\[]" - is File(f) - f = f.relative_to((.)) - if f.exists() - return Text(f) - else - return "\[31;1]$f (not found)\[]" - -func _draw_tree(dep:Dependency, dependencies:{Dependency=|Dependency|}, already_printed:@|Dependency|, prefix="", is_last=yes) - if already_printed.has(dep) - say(prefix ++ (if is_last then "└── " else "├── ") ++ _printable_name(dep) ++ " \[2](recursive)\[]") - return - - say(prefix ++ (if is_last then "└── " else "├── ") ++ _printable_name(dep)) - already_printed.add(dep) - - child_prefix := prefix ++ (if is_last then " " else "│ ") - - children := dependencies[dep] or || - for i,child in children.items - is_child_last := (i == children.length) - _draw_tree(child, dependencies, already_printed, child_prefix, is_child_last) - -func draw_tree(dep:Dependency, dependencies:{Dependency=|Dependency|}) - printed : @|Dependency| - say(_printable_name(dep)) - printed.add(dep) - deps := dependencies[dep] or || - for i,child in deps.items - is_child_last := (i == deps.length) - _draw_tree(child, dependencies, already_printed=printed, is_last=is_child_last) - -func main(files:[Text]) - if files.length == 0 - exit(" - Please provide at least one file! - $_USAGE - ") - - for arg in files - if arg.matches_pattern($Pat/{..}.tm/) - path := Path.from_text(arg).resolved() - dependencies := get_dependency_graph(File(path)) - draw_tree(File(path), dependencies) - else if arg.matches_pattern($Pat/{id}/) - dependencies := get_dependency_graph(Module(arg)) - draw_tree(Module(arg), dependencies) - else - say("\[2]Skipping $arg\[]") - skip - diff --git a/src/compile.c b/src/compile.c index cd6aef72..2ecb8c9e 100644 --- a/src/compile.c +++ b/src/compile.c @@ -12,6 +12,7 @@ #include "cordhelpers.h" #include "enums.h" #include "environment.h" +#include "modules.h" #include "parse.h" #include "stdlib/integers.h" #include "stdlib/nums.h" @@ -1976,10 +1977,13 @@ static CORD _compile_statement(env_t *env, ast_t *ast) CORD suffix = get_id_suffix(Path$as_c_string(path)); return with_source_info(env, ast, CORD_all("$initialize", suffix, "();\n")); } else if (use->what == USE_MODULE) { - const char *name = module_alias(ast); + module_info_t mod = get_module_info(ast); glob_t tm_files; - if (glob(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", name, "/[!._0-9]*.tm"), GLOB_TILDE, NULL, &tm_files) != 0) - code_err(ast, "Could not find library"); + const char *folder = mod.version ? String(mod.name, "_", mod.version) : mod.name; + if (glob(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", folder, "/[!._0-9]*.tm"), GLOB_TILDE, NULL, &tm_files) != 0) { + if (!try_install_module(mod)) + code_err(ast, "Could not find library"); + } CORD initialization = CORD_EMPTY; @@ -4485,10 +4489,13 @@ CORD compile_statement_type_header(env_t *env, Path_t header_path, ast_t *ast) Path_t build_dir = Path$resolved(Path$parent(header_path), Path$current_dir()); switch (use->what) { case USE_MODULE: { - const char *name = module_alias(ast); + module_info_t mod = get_module_info(ast); glob_t tm_files; - if (glob(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", name, "/[!._0-9]*.tm"), GLOB_TILDE, NULL, &tm_files) != 0) - code_err(ast, "Could not find library"); + const char *folder = mod.version ? String(mod.name, "_", mod.version) : mod.name; + if (glob(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", folder, "/[!._0-9]*.tm"), GLOB_TILDE, NULL, &tm_files) != 0) { + if (!try_install_module(mod)) + code_err(ast, "Could not find library"); + } CORD includes = CORD_EMPTY; for (size_t i = 0; i < tm_files.gl_pathc; i++) { diff --git a/src/environment.c b/src/environment.c index 4fdfbd4e..05ebed5a 100644 --- a/src/environment.c +++ b/src/environment.c @@ -7,12 +7,8 @@ #include "cordhelpers.h" #include "environment.h" #include "parse.h" -#include "stdlib/c_strings.h" #include "stdlib/datatypes.h" -#include "stdlib/memory.h" #include "stdlib/paths.h" -#include "stdlib/pointers.h" -#include "stdlib/simpleparse.h" #include "stdlib/tables.h" #include "stdlib/text.h" #include "stdlib/util.h" @@ -793,40 +789,4 @@ void set_binding(env_t *env, const char *name, type_t *type, CORD code) Table$str_set(env->locals, name, new(binding_t, .type=type, .code=code)); } -const char *module_alias(ast_t *use) -{ - static Table_t cache = {}; - const char **cached = Table$get(cache, &use, Table$info(Pointer$info("@", &Memory$info), &CString$info)); - if (cached) return *cached; - const char *name = Match(use, Use)->path; - const char *alias = name; - if (streq(name, "commands")) alias = "commands_v1.0"; - else if (streq(name, "random")) alias = "random_v1.0"; - else if (streq(name, "base64")) alias = "base64_v1.0"; - else if (streq(name, "core")) alias = "core_v1.0"; - else if (streq(name, "patterns")) alias = "patterns_v1.0"; - else if (streq(name, "pthreads")) alias = "pthreads_v1.0"; - else if (streq(name, "shell")) alias = "shell_v1.0"; - else if (streq(name, "time")) alias = "time_v1.0"; - else if (streq(name, "uuid")) alias = "uuid_v1.0"; - else { - Path_t alias_file = Path$sibling(Path$from_str(use->file->filename), Text("modules.ini")); - OptionalClosure_t by_line = Path$by_line(alias_file); - if (by_line.fn) { - OptionalText_t (*next_line)(void*) = by_line.fn; - for (Text_t line; (line=next_line(by_line.userdata)).length >= 0; ) { - char *line_str = Text$as_c_string(line); - const char *line_alias = NULL, *full_version = NULL; - if (!strparse(line_str, &line_alias, "=", &full_version) - && streq(line_alias, name)) { - alias = full_version; - break; - } - } - } - } - Table$set(&cache, &use, &alias, Table$info(Pointer$info("@", &Memory$info), &CString$info)); - return alias; -} - // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 diff --git a/src/environment.h b/src/environment.h index 474bab61..a89935e6 100644 --- a/src/environment.h +++ b/src/environment.h @@ -94,6 +94,5 @@ binding_t *get_namespace_binding(env_t *env, ast_t *self, const char *name); extern type_t *TEXT_TYPE; extern type_t *PATH_TYPE; extern type_t *PATH_TYPE_TYPE; -const char *module_alias(ast_t *use); // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 diff --git a/src/modules.c b/src/modules.c new file mode 100644 index 00000000..71e33eec --- /dev/null +++ b/src/modules.c @@ -0,0 +1,121 @@ +#include "modules.h" +#include "stdlib/memory.h" +#include "stdlib/paths.h" +#include "stdlib/simpleparse.h" +#include "stdlib/pointers.h" +#include "stdlib/tables.h" +#include "stdlib/text.h" +#include "stdlib/types.h" + +#define xsystem(...) ({ int _status = system(String(__VA_ARGS__)); if (!WIFEXITED(_status) || WEXITSTATUS(_status) != 0) errx(1, "Failed to run command: %s", String(__VA_ARGS__)); }) + +module_info_t get_module_info(ast_t *use) +{ + static Table_t cache = {}; + TypeInfo_t *cache_type = Table$info(Pointer$info("@", &Memory$info), Pointer$info("@", &Memory$info)); + module_info_t **cached = Table$get(cache, &use, cache_type); + if (cached) return **cached; + const char *name = Match(use, Use)->path; + module_info_t *info = new(module_info_t, .name=name); + if (streq(name, "commands")) info->version = "v1.0"; + else if (streq(name, "random")) info->version = "v1.0"; + else if (streq(name, "base64")) info->version = "v1.0"; + else if (streq(name, "core")) info->version = "v1.0"; + else if (streq(name, "patterns")) info->version = "v1.0"; + else if (streq(name, "pthreads")) info->version = "v1.0"; + else if (streq(name, "shell")) info->version = "v1.0"; + else if (streq(name, "time")) info->version = "v1.0"; + else if (streq(name, "uuid")) info->version = "v1.0"; + else { + Path_t alias_file = Path$sibling(Path$from_str(use->file->filename), Text("modules.ini")); + OptionalClosure_t by_line = Path$by_line(alias_file); + if (by_line.fn) { + OptionalText_t (*next_line)(void*) = by_line.fn; + find_section:; + for (Text_t line; (line=next_line(by_line.userdata)).length >= 0; ) { + char *line_str = Text$as_c_string(line); + if (line_str[0] == '[' && strncmp(line_str+1, name, strlen(name)) == 0 + && line_str[1+strlen(name)] == ']') + break; + } + for (Text_t line; (line=next_line(by_line.userdata)).length >= 0; ) { + char *line_str = Text$as_c_string(line); + if (line_str[0] == '[') goto find_section; + if (!strparse(line_str, "version=", &info->version) + || !strparse(line_str, "url=", &info->url) + || !strparse(line_str, "git=", &info->git) + || !strparse(line_str, "path=", &info->path) + || !strparse(line_str, "revision=", &info->revision)) + continue; + } + } + } + Table$set(&cache, &use, &info, cache_type); + return *info; + +} + +bool try_install_module(module_info_t mod) +{ + if (mod.git) { + OptionalText_t answer = ask( + Texts(Text("The module \""), Text$from_str(mod.name), Text("\" is not installed.\nDo you want to install it from git URL "), + Text$from_str(mod.git), Text("? [Y/n] ")), + true, true); + if (!(answer.length == 0 || Text$equal_values(answer, Text("Y")) || Text$equal_values(answer, Text("y")))) + return false; + print("Installing ", mod.name, " from git..."); + Path_t tmpdir = Path$unique_directory(Path("/tmp/tomo-module-XXXXXX")); + if (mod.revision) xsystem("git clone --depth=1 --revision ", mod.revision, " ", mod.git, " ", tmpdir); + else xsystem("git clone --depth=1 ", mod.git, " ", tmpdir); + if (mod.path) xsystem("tomo -IL ", tmpdir, "/", mod.path); + else xsystem("tomo -IL ", tmpdir); + Path$remove(tmpdir, true); + return true; + } else if (mod.url) { + OptionalText_t answer = ask( + Texts(Text("The module "), Text$from_str(mod.name), Text(" is not installed.\nDo you want to install it from URL "), + Text$from_str(mod.url), Text("? [Y/n] ")), + true, true); + if (!(answer.length == 0 || Text$equal_values(answer, Text("Y")) || Text$equal_values(answer, Text("y")))) + return false; + + print("Installing ", mod.name, " from URL..."); + + const char *p = strrchr(mod.url, '/'); + if (!p) return false; + const char *filename = p + 1; + p = strchr(filename, '.'); + if (!p) return false; + const char *extension = p + 1; + Path_t tmpdir = Path$unique_directory(Path("/tmp/tomo-module-XXXXXX")); + xsystem("curl ", mod.url, " -o ", tmpdir); + if (streq(extension, ".zip")) + xsystem("unzip ", tmpdir, "/", filename); + else if (streq(extension, ".tar.gz") || streq(extension, ".tar")) + xsystem("tar xf ", tmpdir, "/", filename); + else + return false; + const char *basename = String(string_slice(filename, strcspn(filename, "."))); + if (mod.path) xsystem("tomo -IL ", tmpdir, "/", basename, "/", mod.path); + else xsystem("tomo -IL ", tmpdir, "/", basename); + Path$remove(tmpdir, true); + return true; + } else if (mod.path) { + OptionalText_t answer = ask( + Texts(Text("The module "), Text$from_str(mod.name), Text(" is not installed.\nDo you want to install it from path "), + Text$from_str(mod.path), Text("? [Y/n] ")), + true, true); + if (!(answer.length == 0 || Text$equal_values(answer, Text("Y")) || Text$equal_values(answer, Text("y")))) + return false; + + print("Installing ", mod.name, " from path..."); + xsystem("tomo -IL ", mod.path); + return true; + } + + return false; +} + + +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 diff --git a/src/modules.h b/src/modules.h new file mode 100644 index 00000000..36b05d3d --- /dev/null +++ b/src/modules.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include "ast.h" + +typedef struct { + const char *name, *version, *url, *git, *revision, *path; +} module_info_t; + +module_info_t get_module_info(ast_t *use); +bool try_install_module(module_info_t mod); + +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 diff --git a/src/tomo.c b/src/tomo.c index a2ccc84a..b47cd8e1 100644 --- a/src/tomo.c +++ b/src/tomo.c @@ -16,6 +16,7 @@ #include "ast.h" #include "compile.h" #include "cordhelpers.h" +#include "modules.h" #include "parse.h" #include "stdlib/bools.h" #include "stdlib/bytes.h" @@ -614,14 +615,15 @@ void build_file_dependency_graph(Path_t path, Table_t *to_compile, Table_t *to_l break; } case USE_MODULE: { - const char *name = module_alias(stmt_ast); + module_info_t mod = get_module_info(stmt_ast); + const char *full_name = mod.version ? String(mod.name, "_", mod.version) : mod.name; Text_t lib = Texts(Text("-Wl,-rpath,'"), - Text(TOMO_PREFIX "/share/tomo_"TOMO_VERSION"/installed/"), Text$from_str(name), + Text(TOMO_PREFIX "/share/tomo_"TOMO_VERSION"/installed/"), Text$from_str(full_name), Text("' '" TOMO_PREFIX "/share/tomo_"TOMO_VERSION"/installed/"), - Text$from_str(name), Text("/lib"), Text$from_str(name), Text(SHARED_SUFFIX "'")); + Text$from_str(full_name), Text("/lib"), Text$from_str(full_name), Text(SHARED_SUFFIX "'")); Table$set(to_link, &lib, NULL, Table$info(&Text$info, &Void$info)); - List_t children = Path$glob(Path$from_str(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", name, "/*.tm"))); + List_t children = Path$glob(Path$from_str(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", full_name, "/[!._0-9]*.tm"))); for (int64_t i = 0; i < children.length; i++) { Path_t *child = (Path_t*)(children.data + i*children.stride); Table_t discarded = {.fallback=to_compile}; diff --git a/src/typecheck.c b/src/typecheck.c index 60cef3fc..f97235b4 100644 --- a/src/typecheck.c +++ b/src/typecheck.c @@ -11,6 +11,7 @@ #include "ast.h" #include "cordhelpers.h" #include "environment.h" +#include "modules.h" #include "parse.h" #include "stdlib/paths.h" #include "stdlib/tables.h" @@ -182,13 +183,16 @@ static env_t *load_module(env_t *env, ast_t *module_ast) return load_module_env(env, ast); } case USE_MODULE: { - const char *name = module_alias(module_ast); + module_info_t mod = get_module_info(module_ast); glob_t tm_files; - if (glob(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", name, "/[!._0-9]*.tm"), GLOB_TILDE, NULL, &tm_files) != 0) - code_err(module_ast, "Could not find library"); + const char *folder = mod.version ? String(mod.name, "_", mod.version) : mod.name; + if (glob(String(TOMO_PREFIX"/share/tomo_"TOMO_VERSION"/installed/", folder, "/[!._0-9]*.tm"), GLOB_TILDE, NULL, &tm_files) != 0) { + if (!try_install_module(mod)) + code_err(module_ast, "Could not find or install library"); + } env_t *module_env = fresh_scope(env); - Table$str_set(env->imports, name, module_env); + Table$str_set(env->imports, mod.name, module_env); for (size_t i = 0; i < tm_files.gl_pathc; i++) { const char *filename = tm_files.gl_pathv[i]; -- cgit v1.2.3