Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mtshiba committed Feb 17, 2024
0 parents commit 35245e2
Show file tree
Hide file tree
Showing 23 changed files with 1,232 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
- 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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
build/

test_.*
.log/

.DS_Store
124 changes: 124 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```
10 changes: 10 additions & 0 deletions README_JA.md
Original file line number Diff line number Diff line change
@@ -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/) (パッケージを公開したい場合)
122 changes: 122 additions & 0 deletions doc/JA/architecture.md
Original file line number Diff line number Diff line change
@@ -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)を分割するメリットはない。
13 changes: 13 additions & 0 deletions package.er
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.name = "poise"
.version = "0.0.1"
.description = "The Erg package manager"
.authors = ["Shunsuke Shibayama <[email protected]>"]
.license = "MIT or Apache-2.0"
.repository = "https://github.com/erg-lang/poise"
.type = "app"

.features = {
.debug = []
}

.dependencies = {=}
3 changes: 3 additions & 0 deletions package.lock.er
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.version = "1"
.packages = [
]
71 changes: 71 additions & 0 deletions src/build.er
Original file line number Diff line number Diff line change
@@ -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!"
Loading

0 comments on commit 35245e2

Please sign in to comment.