aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2025-05-20 15:22:41 -0400
committerBruce Hill <bruce@bruce-hill.com>2025-05-20 15:22:41 -0400
commitefb3aae55908cb88f5a9c900d6563603ab792d6a (patch)
treece4293828ab43de4e6440bed0accf871a2d82f94
parent3d313c5956510a807c2ce7d1ffd9c3bfbb708444 (diff)
Add more advanced configuration options to modules.ini and support
automatically downloading and installing from it.
-rw-r--r--CHANGES.md2
-rw-r--r--Makefile3
-rw-r--r--docs/libraries.md32
-rw-r--r--docs/versions.md4
-rw-r--r--examples/README.md2
-rw-r--r--examples/colorful/README.md8
-rw-r--r--examples/colorful/colorful.tm2
-rw-r--r--examples/colorful/modules.ini2
-rw-r--r--examples/coroutines/README.md8
-rw-r--r--examples/http-server/connection-queue.tm2
-rw-r--r--examples/http-server/http-server.tm8
-rw-r--r--examples/http-server/modules.ini8
-rwxr-xr-xexamples/http-server/sample-site/random.tm2
-rw-r--r--examples/ini/ini.tm2
-rw-r--r--examples/ini/modules.ini2
-rw-r--r--examples/tomo-install/CHANGES.md5
-rw-r--r--examples/tomo-install/tomo-install.tm84
-rw-r--r--examples/tomodeps/CHANGES.md5
-rw-r--r--examples/tomodeps/tomodeps.tm117
-rw-r--r--src/compile.c19
-rw-r--r--src/environment.c40
-rw-r--r--src/environment.h1
-rw-r--r--src/modules.c121
-rw-r--r--src/modules.h14
-rw-r--r--src/tomo.c10
-rw-r--r--src/typecheck.c12
26 files changed, 229 insertions, 286 deletions
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 <unistd.h>
use <arpa/inet.h>
use <err.h>
-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 <filename> "[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 <files...>"
-
-_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 <stdbool.h>
+
+#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];