Module search revamp for pkg managers

The search path listed in an import directive can now be an array.  The
top-level search path is appended.  Null and empty strings in the path
terminate any search.  The "." in "." and "./*" is replaced with the
directory containing the file doing the import (for command-line
programs this is the current directory, though that may be a bad idea).

No version numbers or anything of the sort are gratuitously added to the
search paths.

All this makes external package managers possible by allowing
dependencies to be installed local to dependents.
This commit is contained in:
Nicolas Williams committed 2014-12-24 02:31:51 -06:00
1 parent 1cdcff1c01
commit 56ae88d9d5
10 files changed
+167 -80

No files matched your search

+1
View File
@@ -10,6 +10,7 @@
tags tags
jq jq
!tests/modules/lib/jq/
jq.1 jq.1
# Something delightfully recursive happens otherwise # Something delightfully recursive happens otherwise
+41 -27
View File
@@ -2286,45 +2286,58 @@ sections:
body: | body: |
jq has a library/module system. Modules are files whose names end jq has a library/module system. Modules are files whose names end
in `.jq`. Modules should start with a `module` directive. in `.jq`. Modules should (but don't have to) start with a
Programs and modules must `import` their dependencies, if any. `module` directive. Programs and modules must `import` their
dependencies, if any.
A search path is used to search for modules. Each directory in Modules imported by a program are searched for in a default search
the search path is tested twice: once with a directory named "any" path (see below). The `import` directive allows the importer to
added on the end, and once with the jq version number as a alter this path.
directory added on the end.
Paths in the a search path are subject to various substitutions.
For paths starting with "~/", the user's home directory is For paths starting with "~/", the user's home directory is
substituted for "~". substituted for "~".
For paths starting with "$ORIGIN/", the path of the directory of For paths starting with "$ORIGIN/", the path of the jq executable
the dependent is used. For main programs that's the path of the is substituted for "$ORIGIN".
jq executable's containing directory.
For paths starting with "./" or paths that are ".", the path of
the including file is substituted for ".". For top-level programs
given on the command-line, the current directory is used.
Import directives can optionally specify a search path to which
the default is appended.
The default search path is the search path given to the `-L`
command-line option, else the "$JQ_LIBRARY_PATH", if set in the
environment, else `["~/.jq", "$ORIGIN/../lib/jq"]` (on Unix) or
`["~/.jq", "$ORIGIN/lib"]` (on Windows).
The `-L` argument's and "$JQ_LIBRARY_PATH" environment variable's
string values are split on ":" on Unix, or ";" on Windows.
Null and empty string path elements terminate search path
processing.
Modules may be organized into a hierarchy, with name components Modules may be organized into a hierarchy, with name components
separated by double-colons ("::"). For example: "foo::bar". Each separated by double-colons ("::"). For example: "foo::bar". Each
component in the name corresponds to a directory on the component in the name corresponds to a directory on the
filesystem. Consecutive components with the same name are not filesystem.
allowed to avoid ambiguities (e.g., "foo::foo").
For example, a module named "foo::bar" would be searched for in A module named "foo::bar" would be searched for in "foo/bar.jq"
"foo/bar.jq" and "foo/bar/bar.jq" in the given search path. This and "foo/bar/bar.jq" in the given search path. This is intended to
is intended to allow modules to be placed in a directory along allow modules to be placed in a directory along with, for example,
with, for example, version control files, README files, and so on, version control files, README files, and so on, but also to allow
but also to allow for single-file modules. for single-file modules.
The search path used is as follows: Consecutive components with the same name are not allowed to avoid
ambiguities (e.g., "foo::foo").
- the directory indicated in the search key of the `import` For example, with `-L$HOME/.jq` a module `foo` can be found in
directive metadata (see below) `$HOME/.jq/foo.jq` and `$HOME/.jq/foo/foo.jq`.
- otherwise any directory(ies) listed in the `-L` command-line
argument
- otherwise any directory(ies) listed in the `$JQ_LIBRARY_PATH`
environment variable
For example, with `-L$HOME/.jq` A module `foo` can be found in If "$HOME/.jq" is a file, it is sourced into the main program.
`$HOME/.jq/any/foo.jq`, `$HOME/.jq/1.4/foo.jq`,
`$HOME/.jq/any/foo/foo.jq`, and in `$HOME/.jq/foo/foo.jq`.
entries: entries:
- title: "`module NAME {<metadata>};`" - title: "`module NAME {<metadata>};`"
@@ -2349,7 +2362,8 @@ sections:
an object with keys like "version", "repo", "URI", and so on. an object with keys like "version", "repo", "URI", and so on.
The "search" key in the metadata, if present, should have a The "search" key in the metadata, if present, should have a
string value consisting of a single directory path to search. string or array value (array of strings); this is the search
path to be prefixed to the top-level search path.
- title: "`modulemeta`" - title: "`modulemeta`"
body: | body: |
+7 -3
View File
@@ -1004,12 +1004,16 @@ int jq_compile(jq_state *jq, const char* str) {
return jq_compile_args(jq, str, jv_array()); return jq_compile_args(jq, str, jv_array());
} }
jv jq_get_lib_origin(jq_state *jq) { jv jq_get_jq_origin(jq_state *jq) {
return jq_get_attr(jq, jv_string("ORIGIN")); return jq_get_attr(jq, jv_string("JQ_ORIGIN"));
}
jv jq_get_prog_origin(jq_state *jq) {
return jq_get_attr(jq, jv_string("PROGRAM_ORIGIN"));
} }
jv jq_get_lib_dirs(jq_state *jq) { jv jq_get_lib_dirs(jq_state *jq) {
return jq_get_attr(jq, jv_string("LIB_DIRS")); return jq_get_attr(jq, jv_string("JQ_LIBRARY_PATH"));
} }
void jq_set_attrs(jq_state *jq, jv attrs) { void jq_set_attrs(jq_state *jq, jv attrs) {
+2 -1
View File
@@ -24,7 +24,8 @@ void jq_teardown(jq_state **);
void jq_set_attrs(jq_state *, jv); void jq_set_attrs(jq_state *, jv);
jv jq_get_attrs(jq_state *); jv jq_get_attrs(jq_state *);
jv jq_get_lib_origin(jq_state *); jv jq_get_jq_origin(jq_state *);
jv jq_get_prog_origin(jq_state *);
jv jq_get_lib_dirs(jq_state *); jv jq_get_lib_dirs(jq_state *);
void jq_set_attr(jq_state *, jv, jv); void jq_set_attr(jq_state *, jv, jv);
jv jq_get_attr(jq_state *, jv); jv jq_get_attr(jq_state *, jv);
+66 -41
View File
@@ -28,31 +28,19 @@ static int load_library(jq_state *jq, jv lib_path, block *out_block, struct lib_
// 1. lib_path // 1. lib_path
// 2. -L paths passed in on the command line (from jq_state*) // 2. -L paths passed in on the command line (from jq_state*)
// 3. JQ_LIBRARY_PATH environment variable // 3. JQ_LIBRARY_PATH environment variable
jv build_lib_search_chain(jq_state *jq, jv lib_path) { jv build_lib_search_chain(jq_state *jq, jv search_path) {
assert(jv_get_kind(lib_path) == JV_KIND_STRING); assert(jv_get_kind(search_path) == JV_KIND_ARRAY);
jv expanded = jv_array();
jv out_paths = jv_array(); jv_array_foreach(search_path, i, path) {
if (jv_string_length_bytes(jv_copy(lib_path)))
out_paths = jv_array_append(out_paths, lib_path);
else
jv_free(lib_path);
jv lib_dirs = jq_get_lib_dirs(jq);
jv_array_foreach(lib_dirs, i, path) {
if (jv_string_length_bytes(jv_copy(path)) == 0) {
jv_free(path);
continue;
}
path = expand_path(path); path = expand_path(path);
if (jv_is_valid(path)) { if (jv_is_valid(path))
out_paths = jv_array_append(out_paths, path); expanded = jv_array_append(expanded, expand_path(path));
} else { else
jv emsg = jv_invalid_get_msg(path); jv_free(path);
jq_report_error(jq, jv_string_fmt("jq: warning: skipping search path: %s\n",jv_string_value(emsg)));
jv_free(emsg);
}
} }
jv_free(lib_dirs); jv_free(search_path);
return out_paths; expanded = jv_array_concat(expanded, jq_get_lib_dirs(jq));
return expanded;
} }
static jv name2relpath(jv name) { static jv name2relpath(jv name) {
@@ -77,7 +65,7 @@ static jv name2relpath(jv name) {
} }
static jv find_lib(jq_state *jq, jv lib_name, jv lib_search_path) { static jv find_lib(jq_state *jq, jv lib_name, jv lib_search_path) {
assert(jv_get_kind(lib_search_path) == JV_KIND_STRING); assert(jv_get_kind(lib_search_path) == JV_KIND_ARRAY);
assert(jv_get_kind(lib_name) == JV_KIND_STRING); assert(jv_get_kind(lib_name) == JV_KIND_STRING);
jv rel_path = name2relpath(jv_copy(lib_name)); jv rel_path = name2relpath(jv_copy(lib_name));
@@ -89,9 +77,18 @@ static jv find_lib(jq_state *jq, jv lib_name, jv lib_search_path) {
struct stat st; struct stat st;
int ret; int ret;
jv lib_search_paths = build_lib_search_chain(jq, expand_path(lib_search_path)); jv lib_search_paths = build_lib_search_chain(jq, lib_search_path);
jv_array_foreach(lib_search_paths, i, spath) { jv_array_foreach(lib_search_paths, i, spath) {
if (jv_get_kind(spath) == JV_KIND_NULL) {
jv_free(spath);
break;
}
if (jv_get_kind(spath) != JV_KIND_STRING ||
strcmp(jv_string_value(spath), "") == 0) {
jv_free(spath);
continue; /* XXX report non-strings in search path?? */
}
jv testpath = jq_realpath(jv_string_fmt("%s/%s.jq", jv testpath = jq_realpath(jv_string_fmt("%s/%s.jq",
jv_string_value(spath), jv_string_value(spath),
jv_string_value(rel_path))); jv_string_value(rel_path)));
@@ -125,7 +122,18 @@ static int version_matches(jq_state *jq, block importer, block module) {
return 1; return 1;
} }
static int process_dependencies(jq_state *jq, jv lib_origin, block *src_block, struct lib_loading_state *lib_state) { static jv default_search(jv value) {
if (!jv_is_valid(value)) {
jv_free(value);
return JV_ARRAY(jv_string("."), jv_string("$ORIGIN"));
}
if (jv_get_kind(value) != JV_KIND_ARRAY)
return JV_ARRAY(value);
return value;
}
// XXX Split this into a util that takes a callback, and then...
static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block *src_block, struct lib_loading_state *lib_state) {
jv deps = block_take_imports(src_block); jv deps = block_take_imports(src_block);
block bk = *src_block; block bk = *src_block;
int nerrors = 0; int nerrors = 0;
@@ -137,26 +145,38 @@ static int process_dependencies(jq_state *jq, jv lib_origin, block *src_block, s
jv_free(as); jv_free(as);
as = jv_string(""); as = jv_string("");
} }
jv search = jv_object_get(dep, jv_string("search")); jv search = default_search(jv_object_get(dep, jv_string("search")));
if (!jv_is_valid(search)) { jv_array_foreach(search, k, search_elt) {
jv_free(search); if (strcmp(".",jv_string_value(search_elt)) == 0) {
search = jv_string(""); jv tsearch = jv_copy(lib_origin);
} jv_free(search_elt);
if (strncmp("$ORIGIN/",jv_string_value(search),8) == 0) { search = jv_array_set(search, k, tsearch);
jv tsearch = jv_string_fmt("%s/%s", } else if (strncmp("./",jv_string_value(search_elt),sizeof("./")-1) == 0) {
jv_string_value(lib_origin), jv tsearch = jv_string_fmt("%s/%s",
jv_string_value(search) + sizeof ("$ORIGIN/") - 1); jv_string_value(lib_origin),
jv_free(search); jv_string_value(search_elt) + sizeof ("./") - 1);
search = tsearch; jv_free(search_elt);
search = jv_array_set(search, k, tsearch);
} else if (strncmp("$ORIGIN/",jv_string_value(search_elt),sizeof("$ORIGIN/")-1) == 0) {
jv tsearch = jv_string_fmt("%s/%s",
jv_string_value(jq_origin),
jv_string_value(search_elt) + sizeof ("$ORIGIN/") - 1);
jv_free(search_elt);
search = jv_array_set(search, k, tsearch);
} else {
jv_free(search_elt);
}
} }
jv lib_path = find_lib(jq, name, search); jv lib_path = find_lib(jq, name, search);
// XXX ...move the rest of this into a callback.
if (!jv_is_valid(lib_path)) { if (!jv_is_valid(lib_path)) {
jv emsg = jv_invalid_get_msg(lib_path); jv emsg = jv_invalid_get_msg(lib_path);
jq_report_error(jq, jv_string_fmt("jq: error: %s\n",jv_string_value(emsg))); jq_report_error(jq, jv_string_fmt("jq: error: %s\n",jv_string_value(emsg)));
jv_free(emsg); jv_free(emsg);
jv_free(lib_origin);
jv_free(as); jv_free(as);
jv_free(deps); jv_free(deps);
jv_free(jq_origin);
jv_free(lib_origin);
return 1; return 1;
} }
uint64_t state_idx = 0; uint64_t state_idx = 0;
@@ -186,6 +206,7 @@ static int process_dependencies(jq_state *jq, jv lib_origin, block *src_block, s
jv_free(as); jv_free(as);
} }
jv_free(lib_origin); jv_free(lib_origin);
jv_free(jq_origin);
jv_free(deps); jv_free(deps);
return nerrors; return nerrors;
} }
@@ -208,7 +229,9 @@ static int load_library(jq_state *jq, jv lib_path, block *out_block, struct lib_
lib_state->names[state_idx] = strdup(jv_string_value(lib_path)); lib_state->names[state_idx] = strdup(jv_string_value(lib_path));
lib_state->defs[state_idx] = program; lib_state->defs[state_idx] = program;
char *lib_origin = strdup(jv_string_value(lib_path)); char *lib_origin = strdup(jv_string_value(lib_path));
nerrors += process_dependencies(jq, jv_string(dirname(lib_origin)), &lib_state->defs[state_idx], lib_state); nerrors += process_dependencies(jq, jq_get_jq_origin(jq),
jv_string(dirname(lib_origin)),
&lib_state->defs[state_idx], lib_state);
free(lib_origin); free(lib_origin);
*out_block = lib_state->defs[state_idx]; *out_block = lib_state->defs[state_idx];
} }
@@ -219,8 +242,10 @@ static int load_library(jq_state *jq, jv lib_path, block *out_block, struct lib_
return nerrors; return nerrors;
} }
// FIXME It'd be nice to have an option to search the same search path
// as we do in process_dependencies.
jv load_module_meta(jq_state *jq, jv modname) { jv load_module_meta(jq_state *jq, jv modname) {
jv lib_path = find_lib(jq, modname, jv_string("")); jv lib_path = find_lib(jq, modname, jv_array());
jv meta = jv_null(); jv meta = jv_null();
jv data = jv_load_file(jv_string_value(lib_path), 1); jv data = jv_load_file(jv_string_value(lib_path), 1);
if (jv_is_valid(data)) { if (jv_is_valid(data)) {
@@ -249,7 +274,7 @@ int load_program(jq_state *jq, struct locfile* src, block *out_block) {
if (nerrors) if (nerrors)
return nerrors; return nerrors;
nerrors = process_dependencies(jq, jq_get_lib_origin(jq), &program, &lib_state); nerrors = process_dependencies(jq, jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state);
block libs = gen_noop(); block libs = gen_noop();
for (uint64_t i = 0; i < lib_state.ct; ++i) { for (uint64_t i = 0; i < lib_state.ct; ++i) {
free(lib_state.names[i]); free(lib_state.names[i]);
+24 -4
View File
@@ -10,6 +10,7 @@
#include "jv.h" #include "jv.h"
#include "jq.h" #include "jq.h"
#include "jv_alloc.h" #include "jv_alloc.h"
#include "util.h"
#include "version.h" #include "version.h"
int jq_testsuite(int argc, char* argv[]); int jq_testsuite(int argc, char* argv[]);
@@ -210,7 +211,7 @@ int main(int argc, char* argv[]) {
int jq_flags = 0; int jq_flags = 0;
size_t short_opts = 0; size_t short_opts = 0;
jv program_arguments = jv_array(); jv program_arguments = jv_array();
jv lib_search_paths = jv_array(); jv lib_search_paths = jv_null();
for (int i=1; i<argc; i++, short_opts = 0) { for (int i=1; i<argc; i++, short_opts = 0) {
if (further_args_are_files) { if (further_args_are_files) {
input_filenames[ninput_files++] = argv[i]; input_filenames[ninput_files++] = argv[i];
@@ -225,6 +226,8 @@ int main(int argc, char* argv[]) {
} }
} else { } else {
if (argv[i][1] == 'L') { if (argv[i][1] == 'L') {
if (jv_get_kind(lib_search_paths) == JV_KIND_NULL)
lib_search_paths = jv_array();
if (argv[i][2] != 0) { // -Lname (faster check than strlen) if (argv[i][2] != 0) { // -Lname (faster check than strlen)
lib_search_paths = jv_array_append(lib_search_paths, jv_string(argv[i]+2)); lib_search_paths = jv_array_append(lib_search_paths, jv_string(argv[i]+2));
} else if (i >= argc - 1) { } else if (i >= argc - 1) {
@@ -376,7 +379,8 @@ int main(int argc, char* argv[]) {
} }
char *penv = getenv("JQ_LIBRARY_PATH"); char *penv = getenv("JQ_LIBRARY_PATH");
if (penv) { if (penv && jv_get_kind(lib_search_paths) == JV_KIND_NULL) {
// Use $JQ_LIBRARY_PATH
#ifdef WIN32 #ifdef WIN32
#define PATH_ENV_SEPARATOR ";" #define PATH_ENV_SEPARATOR ";"
#else #else
@@ -384,15 +388,22 @@ int main(int argc, char* argv[]) {
#endif #endif
lib_search_paths = jv_array_concat(lib_search_paths,jv_string_split(jv_string(penv),jv_string(PATH_ENV_SEPARATOR))); lib_search_paths = jv_array_concat(lib_search_paths,jv_string_split(jv_string(penv),jv_string(PATH_ENV_SEPARATOR)));
#undef PATH_ENV_SEPARATOR #undef PATH_ENV_SEPARATOR
} else if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) {
// Use compiled-in default JQ_LIBRARY_PATH
#ifdef WIN32
lib_search_paths = JV_ARRAY(jv_string("~/.jq"), jv_string("$ORIGIN/lib"));
#else
lib_search_paths = JV_ARRAY(jv_string("~/.jq"), jv_string("$ORIGIN/../lib/jq"));
#endif
} }
jq_set_attr(jq, jv_string("LIB_DIRS"), lib_search_paths); jq_set_attr(jq, jv_string("JQ_LIBRARY_PATH"), lib_search_paths);
char *origin = strdup(argv[0]); char *origin = strdup(argv[0]);
if (origin == NULL) { if (origin == NULL) {
fprintf(stderr, "Error: out of memory\n"); fprintf(stderr, "Error: out of memory\n");
exit(1); exit(1);
} }
jq_set_attr(jq, jv_string("ORIGIN"), jv_string(dirname(origin))); jq_set_attr(jq, jv_string("JQ_ORIGIN"), jv_string(dirname(origin)));
free(origin); free(origin);
if (strchr(JQ_VERSION, '-') == NULL) if (strchr(JQ_VERSION, '-') == NULL)
@@ -436,6 +447,12 @@ int main(int argc, char* argv[]) {
} }
if (options & FROM_FILE) { if (options & FROM_FILE) {
char *program_origin = strdup(program);
if (program_origin == NULL) {
perror("malloc");
exit(2);
}
jv data = jv_load_file(program, 1); jv data = jv_load_file(program, 1);
if (!jv_is_valid(data)) { if (!jv_is_valid(data)) {
data = jv_invalid_get_msg(data); data = jv_invalid_get_msg(data);
@@ -444,9 +461,12 @@ int main(int argc, char* argv[]) {
ret = 2; ret = 2;
goto out; goto out;
} }
jq_set_attr(jq, jv_string("PROGRAM_ORIGIN"), jq_realpath(jv_string(dirname(program_origin))));
compiled = jq_compile_args(jq, jv_string_value(data), program_arguments); compiled = jq_compile_args(jq, jv_string_value(data), program_arguments);
free(program_origin);
jv_free(data); jv_free(data);
} else { } else {
jq_set_attr(jq, jv_string("PROGRAM_ORIGIN"), jq_realpath(jv_string("."))); // XXX is this good?
compiled = jq_compile_args(jq, program, program_arguments); compiled = jq_compile_args(jq, program, program_arguments);
} }
if (!compiled){ if (!compiled){
+11 -2
View File
@@ -1,5 +1,14 @@
module c {whatever:null}; module c {whatever:null};
import a as foo; import a as foo;
import d as d {search:"$ORIGIN/"}; import d as d {search:"./"};
import d {search:"./"};
import e as e {search:"./../lib/jq"};
import f as f {search:"./../lib/jq"};
def a: 0; def a: 0;
def c: foo::a + "c" + d::meh; def c:
if meh != d::meh then error("import into namespace doesn't work")
elif foo::a != "a" then error("foo::a didn't work as expected")
elif d::meh != "meh" then error("d::meh didn't work as expected")
elif e::bah != "bah" then error("e::bah didn't work as expected")
elif f::f != "f is here" then error("f::f didn't work as expected")
else foo::a + "c" + d::meh + e::bah end;
+1
View File
@@ -0,0 +1 @@
def bah: "bah";
+1
View File
@@ -0,0 +1 @@
def f: "f is here";
+13 -2
View File
@@ -137,12 +137,12 @@ if ! $VALGRIND $Q ./jq -ner -L $mods 'import a as foo; import b as bar; import a
exit 1 exit 1
fi fi
if ! $VALGRIND $Q ./jq -ner -L $mods 'import c as foo; [foo::a, foo::c] | . == [0,"acmeh"]' > /dev/null; then if ! $VALGRIND $Q ./jq -ner -L $mods 'import c as foo; [foo::a, foo::c] | . == [0,"acmehbah"]' > /dev/null; then
echo "Module system appears to be broken" 1>&2 echo "Module system appears to be broken" 1>&2
exit 1 exit 1
fi fi
if [ "`$VALGRIND $Q ./jq -cner -L $mods '\"c\" | modulemeta'`" != '{"whatever":null,"name":"c","deps":[{"as":"foo","name":"a"},{"search":"$ORIGIN/","as":"d","name":"d"}]}' ]; then if [ "`$VALGRIND $Q ./jq -cner -L $mods '\"c\" | modulemeta'`" != '{"whatever":null,"name":"c","deps":[{"as":"foo","name":"a"},{"search":"./","as":"d","name":"d"},{"search":"./","name":"d"},{"search":"./../lib/jq","as":"e","name":"e"},{"search":"./../lib/jq","as":"f","name":"f"}]}' ]; then
echo "modulemeta builtin appears to be broken" 1>&2 echo "modulemeta builtin appears to be broken" 1>&2
exit 1 exit 1
fi fi
@@ -171,3 +171,14 @@ if [ -n "$VALGRIND" ] && ! grep 'ERROR SUMMARY: 0 errors from 0 contexts' $d/out
cat $d/out cat $d/out
exit 1 exit 1
fi fi
if ! $VALGRIND ./jq -ner -L $mods -f $mods/test_bind_order.jq > $d/out 2>&1; then
echo "Import bind order is broken" 1>&2
exit 1
fi
if [ -n "$VALGRIND" ] && ! grep 'ERROR SUMMARY: 0 errors from 0 contexts' $d/out > /dev/null; then
echo "Memory errors when programs have syntax errors" 1>&2
cat $d/out
exit 1
fi