diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..9608842 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GH_CLI_AUTH_TOKEN }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - uses: erg-lang/setup-erg@v2.1 + - name: Build + run: | + erg --version + timeout 30s erg src/main.er + - name: Install + run: | + timeout 30s erg src/main.er -- install + - name: Test + run: | + poise --version + poise help + poise build + poise check + poise clean + poise run + poise metadata + poise metadata --format json + poise install + echo n | poise publish --debug diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c83e95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ + +test_.* +.log/ + +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b3264f --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +# poise + +The Erg package manager + +This package manager is bundled with erg and is available via the `erg pack` subcommand. See [here](https://github.com/erg-lang/erg/blob/main/doc/EN/tools/pack.md) for information on how to use the command. + +## Requirements + +* Git +* [Github CLI](https://cli.github.com/) (if you want to publish packages) + +## Bootstrap + +```sh +erg src/main.er -- install +``` + +## Usage + +Actually, poise is inspired by cargo (Rust's package manager) and has almost the same command options. + +### Create a new package + +* Creating a new package in the current directory + +```sh +erg pack init +``` + +* Making a new directory and creating a package + +```sh +erg pack new package_name +``` + +### Build a package + +This generates the artifacts in the `build` directory. + +```sh +erg pack build +``` + +### Check a package + +This does not generate the artifacts. + +```sh +erg pack check +``` + +### Run a package + +```sh +erg pack run +``` + +### Test a package + +This runs the test subroutines (named with `test_` prefix) in the `tests` directory. + +```sh +erg pack test +``` + +### Publish a package + +This publishes the package to [the registry](https://github.com/erg-lang/package-index). + +```sh +erg pack publish +``` + +### Install a package + +* Install the package from the current directory + +```sh +erg pack install +``` + +* Install the package from the registry + +```sh +erg pack install package_name +``` + +### Uninstall a package + +* Uninstall the package from the current directory + +```sh +erg pack uninstall +``` + +* Uninstall the package by specifying the name + +```sh +erg pack uninstall package_name +``` + +### Update dependencies + +```sh +erg pack update +``` + +### Display the package information + +```sh +erg pack metadata +``` + +* Display the package information with json format + +```sh +erg pack metadata --format json +``` + +### Clean the build directory + +```sh +erg pack clean +``` diff --git a/README_JA.md b/README_JA.md new file mode 100644 index 0000000..8b7f84c --- /dev/null +++ b/README_JA.md @@ -0,0 +1,10 @@ +# poise + +Ergパッケージマネージャ + +このパッケージマネージャはergに同梱されており、`erg pack`サブコマンドで利用できます。コマンドの利用方法については[こちら](https://github.com/erg-lang/erg/blob/main/doc/JA/tools/pack.md)を参照してください。 + +## Requirements + +* Git +* [Github CLI](https://cli.github.com/) (パッケージを公開したい場合) diff --git a/doc/JA/architecture.md b/doc/JA/architecture.md new file mode 100644 index 0000000..6b88013 --- /dev/null +++ b/doc/JA/architecture.md @@ -0,0 +1,122 @@ +# アーキテクチャ + +The Erg package manager (コードネーム: poise)はErgを用いて実装されている。現状では実用可能なErgのバックエンドがCPythonバックエンドしかないため、poiseは単体のバイナリとしては提供されておらず、`erg pack`サブコマンドが内部的に呼び出すアプリケーションパッケージとなっている。将来的にErgにネイティブコードバイナリが追加された場合でもパッケージ管理は基本的に`erg pack`で行う。 + +poiseは以下のコマンドを持つ。 + +* `init`: パッケージを初期化する +* `install`: パッケージをインストールする(`erg install`と同じ、パッケージの依存関係を追加する場合は`add`) +* `update`: パッケージをアップデートする +* `add`: パッケージの依存関係を追加する +* `clean`: パッケージをクリーニングする(キャッシュの削除など) +* `build`: パッケージをビルドする(`erg build`と同じ) +* `run`: パッケージをビルドし、実行する +* `publish`: packages.erg-lang.orgにパッケージを公開する +* `test`: パッケージのテストを実行する(`erg test`と同じ) + +各サブコマンドの内部処理について解説する。 + +## init + +規定のディレクトリ構成をセットアップする。規定のディレクトリ構成は以下の通り[^1]。 + +```console +/package # package root directory, this is also the package name + /build # Directory to store build results + /debug # Artifacts during debug build + /release # Artifacts of release build + /doc # Documents (in addition, by dividing into subdirectories such as `en`, `ja` etc., it is possible to correspond to each language) + /src # source code + /main.er # file that defines the main function + /tests # Directory to store (black box) test files + /package.er # file that defines package settings +``` + +initの仕事はbuildを除くディレクトリを自動で作成することである。`--app`と`--lib`はそれぞれアプリケーションパッケージとライブラリパッケージを作成する。`--app`を指定した場合、`src`以下に`main.er`が作成される。`--lib`を指定した場合、`src/lib.er`が作成される。両方指定することも可能であり、`src/main.er`と`src/lib.er`が作成される。 + +## install + +アプリケーションパッケージをインストールする。パッケージ名を指定しなかった場合、`package.er`に記述された依存関係をインストールする。パッケージは`.erg`以下にキャッシュされ、他のパッケージでキャッシュが再利用される場合がある。 + +アプリケーションバッケージのインストールについて内部処理のフローを説明する。 + +1. `erg-lang/package-index`からパッケージのメタデータを取得する + +* indexはcargoに倣いsparse registryとして実装されている。すなわち、全てのパッケージ情報を単一のjsonなどで管理するのではなく、名前ごとに複数のディレクトリに分割し、そのディレクトリ内にパッケージのメタデータをjsonなどで管理する。パッケージのメタデータはjson形式である。 + + * 例: + * `erg`: `package-index/certified/e/erg.json` + * `foo-bar`: `package-index/certified/f/foo-bar.json` + * `foo/bar`: `package-index/developers/foo/b/bar.json` + +ergのパッケージレジストリはまず開発者`developers`ごとに名前空間が別れていることに注意されたい。その後リクエストのあったパッケージは審査を経て`certified`にも登録される。 +パッケージをインストールする際に開発者名が指定されなかった場合`certified`名前空間から検索されることになる。見つからなかった場合、各開発者の名前空間から検索される。 + +jsonの中身は以下のようにバージョンごとに整列されている。つまり、`package.er`から`name`と`version`を抜き、その他の情報がjsonにシリアライズされて配置される。 +大小関係の判定はsemverに従う。 + +```json +{ + "versions": { + "0.1.0": { + "description": "an awesome package", + "dependencies": { + "foo": "0.1.0" + }, + ... + } + }, + ... +} +``` + +2. リソースのダウンロード + +後述するようにパッケージはキャッシュされるので、既に同一バージョンの同一パッケージがダウンロードされている場合このステップは省かれる。 + +特にバージョンが指定されなかった場合、json内の一番下のバージョンがインストールされる。 + +Erg package systemでは、再現性のためパッケージは全て圧縮されてindex内に保存される。圧縮形式はtar.gzであり、jsonと同じディレクトリに配置される。例えば、`erg`の場合は`package-index/certified/e/erg/0.1.0.tar.gz`となる。 + +さらにjson内に記述されているdependenciesから再帰的に依存関係を解決し、必要なパッケージをダウンロードする。 + +ダウンロードされたパッケージは解凍されて`.erg`以下に配置される。例えば、`erg`の場合は`.erg/packages/github.com/certified/erg/0.1.0`となる。`foo/bar`の場合は`.erg/packages/github.com/developers/foo/bar/0.1.0`となる。 + +複数のモジュールが同一のパッケージの別バージョンを利用している場合、新しい方のバージョンのみを用いることができないかトライされる。これに失敗しても別バージョンが追加でインストールされるだけでビルドは継続される。この依存関係解決の結果は後述するpackage.lock.erに保存される。 + +パッケージをどのように読み込みリンクするかはコンパイラの責務となる。具体的には、コンパイラはpackage.erがプロジェクトルートにある場合、その中のdependenciesをプロジェクト内で使えるパッケージとして認識する。実際の名前解決には後述するpackage.lock.erを用いる。 + +3. ロックファイルの生成 + +これは2.と並行して行われる。パッケージのバージョンはsemantic versioningに従って範囲指定することができる。そしてパッケージは日々アップデートされるので、package.erの情報だけでは再現性が担保されない。そこで、パッケージのバージョンを固定するためにロックファイルが用意されている。ロックファイルは`package.lock.er`という名前で`package.er`と同一のディレクトリに配置される。 + +例: + +```erg +.packages = { + .foo = { .version = "0.1.0", features = ["debug"] }, + .bar = { .version = "0.1.1" }, + ... +} +``` + +ビルド時にコンパイラはpackage.lock.erを見ながらパッケージを名前解決する。package.lock.erは手動で編集することも可能であるが、コンパイラはパッケージ管理の責務を負わないので編集後にergcを用いてコンパイルすると名前解決に失敗する可能性がある。`erg pack build`/`erg build`ならば毎回package.lock.erの検証を行うので安全である。 + +## update + +アプリケーションパッケージをアップデートする。パッケージ名を指定しなかった場合、`package.er`に記述された依存関係をアップデートする。 + +依存関係のアップデートは貪欲(greedy)に行われる。例えばパッケージAとBがCに依存していて、Aの場合はCのバージョンアップが可能だがBの場合は不可能な場合、Aのみがアップデートされる。従って複数のパッケージ間で汎用的に共用されるパッケージは多数のバージョンが内部的に併存する場合がある。 + +## build + +パッケージをビルドする。poiseはpackage.lock.erを検証し、パスすればコンパイラにコンパイルを命令する。 +コンパイルの成果物(.pycファイルまたはネイティブコード)はbuild以下に配置される。デフォルトではdebugビルドが行われ、成果物は`build/debug`以下に出力されるが、`--release`を指定することでreleaseビルドが行われ、`build/release`以下に出力される。これはpackage.erのあるプロジェクトの場合コンパイラが配置する。 + +現在は未実装だが、コンパイラがインクリメンタルビルドに対応した場合、`build/{debug, release}`以下にビルド成果物がキャッシュされる。 + +ビルド時はtests、examples以下のファイルも検査される。またファイル内のdoc commentsもergコードブロックがあれば検査される。 + +--- + +[^1]: パッケージ内で利用されるサブパッケージは`packs`以下に配置することが推奨される。しかしErgの場合モジュール=1ファイル単位でキャッシュ&並列コンパイルされるのでRustほどパッケージ(crate)を分割するメリットはない。 diff --git a/package.er b/package.er new file mode 100644 index 0000000..82e75cf --- /dev/null +++ b/package.er @@ -0,0 +1,13 @@ +.name = "poise" +.version = "0.0.1" +.description = "The Erg package manager" +.authors = ["Shunsuke Shibayama "] +.license = "MIT or Apache-2.0" +.repository = "https://github.com/erg-lang/poise" +.type = "app" + +.features = { + .debug = [] +} + +.dependencies = {=} diff --git a/package.lock.er b/package.lock.er new file mode 100644 index 0000000..beb13f3 --- /dev/null +++ b/package.lock.er @@ -0,0 +1,3 @@ +.version = "1" +.packages = [ +] diff --git a/src/build.er b/src/build.er new file mode 100644 index 0000000..4361fa1 --- /dev/null +++ b/src/build.er @@ -0,0 +1,71 @@ +{time!;} = pyimport "time" +{mkdir!;} = pyimport "os" +{exists!;} = pyimport "os/path" +{move!;} = pyimport "shutil" +subprocess = pyimport "subprocess" +ac = import "ansicolor" + +{Versions!;} = import "download" +{Config;} = import "cfg" +{download_dependencies_on_demand!;} = import "download" +{load_package_er_as_json!;} = import "metadata" + +.compile_command! vers: Versions! = + cmd as Array!(Str, _) = !["erg", "compile"] + metadata = load_package_er_as_json!() + if! metadata.get("py_command") isnot! None, do!: + cmd.push! "--py-command" + py_command = metadata["py_command"] + assert py_command in Str + cmd.push! py_command + # pass dependencies using `--use-package` option + for! vers.items(), ((name, vers),) => + for! vers, (pack,) => + if! pack.path == None: + do!: + cmd.push! "--use-package" + cmd.push! "\{name}" + cmd.push! pack.as_name + cmd.push! "\{pack.version}" + do!: + cmd.push! "--use-local-package" + cmd.push! "\{name}" + cmd.push! pack.as_name + cmd.push! "\{pack.version}" + cmd.push! "\{pack.path}" + cmd + +.build! cfg: Config = + start = time!() + vers = download_dependencies_on_demand!() + cmd = .compile_command! vers + print! "\{ac.GREEN}Building\{ac.RESET}: \{cfg.project_root.stem}" + metadata = load_package_er_as_json!() + if! metadata.get("pre_build") isnot! None, do!: + path = metadata["pre_build"] + assert path in Str + print! "\{ac.GREEN}Running\{ac.RESET}: \{path} (pre-build script)" + res = subprocess.run!(["erg", "run", path]) + if! res.returncode != 0, do!: + panic "\{ac.RED}Error\{ac.RESET}: pre-build failed!" + entry as Str = if exists!("src/main.er"): + do "src/main.er" + do "src/lib.er" + cmd.push! entry + res = subprocess.run! cmd + if! res.returncode != 0, do!: + print! "\{ac.RED}Error\{ac.RESET}: build failed!" + exit 1 + if! not(exists!("build")), do!: + mkdir! "build" + discard move! entry.replace(".er", ".pyc"), "build/\{cfg.project_root.stem}.pyc" + end = time!() + elapsed = end - start + print! "\{ac.GREEN}Finished\{ac.RESET}: elapsed \{"{:.3g}".format(elapsed)}s" + if! metadata.get("post_build") isnot! None, do!: + path = metadata["post_build"] + assert path in Str + print! "\{ac.GREEN}Running\{ac.RESET}: \{path} (post-build script)" + res = subprocess.run!(["erg", "run", path]) + if! res.returncode != 0, do!: + panic "\{ac.RED}Error\{ac.RESET}: post-build failed!" diff --git a/src/cfg.er b/src/cfg.er new file mode 100644 index 0000000..fa60f02 --- /dev/null +++ b/src/cfg.er @@ -0,0 +1,28 @@ +{Path;} = pyimport "pathlib" +os = pyimport "os" +sys = pyimport "sys" + +.Config = Class { + .project_root = Path; + # build, init, install, etc. + .mode = Str; + .debug = Bool; + .lib = Bool; + .no_verify = Bool; + .allow_dirty = Bool; + .certified = Bool; + .args = Array(Str); +} +.Config. + parse!(): Self = + mode = if len(sys.argv) > 1, do sys.argv[1], do "help" + Self { + .project_root = Path os.getcwd!(); + .mode; + .debug = "--debug" in sys.argv; + .lib = "--lib" in sys.argv; + .no_verify = "--no-verify" in sys.argv; + .allow_dirty = "--allow-dirty" in sys.argv; + .certified = "--certified" in sys.argv; + .args = sys.argv[1..10000]; + } diff --git a/src/check.er b/src/check.er new file mode 100644 index 0000000..85070a8 --- /dev/null +++ b/src/check.er @@ -0,0 +1,62 @@ +{time!;} = pyimport "time" +{exists!;} = pyimport "os/path" +sub = pyimport "subprocess" +ac = import "ansicolor" + +{download_dependencies_on_demand!;} = import "download" +{load_package_er_as_json!;} = import "metadata" +{Config;} = import "cfg" + +.check!(cfg: Config) = + start = time!() + vers = download_dependencies_on_demand!() + print! "\{ac.GREEN}Checking\{ac.RESET}: \{cfg.project_root.stem}" + metadata = load_package_er_as_json!() + if! metadata.get("pre_build") isnot! None, do!: + path = metadata["pre_build"] + assert path in Str + print! "\{ac.GREEN}Running\{ac.RESET}: \{path} (pre-build script)" + res = sub.run!(["erg", "run", path]) + if! res.returncode != 0, do!: + panic "\{ac.RED}Error\{ac.RESET}: pre-build failed!" + typ = metadata.get("type", "app") + assert typ in Str + entry = match typ: + "app" -> "src/main.er" + "lib" -> "src/lib.er" + "decl" -> "src/lib.d.er" + _ -> panic "unknown package type: \{typ}" + if not(exists!(entry)), do: + panic "\{ac.RED}Error\{ac.RESET}: entry point \{entry} not found." + cmd = !["erg", "check"] + for! vers.items(), ((name, vers),) => + for! vers, (pack,) => + if! pack.path == None: + do!: + cmd.push! "--use-package" + cmd.push! "\{name}" + cmd.push! pack.as_name + cmd.push! "\{pack.version}" + do!: + cmd.push! "--use-local-package" + cmd.push! "\{name}" + cmd.push! pack.as_name + cmd.push! "\{pack.version}" + cmd.push! "\{pack.path}" + cmd.push! entry + res = sub.run!(cmd, capture_output:=True) + if! res.returncode != 0, do!: + print! "\{ac.RED}Error\{ac.RESET}: compilation failed!" + if! res.stderr in Bytes, do!: + print! res.stderr.decode("utf-8") + exit 1 + if! metadata.get("post_build") isnot! None, do!: + path = metadata["post_build"] + assert path in Str + print! "\{ac.GREEN}Running\{ac.RESET}: \{path} (post-build script)" + res = sub.run!(["erg", "run", path]) + if! res.returncode != 0, do!: + panic "\{ac.RED}Error\{ac.RESET}: post-build failed!" + end = time!() + elapsed = end - start + print! "\{ac.GREEN}Finished\{ac.RESET}: elapsed \{"{:.3g}".format(elapsed)}s" diff --git a/src/clean.er b/src/clean.er new file mode 100644 index 0000000..24fc11b --- /dev/null +++ b/src/clean.er @@ -0,0 +1,9 @@ +{exists!;} = pyimport "os/path" +{rmtree!;} = pyimport "shutil" + +{Config;} = import "cfg" + +.clean! _: Config, path: PathLike = + print! "Cleaning up build directory..." + if! exists!("\{path}/build"), do!: + rmtree! "\{path}/build" diff --git a/src/download.er b/src/download.er new file mode 100644 index 0000000..e373671 --- /dev/null +++ b/src/download.er @@ -0,0 +1,272 @@ +zipfile = pyimport "zipfile" +os = pyimport "os" +sub = pyimport "subprocess" + +{SemVer;} = import "semver" +ac = import "ansicolor" + +{load_package_er_as_json!; load_er_as_json!} = import "metadata" + +erg_path = os.environ["ERG_PATH"] + +.PackageName = Class { + .namespace = Str; # "certified" or developer + .name = Str; +} +.PackageName|<: Show|. + __str__ ref self = "\{self.namespace}/\{self.name}" +.PackageName|<: Eq|. + __eq__(self, other: PackageName): Bool = + self.namespace == other.namespace and self.name == other.name +.PackageName|<: Hash|. + __hash__(self): Int = + hash(self.namespace) + hash(self.name) +.PackageName. + parse(s: Str): PackageName = + if SemVer.is_valid(s), do: + panic "\{ac.RED}Error\{ac.RESET}: invalid package name `\{s}`" + segments = s.split("/") + match len(segments): + 1 => .PackageName { .namespace = "certified"; .name = segments[0] } + 2 => .PackageName { .namespace = segments[0]; .name = segments[1] } + _ => todo "\{ac.RED}Error\{ac.RESET}: invalid package name `\{s}`" + @Override + __repr__ self = self.__str__() + dir_name self = + if self.namespace == "certified": + do "certified/\{self.name}" + do "developers/\{self.namespace}/\{self.name}" + +.clone_package_index_if_not_exists!() = + if! not(os.path.exists!("\{erg_path}/package-index")), do!: + res = sub.run! ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", "https://github.com/erg-lang/package-index.git", "\{erg_path}/package-index"] + # discard sub.run! ["git", "clone", "https://github.com/erg-lang/package-index.git", "\{erg_path}/package-index"] + if! res.returncode != 0, do!: + assert res.stderr in Bytes + panic "failed to clone package-index:\n\{res.stderr.decode("utf-8")}" + +.enter_package_index!() = + .clone_package_index_if_not_exists!() + os.chdir! "\{erg_path}/package-index" + res0 = sub.run! ["git", "checkout", "main"], capture_output:=True + if! res0.returncode != 0, do!: + assert res0.stderr in Bytes + panic "failed to checkout package-index:\n\{res0.stderr.decode("utf-8")}" + res = sub.run! ["git", "pull", "--ff-only"], capture_output:=True + if! res.returncode != 0, do!: + assert res.stderr in Bytes + panic "failed to pull package-index:\n\{res.stderr.decode("utf-8")}" + +.cd_package! name: .PackageName = + .enter_package_index!() + os.chdir! name.dir_name() + +.download!(pkg: .PackageName) = + .enter_package_index!() + package_path = pkg.dir_name() + if! not(os.path.exists!(package_path)), do!: + print! "\{ac.BLUE}Downloading\{ac.RESET}: \{pkg}..." + res = sub.run! ["git", "sparse-checkout", "add", package_path], capture_output:=True + if! res.returncode != 0, do!: + assert res.stderr in Bytes + panic "failed to sparse-checkout package-index:\n\{res.stderr.decode("utf-8")}" + if not(os.path.exists!(package_path)), do: + panic "\{ac.RED}Error\{ac.RESET}: package `\{pkg}` not found" + package_path + +.download_cd!(pkg: .PackageName, path: Str or NoneType) = + path_ = if! path != None: + do path + do! .download! pkg + os.chdir! path_ + path_ + +download_git!(url: Str) = + namespace = url.split("/")[-2] + name = url.split("/")[-1].replace(".git", "") + path = "\{erg_path}/lib/pkgs/\{namespace}/\{name}" + if! not(os.path.exists!(path)), do!: + print! "\{ac.BLUE}Downloading\{ac.RESET}: \{url}..." + discard sub.run! ["git", "clone", url, path], capture_output:=True + path + +Package = Class { + .as_name = Str; + .version = SemVer; + .path = Str or NoneType; + .type = { "lib", "app", "decl", "hybrid" }; + .used_in = { "build", "run", "test" }; +} +Package. + new as_name, version, path, type, used_in := "run" = + Package { .as_name; .version; .path; .type; .used_in } +.Versions! = Class Dict! { .PackageName: Array!(Package) } +.Versions!|<: Show|. + __str__ ref self = "\{self::base}" +.Versions!|<: Eq|. + __eq__(ref self, other: Ref .Versions!): Bool = + hasattr(other, "::base") and self::base == other::base +.Versions!. + new!() = + cwd = os.getcwd!() + .enter_package_index!() + discard sub.run! ["git", "sparse-checkout", "set"], capture_output:=True + os.chdir! cwd + .Versions! !{:} + insert!(ref! self, name: .PackageName, as_name: Str, version: SemVer, path: Str or NoneType, type) = + if! self::base.get(name) == None: + do!: + self::base.insert! name, ![Package.new(as_name, version, path, type)] + do!: + if! all(map((pack) -> not(pack.version.compatible_with(version)), self::base[name])), do!: + self::base[name].push!(Package.new(as_name, version, path, type)) + extend!(ref! self, other: .Versions!) = + self::base.merge! other::base + items(ref self): DictItems((PackageName, [Package; _])) = + self::base.items() + ''' + Dependencies include the package itself + ''' + resolve_dependencies_of!(ref! self, name: Str, as_name: Str, version: Str or NoneType := None, path: Str or NoneType := None) = + cwd = os.getcwd!() + ver = if! version != None: + do SemVer.from_str(version) + do! get_latest_version! name, path + pkg_name = .PackageName.parse(name) + if! pkg_name in self::base, do!: + os.chdir! cwd + self.resolve_dependencies_of!::return None + _ = .download_cd! pkg_name, path + metadata = load_package_er_as_json!() + type = metadata["type"] + assert type in {"lib", "app", "decl", "hybrid"}, "invalid package type: \{type}" + self.insert!(pkg_name, as_name, ver, path, type) + print! "\{ac.GREEN}Resolving dependencies\{ac.RESET}: \{name} v\{ver}" + self.resolve_dependencies!() + os.chdir! cwd + check_dependencies!(ref! self, deps: {Str: Obj}) = + for! deps.items(), ((as_name, value),) => + match! value: + (ver: Str) => + self.resolve_dependencies_of!(as_name, as_name, ver) + { name; version } => + self.resolve_dependencies_of!(name, as_name, version) + (dic: { Str: Obj }) => + name = dic["name"] + version = dic.get("version") + path = dic.get("path") + git = dic.get("git") + assert name in Str + assert version in (Str or NoneType) + assert path in (Str or NoneType) + assert git in (Str or NoneType) + if! git != None: + do!: + path = download_git! git + self.resolve_dependencies_of!(name, as_name, version, path) + do!: self.resolve_dependencies_of!(name, as_name, version, path) + other => + panic "unknown dependency value: \{other}" + ''' + Returns the package name and versions that should be downloaded + ''' + resolve_dependencies!(ref! self) = + # use package.lock.er if exists + lock_file = Self.from_package_lock_er!() + if! lock_file != None, do!: + self.extend! lock_file + self.resolve_dependencies!::return None + json = load_package_er_as_json!() + deps = json.get("dependencies", None) + if! deps is! None, do!: + self.resolve_dependencies!::return None + assert deps in {Str: Obj} + self.check_dependencies! deps + test_deps = json.get("test_dependencies", None) + if! test_deps is! None, do!: + self.resolve_dependencies!::return None + assert test_deps in {Str: Obj} + self.check_dependencies! test_deps + from_packages!(packages: [{ Str: Str }; _]): .Versions! = + vers = .Versions!.new!() + for! packages, (pkg: {Str: Str}) => + type = pkg.get("type", "lib") + assert type in {"lib", "app", "decl", "hybrid"}, "invalid package type: \{type}" + vers.insert!( + .PackageName.parse(pkg["name"]), + pkg["as_name"], + SemVer.from_str(pkg["version"]), + pkg.get("path"), + type, + ) + vers + from_package_lock_er!(): .Versions! or NoneType = + if! not(os.path.exists! "package.lock.er"), do!: + .Versions!.from_package_lock_er!::return None + data = load_er_as_json! "package.lock.er" + assert data["packages"] in Array { Str: Str } + .Versions!.from_packages! data["packages"] + install_if_not_exists!(ref self, pip_check: Bool) = + for! self::base.items(), ((pkg, vers),) => + for! vers, (pack,) => + if! pack.path == None, do!: + pkg_path = "\{erg_path}/lib/pkgs/\{pkg}/\{pack.version}" + if! not(os.path.exists!(pkg_path)), do!: + zip_path = "\{erg_path}/package-index/\{pkg.dir_name()}/\{pkg.name}-\{pack.version}.zip" + if! not(os.path.exists!(zip_path)), do!: + cwd = os.getcwd!() + discard .download! pkg + os.chdir! cwd + zf = zipfile.ZipFile! zip_path + zf.extractall! pkg_path + if! pip_check and pack.type == "decl", do!: + installed = sub.run! ["pip", "show", pkg.name], capture_output:=True + if! installed.returncode != 0, do!: + print! "\{ac.BLUE}Installing\{ac.RESET}: \{pkg.name}..." + res = sub.run! ["pip", "install", pkg.name], capture_output:=False + if! res.returncode != 0, do!: + assert res.stderr in Bytes + panic "failed to install \{pkg.name}:\n\{res.stderr.decode("utf-8")}" + +get_latest_version!(name: Str, path: Str or NoneType): SemVer = + cwd = os.getcwd!() + pkg_name = .PackageName.parse(name) + _ = .download_cd! pkg_name, path + json = load_package_er_as_json!() + os.chdir! cwd + ver = json["version"] + assert ver in Str + SemVer.from_str ver + +.download_dependencies!() = + deps = .Versions!.new!() + deps.resolve_dependencies!() + deps.install_if_not_exists! True + make_lock_file! deps + deps + +.download_dependencies_on_demand!() = + deps = .Versions!.from_package_lock_er!() + if! deps != None: + do!: + deps.install_if_not_exists! False + deps + do! .download_dependencies!() + +make_lock_file!(vers: Ref .Versions!) = + buffer = !".version = \"1\"\n" + buffer.push! ".packages = [\n" + for! vers.items(), ((name, vers),) => + for! vers, (pack) => + buffer.push! " {\n" + buffer.push! " .name = \"\{name}\";\n" + buffer.push! " .as_name = \"\{pack.as_name}\";\n" + buffer.push! " .version = \"\{pack.version}\";\n" + buffer.push! " .type = \"\{pack.type}\";\n" + buffer.push! " .used_in = \"\{pack.used_in}\";\n" + if! pack.path != None, do!: + buffer.push! " .path = \"\{pack.path}\";\n" + buffer.push! " },\n" + buffer.push! "]\n" + with! open!("package.lock.er", "w"), f => + discard f.write! str buffer diff --git a/src/help.er b/src/help.er new file mode 100644 index 0000000..db8b5d9 --- /dev/null +++ b/src/help.er @@ -0,0 +1,18 @@ +{Config;} = import "cfg" + +.help! _: Config = print! """ +USAGE: + erg pack [COMMAND] [ARGS]... + +Commands: + build Build the project + clean Clear the cache + help Print this help message + init Initialize the project + install Install specified packages + uninstall Uninstall specified packages + metadata Print the project metadata + publish Publish the project to package-index + run Run the project + update Update the project dependencies +""" diff --git a/src/initializ.er b/src/initializ.er new file mode 100644 index 0000000..f8cfd27 --- /dev/null +++ b/src/initializ.er @@ -0,0 +1,67 @@ +{chdir!;} = pyimport "os" +sub = pyimport "subprocess" +{exists!;} = pyimport "os/path" +{mkdir!;} = pyimport "os" +ac = import "ansicolor" + +{Config;} = import "cfg" + +initialize_tests! cfg: Config, path: PathLike = + mkdir! "\{path}/tests" + with! open!("\{path}/tests/test.er", "w"), f => + name = cfg.project_root.stem + discard f.write! "\{name} = import \"\{name}\"\n\n" + discard f.write! ".test_add() =\n" + discard f.write! " assert \{name}.add(1, 1) == 2\n" + +.initialize! cfg: Config, path: PathLike = + if! not(exists!("\{path}/.gitignore")), do!: + with! open!("\{path}/.gitignore", "w"), f => + discard f.write! "build/\n" + if! not(exists!("\{path}/src")), do!: + mkdir! "\{path}/src" + if! cfg.lib: + do!: + with! open!("\{path}/src/lib.er", "w"), f => + discard f.write! ".add l, r = l + r\n" + initialize_tests! cfg, path + do!: + with! open!("\{path}/src/main.er", "w"), f => + discard f.write! "print! \"Hello, world!\"\n" + if! not(exists!("\{path}/doc")), do!: + mkdir! "\{path}/doc" + if! not(exists!("\{path}/package.er")), do!: + with! open!("\{path}/package.er", "w"), f => + raw_author = sub.run!(["git", "config", "user.name"], capture_output:=True).stdout + author = if raw_author != None: + do: ".authors = [\"\{raw_author.decode().strip()}\"]\n" + do: "" + discard f.write! """.name = \"\{cfg.project_root.stem}\" +.version = \"0.1.0\" +.type = \{if cfg.lib, do "\"lib\"", do "\"app\" # or \"lib\""} +\{author} +# .license = \"MIT\" +# .description = \"\" +# specify description, license, etc. +""" + print! "\{ac.GREEN}Initialized\{ac.RESET}: \{if cfg.lib, do "library", do "application"} package" + +.new! cfg: Config = + name = cfg.args[1] + new_path = cfg.project_root.joinpath(name) + if new_path.exists!(), do: + panic "\{ac.RED}Error\{ac.RESET}: \{new_path} already exists" + new_path.mkdir!() + chdir! new_path + cfg_ = Config.new { + .project_root = new_path; + .mode = cfg.mode; + .debug = cfg.debug; + .lib = cfg.lib; + .no_verify = cfg.no_verify; + .allow_dirty = cfg.allow_dirty; + .certified = cfg.certified; + .args = cfg.args; + } + .initialize! cfg_, "." + print! "\{ac.GREEN}Created\{ac.RESET}: \{name}" diff --git a/src/install.er b/src/install.er new file mode 100644 index 0000000..8cc4193 --- /dev/null +++ b/src/install.er @@ -0,0 +1,63 @@ +platform = pyimport "platform" +pathlib = pyimport "pathlib" +{unpack_archive!;} = pyimport "shutil" +{copy!;} = pyimport "shutil" +{getcwd!; getenv!; chdir!; chmod!} = pyimport "os" +ac = import "ansicolor" + +{load_package_er_as_json!;} = import "metadata" +{build!;} = import "build" +{Config;} = import "cfg" +{download!; PackageName} = import "download" + +.install! cfg: Config, path: PathLike or NoneType = + if! path isnot! None: + do!: .install_package! cfg, path + do!: .install_local! cfg + +.install_local! cfg: Config = + metadata = load_package_er_as_json!() + type = metadata.get("type", "lib") + assert type in Str + if type == "lib", do: + panic """\{ac.RED}Error\{ac.RESET}: cannot install \{metadata["name"]} bacause it is a library. +To use it as a dependency, add it to the "dependencies" list in the package.er file.""" + build! cfg + name = cfg.project_root.stem + erg_path = getenv! "ERG_PATH" + pyc_path_old = "build/\{name}.pyc" + pyc_path = "\{erg_path}/bin/\{name}.pyc" + copy! pyc_path_old, pyc_path + exec_path = if platform.system!() == "Windows": + do "\{erg_path}/bin/\{name}.bat" + do "\{erg_path}/bin/\{name}" + with! open!(exec_path, "w"), f => + code = if platform.system!() == "Windows": + do "@echo off\npython \{pyc_path} %*" + do "python3 \{pyc_path} $@" + discard f.write! code + chmod! exec_path, 0o755 + print! "\{ac.GREEN}Installed\{ac.RESET}: \{name} v\{metadata.get("version", "???")} (\{exec_path})" + +.install_package! cfg: Config, p: PathLike = + path = PackageName.parse str p + package_path = download! path + chdir! package_path + json = load_package_er_as_json!() + zip_name = "\{json["name"]}-\{json["version"]}.zip" + package_name = json["name"] + assert package_name in Str + unpack_archive! zip_name, package_name + chdir! package_name + build_cfg = Config.new({ + .mode = "install"; + .project_root = pathlib.Path getcwd!(); + .debug = cfg.debug; + .lib = cfg.lib; + .no_verify = cfg.no_verify; + .allow_dirty = cfg.allow_dirty; + .certified = cfg.certified; + .args = cfg.args; + }) + .install_local! build_cfg + diff --git a/src/main.er b/src/main.er new file mode 100644 index 0000000..6cf8963 --- /dev/null +++ b/src/main.er @@ -0,0 +1,35 @@ +{Config;} = import "cfg" +{build!;} = import "build" +{check!;} = import "check" +{clean!;} = import "clean" +{initialize!; new!} = import "initializ" +{install!;} = import "install" +{uninstall!;} = import "uninstall" +{publish!;} = import "publish" +{run!;} = import "runn" +{help!;} = import "help" +{metadata!;} = import "metadata" +{test!;} = import "test" +{update!;} = import "updat" + +main!() = + cfg = Config.parse!() + match cfg.mode: + "build" => build! cfg + "check" => check! cfg + "clean" => clean! cfg, "." + "help" => help! cfg + "init" => initialize! cfg, "." + "run" => run! cfg, "." + "install" => install! cfg, cfg.args.get(1) + "uninstall" => uninstall! cfg, cfg.args.get(1) + "publish" => publish! cfg + "metadata" => metadata! cfg + "new" => new! cfg + "test" => test! cfg + "update" => update! cfg + "--version" => print! "poise 0.0.1" + mode => todo "unknown mode: \{mode}" + +if! __name__ == "__main__", do!: + main!() diff --git a/src/metadata.er b/src/metadata.er new file mode 100644 index 0000000..04e10f4 --- /dev/null +++ b/src/metadata.er @@ -0,0 +1,43 @@ +os = pyimport "os" +json = pyimport "json" +sub = pyimport "subprocess" +{Config;} = import "cfg" + +Options = Class { + .format = Str +} +Options. + parse! args: [Str; _] = + format = !"erg" + idx = !1 + while! do! args.get(idx) != None, do!: + match! args.get(idx): + "--format" => + fmt = args.get(idx + 1) + assert fmt in Str, "No format specified" + format.update! _ -> fmt + _ => None + idx.inc!() + Self { .format; } + +.load_er_as_json!(file) = + json_path = file.replace ".er", ".json" + discard sub.run! ["erg", "transpile", "--transpile-target", "json", file] + res = with! open!(json_path), f => + data = json.loads f.read!() + assert data in {Str: Obj} + data + os.remove! json_path + res + +.load_package_er_as_json!() = .load_er_as_json! "package.er" + +.metadata! cfg: Config = + options = Options.parse! cfg.args + match options.format: + "json" => + print! .load_package_er_as_json!() + _ => + with! open!("package.er"), f => + s = f.read!() + print! s diff --git a/src/publish.er b/src/publish.er new file mode 100644 index 0000000..3b0df3d --- /dev/null +++ b/src/publish.er @@ -0,0 +1,108 @@ +json = pyimport "json" +sub = pyimport "subprocess" +{make_archive!; rmtree!; copy!} = pyimport "shutil" +os = pyimport "os" +ac = import "ansicolor" +{SemVer;} = import "semver" + +{Config;} = import "cfg" +{check!;} = import "check" +{load_package_er_as_json!;} = import "metadata" +{enter_package_index!; clone_package_index_if_not_exists!;} = import "download" +{download_dependencies_on_demand!;} = import "download" + +.get_github_username!() = + res = sub.run! ["gh", "api", "user"], capture_output:=True + out = res.stdout + assert out in Bytes + data = json.loads out.decode "utf-8" + assert data in {Str: Obj} + login = data["login"] + assert login in Str + login + +check_commit!(cfg: Config) = + res = sub.run! ["git", "status", "--porcelain"], capture_output:=True + out = res.stdout + assert out in Bytes + if out != bytes() and not(cfg.allow_dirty), do: + panic """\{ac.RED}Error\{ac.RESET}: There are uncommitted changes in the current branch. +If you want to publish anyway, use the `--allow-dirty` flag.""" + +validate_package_er(metadata: {Str: Str or Array(Str) or Record}) = + if metadata.get("name") == None: + do panic "\{ac.RED}Error\{ac.RESET} in package.er: `name` is missing" + do assert metadata["name"] in Str, "\{ac.RED}Error\{ac.RESET} in package.er: `name` must be a string" + if metadata.get("version") == None: + do panic "\{ac.RED}Error\{ac.RESET} in package.er: `version` is missing" + do: + assert metadata["version"] in Str, "\{ac.RED}Error\{ac.RESET} in package.er: `version` must be a string or a Version" + discard SemVer.from_str metadata["version"] + if metadata.get("description") == None: + do panic "\{ac.RED}Error\{ac.RESET} in package.er: `description` is missing" + do assert metadata["description"] in Str, "\{ac.RED}Error\{ac.RESET} in package.er: `description` must be a string" + if metadata.get("license") == None: + do panic "\{ac.RED}Error\{ac.RESET} in package.er: `license` is missing" + do assert metadata["license"] in Str, "\{ac.RED}Error\{ac.RESET} in package.er: `license` must be a string" + if metadata.get("type") == None: + do panic "\{ac.RED}Error\{ac.RESET} in package.er: `type` is missing (\"app\", \"lib\", \"hybrid\" or \"decl\")" + do: + assert metadata["type"] in Str + assert metadata["type"] in {"app", "lib", "hybrid", "decl"}: + "\{ac.RED}Error\{ac.RESET} in package.er: `type` must be \"app\", \"lib\", \"hybrid\" or \"decl\"" + if metadata.get("authors") == None: + do panic "\{ac.RED}Error\{ac.RESET} in package.er: `authors` is missing" + do assert metadata["authors"] in Array(Str): + "\{ac.RED}Error\{ac.RESET} in package.er: `authors` must be a list of strings" + +create_pr! cfg: Config, user_name, metadata = + discard sub.run! ["git", "sparse-checkout", "disable"], capture_output:=True + branch_name = "\{user_name}/\{cfg.project_root.stem}" + dir_name = if cfg.certified, do "certified/\{cfg.project_root.stem}", do "developers/\{user_name}/\{cfg.project_root.stem}" + version = SemVer.from_str metadata["version"] + file_name = "\{cfg.project_root.stem}-\{version}" + if! sub.run!(["git", "show-ref", "refs/heads/\{branch_name}"], capture_output:=True).returncode == 0: + do!: + discard sub.run! ["git", "checkout", branch_name], capture_output:=True + # discard subprocess.run! ["git", "pull", "--ff-only", "origin", branch_name] + do!: + discard sub.run! ["git", "checkout", "-b", branch_name], capture_output:=True + res0 = sub.run! ["git", "add", "\{dir_name}/package.er"], capture_output:=True + if res0.returncode != 0: + panic "\{ac.RED}Error\{ac.RESET}: Could not add package.er to the index" + discard sub.run! ["git", "add", "\{dir_name}/\{file_name}.zip"], capture_output:=True + discard sub.run! ["git", "commit", "-m", "publish \{cfg.project_root.stem}"] #, capture_output:=True + discard sub.run! ["git", "push", "--set-upstream", "origin", branch_name], capture_output:=True + title = "publish \{cfg.project_root.stem}@\{version}" + body = "Add `\{cfg.project_root.stem}@\{version}` to package-index/\{dir_name}." + discard sub.run! ["gh", "pr", "create", "--base", "main", "--head", branch_name, "--title", title, "--body", body] + discard sub.run! ["git", "checkout", "main"], capture_output:=True + +.publish! cfg: Config = + discard download_dependencies_on_demand!() + check_commit! cfg + metadata = load_package_er_as_json!() + assert metadata in {Str: Str or Array(Str) or Record} + validate_package_er metadata + if! not(cfg.no_verify), do!: + check!(cfg) + erg_path = os.environ["ERG_PATH"] + user_name = .get_github_username!() + clone_package_index_if_not_exists!() + package_path = if cfg.certified, do "\{erg_path}/package-index/certified/\{cfg.project_root.stem}" , do "\{erg_path}/package-index/developers/\{user_name}/\{cfg.project_root.stem}" + assert metadata["version"] in Str, "\{ac.RED}Error\{ac.RESET} in package.er: `version` must be a semver string" + version = SemVer.from_str metadata["version"] + file_name = "\{cfg.project_root.stem}-\{version}" + dst = "\{package_path}/\{file_name}" + if! os.path.exists!(dst), do!: + rmtree! dst + print! "\{ac.GREEN}Archiving\{ac.RESET}: \{file_name}.zip" + discard make_archive! dst, "zip", "." + discard copy! "package.er", "\{package_path}/package.er" + print! "\{ac.GREEN}Archived\{ac.RESET}: \{file_name}.zip" + enter_package_index!() + if! cfg.debug, do!: + ok = input! "Are you sure you want to register \{cfg.project_root.stem} in the package index? [y/n] " + if ok != "y": + exit 0 + create_pr! cfg, user_name, metadata diff --git a/src/runn.er b/src/runn.er new file mode 100644 index 0000000..58b6c32 --- /dev/null +++ b/src/runn.er @@ -0,0 +1,25 @@ +{which!;} = pyimport "shutil" +platform = pyimport "platform" +{run! = r!;} = pyimport "subprocess" +ac = import "ansicolor" + +{build!;} = import "build" +{load_package_er_as_json!;} = import "metadata" +{Config;} = import "cfg" + +.run! cfg: Config, _: PathLike = + metadata = load_package_er_as_json!() + assert metadata.get("type") in Str, "\{ac.RED}Error\{ac.RESET} in package.er: required field `type` must be a string" + if! metadata.get("type") == "lib", do!: + print! "\{ac.RED}Error\{ac.RESET}: \{cfg.project_root.stem} is not an executable but a library" + exit 1 + build! cfg + # args[0] == "run" + print! "\{ac.GREEN}Running\{ac.RESET}: \{cfg.project_root.stem}" + opt_py_command = metadata.get("py_command") + assert opt_py_command in (Str or NoneType) + py_command = if! opt_py_command != None, do opt_py_command, do! if platform.system!() == "Windows": + do "python" + do "python3" + assert which!(py_command) != None, "\{ac.RED}Error\{ac.RESET}: \{py_command} not found" + discard r! [py_command, "./build/\{cfg.project_root.stem}.pyc"] + cfg.args[1..10000] diff --git a/src/test.er b/src/test.er new file mode 100644 index 0000000..3489771 --- /dev/null +++ b/src/test.er @@ -0,0 +1,61 @@ +sys = pyimport "sys" +os = pyimport "os" +sub = pyimport "subprocess" +imp = pyimport "importlib" +ac = import "ansicolor" +exc = import "exception" +{SemVer;} = import "semver" + +{compile_command!;} = import "build" +{Config;} = import "cfg" +{Versions!; download_dependencies_on_demand!} = import "download" +{load_package_er_as_json!;} = import "metadata" + +.test! cfg: Config = + discard download_dependencies_on_demand!() + metadata = load_package_er_as_json!() + files = sorted os.listdir! "tests" + n_tests = !0 + n_passed = !0 + sys.path.insert! 0, os.getcwd!() + "/tests" + for! files, file => + if! file.endswith(".er"), do!: + print! "\{ac.GREEN}Compiling\{ac.RESET}: \{file}" + path = "tests/" + file + name = metadata["name"] + version = SemVer.from_str str metadata["version"] + assert name in Str + # TODO: filter out src dependencies + vers = Versions!.from_package_lock_er!() + cmd = compile_command! if! vers != None, do! vers, do! Versions!.new!() + + cmd.push! "--use-local-package" + cmd.push! name + cmd.push! name + cmd.push! str(version) + cmd.push! str(cfg.project_root.absolute()) + + cmd.push! path + res = sub.run! cmd, capture_output:=True + if! res.returncode != 0, do!: + print! "\{ac.RED}Error\{ac.RESET} in \{file}: compilation failed." + assert res.stderr in Bytes + panic res.stderr.decode("utf-8") + mod = imp.import_module file.replace(".er", "") + for! mod.__dict__.items(), ((name, value),) => + if! name.startswith("test_"), do!: + n_tests.inc!() + func_name = getattr(value, "__name__", "???") + print! "\{ac.GREEN}Running\{ac.RESET}:", func_name + assert value in () => NoneType + exc.try! value: + (exception) => + print! "\{ac.RED}Error\{ac.RESET}:", func_name + print! "Exception: \{exception}" + () => + print! "\{ac.GREEN}OK\{ac.RESET}:", func_name + n_passed.inc!() + os.remove! path.replace(".er", ".pyc") + res = if n_passed == n_tests, do "\{ac.GREEN}OK\{ac.RESET}", do "\{ac.RED}FAILED\{ac.RESET}" + print! "\{ac.YELLOW}Test result\{ac.RESET}: \{res}, \{n_passed} of \{n_tests} passed" + if n_passed != n_tests, do exit 1 diff --git a/src/uninstall.er b/src/uninstall.er new file mode 100644 index 0000000..f7a42ac --- /dev/null +++ b/src/uninstall.er @@ -0,0 +1,43 @@ +{exists!;} = pyimport "os/path" +{remove!;} = pyimport "os" +platform = pyimport "platform" +{getenv!;} = pyimport "os" +ac = import "ansicolor" + +{load_package_er_as_json!;} = import "metadata" +{Config;} = import "cfg" + +.uninstall! cfg: Config, path: Str or NoneType = + if! path isnot! None: + do!: .uninstall_package! cfg, path + do!: .uninstall_local! cfg + +.uninstall_local! cfg: Config = + metadata = load_package_er_as_json!() + type = metadata.get("type", "lib") + assert type in Str + name = cfg.project_root.stem + if type == "lib", do: + panic "\{ac.RED}Error\{ac.RESET}: cannot uninstall \{metadata["name"]} bacause it is a library." + erg_path = getenv! "ERG_PATH" + pyc_path = "\{erg_path}/bin/\{name}.pyc" + exec_path = if platform.system!() == "Windows": + do "\{erg_path}/bin/\{name}.bat" + do "\{erg_path}/bin/\{name}" + if not(exists!(exec_path)), do: + panic "\{ac.RED}Error\{ac.RESET}: \{name} is not installed." + remove! exec_path + remove! pyc_path + print! "\{ac.GREEN}Uninstalled\{ac.RESET}: \{name} v\{metadata.get("version", "???")} (\{exec_path})" + +.uninstall_package! _: Config, name: Str = + erg_path = getenv! "ERG_PATH" + pyc_path = "\{erg_path}/bin/\{name}.pyc" + exec_path = if platform.system!() == "Windows": + do "\{erg_path}/bin/\{name}.bat" + do "\{erg_path}/bin/\{name}" + if not(exists!(exec_path)), do: + panic "\{ac.RED}Error\{ac.RESET}: \{name} is not installed." + remove! exec_path + remove! pyc_path + print! "\{ac.GREEN}Uninstalled\{ac.RESET}: \{name} (\{exec_path})" diff --git a/src/updat.er b/src/updat.er new file mode 100644 index 0000000..d9b5727 --- /dev/null +++ b/src/updat.er @@ -0,0 +1,10 @@ +{exists!;} = pyimport "os/path" +{remove!;} = pyimport "os" + +{download_dependencies!;} = import "download" +{Config;} = import "cfg" + +.update! _: Config = + if! exists!("package.lock.er"), do!: + remove! "package.lock.er" + discard download_dependencies!() diff --git a/tests/test.er b/tests/test.er new file mode 100644 index 0000000..67970a4 --- /dev/null +++ b/tests/test.er @@ -0,0 +1,2 @@ +.test_function!() = + print! "OK"