From f36970cd7c9d5c4a7bc69b40867d270c2d7ac851 Mon Sep 17 00:00:00 2001 From: odino Date: Wed, 18 Sep 2019 16:39:50 +0400 Subject: [PATCH 1/5] Fixes to the ABS installer PR * if the package file is not present, calls to `require` shouldn't fail * a package `user/package` might contain different abs files (eg. `user/package/file.abs`). When we resolve calls to `require("package/file.abs")` we should only take `package`, and not the whole `package/file.abs` * saving aliases was only working for the first package installed, now the installer will save all modules that are being installed locally * when including a module, if a file is not specified (`require("module")`), we will try to load the `index.abs` file by default --- install/install.go | 15 +++++++-------- util/util.go | 45 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/install/install.go b/install/install.go index bb7bdee3..2f23ca38 100644 --- a/install/install.go +++ b/install/install.go @@ -18,7 +18,6 @@ func valid(module string) bool { } func Install(module string) { - if !valid(module) { fmt.Printf(`Error reading URL. Please use "github.com/USER/REPO" format to install`) return @@ -44,7 +43,7 @@ func Install(module string) { return } -func PrintLoader(done chan int64, message string) { +func printLoader(done chan int64, message string) { var stop bool = false symbols := []string{"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "} i := 0 @@ -92,7 +91,7 @@ func getZip(module string) error { url := fmt.Sprintf("https://%s/archive/master.zip", module) done := make(chan int64) - go PrintLoader(done, "Downloading archive") + go printLoader(done, "Downloading archive") req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -189,21 +188,19 @@ func createAlias(module string) (string, error) { data := make(map[string]string) moduleName := filepath.Base(module) + // Appending "master" as Github zip file has "-master" suffix modulePath := fmt.Sprintf("./vendor/%s-master", module) // If package.abs.json file is empty if len(b) == 0 { - // Appending "master" as Github zip file has "-master" suffix // Add alias key-value pair to file data[moduleName] = modulePath - } else { err = json.Unmarshal(b, &data) if err != nil { fmt.Printf("Could not unmarshal alias json %s\n", err) return "", err } - // module already installed and aliased if data[moduleName] == modulePath { return moduleName, nil @@ -211,12 +208,14 @@ func createAlias(module string) (string, error) { if data[moduleName] != "" { fmt.Printf("This module could not be aliased because module of same name exists\n") - moduleName = module - data[moduleName] = modulePath + return modulePath, nil } + + data[moduleName] = modulePath } newData, err := json.MarshalIndent(data, "", " ") + if err != nil { fmt.Printf("Could not marshal alias json when installing module %s\n", err) return "", err diff --git a/util/util.go b/util/util.go index 91a8b9a8..aa4bcba6 100644 --- a/util/util.go +++ b/util/util.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "strconv" + "strings" "github.com/abs-lang/abs/object" ) @@ -103,16 +104,52 @@ func UniqueStrings(slice []string) []string { return list } +// ReadAliasFromFile translates a path alias +// to the full path in the filesystem. func ReadAliasFromFile(path string) (string, error) { var packageAlias map[string]string - a, _ := ioutil.ReadFile("./packages.abs.json") - err := json.Unmarshal(a, &packageAlias) + a, err := ioutil.ReadFile("./packages.abs.json") + + // We couldn't open the packages, file, possibly doesn't exists + // and the code shouldn't fail + if err != nil { + return path, nil + } + + // Try to decode the packages file: + // if an error occurs we will simply + // ignore it + err = json.Unmarshal(a, &packageAlias) if err != nil { return path, err } - if packageAlias[path] != "" { - return packageAlias[path], nil + // An alias can come in different forms: + // - package + // - package/file.abs + // but we only really need to resolve the + // first path in the alias. + parts := strings.Split(path, string(os.PathSeparator)) + + if len(parts) < 1 { + return path, nil + } + + if packageAlias[parts[0]] != "" { + // If we are able to resolve a path, then + // we should join in back with the rest of the + // paths + p := []string{packageAlias[parts[0]]} + p = append(p, parts[1:]...) + + // If our path didn't end with an ABS file, + // let's assume it's a directory and we will + // auto-include the index.abs file from it + if filepath.Ext(path) != ".abs" { + p = append(p, "index.abs") + } + + return filepath.Join(p...), nil } return path, nil } From a28c2f4c9e8c81bbadf0e7b97289eb013fb34dee Mon Sep 17 00:00:00 2001 From: odino Date: Wed, 18 Sep 2019 17:37:25 +0400 Subject: [PATCH 2/5] simplified function to turn alias into full path to make it testable --- evaluator/functions.go | 33 +++++++++++++++++++-------------- util/util.go | 23 ++--------------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/evaluator/functions.go b/evaluator/functions.go index c7b19b39..1bde923c 100644 --- a/evaluator/functions.go +++ b/evaluator/functions.go @@ -4,6 +4,7 @@ import ( "bufio" "crypto/rand" "encoding/csv" + "encoding/json" "fmt" "io/ioutil" "math" @@ -1574,11 +1575,26 @@ func sourceFn(tok token.Token, env *object.Environment, args ...object.Object) o // require("file.abs") var history = make(map[string]string) -type StringFn func(string) (string, error) +var packageAliases map[string]string +var packageAliasesLoaded bool func requireFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object { - getAlias := Memoize(util.ReadAliasFromFile) - a, error := getAlias(args[0].Inspect()) + if !packageAliasesLoaded { + a, err := ioutil.ReadFile("./packages.abs.json") + + // We couldn't open the packages, file, possibly doesn't exists + // and the code shouldn't fail + if err == nil { + // Try to decode the packages file: + // if an error occurs we will simply + // ignore it + json.Unmarshal(a, &packageAliases) + } + + packageAliasesLoaded = true + } + + a, error := util.UnaliasPath(args[0].Inspect(), packageAliases) if error != nil { return newError(tok, "error resolving '%s': %s\n", args[0].Inspect(), error.Error()) } @@ -1588,17 +1604,6 @@ func requireFn(tok token.Token, env *object.Environment, args ...object.Object) return doSource(tok, e, file, args...) } -func Memoize(fn StringFn) StringFn { - return func(str string) (string, error) { - if res, ok := history[str]; ok { - return res, nil - } - res, err := fn(str) - history[str] = res - return res, err - } -} - func doSource(tok token.Token, env *object.Environment, fileName string, args ...object.Object) object.Object { err := validateArgs(tok, "source", args, 1, [][]string{{object.STRING_OBJ}}) if err != nil { diff --git a/util/util.go b/util/util.go index aa4bcba6..804f3948 100644 --- a/util/util.go +++ b/util/util.go @@ -1,8 +1,6 @@ package util import ( - "encoding/json" - "io/ioutil" "os" "os/user" "path/filepath" @@ -104,26 +102,9 @@ func UniqueStrings(slice []string) []string { return list } -// ReadAliasFromFile translates a path alias +// UnaliasPath translates a path alias // to the full path in the filesystem. -func ReadAliasFromFile(path string) (string, error) { - var packageAlias map[string]string - a, err := ioutil.ReadFile("./packages.abs.json") - - // We couldn't open the packages, file, possibly doesn't exists - // and the code shouldn't fail - if err != nil { - return path, nil - } - - // Try to decode the packages file: - // if an error occurs we will simply - // ignore it - err = json.Unmarshal(a, &packageAlias) - if err != nil { - return path, err - } - +func UnaliasPath(path string, packageAlias map[string]string) (string, error) { // An alias can come in different forms: // - package // - package/file.abs From 6048024a0946e63da12a9490a41933baa92bf52a Mon Sep 17 00:00:00 2001 From: odino Date: Wed, 18 Sep 2019 17:49:44 +0400 Subject: [PATCH 3/5] added tests to resolve module aliases --- evaluator/functions.go | 6 +----- util/util.go | 8 ++++---- util/util_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 util/util_test.go diff --git a/evaluator/functions.go b/evaluator/functions.go index 1bde923c..ce9d0a49 100644 --- a/evaluator/functions.go +++ b/evaluator/functions.go @@ -1594,11 +1594,7 @@ func requireFn(tok token.Token, env *object.Environment, args ...object.Object) packageAliasesLoaded = true } - a, error := util.UnaliasPath(args[0].Inspect(), packageAliases) - if error != nil { - return newError(tok, "error resolving '%s': %s\n", args[0].Inspect(), error.Error()) - } - + a := util.UnaliasPath(args[0].Inspect(), packageAliases) file := filepath.Join(env.Dir, a) e := object.NewEnvironment(env.Writer, filepath.Dir(file)) return doSource(tok, e, file, args...) diff --git a/util/util.go b/util/util.go index 804f3948..03f6d66d 100644 --- a/util/util.go +++ b/util/util.go @@ -104,7 +104,7 @@ func UniqueStrings(slice []string) []string { // UnaliasPath translates a path alias // to the full path in the filesystem. -func UnaliasPath(path string, packageAlias map[string]string) (string, error) { +func UnaliasPath(path string, packageAlias map[string]string) string { // An alias can come in different forms: // - package // - package/file.abs @@ -113,7 +113,7 @@ func UnaliasPath(path string, packageAlias map[string]string) (string, error) { parts := strings.Split(path, string(os.PathSeparator)) if len(parts) < 1 { - return path, nil + return path } if packageAlias[parts[0]] != "" { @@ -130,7 +130,7 @@ func UnaliasPath(path string, packageAlias map[string]string) (string, error) { p = append(p, "index.abs") } - return filepath.Join(p...), nil + return filepath.Join(p...) } - return path, nil + return path } diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 00000000..967d7de4 --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,27 @@ +package util + +import ( + "testing" +) + +func TestUnaliasPath(t *testing.T) { + tests := []struct { + path string + aliases map[string]string + expected string + }{ + {"test", map[string]string{}, "test"}, + {"test/sample.abs", map[string]string{}, "test/sample.abs"}, + {"test/sample.abs", map[string]string{"test": "path"}, "path/sample.abs"}, + {"test", map[string]string{"test": "path"}, "path/index.abs"}, + {"./test", map[string]string{"test": "path"}, "./test"}, + } + + for _, tt := range tests { + res := UnaliasPath(tt.path, tt.aliases) + + if res != tt.expected { + t.Fatalf("error unaliasing path, expected %s, got %s", tt.expected, res) + } + } +} From 17fbb5d583ba7225d7fb2af30cccf7308b62003f Mon Sep 17 00:00:00 2001 From: odino Date: Wed, 18 Sep 2019 17:59:26 +0400 Subject: [PATCH 4/5] winzozz... --- util/util_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/util/util_test.go b/util/util_test.go index 967d7de4..0f98addc 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -1,6 +1,7 @@ package util import ( + "os" "testing" ) @@ -11,10 +12,10 @@ func TestUnaliasPath(t *testing.T) { expected string }{ {"test", map[string]string{}, "test"}, - {"test/sample.abs", map[string]string{}, "test/sample.abs"}, - {"test/sample.abs", map[string]string{"test": "path"}, "path/sample.abs"}, - {"test", map[string]string{"test": "path"}, "path/index.abs"}, - {"./test", map[string]string{"test": "path"}, "./test"}, + {"test" + string(os.PathSeparator) + "sample.abs", map[string]string{}, "test" + string(os.PathSeparator) + "sample.abs"}, + {"test" + string(os.PathSeparator) + "sample.abs", map[string]string{"test": "path"}, "path" + string(os.PathSeparator) + "sample.abs"}, + {"test", map[string]string{"test": "path"}, "path" + string(os.PathSeparator) + "index.abs"}, + {"." + string(os.PathSeparator) + "test", map[string]string{"test": "path"}, "." + string(os.PathSeparator) + "test"}, } for _, tt := range tests { From be6ed0bf274dae0263daa6d6367822ea8e389392 Mon Sep 17 00:00:00 2001 From: odino Date: Wed, 18 Sep 2019 18:33:03 +0400 Subject: [PATCH 5/5] Added docs + automatic index.abs file for all requires that don't end with a .abs file --- .gitignore | 1 + docs/_includes/toc.md | 1 + docs/misc/3pl.md | 92 ++++++++++++++++++++++++++++++++++ docs/types/builtin-function.md | 2 +- evaluator/evaluator_test.go | 8 +-- util/util.go | 19 ++++--- util/util_test.go | 4 +- 7 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 docs/misc/3pl.md diff --git a/.gitignore b/.gitignore index 6040af99..c287260e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ docs/_site *.ignore +test-ignore-* vendor builds/* !builds/.gitkeep diff --git a/docs/_includes/toc.md b/docs/_includes/toc.md index d42d1d29..89a31e83 100644 --- a/docs/_includes/toc.md +++ b/docs/_includes/toc.md @@ -28,6 +28,7 @@ ## Miscellaneous +* [Installing 3rd party libraries](/misc/3pl) * [Errors](/misc/error) * [Configuring the REPL](/misc/configuring-the-repl) * [Runtime](/misc/runtime) diff --git a/docs/misc/3pl.md b/docs/misc/3pl.md new file mode 100644 index 00000000..278edb61 --- /dev/null +++ b/docs/misc/3pl.md @@ -0,0 +1,92 @@ +# Installing 3rd party libraries + +The ABS interpreter comes with a built-in installer for 3rd party libraries, +very similar to `npm install`, `pip install` or `go get`. + +The installer, budled since the `1.8.0` release, is currently **experimental** +and a few things might change. + +In order to install a package, you simply need to run `abs get`: + +``` bash +$ abs get github.com/abs-lang/abs-sample-module +🌘 - Downloading archive +Unpacking... +Creating alias... +Install Success. You can use the module with `require("abs-sample-module")` +``` + +Modules will be saved under the `vendor/$MODULE-master` directory. Each module +also gets an alias to facilitate requiring them in your code, meaning that +both of these forms are supported: + +``` +⧐ require("abs-sample-module/sample.abs") +{"another": f() {return hello world;}} + +⧐ require("vendor/github.com/abs-lang/abs-sample-module-master/sample.abs") +{"another": f() {return hello world;}} +``` + +Note that the `-master` prefix [will be removed](https://github.com/abs-lang/abs/issues/286) in future versions of ABS. + +Module aliases are saved in the `packages.abs.json` file +which is created in the same directory where you run the +`abs get ...` command: + +``` +$ abs get github.com/abs-lang/abs-sample-module +🌗 - Downloading archive +Unpacking... +Creating alias... +Install Success. You can use the module with `require("abs-sample-module")` + +$ cat packages.abs.json +{ + "abs-sample-module": "./vendor/github.com/abs-lang/abs-sample-module-master" +} +``` + +If an alias is already taken, the installer will let you know that you +will need to use the full path when requiring the module: + +``` +$ echo '{"abs-sample-module": "xyz"}' > packages.abs.json + +$ abs get github.com/abs-lang/abs-sample-module +🌘 - Downloading archive +Unpacking... +Creating alias...This module could not be aliased because module of same name exists + +Install Success. You can use the module with `require("./vendor/github.com/abs-lang/abs-sample-module-master")` +``` + +When requiring a module, ABS will try to load the `index.abs` file unless +another file is specified: + +``` +$ ~/projects/abs/builds/abs +Hello alex, welcome to the ABS (1.8.0) programming language! +Type 'quit' when you're done, 'help' if you get lost! + +⧐ require("abs-sample-module") +{"another": f() {return hello world;}} + +⧐ require("abs-sample-module/index.abs") +{"another": f() {return hello world;}} + +⧐ require("abs-sample-module/another.abs") +f() {return hello world;} +``` + +## Supported hosting platforms + +Currently, the installer supports modules hosted on: + +* GitHub + +## Next + +That's about it for this section! + +You can now head over to read a little bit about [errors](/misc/error). \ No newline at end of file diff --git a/docs/types/builtin-function.md b/docs/types/builtin-function.md index 6610caaa..7a5aafaf 100644 --- a/docs/types/builtin-function.md +++ b/docs/types/builtin-function.md @@ -339,4 +339,4 @@ statements until changed. That's about it for this section! -You can now head over to read a little bit about [errors](/misc/error). \ No newline at end of file +You can now head over to read a little bit about [how to install 3rd party libraries](/misc/3pl). \ No newline at end of file diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index cc0d760c..a4a20240 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -945,10 +945,10 @@ c")`, []string{"a", "b", "c"}}, {`sleep(0.01)`, nil}, {`$()`, ""}, {`a = 1; eval("a")`, 1}, - {`"a = 2; return 10" >> "test-source-vs-require.abs.ignore"; a = 1; x = source("test-source-vs-require.abs.ignore"); a`, 2}, - {`"a = 2; return 10" >> "test-source-vs-require.abs.ignore"; a = 1; x = require("test-source-vs-require.abs.ignore"); a`, 1}, - {`"a = 2; return 10" >> "test-source-vs-require.abs.ignore"; a = 1; x = source("test-source-vs-require.abs.ignore"); x`, 10}, - {`"a = 2; return 10" >> "test-source-vs-require.abs.ignore"; a = 1; x = require("test-source-vs-require.abs.ignore"); x`, 10}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = source("test-ignore-source-vs-require.abs"); a`, 2}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = require("test-ignore-source-vs-require.abs"); a`, 1}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = source("test-ignore-source-vs-require.abs"); x`, 10}, + {`"a = 2; return 10" >> "test-ignore-source-vs-require.abs"; a = 1; x = require("test-ignore-source-vs-require.abs"); x`, 10}, {`[[1,2,3], [2,3,4]].tsv()`, "1\t2\t3\n2\t3\t4"}, {`[1].tsv()`, "tsv() must be called on an array of arrays or objects, such as [[1, 2, 3], [4, 5, 6]], '[1]' given"}, {`[{"c": 3, "b": "hello"}, {"b": 20, "c": 0}].tsv()`, "b\tc\nhello\t3\n20\t0"}, diff --git a/util/util.go b/util/util.go index 03f6d66d..e5a06c43 100644 --- a/util/util.go +++ b/util/util.go @@ -122,15 +122,18 @@ func UnaliasPath(path string, packageAlias map[string]string) string { // paths p := []string{packageAlias[parts[0]]} p = append(p, parts[1:]...) + path = filepath.Join(p...) + } + return appendIndexFile(path) +} - // If our path didn't end with an ABS file, - // let's assume it's a directory and we will - // auto-include the index.abs file from it - if filepath.Ext(path) != ".abs" { - p = append(p, "index.abs") - } - - return filepath.Join(p...) +// If our path didn't end with an ABS file (.abs), +// let's assume it's a directory and we will +// auto-include the index.abs file from it +func appendIndexFile(path string) string { + if filepath.Ext(path) != ".abs" { + return filepath.Join(path, "index.abs") } + return path } diff --git a/util/util_test.go b/util/util_test.go index 0f98addc..2f62bb53 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -11,11 +11,11 @@ func TestUnaliasPath(t *testing.T) { aliases map[string]string expected string }{ - {"test", map[string]string{}, "test"}, + {"test", map[string]string{}, "test" + string(os.PathSeparator) + "index.abs"}, {"test" + string(os.PathSeparator) + "sample.abs", map[string]string{}, "test" + string(os.PathSeparator) + "sample.abs"}, {"test" + string(os.PathSeparator) + "sample.abs", map[string]string{"test": "path"}, "path" + string(os.PathSeparator) + "sample.abs"}, {"test", map[string]string{"test": "path"}, "path" + string(os.PathSeparator) + "index.abs"}, - {"." + string(os.PathSeparator) + "test", map[string]string{"test": "path"}, "." + string(os.PathSeparator) + "test"}, + {"." + string(os.PathSeparator) + "test", map[string]string{"test": "path"}, "test" + string(os.PathSeparator) + "index.abs"}, } for _, tt := range tests {