├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFIEST.in ├── Makefile ├── README.md ├── apkg ├── __init__.py ├── __version__.py ├── apkg.py ├── commands │ ├── __init__.py │ ├── clean.py │ ├── create.py │ ├── environment.py │ ├── freeze.py │ ├── info.py │ ├── init.py │ ├── install.py │ ├── list.py │ ├── nixos.py │ ├── search.py │ ├── templates │ │ ├── __init__.py │ │ ├── gitignore.template │ │ ├── library.agda-lib │ │ └── library.agda-pkg │ ├── uninstall.py │ ├── update.py │ ├── upgrade.py │ └── write_defaults.py ├── config.py ├── service │ ├── __init__.py │ ├── database.py │ ├── logging.py │ ├── readLibFile.py │ └── utils.py └── support │ ├── __init__.py │ └── nixos │ ├── agda_requirements.txt │ ├── deps.nix │ ├── emacs.nix │ ├── hello-world.agda │ └── shell.nix ├── deploy.py ├── index.html ├── requirements.txt ├── setup.py └── tests ├── basic.py ├── library.agda-lib └── library.agda-pkg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | .agda 28 | *.agdai 29 | examples/hello/hello-world 30 | MAlonzo 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Environments 55 | .env 56 | .venv 57 | env/ 58 | venv/ 59 | ENV/ 60 | env.bak/ 61 | venv.bak/ 62 | *.aux 63 | *.fdb_latexmk 64 | *.fls 65 | *.log 66 | 67 | # Editor files 68 | *~ 69 | .emacs 70 | .emacs_user_config 71 | .vscode/ 72 | # Agda temporariy files -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file was adapted from https://github.com/hvr/multi-ghc-travis 2 | os: 3 | osx 4 | 5 | before_install: 6 | - brew update 7 | # - brew install python3 8 | 9 | - brew install ghc 10 | - brew install agda 11 | 12 | # - brew install tree 13 | 14 | script: 15 | - pip3 install -r requirements.txt 16 | - pip3 install . 17 | - make test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jonathan Prieto-Cubides and contributors 4 | listed at https://github.com/agda/agda-pkg/graphs/contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFIEST.in: -------------------------------------------------------------------------------- 1 | recursive-include apkg * 2 | 3 | include README.md 4 | include LICENSE.md 5 | 6 | include apkg/commands/templates/gitignore.template 7 | include apkg/commands/templates/library.agda-lib 8 | include apkg/commands/templates/library.agda-pkg 9 | 10 | include apkg/support/nixos/hello-world.agda 11 | include apkg/support/nixos/agda_requirements.txt 12 | include apkg/support/nixos/shell.nix 13 | include apkg/support/nixos/deps.nix 14 | include apkg/support/nixos/emacs.nix 15 | 16 | global-exclude *.py[co] __pycache__ *.so *.pyd 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-testing 2 | clean-testing: 3 | rm -Rf ~/.apkg@* && rm -Rf .agda 4 | 5 | # python -m unittest tests/basic.py 6 | 7 | .PHONY : clean 8 | clean: 9 | - rm -Rf agda_pkg.egg-info 10 | 11 | 12 | 13 | # Silly testing 14 | 15 | .PHONY : test 16 | test: 17 | @echo "T.help ++++++++++++++++++++++++++++++++++++++++++++++++++++" 18 | @apkg --help \ 19 | && echo "T.version +++++++++++++++++++++++++++++++++++++++++++++++++" \ 20 | && apkg --version \ 21 | && echo "T.- ++++++++++++++++++++++++++++++++++++++++++++++++++++++" \ 22 | && apkg \ 23 | && echo "T.clean +++++++++++++++++++++++++++++++++++++++++++++++++++" \ 24 | && apkg clean --yes \ 25 | && echo "T.init ++++++++++++++++++++++++++++++++++++++++++++++++++++" \ 26 | && apkg init \ 27 | && echo "T.list ++++++++++++++++++++++++++++++++++++++++++++++++++++" \ 28 | && apkg list \ 29 | && echo "T.list --field name +++++++++++++++++++++++++++++++++++++++" \ 30 | && apkg list --field name \ 31 | && echo "T.list --field url ++++++++++++++++++++++++++++++++++++++++" \ 32 | && apkg list --field url \ 33 | && echo "T.list --field version ++++++++++++++++++++++++++++++++++++" \ 34 | && apkg list --field version \ 35 | && echo "T.upgrade +++++++++++++++++++++++++++++++++++++++++++++++++" \ 36 | && apkg upgrade \ 37 | && echo "T.freeze (nothing)+++++++++++++++++++++++++++++++++++++++++" \ 38 | && apkg freeze \ 39 | && echo "T.search +++++++++++++++++++++++++++++++++++++++++++++++++" \ 40 | && apkg search standard \ 41 | && echo "T.install ++++++++++++++++++++++++++++++++++++++++++++++++" \ 42 | && apkg install standard-library --version v1.1 --yes \ 43 | && echo "T.freeze +++++++++++++++++++++++++++++++++++++++++++++++++" \ 44 | && apkg freeze 45 | 46 | 47 | .PHONY : test-install-github 48 | test-install-github: 49 | @echo "++++++++++++++++++++++++++++++++++++++++++++++++" \ 50 | && apkg clean \ 51 | && apkg init \ 52 | && apkg install --github jonaprieto/agda-prop \ 53 | && apkg install --github agda/agda-stdlib --version v0.16 \ 54 | && apkg install --git http://github.com/jonaprieto/agda-metis.git 55 | && apkg freeze 56 | 57 | 58 | .PHONY : test-local 59 | test-local: 60 | @echo "++++++++++++++++++++++++++++++++++++++++++++++++" \ 61 | && apkg clean \ 62 | && apkg init \ 63 | && rm -Rf /tmp/agda-stdlib \ 64 | && cd /tmp/ && git clone http://github.com/agda/agda-stdlib \ 65 | && rm -Rf /tmp/agda-prop \ 66 | && cd /tmp/ && git clone http://github.com/jonaprieto/agda-prop \ 67 | && rm -Rf /tmp/agda-metis \ 68 | && cd /tmp/ && git clone http://github.com/jonaprieto/agda-metis \ 69 | && echo "++++++++++++++++++++++++++++++++++++++++++++++++" \ 70 | && cd /tmp/agda-stdlib && apkg install --no-dependencies \ 71 | && cd /tmp/agda-prop && apkg install --no-dependencies \ 72 | && cd /tmp/agda-metis && apkg install --no-dependencies \ 73 | && cd /tmp/agda-metis && make test 74 | && apkg freeze 75 | 76 | 77 | .PHONY : test-local-with-dependencies 78 | test-local-with-dependencies: 79 | @echo "++++++++++++++++++++++++++++++++++++++++++++++++" \ 80 | && apkg clean \ 81 | && apkg init \ 82 | && rm -Rf /tmp/agda-metis \ 83 | && cd /tmp/ && git clone http://github.com/jonaprieto/agda-metis \ 84 | && echo "++++++++++++++++++++++++++++++++++++++++++++++++" \ 85 | && cd /tmp/agda-metis && apkg install \ 86 | && cd /tmp/agda-metis && make test \ 87 | && apkg freeze 88 | 89 | 90 | .PHONY : all-tests 91 | all-tests: 92 | make test 93 | && make test-local 94 | && make test-local-with-dependencies 95 | && make test-install-github 96 | 97 | .PHONY : TODO 98 | TODO : 99 | @find agda-pkg -type d \( -path './.git' -o -path './dist' -o -path './build' -o -path './venv' \) -prune -o -print \ 100 | | xargs grep -I 'TODO' \ 101 | | sort 102 | 103 | .PHONY: pip-package 104 | pip-package: 105 | rm -Rf dist 106 | rm -Rf build 107 | python3 setup.py build 108 | python3 setup.py sdist 109 | python3 setup.py bdist_wheel --universal 110 | twine upload dist/* 111 | 112 | # pip install twine 113 | # $(eval VERSION := $(shell bash -c 'read -p "Version: " pwd; echo $$pwd')) 114 | 115 | .PHONY : deploy 116 | deploy : 117 | @python3 deploy.py 118 | 119 | 120 | .PHONY: push 121 | push: 122 | make pip-package 123 | git push origin master --tags 124 | 125 | .PHONY : downloads 126 | downloads: 127 | pypinfo agda-pkg country 128 | pypinfo agda-pkg version 129 | pypinfo agda-pkg 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi](https://img.shields.io/pypi/v/agda-pkg.svg)](https://pypi.python.org/pypi/agda-pkg) 2 | [![versions](https://img.shields.io/pypi/pyversions/agda-pkg.svg)](https://github.com/agda/agda-pkg) 3 | [![Build Status](https://travis-ci.org/agda/agda-pkg.svg?branch=master)](https://travis-ci.org/agda/agda-pkg) 4 | [![downloads](https://img.shields.io/pypi/dm/agda-pkg.svg)](https://pypistats.org/packages/agda-pkg) 5 | 6 | 7 | **Agda-pkg** is a simple tool to manage [Agda](http://github.com/agda/agda) libraries with extra features like 8 | installing libraries from different kind of sources. 9 | 10 | This tool does not modify `Agda` at all, it just manages systematically the directory 11 | `.agda` with `.agda/defaults` and `.agda/libraries` files used by 12 | Agda to locate the available libraries. For more information about how Agda package 13 | system works, please read the official documentation 14 | [here](https://agda.readthedocs.io/en/v2.6.0/tools/package-system.html). 15 | 16 | ## Quick Start 17 | 18 | The most common usages of agda-pkg are the following: 19 | 20 | - To install `Agda-pkg` just run the following command: 21 | 22 | ```bash 23 | $ pip3 install agda-pkg 24 | ``` 25 | 26 | - To install your library, go to the root directory of your source code and run: 27 | 28 | ```bash 29 | $ apkg install --editable . 30 | ``` 31 | 32 | - To install a library from [agda/package-index](http://github.com/agda/package-index). 33 | 34 | ```bash 35 | $ apkg init 36 | $ apkg install standard-library 37 | $ apkg install plfa@dev-20.07 38 | ``` 39 | 40 | - To install a *Github repository* with a *specific version release*: 41 | 42 | ```bash 43 | $ apkg install --github agda/agda-stdlib --version v1.3 44 | ``` 45 | 46 | - To install a library from a Github repository with a *specific branch* with a *specific library name*: 47 | 48 | ```bash 49 | $ apkg install --github plfa/plfa.github.io --branch dev --name plfa 50 | ``` 51 | 52 | 53 | ## Indexed libraries 54 | 55 | 56 | 57 | **Library name** | **Latest version** | **URL** 58 | -----|-----|----- 59 | agda-base | v0.2 | https://github.com/pcapriotti/agda-base.git 60 | agda-categories | v0.1.3.1 | https://github.com/agda/agda-categories.git 61 | agda-metis | v0.2.1 | https://github.com/jonaprieto/agda-metis.git 62 | agda-prelude | df679cf | https://github.com/UlfNorell/agda-prelude.git 63 | agda-prop | v0.1.2 | https://github.com/jonaprieto/agda-prop.git 64 | agda-real | e1558b62 | https://gitlab.com/pbruin/agda-real.git 65 | agda-ring-solver | d1ed21c | https://github.com/oisdk/agda-ring-solver.git 66 | agdarsec | v0.3.0 | https://github.com/gallais/agdarsec.git 67 | alga-theory | 0fdb96c | https://github.com/algebraic-graphs/agda.git 68 | ataca | a9a7c06 | https://github.com/jespercockx/ataca.git 69 | cat | v1.6.0 | https://github.com/fredefox/cat.git 70 | cubical | v0.2 | https://github.com/agda/cubical.git 71 | FiniteSets | c8c2600 | https://github.com/L-TChen/FiniteSets.git 72 | fotc | apia-1.0.2 | https://github.com/asr/fotc.git 73 | generic | f448ab3 | https://github.com/effectfully/Generic.git 74 | hott-core | 1037d82 | https://github.com/HoTT/HoTT-Agda.git 75 | hott-theorems | 1037d82 | https://github.com/HoTT/HoTT-Agda.git 76 | HoTT-UF-Agda | 9d0f38e | https://github.com/martinescardo/HoTT-UF-Agda-Lecture-Notes.git 77 | ial | v1.5.0 | https://github.com/cedille/ial.git 78 | lightweight-prelude | b2d440a | https://github.com/L-TChen/agda-lightweight-prelude.git 79 | MtacAR | 5417230 | https://github.com/L-TChen/MtacAR.git 80 | plfa | dev-20.07 | https://github.com/plfa/plfa.github.io.git 81 | routing-library | thesis | https://github.com/MatthewDaggitt/agda-routing.git 82 | standard-library | v1.4-rc1 | https://github.com/agda/agda-stdlib.git 83 | 84 | 85 | # Usage manual 86 | 87 | ## Initialisation of the package index 88 | 89 | The easiest way to install libraries is by using [the package index]. 90 | `agda-pkg` uses a local database to maintain a register of all 91 | libraries available in your system. To initialize the index and the 92 | database please run the following command: 93 | 94 | ```bash 95 | $ apkg init 96 | Indexing libraries from https://github.com/agda/package-index.git 97 | ``` 98 | 99 | **Note**. To use a different location for your agda files `defaults` 100 | and `libraries`, you can set up the environment variable `AGDA_DIR` 101 | before run `apkg` as follows: 102 | 103 | ```bash 104 | $ export AGDA_DIR=$HOME/.agda 105 | ``` 106 | 107 | Other way is to create a directory `.agda` in your directory and run 108 | `agda-pkg` from that directory. `agda-pkg` will prioritize the `.agda` 109 | directory in the current directory. 110 | 111 | ## Help command 112 | 113 | Check all the options of a command or subcommand by using the flag `--help`. 114 | 115 | ```bash 116 | $ apkg --help 117 | $ apkg install --help 118 | ``` 119 | 120 | ## Upgrade the package index 121 | 122 | Recall updating the index every once in a while using `upgrade`. 123 | 124 | ```bash 125 | $ apkg upgrade 126 | Updating Agda-Pkg from https://github.com/agda/package-index.git 127 | ``` 128 | 129 | If you want to index your library go to [the package index] and make [PR]. 130 | 131 | ## Environmental variables 132 | 133 | If there is an issue with your installation or you suspect something 134 | is going wrong. You might want to see the environmental variables used by apkg. 135 | 136 | ```bash 137 | $ apkg environment 138 | ``` 139 | 140 | 141 | ## List all the packages available 142 | 143 | To see all the packages available run the following command: 144 | 145 | ```bash 146 | $ apkg list 147 | ``` 148 | 149 | The `list` command option comes with the flag `--full` to display more details. 150 | 151 | 152 | ## Installation of packages 153 | 154 | Install a library is now easy. We have multiple ways to install a package. 155 | 156 | - from the [package-index](http://github.com/agda/package-index) 157 | 158 | ``` 159 | $ apkg install standard-library 160 | ``` 161 | 162 | - from a [local directory] 163 | 164 | ```bash 165 | $ apkg install . 166 | ``` 167 | 168 | or even much simpler: 169 | 170 | ```bash 171 | $ apkg install 172 | ``` 173 | 174 | Installing a library creates a copy for agda in the directory assigned 175 | by agda-pkg. If you want your current directory to be taken into 176 | account for any changes use the `--editable` option. as shown below. 177 | 178 | 179 | ```bash 180 | $ apkg install --editable . 181 | ``` 182 | 183 | - from a github repository 184 | 185 | ```bash 186 | $ apkg install --github agda/agda-stdlib --version v1.1 187 | ``` 188 | 189 | - from a git repository 190 | 191 | ```bash 192 | $ apkg install http://github.com/jonaprieto/agda-prop.git 193 | ``` 194 | 195 | To specify the version of a library, we use the flag `--version` 196 | 197 | ```bash 198 | $ apkg install standard-library --version v1.0 199 | ``` 200 | 201 | Or simpler by using `@` or `==` as it follows. 202 | 203 | ```bash 204 | $ apkg install standard-library@v1.0 205 | $ apkg install standard-library==v1.0 206 | ``` 207 | 208 | ### Multiple packages at once 209 | 210 | To install multiple libraries at once, we have two options: 211 | 212 | 1. Using the inline method 213 | 214 | ```bash 215 | $ apkg install standard-library agda-base 216 | ``` 217 | 218 | Use `@` or `==` if you need a specific version, see above 219 | examples. 220 | 221 | 2. Using a requirement file: 222 | 223 | Generate a requirement file using `apkg freeze`: 224 | 225 | ```bash 226 | $ apkg freeze > requirements.txt 227 | $ cat requirements.txt 228 | standard-library==v1.1 229 | ``` 230 | 231 | Now, use the flag `-r` to install all the listed libraries 232 | in this file: 233 | 234 | ```bash 235 | $ apkg install -r requirements.txt 236 | ``` 237 | 238 | Check all the options of this command with the help information: 239 | 240 | ```bash 241 | $ apkg install --help 242 | ``` 243 | 244 | ## Uninstalling a package 245 | 246 | Uninstalling a package will remove the library from the visible libraries for Agda. 247 | 248 | - using the name of the library 249 | 250 | ```bash 251 | $ apkg uninstall standard-library 252 | ``` 253 | 254 | - infering the library name from the current directory 255 | 256 | ```bash 257 | $ apkg uninstall . 258 | ``` 259 | 260 | And if we want to remove the library completely (the sources and 261 | everything), we use the flag `--remove-cache`. 262 | 263 | ```bash 264 | $ apkg uninstall standard-library --remove-cache 265 | ``` 266 | 267 | ## Update a package to latest version 268 | 269 | We can get the latest version of a package from 270 | the versions registered in the package-index. 271 | 272 | - Update all the installed libraries: 273 | 274 | ```bash 275 | $ apkg update 276 | ``` 277 | 278 | - Update a specific list of libraries. If some 279 | library is not installed, this command will installed 280 | the latest version of it. 281 | 282 | ```bash 283 | $ apkg update standard-library agdarsec 284 | ``` 285 | 286 | ## See packages installed 287 | 288 | 289 | ```bash 290 | $ apkg freeze 291 | standard-library==v1.1 292 | ``` 293 | 294 | This command is useful to keep in a file the versions used for your project 295 | to install them later. 296 | 297 | 298 | ```bash 299 | $ apkg freeze > requirements.txt 300 | ``` 301 | 302 | To install from this requirement file run this command. 303 | 304 | 305 | ```bash 306 | $ apkg install < requirements.txt 307 | ``` 308 | 309 | ## Approximate search of packages 310 | 311 | We make a search (approximate) by using keywords and title of the 312 | packages from the index. To perform such a search, see the following 313 | example: 314 | 315 | 316 | ```bash 317 | $ apkg search metis 318 | 1 result in 0.0012656739999998834seg 319 | cubical 320 | url: https://github.com/agda/cubical.git 321 | installed: False 322 | ``` 323 | 324 | ## Get all the information of a package 325 | 326 | 327 | ```bash 328 | $ apkg info cubical 329 | ``` 330 | 331 | # Creating a library for Agda-Pkg 332 | 333 | In this section, we describe how to build a library. 334 | 335 | To build a project using `agda-pkg`, we just run the following command: 336 | 337 | ```bash 338 | $ apkg create 339 | ``` 340 | 341 | Some questions are going to be prompted in order to create 342 | the necessary files for Agda and for Agda-Pkg. 343 | 344 | The output is a folder like the following showed below. 345 | 346 | ## Directory structure of an agda library 347 | 348 | A common Agda library has the following structure: 349 | 350 | ``` 351 | $ tree -L 1 mylibrary/ 352 | mylibrary/ 353 | ├── LICENSE 354 | ├── README.md 355 | ├── mylibrary.agda-lib 356 | ├── mylibrary.agda-pkg 357 | ├── src 358 | └── test 359 | 360 | 2 directories, 3 files 361 | ``` 362 | 363 | ## .agda-lib library file 364 | 365 | ```yaml 366 | $ cat mylibrary.agda-lib 367 | name: mylibrary -- Comment 368 | depend: LIB1 LIB2 369 | LIB3 370 | LIB4 371 | include: PATH1 372 | PATH2 373 | PATH3 374 | ``` 375 | 376 | ## .agda-pkg library file 377 | 378 | This file only works for `agda-pkg`. The idea of 379 | this file is to provide more information about the 380 | package, pretty similar to the cabal files in Haskell. 381 | This file has priority over its version `.agda-lib`. 382 | 383 | ```yaml 384 | $ cat mylibrary.agda-pkg 385 | name: mylibrary 386 | version: v0.0.1 387 | author: 388 | - AuthorName1 389 | - AuthorName2 390 | category: cat1, cat2, cat3 391 | homepage: http://github.com/user/mylibrary 392 | license: MIT 393 | license-file: LICENSE.md 394 | source-repository: http://github.com/user/mylibrary.git 395 | tested-with: 2.6.0 396 | description: Put here a description. 397 | 398 | include: 399 | - PATH1 400 | - PATH2 401 | - PATH3 402 | depend: 403 | - LIB1 404 | - LIB2 405 | - LIB3 406 | - LIB4 407 | ``` 408 | 409 | 410 | ## Using with Nix or NixOS 411 | 412 | A `nix-shell` environment that loads `agda-pkg` as well as `agda` and 413 | `agda-mode` for Emacs is available. To use this, `apkg` can put the necessary 414 | files in your project folder by running one of the following commands: 415 | 416 | ```bash 417 | $ curl -L https://gist.github.com/jonaprieto/53e55263405ee48a831d700f27843931/download | tar -xvz --strip-components=1 418 | ``` 419 | 420 | or if you already have installed agda-pkg: 421 | 422 | ```bash 423 | $ apkg nixos 424 | ``` 425 | 426 | Then, you will have the following files: 427 | 428 | ```bash 429 | ./hello-world.agda 430 | ./agda_requirements.txt 431 | ./shell.nix 432 | ./deps.nix 433 | ./emacs.nix 434 | ``` 435 | 436 | From where you can run the nix shell. 437 | 438 | ```bash 439 | $ nix-shell 440 | ``` 441 | 442 | To launch Emacs with `agda-mode` enabled, run `mymacs` in the newly launched 443 | shell; `mymacs` will also load your `~/.emacs` file if it exists. If you are 444 | using [Spacemacs](https://www.spacemacs.org), you will need to edit `shell.nix` 445 | to use `~/.spacemacs` instead. 446 | 447 | The files provided by the commands above are also available in this repository 448 | (`apkg/support/nix`) and in a third-party [example 449 | repository](https://github.com/bbarker/LearningAgda) to give an idea of exactly 450 | which files need to be copied to your project. 451 | 452 | **Example**: 453 | 454 | ```agda 455 | $ cat hello-world.agda 456 | module hello-world where 457 | open import IO 458 | main = run (putStrLn "Hello, World!") 459 | ``` 460 | 461 | Run `mymacs hello-world.agda` then type `C-c C-x C-c` in emacs to compile the loaded hello world file. 462 | 463 | ### Configuration 464 | 465 | Edit any of the `nix` expressions as needed. In particular: 466 | 467 | 0. To add Agda dependencies via `agda-pkg`, edit `agda_requirements.txt` 468 | 1. To add more 4Haskell or other system dependencies or other 469 | target-language dependencies, edit `deps.nix`. 470 | 2. To add or alter the editor used, change the `myEmacs` 471 | references in `shell.nix` or add similar derivations. 472 | 3. Optionally, create `.emacs_user_config` in the repository root directory and 473 | add any additional config, such as `(setq agda2-backend "GHC")` to use GHC by 474 | default when compiling Agda files from emacs. 475 | 476 | # About 477 | 478 | This is a proof of concept of an Agda Package Manager. 479 | Contributions are always welcomed. 480 | 481 | 482 | 483 | [the package index]: https://github.com/agda/package-index 484 | [local directory]: https://agda.readthedocs.io/en/v2.5.4/tools/package-system.html 485 | [PR]: https://github.com/agda/package-index/pull/new/master 486 | -------------------------------------------------------------------------------- /apkg/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | 14 | from git import * 15 | from pony.orm import * 16 | 17 | from distlib.index import PackageIndex 18 | from natsort import natsorted 19 | 20 | from .__version__ import __version__ 21 | 22 | # ---------------------------------------------------------------------------- 23 | 24 | 25 | try: 26 | index = PackageIndex() 27 | info = index.search('agda-pkg') 28 | versions = natsorted([e["version"] for e in info if e["name"] == "agda-pkg"]) 29 | 30 | 31 | if len(versions) > 0: 32 | lastversion = versions[-1] 33 | msg = "Your Agda-Pkg version is {cversion}, however version {lversion} is available.\n" + \ 34 | "Consider upgrading via 'pip install --upgrade agda-pkg'." 35 | msg = msg.format(cversion = __version__ , lversion = lastversion) 36 | orden = [lastversion, __version__] 37 | if orden != natsorted(orden): 38 | click.echo(click.style(msg, fg='yellow', bold=True)) 39 | 40 | # Check if the index is updated. 41 | 42 | from .config import REPO 43 | origin = REPO.remotes["origin"] 44 | repo = REPO.git 45 | origin.fetch() 46 | status = repo.status() 47 | if "is behind" in status: 48 | packageURL = [url for url in REPO.remote().urls][0] 49 | msg = "Your package-index database is outdated with:\n" + \ 50 | " " + packageURL + "\n" +\ 51 | "Consider upgrading it by running the command:\n" +\ 52 | " $ apkg upgrade\n" 53 | 54 | click.echo(click.style(msg, fg='yellow', bold=True)) 55 | except: 56 | pass 57 | 58 | 59 | -------------------------------------------------------------------------------- /apkg/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.51" 2 | __message__ = "Closes #40 #35" 3 | -------------------------------------------------------------------------------- /apkg/apkg.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | from .commands import * 14 | from .commands.clean import clean 15 | from .commands.create import create 16 | from .commands.freeze import freeze 17 | from .commands.list import list 18 | from .commands.info import info 19 | from .commands.init import init 20 | from .commands.install import install 21 | from .commands.nixos import nixos 22 | from .commands.uninstall import uninstall 23 | from .commands.search import search 24 | from .commands.update import update 25 | from .commands.upgrade import upgrade 26 | from .commands.environment import environment 27 | from .commands.write_defaults import write_defaults 28 | 29 | # ---------------------------------------------------------------------------- 30 | 31 | @click.group() 32 | @click.version_option() 33 | def cli(): 34 | """A package manager for Agda.""" 35 | 36 | cli.add_command(init) 37 | cli.add_command(nixos) 38 | cli.add_command(install) 39 | cli.add_command(uninstall) 40 | cli.add_command(freeze) 41 | cli.add_command(list) 42 | cli.add_command(info) 43 | cli.add_command(clean) 44 | cli.add_command(create) 45 | cli.add_command(search) 46 | cli.add_command(update) 47 | cli.add_command(upgrade) 48 | cli.add_command(environment) 49 | cli.add_command(write_defaults) 50 | -------------------------------------------------------------------------------- /apkg/commands/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | -------------------------------------------------------------------------------- /apkg/commands/clean.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | import shutil 13 | 14 | from pathlib import Path 15 | 16 | from ..config import AGDA_PKG_PATH, AGDA_DIR_PATH 17 | from ..service.logging import logger, clog 18 | 19 | # ---------------------------------------------------------------------------- 20 | 21 | rmdirs = [ AGDA_PKG_PATH , AGDA_DIR_PATH ] 22 | msg1 = ('\n '.join(map(lambda x : x.as_posix(), rmdirs))) 23 | promptMessage = 'Directories:\n ' + msg1 + '\nDo you really want to remove these directories?' 24 | 25 | # -- Command def. 26 | @click.group() 27 | def clean(): pass 28 | 29 | @clean.command() 30 | @click.confirmation_option(prompt=promptMessage) 31 | @clog.simple_verbosity_option(logger) 32 | def clean(): 33 | """Remove the directories used by Agda-Pkg.""" 34 | 35 | for dir in rmdirs: 36 | try: 37 | shutil.rmtree(dir) 38 | logger.info(dir.as_posix() + " (deleted)") 39 | except Exception as e: 40 | logger.error(e) 41 | -------------------------------------------------------------------------------- /apkg/commands/create.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | import shutil 13 | import yaml 14 | import os 15 | import uuid 16 | 17 | 18 | from pathlib import Path 19 | from distutils.dir_util import remove_tree 20 | from jinja2 import Environment, FileSystemLoader 21 | 22 | from ..config import ( AGDA_VERSION, LIB_SUFFIX, PKG_SUFFIX ) 23 | from ..service.logging import logger, clog 24 | 25 | # ---------------------------------------------------------------------------- 26 | 27 | basedir = os.path.dirname(os.path.realpath(__file__)) 28 | 29 | # -- Command def. 30 | @click.group() 31 | def create(): pass 32 | 33 | @create.command() 34 | @click.option('--yes' 35 | , type=bool 36 | , is_flag=True 37 | , help='Yes for everything.') 38 | @clog.simple_verbosity_option(logger) 39 | def create(yes): 40 | """Help to create an Agda Library from a skeleton.""" 41 | 42 | # name = "lib" 43 | # depend = ["sta1", "sta2"] 44 | # include = ["src", "src2", "src3"] 45 | # authors = ["Jonathan", "NadieMas"] 46 | # version = "v2123" 47 | # homepage = "http:////////" 48 | # license = "MIIIIIT" 49 | # sourceRepository = "htgithub..." 50 | # description = "joderjoder" 51 | # categories = ["cat1", "cat2"] 52 | # testedWith = ["2.5.4", "2.5.4"] 53 | 54 | name = click.prompt('Library name' 55 | , default="test" 56 | , type=str) 57 | 58 | include = set() 59 | if click.confirm('Do you want to add an include path?'): 60 | include.add(click.prompt('PATH', default="src", type=str)) 61 | while click.confirm('Another include?'): 62 | include.add(click.prompt('PATH', type=str)) 63 | 64 | depend = set() 65 | if click.confirm('Do you want to add a dependency (Library name)?'): 66 | depend.add(click.prompt('Dependency name', default="standard-library", type=str)) 67 | while click.confirm('Another dependency?'): 68 | depend.add(click.prompt('Dependency name', type=str)) 69 | 70 | moreInfo = click.confirm( 71 | "Do you want add more information?\n"+ 72 | "This will create an Agda-Pkg file (.agda-pkg) more verbose.") 73 | 74 | if moreInfo: 75 | version = click.prompt('version (format: vX.X.X)', default="v0.0.1", type=str) 76 | 77 | description = click.prompt('Library description' 78 | , default= name + " is an Agda library ..." 79 | , type=str) 80 | 81 | categories = set() 82 | if click.confirm('Do you want to add a category or keyword?'): 83 | categories.add(click.prompt('category/keyword', type=str)) 84 | while click.confirm('Another category or keyword?'): 85 | categories.add(click.prompt('category/keyword', type=str)) 86 | 87 | authors = set() 88 | if click.confirm('Do you want add an author?'): 89 | authors.add(click.prompt('Author name', type=str)) 90 | if click.confirm('Another author?'): 91 | authors.add(click.prompt('Author name', type=str)) 92 | 93 | homepage = click.prompt('Homepage/website', default="None", type=str) 94 | license = click.prompt('license', default="MIT", type=str) 95 | sourceRepository = click.prompt('source repository (e.g., http://...'+name, default="", type=str) 96 | 97 | testedWith = set() 98 | testedWith.add( click.prompt('Tested with Agda version (X.X.X)', default=AGDA_VERSION, type=str) ) 99 | while click.confirm('Add another Agda version?'): 100 | testedWith.add( click.prompt('Agda version', type=str) ) 101 | 102 | pwd = Path().cwd() 103 | libPath = pwd.joinpath(name) 104 | if libPath.exists(): 105 | if yes or click.confirm("Delete the directory (" + name + ")?", abort=True): 106 | remove_tree(libPath.as_posix()) 107 | else: 108 | newDirName = name + "-" + str(uuid.uuid1()) 109 | click.echo("Saving on ({})".format(newDirName)) 110 | libPath = pwd.joinpath(newDirName) 111 | 112 | templates = Path(basedir).joinpath('templates') 113 | 114 | libPath.mkdir() 115 | libPath.joinpath("README.md").touch() 116 | libPath.joinpath("LICENSE.md").touch() 117 | 118 | libPath.joinpath(".gitignore")\ 119 | .write_text(templates.joinpath("gitignore.template").read_text()) 120 | 121 | for dir in include: 122 | libPath.joinpath(dir).mkdir() 123 | 124 | 125 | env = Environment( loader=FileSystemLoader(templates.as_posix()) 126 | , trim_blocks=False 127 | , lstrip_blocks=False) 128 | 129 | libFile = env.get_template('library.agda-lib') 130 | pkgFile = env.get_template('library.agda-pkg') 131 | 132 | 133 | output = libFile.render(name=name, depend=depend, include=include) 134 | 135 | fileName = libPath.joinpath(name + LIB_SUFFIX) 136 | with open(fileName.as_posix(), 'w') as f: 137 | f.write(output) 138 | 139 | output = pkgFile.render( name=name 140 | , depend=list(depend) 141 | , include=list(include) 142 | , authors=list(authors) 143 | , version=version 144 | , homepage=homepage 145 | , license=license 146 | , sourceRepository=sourceRepository 147 | , description=description 148 | , categories=list(categories) 149 | , testedWith=list(testedWith) 150 | ) 151 | 152 | fileName = libPath.joinpath(name + PKG_SUFFIX) 153 | with open(fileName.as_posix(), 'w') as f: 154 | f.write(output) 155 | 156 | # pkgFile = env.get_template('library.agda-pkg') 157 | -------------------------------------------------------------------------------- /apkg/commands/environment.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | from ..config import ( AGDA_DEFAULTS_PATH 13 | , AGDA_DIR_PATH 14 | , AGDA_LIBRARIES_PATH 15 | , AGDA_PKG_PATH 16 | , AGDA_VERSION 17 | , DATABASE_FILE_NAME 18 | , DATABASE_FILE_PATH 19 | , DATABASE_SEARCH_INDEXES_PATH 20 | , GITHUB_USER 21 | , INDEX_REPOSITORY_BRANCH 22 | , INDEX_REPOSITORY_NAME 23 | , INDEX_REPOSITORY_PATH 24 | , INDEX_REPOSITORY_URL 25 | , PACKAGE_SOURCES_PATH 26 | , PKG_SUFFIX 27 | , LIB_SUFFIX 28 | ) 29 | from ..service.logging import logger, clog 30 | 31 | 32 | # ---------------------------------------------------------------------------- 33 | 34 | # -- Command def. 35 | @click.group() 36 | def environment(): pass 37 | 38 | @environment.command() 39 | @clog.simple_verbosity_option(logger) 40 | def environment(): 41 | """Show environmental variables""" 42 | click.echo("AGDA_DEFAULTS_PATH: " + AGDA_DEFAULTS_PATH.as_posix()) 43 | click.echo("AGDA_DIR_PATH: "+ AGDA_DIR_PATH.as_posix()) 44 | click.echo("AGDA_LIBRARIES_PATH: "+ AGDA_LIBRARIES_PATH.as_posix()) 45 | click.echo("AGDA_PKG_PATH: "+ AGDA_PKG_PATH.as_posix()) 46 | click.echo("AGDA_VERSION: "+ AGDA_VERSION) 47 | click.echo("DATABASE_FILE_NAME: "+ DATABASE_FILE_NAME) 48 | click.echo("DATABASE_FILE_PATH: "+ DATABASE_FILE_PATH.as_posix()) 49 | click.echo("DATABASE_SEARCH_INDEXES_PATH: "+ DATABASE_SEARCH_INDEXES_PATH.as_posix()) 50 | click.echo("GITHUB_USER: "+ GITHUB_USER) 51 | click.echo("INDEX_REPOSITORY_BRANCH: "+ INDEX_REPOSITORY_BRANCH) 52 | click.echo("INDEX_REPOSITORY_NAME: "+ INDEX_REPOSITORY_NAME) 53 | click.echo("INDEX_REPOSITORY_PATH: "+ INDEX_REPOSITORY_PATH.as_posix()) 54 | click.echo("INDEX_REPOSITORY_URL: "+ INDEX_REPOSITORY_URL) 55 | click.echo("PACKAGE_SOURCES_PATH: "+ PACKAGE_SOURCES_PATH.as_posix()) 56 | click.echo("PKG_SUFFIX: "+ PKG_SUFFIX) 57 | click.echo("LIB_SUFFIX: "+ LIB_SUFFIX) 58 | -------------------------------------------------------------------------------- /apkg/commands/freeze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | import logging 13 | import click_log 14 | 15 | from pony.orm import db_session, select 16 | 17 | from ..service.database import db 18 | from ..service.database import ( Library , LibraryVersion ) 19 | from ..service.logging import logger, clog 20 | 21 | # ---------------------------------------------------------------------------- 22 | 23 | # -- Command def. 24 | @click.group() 25 | def freeze(): pass 26 | 27 | @freeze.command() 28 | @clog.simple_verbosity_option(logger) 29 | @db_session 30 | def freeze(): 31 | """List of installed packages.""" 32 | 33 | for library in select(l for l in Library if l.installed): 34 | installedVersion = library.getInstalledVersion() 35 | if installedVersion is not None: 36 | click.echo(installedVersion.freezeName) 37 | -------------------------------------------------------------------------------- /apkg/commands/info.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | import logging 13 | 14 | from pprint import pprint 15 | from pony.orm import db_session 16 | 17 | from ..service.readLibFile import readLibFile 18 | from ..service.database import db, pw 19 | from ..service.database import ( Library 20 | , LibraryVersion 21 | , Keyword 22 | , TestedWith 23 | , Dependency 24 | ) 25 | from ..service.logging import logger, clog 26 | 27 | # ---------------------------------------------------------------------------- 28 | 29 | # -- Command def. 30 | @click.group() 31 | def info(): pass 32 | 33 | @info.command() 34 | @click.argument('libname') 35 | @click.option('--version' 36 | , type=str 37 | , default="" 38 | , help='Specific the package version.') 39 | @click.option('--field' 40 | , type=str 41 | , default="" 42 | , help='Show a specific field') 43 | @clog.simple_verbosity_option(logger) 44 | @db_session 45 | def info(libname, version, field): 46 | """Show information about installed packages.""" 47 | 48 | library = Library.get(name = libname) 49 | 50 | if library is None: 51 | logger.error(" The library is not visible for agda-pkg.") 52 | return 53 | 54 | libVersion = LibraryVersion.get(library = library, name = version) 55 | 56 | if libVersion is None: 57 | libVersion = library.getInstalledVersion() 58 | 59 | if libVersion is None: 60 | libVersion = library.getLatestVersion() 61 | 62 | if libVersion is None: 63 | logger.error("The version does not exist. Try $ apkg search " + libname) 64 | return 65 | 66 | info = libVersion.info 67 | if field != "" : 68 | if field in info.keys(): 69 | click.echo(info[field]) 70 | return 71 | else: 72 | logger.error(" the field ("+ field + ") does not exist.") 73 | return 74 | 75 | pprint(info, indent=2) 76 | 77 | -------------------------------------------------------------------------------- /apkg/commands/init.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | from pathlib import Path 14 | from pony.orm import db_session, commit, select 15 | 16 | from ..config import ( PACKAGE_SOURCES_PATH 17 | , INDEX_REPOSITORY_PATH 18 | , INDEX_REPOSITORY_URL 19 | , REPO 20 | ) 21 | 22 | from ..service.database import db 23 | from ..service.database import ( Library 24 | , LibraryVersion 25 | , Keyword 26 | , Dependency 27 | ) 28 | from ..service.logging import logger, clog 29 | 30 | # ---------------------------------------------------------------------------- 31 | 32 | # -- Command def. 33 | @click.group() 34 | def init(): pass 35 | 36 | @init.command() 37 | @clog.simple_verbosity_option(logger) 38 | @click.option('--drop-tables', type=bool, default=True) 39 | def init(drop_tables): 40 | """Initialize Agda-Pkg state.""" 41 | 42 | if drop_tables: 43 | db.drop_all_tables(with_all_data=True) 44 | db.create_tables() 45 | 46 | f = INDEX_REPOSITORY_PATH 47 | 48 | src = f.joinpath("src") 49 | click.echo("Indexing libraries from " + INDEX_REPOSITORY_URL) 50 | click.echo(" Current version of the index: " + str(REPO.commit())) 51 | 52 | with db_session: 53 | 54 | for lib in src.glob("*"): 55 | 56 | name = lib.name 57 | url = Path(lib).joinpath("url").read_text() 58 | library = Library.get(name = name, url = url) 59 | 60 | if library is None: 61 | library = Library(name = name, url = url) 62 | 63 | for version in lib.joinpath("versions").glob("*"): 64 | if version.is_dir(): 65 | libVersion = LibraryVersion.get( library=library 66 | , name=version.name 67 | , fromIndex=True 68 | ) 69 | if libVersion is None: 70 | libVersion = LibraryVersion( library=library 71 | , name=version.name 72 | , fromIndex=True 73 | ) 74 | 75 | if version.joinpath("sha1").exists(): 76 | libVersion.sha = version.joinpath("sha1").read_text() 77 | libVersion.origin = url 78 | libVersion.fromGit = True 79 | else: 80 | logger.error(version.name + " no valid!.") 81 | libVersion.delete() 82 | 83 | commit() 84 | 85 | # With all libraries indexed, we proceed to create the dependencies 86 | # as objects for the index. 87 | 88 | for lib in src.glob("*"): 89 | 90 | library = Library.get(name = lib.name) 91 | 92 | for version in library.getSortedVersions(): 93 | # click.echo(version.freezeName) 94 | 95 | info = version.readInfoFromLibFile() 96 | version.depend.clear() 97 | for depend in info.get("depend", []): 98 | if type(depend) == list: 99 | logger.info("no supported yet but the format is X.X <= name <= Y.Y") 100 | else: 101 | dependency = Library.get(name = depend) 102 | if dependency is not None: 103 | version.depend.add(Dependency(library = dependency)) 104 | else: 105 | logger.warning(depend + " is not in the index.") 106 | 107 | info = version.readInfoFromLibFile() 108 | 109 | keywords = info.get("keywords", []) + info.get("category", []) 110 | keywords = list(set(keywords)) 111 | 112 | for word in keywords: 113 | keyword = Keyword.get(word = word) 114 | 115 | if keyword is None: 116 | keyword = Keyword(word = word) 117 | 118 | if not library in keyword.libraries: 119 | keyword.libraries.clear() 120 | keyword.libraries.add(library) 121 | 122 | if not version in keyword.libVersions: 123 | keyword.libVersions.clear() 124 | keyword.libVersions.add(version) 125 | 126 | 127 | libraries = select(l for l in Library)[:] 128 | 129 | click.echo( str(len(libraries)) + " librar" \ 130 | + ("ies" if len(libraries) != 1 else "y") \ 131 | + " indexed." 132 | ) 133 | -------------------------------------------------------------------------------- /apkg/commands/install.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | import git 14 | import humanize 15 | import os 16 | import random 17 | import requests 18 | import subprocess 19 | import time 20 | import uuid 21 | import re 22 | 23 | from distutils.dir_util import copy_tree, remove_tree 24 | from pathlib import Path 25 | from pony.orm import db_session, commit 26 | from tempfile import TemporaryDirectory 27 | 28 | from ..config import ( PACKAGE_SOURCES_PATH 29 | , INDEX_REPOSITORY_PATH 30 | , PKG_SUFFIX 31 | , GITHUB_DOMAIN 32 | , LIB_SUFFIX 33 | , GITHUB_API 34 | ) 35 | 36 | from ..service.database import db 37 | from ..service.database import ( Library 38 | , LibraryVersion 39 | , Keyword 40 | , Dependency 41 | ) 42 | 43 | from ..service.logging import logger, clog 44 | from ..service.readLibFile import readLibFile 45 | from ..service.utils import isURL, isGit, isIndexed, isLocal 46 | 47 | from .uninstall import uninstallLibrary 48 | from .write_defaults import write_defaults 49 | 50 | # ---------------------------------------------------------------------------- 51 | # -- Install command variants 52 | 53 | # -- Command def. 54 | @click.group() 55 | def install(): pass 56 | 57 | # -- Defaults 58 | 59 | option = { 'branch' : "master" 60 | , 'cache' : False 61 | , 'editable' : False 62 | , 'git' : False 63 | , 'github' : False 64 | , 'libnames' : () 65 | , 'libname' : None 66 | , 'local' : False 67 | , 'name' : '*' 68 | , 'no_defaults' : False 69 | , 'no_dependencies' : False 70 | , 'requirement' : None 71 | , 'src' : '' 72 | , 'url' : None 73 | , 'version' : '' 74 | , 'yes' : False 75 | , 'installing_depend' : False 76 | } 77 | 78 | # ---------------------------------------------------------------------------- 79 | 80 | @db_session 81 | def installFromLocal(): 82 | 83 | global option 84 | 85 | if len(option["pathlib"]) == 0 or option["pathlib"] == ".": 86 | option["pathlib"] = Path().cwd() 87 | else: 88 | option["pathlib"] = Path(option["pathlib"]) 89 | 90 | pwd = option["pathlib"].joinpath(Path(option["src"])) 91 | 92 | if not Path(pwd).exists(): 93 | logger.error(pwd + " doesn't exist!") 94 | return None 95 | 96 | agdaLibFiles = [ f for f in pwd.glob(option["name"] + LIB_SUFFIX) if f.is_file() ] 97 | agdaPkgFiles = [ f for f in pwd.glob(option["name"] + PKG_SUFFIX) if f.is_file() ] 98 | 99 | if len(agdaLibFiles) + len(agdaPkgFiles) == 0: 100 | logger.error(" no libraries (" + LIB_SUFFIX + " or "\ 101 | + PKG_SUFFIX + ") files detected." ) 102 | return None 103 | 104 | libFile = Path("") 105 | if len(agdaPkgFiles) == 1: 106 | libFile = agdaPkgFiles[0] 107 | elif len(agdaLibFiles) == 1: 108 | libFile = agdaLibFiles[0] 109 | else: 110 | logger.error("[!] Use --name to specify the library name.") 111 | return None 112 | 113 | logger.info("Library file detected: " + libFile.name) 114 | 115 | info = readLibFile(libFile) 116 | 117 | option["name"] = info.get("name", "") 118 | 119 | if len(option["name"]) == 0: 120 | option["name"] = option["pathlib"].name 121 | 122 | versionName = option["version"] 123 | 124 | if versionName == "" : 125 | versionName = str(info.get("version", "")) 126 | if versionName == "" and pwd.joinpath(".git").exists(): 127 | try: 128 | with open(os.devnull, 'w') as devnull: 129 | result = subprocess.run( ["git", "describe", "--tags", "--long"] 130 | , stdout=subprocess.PIPE 131 | , stderr=devnull 132 | , cwd=pwd.as_posix() 133 | ) 134 | versionName = result.stdout.decode() 135 | except: pass 136 | 137 | if versionName == "" and pwd.joinpath(".git").exists(): 138 | try: 139 | with open(os.devnull, 'w') as devnull: 140 | result = subprocess.run( ["git", "rev-parse", "HEAD"] 141 | , stdout=subprocess.PIPE 142 | , stderr=devnull 143 | , cwd=pwd.as_posix() 144 | ) 145 | versionName = result.stdout.decode()[:8] 146 | except: pass 147 | 148 | if versionName == "": 149 | versionName = str(uuid.uuid1()) 150 | 151 | logger.info("Library version: " + versionName) 152 | 153 | # At this point we have the name from the local library 154 | library = Library.get(name=option["name"]) 155 | 156 | if library is None: 157 | library = Library( name=option["name"] ) 158 | 159 | versionLibrary = LibraryVersion.get(library=library, name=versionName) 160 | 161 | if versionLibrary is not None: 162 | 163 | if versionLibrary.installed: 164 | logger.warning("This version ({}) is already installed." 165 | .format(versionLibrary.freezeName)) 166 | if option["yes"] or click.confirm('Do you want to uninstall it first?'): 167 | try: 168 | uninstallLibrary( libname=option["name"] 169 | , database=False 170 | , remove_cache=True 171 | ) 172 | except Exception as e: 173 | logger.error(e) 174 | return None 175 | else: 176 | 177 | versionNameProposed = "{}-{}".format(versionName, uuid.uuid1()) 178 | logger.warning("Version suggested: " + option["name"] + "@" + versionNameProposed) 179 | 180 | if click.confirm('Do you want to install it using this version?', abort=True): 181 | versionLibrary = LibraryVersion( library=library 182 | , name=versionNameProposed 183 | , cached=True 184 | , editable=option["editable"] 185 | ) 186 | else: 187 | versionLibrary = LibraryVersion( library=library 188 | , name=versionName 189 | , cached=True 190 | , editable=option["editable"] 191 | ) 192 | 193 | if option["editable"]: 194 | versionLibrary.origin = pwd.as_posix() 195 | versionLibrary.editable = True 196 | option["editable"] = False # Just used editable once 197 | 198 | try: 199 | if not(versionLibrary.editable): 200 | if versionLibrary.sourcePath.exists(): 201 | remove_tree(versionLibrary.sourcePath.as_posix()) 202 | 203 | logger.info("Adding " + versionLibrary.sourcePath.as_posix()) 204 | copy_tree(pwd.as_posix(), versionLibrary.sourcePath.as_posix()) 205 | 206 | except Exception as e: 207 | logger.error(e) 208 | logger.error("Fail to copy directory (" + pwd.as_posix() + ")") 209 | return None 210 | 211 | commit() 212 | 213 | try: 214 | info = versionLibrary.readInfoFromLibFile() 215 | 216 | keywords = info.get("keywords", []) + info.get("category", []) 217 | keywords = list(set(keywords)) 218 | 219 | for word in keywords: 220 | 221 | keyword = Keyword.get_for_update(word=word) 222 | 223 | if keyword is None: 224 | keyword = Keyword(word = word) 225 | 226 | if not library in keyword.libraries: 227 | keyword.libraries.add(library) 228 | 229 | if not versionLibrary in keyword.libVersions: 230 | keyword.libVersions.add(versionLibrary) 231 | 232 | versionLibrary.depend.clear() 233 | for depend in info.get("depend",[]): 234 | 235 | if type(depend) == list: 236 | logger.info("no supported yet.") 237 | else: 238 | if "@" in depend: 239 | dname, _ = depend.split("@") 240 | else: 241 | dname = depend 242 | dependency = Library.get(name=dname) 243 | 244 | if dependency is not None: 245 | versionLibrary.depend.add(Dependency(library=dependency)) 246 | 247 | if not option["no_dependencies"] and not dependency.installed: 248 | 249 | oldOption = option 250 | option["libname"] = dependency.name 251 | option["version"] = "" 252 | option["name"] = "*" 253 | option["libnames"] = () 254 | installFromIndex() 255 | option = oldOption 256 | 257 | else: 258 | logger.warning(depend + " is not in the index") 259 | 260 | versionLibrary.install( not(option["no_defaults"]) ) 261 | 262 | commit() 263 | return versionLibrary 264 | 265 | except Exception as e: 266 | try: 267 | if not(versionLibrary.editable) and versionLibrary.sourcePath.exists(): 268 | remove_tree(versionLibrary.sourcePath.as_posix()) 269 | except: 270 | logger.error(" fail to remove the sources: {}" 271 | .format(versionLibrary.sourcePath.as_poasix()) 272 | ) 273 | 274 | logger.error(e) 275 | return None 276 | 277 | 278 | # ---------------------------------------------------------------------------- 279 | def installFromGit(): 280 | 281 | global option 282 | 283 | if not isGit(option.get("url", "")): 284 | logger.error("This is not a git repository.\ 285 | You may want to add '.git' at the end of your URL.") 286 | return None 287 | 288 | logger.info("Installing from git: %s" % option["url"] ) 289 | 290 | with TemporaryDirectory() as tmpdir: 291 | 292 | try: 293 | click.echo("Using temporary directory: {}".format(tmpdir)) 294 | 295 | if Path(tmpdir).exists(): 296 | remove_tree(tmpdir) 297 | 298 | # To display a nice progress bar, we need the size of 299 | # the repository, so let's try to get that number 300 | 301 | # -- SIZE Repo 302 | 303 | size = 0 304 | if "github" in option["url"]: 305 | 306 | reporef = option["url"].split("github.com")[-1] 307 | infourl = GITHUB_API + reporef.split(".git")[0] 308 | 309 | response = requests.get(infourl, stream=True) 310 | 311 | if not response.ok: 312 | logger.error("Request failed: %d" % response.status_code) 313 | return None 314 | 315 | info = response.json() 316 | size = int(info.get("size", 0)) 317 | 318 | else: 319 | 320 | response = requests.get(option["url"], stream=True) 321 | if not response.ok: 322 | logger.error("Request failed: %d" % response.status_code) 323 | return None 324 | 325 | size_length = response.headers.get('content-length') 326 | size = 0 327 | if size_length is None: 328 | for _ in response.iter_content(1024): 329 | size += 1024 330 | else: 331 | size = size_length 332 | size = int(size) 333 | 334 | logger.info("Downloading " + option["url"] \ 335 | + " (%s)" % str(humanize.naturalsize(size, binary=True))) 336 | 337 | with click.progressbar( length=10*size 338 | , bar_template='|%(bar)s| %(info)s %(label)s' 339 | , fill_char=click.style('█', fg='cyan') 340 | , empty_char=' ' 341 | , width=30 342 | ) as bar: 343 | 344 | class Progress(git.remote.RemoteProgress): 345 | 346 | total, past = 0 , 0 347 | 348 | def update(self, op_code, cur_count, max_count=None, message=''): 349 | 350 | if cur_count < 10: 351 | self.past = self.total 352 | self.total = self.past + int(cur_count) 353 | 354 | bar.update(self.total) 355 | 356 | click.echo("Git branch: " + option["branch"]) 357 | 358 | REPO = git.Repo.clone_from( option["url"] 359 | , tmpdir 360 | , branch=option["branch"] 361 | , progress=Progress() 362 | ) 363 | 364 | if option["version"] != "": 365 | try: 366 | # Seen on https://goo.gl/JVs8jJ 367 | REPO.git.checkout(option["version"]) 368 | 369 | except Exception as e: 370 | logger.error(e) 371 | logger.error(" version or tag not found ({})".format(option["version"])) 372 | return None 373 | 374 | option["pathlib"] = tmpdir 375 | 376 | libVersion = installFromLocal() 377 | 378 | if libVersion is None: 379 | logger.error(" we couldn't install the version you specified.") 380 | return None 381 | 382 | libVersion.fromGit = True 383 | libVersion.origin = option["url"] 384 | libVersion.library.url = option["url"] 385 | libVersion.library.default = not(option["no_defaults"]) 386 | 387 | if option["version"] != "": 388 | libVersion.sha = REPO.head.commit.hexsha 389 | 390 | commit() 391 | return libVersion 392 | 393 | except Exception as e: 394 | logger.error(e) 395 | logger.error("There was an error when installing the library. May you want to run init?") 396 | return None 397 | 398 | # ---------------------------------------------------------------------------- 399 | @db_session 400 | def installFromIndex(): 401 | 402 | global option 403 | 404 | if len(option["libname"]) == 0: 405 | logger.info("Nothing to install.") 406 | return 407 | 408 | if "@" in option["libname"]: 409 | option["libname"], option["version"] = option["libname"].split("@") 410 | elif "==" in option["libname"]: 411 | option["libname"], option["version"] = option["libname"].split("==") 412 | 413 | # Check first if the library is in the cache 414 | logger.info("Installing ({}) from the index...".format(option["libname"])) 415 | library = Library.get(name=option["libname"]) 416 | 417 | if library is not None: 418 | 419 | versionLibrary = None 420 | 421 | if option["version"] == "": 422 | # we'll try to install the latest git version 423 | versionLibrary = library.getLatestVersion() 424 | option["version"] = versionLibrary.name 425 | 426 | else: 427 | versionLibrary = LibraryVersion.get( library=library 428 | , name=option["version"] 429 | , fromIndex=True 430 | , fromGit=True 431 | ) 432 | if versionLibrary is None and \ 433 | option["version"] != "" and \ 434 | not(option["version"].startswith("v")): 435 | # try with 436 | 437 | versionLibrary = LibraryVersion.get( library=library 438 | , name= "v" + option["version"] 439 | , fromIndex=True 440 | , fromGit=True 441 | ) 442 | if versionLibrary is not None: 443 | versionLibrary.name = "v" + option["version"] 444 | 445 | if versionLibrary is None: 446 | if option["version"] != "": 447 | logger.error(" the version (" + option["version"] +") is not available in the index.") 448 | else: 449 | logger.error(" no versions for this library. Please initialize the index.") 450 | return None 451 | 452 | if versionLibrary.installed: 453 | logger.info("Requirement already satisfied.") 454 | return versionLibrary 455 | 456 | elif versionLibrary.cached and \ 457 | (option["yes"] or click.confirm('Do you want to install the cached version?')): 458 | versionLibrary.install() 459 | return versionLibrary 460 | 461 | else: 462 | option["url"] = versionLibrary.library.url 463 | option["branch"] = "master" 464 | 465 | versionLibrary = installFromGit() 466 | 467 | if versionLibrary is not None: 468 | versionLibrary.fromIndex = True 469 | versionLibrary.cached = True 470 | 471 | return versionLibrary 472 | else: 473 | logger.error("Library not available.") 474 | return None 475 | 476 | # ---------------------------------------------------------------------------- 477 | def installFromURL(): 478 | return None 479 | 480 | # ---------------------------------------------------------------------------- 481 | @install.command() 482 | @click.argument('libnames', nargs=-1) 483 | @click.option('--src' 484 | , type=str 485 | , default=option["src"] 486 | , help='Directory to the source.') 487 | @click.option('--version' 488 | , type=str 489 | , default=option["version"] 490 | , help='Version, tag or commit.') 491 | @click.option('--no-defaults' 492 | , type=bool 493 | , is_flag=True 494 | , help='No default library.') 495 | @click.option('--cache' 496 | , type=bool 497 | , is_flag=True 498 | , default=option["cache"] 499 | , help='Cache available.') 500 | @click.option('--editable' 501 | , type=bool 502 | , is_flag=True 503 | , default=option["editable"] 504 | , help='Install a local library in editable mode') 505 | @click.option('--local' 506 | , type=bool 507 | , is_flag=True 508 | , help='Force to install just local packages.') 509 | @click.option('--name' 510 | , type=str 511 | , help='Help to disambiguate when many library files exist\ 512 | in the same directory.') 513 | @click.option('--url' 514 | , type=bool 515 | , is_flag=True 516 | , help='From a url address.') 517 | @click.option('--git' 518 | , type=bool 519 | , is_flag=True 520 | , help='From a git repository.') 521 | @click.option('--github' 522 | , type=bool 523 | , is_flag=True 524 | , help='From a github repository.') 525 | @click.option('--branch' 526 | , type=str 527 | , default="master" 528 | , help='From a git repository.') 529 | @click.option('--no-dependencies' 530 | , type=bool 531 | , is_flag=True 532 | , help='Do not install dependencies.') 533 | @click.option('-r' 534 | , '--requirement' 535 | , type=click.Path(exists=True) 536 | , help='Use a requirement file.') 537 | @click.option('--yes' 538 | , type=bool 539 | , is_flag=True 540 | , help='Yes for everything.') 541 | @clog.simple_verbosity_option(logger) 542 | @click.pass_context 543 | @db_session 544 | def install( ctx, libnames, src, version, no_defaults 545 | , cache, editable, local, name, url, git, github, branch 546 | , no_dependencies,requirement, yes): 547 | 548 | """Install one or more packages.""" 549 | 550 | global option 551 | 552 | option.update({k : v for k, v in ctx.__dict__["params"].items() 553 | if v is not None} ) 554 | 555 | option["libnames"] = list(set(libnames)) 556 | 557 | if requirement: 558 | try: 559 | option["libnames"] += Path(requirement).read_text().split() 560 | except Exception as e: 561 | logger.error(e) 562 | logger.error(" installation failed.") 563 | return 564 | 565 | if len(option["libnames"]) > 1 and option["version"] != "": 566 | return logger.error("--version option only works for one library.\n\ 567 | Please consider to use nameLibrary@versionNumber.") 568 | 569 | if (option["git"] or option["github"]) and option["url"]: 570 | return logger.error("--git and --url are incompatible") 571 | 572 | if len(option["libnames"]) == 0: 573 | option["libnames"] = ["."] 574 | 575 | if option["github"]: 576 | option["git"] = True 577 | option["github"] = True 578 | 579 | for libname in option["libnames"]: 580 | 581 | if "@" in libname: 582 | option["libname"], option["version"] = libname.split("@") 583 | elif "==" in libname: 584 | option["libname"], option["version"] = libname.split("==") 585 | else: 586 | option["libname"] = libname 587 | 588 | if option["github"]: 589 | if not option["libname"].startswith(GITHUB_DOMAIN): 590 | option["libname"] = GITHUB_DOMAIN + option["libname"] 591 | if not option["libname"].endswith(".git"): 592 | option["libname"] = option["libname"] + ".git" 593 | 594 | option["pathlib"] = option["libname"] 595 | option["url"] = option["libname"] 596 | 597 | vLibrary = None 598 | 599 | try: 600 | 601 | if option["local"]: 602 | vLibrary = installFromLocal() 603 | 604 | elif isIndexed(option["libname"]): 605 | vLibrary = installFromIndex() 606 | 607 | elif git or isGit(option["libname"]): 608 | vLibrary = installFromGit() 609 | 610 | elif isLocal(option["pathlib"]): 611 | vLibrary = installFromLocal() 612 | 613 | except Exception as e: 614 | logger.error("Unsuccessfully installation {}." 615 | .format(option["libname"] if name =="*" else name)) 616 | continue 617 | 618 | if vLibrary is not None: 619 | logger.info("Successfully installed ({}@{})." 620 | .format(vLibrary.library.name, vLibrary.name)) 621 | logger.info("\tSource code available on: {}" 622 | .format(vLibrary.sourcePath)) 623 | else: 624 | logger.info("Unsuccessfully installation ({}).".format(option["libname"])) 625 | 626 | ctx.invoke(write_defaults, yes = yes) 627 | -------------------------------------------------------------------------------- /apkg/commands/list.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | import logging 13 | import click_log as clog 14 | 15 | from operator import attrgetter, itemgetter 16 | from pony.orm import db_session, select 17 | from natsort import natsorted 18 | 19 | from ..service.database import db 20 | from ..service.database import ( Library , LibraryVersion ) 21 | from ..service.logging import logger, clog 22 | 23 | # ---------------------------------------------------------------------------- 24 | 25 | # -- Command def. 26 | @click.group() 27 | def list(): pass 28 | 29 | 30 | listFields = ["name", "version", "url"] 31 | 32 | 33 | @list.command() 34 | @clog.simple_verbosity_option(logger) 35 | @click.option('--full' 36 | , type=bool 37 | , is_flag=True 38 | , help='Show name, version and description per package.' 39 | ) 40 | @click.option('--field' 41 | , type=str 42 | , default="" 43 | , help='Show a specific field e.g.: name, version, url') 44 | @db_session 45 | def list(full, field): 46 | """List all installed packages.""" 47 | 48 | short = not full 49 | 50 | libraries = select(l for l in Library if l)[:] 51 | libraries = natsorted(libraries, key=lambda x : attrgetter('name')(x).lower()) 52 | 53 | if len(libraries) == 0: 54 | logger.info("[!] No libraries available to list.") 55 | logger.info(" Consider run the following command:") 56 | logger.info(" $ apkg init") 57 | return 58 | 59 | 60 | 61 | orderFields = [ 62 | #, "library" 63 | #, "sha" 64 | "description" 65 | # , "license" 66 | # , "include" 67 | # , "depend" 68 | # , "testedWith" 69 | , "keywords" 70 | # , "installed" 71 | # , "cached" 72 | # , "fromIndex" 73 | # , "fromUrl" 74 | # , "fromGit" 75 | , "origin" 76 | # , "default" 77 | ] 78 | 79 | i = 0 80 | if short and field == "": 81 | logger.info("{:<20.20} {:<15.20} {:.72}" 82 | .format("Library name", "Latest version", "URL")) 83 | logger.info("-"*105) 84 | 85 | for library in libraries: 86 | v = library.getLatestVersion() 87 | if v is not None: 88 | if not short: 89 | 90 | logger.info(v.library.name) 91 | logger.info("="*len(v.library.name)) 92 | 93 | info = v.info 94 | 95 | for k in orderFields: 96 | val = info.get(k, None) 97 | if val is not None or val != "" or len(val) > 0: 98 | click.echo("{0}: {1}".format(k,val)) 99 | 100 | vs = ','.join(str(ver) for ver in v.library.versions) 101 | 102 | if len(vs) > 0: 103 | print("Versions:", vs) 104 | 105 | else: 106 | if field in listFields: 107 | if field == "name": 108 | print(v.library.name) 109 | elif field == "version": 110 | print(v.name) 111 | else: 112 | print(v.library.url) 113 | else: 114 | print("{:<20.20} {:<15.20} {:.72}" 115 | .format(v.library.name,v.name,v.library.url)) 116 | 117 | i += 1 118 | if not short and i < len(libraries): 119 | logger.info("") -------------------------------------------------------------------------------- /apkg/commands/nixos.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | 10 | # ---------------------------------------------------------------------------- 11 | 12 | import click 13 | import os 14 | 15 | from pathlib import Path 16 | from distutils.dir_util import copy_tree 17 | 18 | from ..config import ( AGDA_VERSION 19 | , LIB_SUFFIX 20 | , PKG_SUFFIX 21 | , SUPPORT_FILES_PATH 22 | ) 23 | from ..service.logging import logger, clog 24 | 25 | 26 | # ---------------------------------------------------------------------------- 27 | 28 | # -- Command def. 29 | @click.group() 30 | def nixos(): pass 31 | 32 | @nixos.command() 33 | @click.option('--yes' 34 | , type=bool 35 | , is_flag=True 36 | , help='Yes for everything.') 37 | @clog.simple_verbosity_option(logger) 38 | def nixos(yes): 39 | """Set up a NixOS environment for Agda""" 40 | MSG = "Agda-pkg will copy the following files to the current directory." 41 | click.echo(MSG) 42 | for file in SUPPORT_FILES_PATH.iterdir(): 43 | print(file.as_posix()) 44 | if click.confirm('Do you want to proceed?'): 45 | pwd = Path().cwd() 46 | copy_tree(SUPPORT_FILES_PATH.as_posix(), pwd.as_posix(), update=1, verbose=1) 47 | -------------------------------------------------------------------------------- /apkg/commands/search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | from pony.orm import db_session 14 | from ponywhoosh import PonyWhoosh, search, full_search 15 | 16 | from ..config import ( AGDA_DEFAULTS_PATH 17 | , AGDA_DIR_PATH 18 | , AGDA_LIBRARIES_PATH 19 | , AGDA_PKG_PATH 20 | , AGDA_VERSION 21 | , DATABASE_FILE_NAME 22 | , DATABASE_FILE_PATH 23 | , DATABASE_SEARCH_INDEXES_PATH 24 | , GITHUB_USER 25 | , INDEX_REPOSITORY_BRANCH 26 | , INDEX_REPOSITORY_NAME 27 | , INDEX_REPOSITORY_PATH 28 | , INDEX_REPOSITORY_URL 29 | , REPO 30 | ) 31 | 32 | from ..service.readLibFile import readLibFile 33 | from ..service.database import db, pw 34 | from ..service.database import ( Library 35 | , LibraryVersion 36 | , Keyword 37 | , TestedWith 38 | , Dependency 39 | ) 40 | from ..service.logging import logger, clog 41 | 42 | # ---------------------------------------------------------------------------- 43 | 44 | @click.group() 45 | def search(): pass 46 | 47 | @search.command() 48 | @click.argument('term') 49 | @click.option('--field', '-f', type=str, default=None) 50 | def search(term, field): 51 | """Search into the package index.""" 52 | 53 | possibleFields = ["name", "url", "description", "word"] 54 | 55 | results = \ 56 | pw.search( 57 | term 58 | , models = ["Library", "Keyword"] 59 | , fields = (field if field is not None else possibleFields) 60 | , include_entity = True 61 | , something = True 62 | ) 63 | libraries = results["results"].get('Library', {'items':[]})['items'] 64 | 65 | click.echo( str(len(libraries)) + " result" \ 66 | + ("s" if len(libraries) != 1 else "") \ 67 | + " in " + str(results['runtime']) + "seg" 68 | ) 69 | 70 | if len(libraries) > 0: 71 | logger.info( "matches: " + str(results['matched_terms'])) 72 | logger.info("") 73 | 74 | for result in libraries: 75 | if result["model"] == "Library": 76 | 77 | # click.echo(result) 78 | click.echo(result["entity"]["name"]) 79 | 80 | # logger.info("="*len(result["entity"]["name"])) 81 | # del result["entity"]["name"] 82 | 83 | for k, v in result["entity"].items(): 84 | if v != None and k not in [ "name" , "default" ]: 85 | click.echo("{0}: {1}".format(k,v)) 86 | click.echo("") 87 | -------------------------------------------------------------------------------- /apkg/commands/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agda/agda-pkg/8d5cbcd76c1e4d150ad35ce39d1573c1f10f9fcd/apkg/commands/templates/__init__.py -------------------------------------------------------------------------------- /apkg/commands/templates/gitignore.template: -------------------------------------------------------------------------------- 1 | ### Agda ### 2 | *.agdai 3 | *.hi 4 | *.o 5 | *.agda# 6 | *.agda~ 7 | .#*.agda 8 | ./agda-stdlib -------------------------------------------------------------------------------- /apkg/commands/templates/library.agda-lib: -------------------------------------------------------------------------------- 1 | -- File generated by Agda-Pkg 2 | name: {{name}} 3 | {% if depend|length > 0 -%} 4 | depend: {{depend|join(' ')}} 5 | {%- endif %} 6 | {% if include|length > 0 -%} 7 | include: {{include|join(' ')}} 8 | {%- endif %} 9 | -- End {{"\n"}} 10 | -------------------------------------------------------------------------------- /apkg/commands/templates/library.agda-pkg: -------------------------------------------------------------------------------- 1 | # File generated by Agda-Pkg 2 | name: {{name}} 3 | version: {{version}} 4 | {% if authors|length > 0 -%} 5 | author: {% if authors|length == 1 -%}{{authors[0]}} 6 | {%- else -%} 7 | {%- for author in authors %} 8 | - {{author}} 9 | {%- endfor %} 10 | {%- endif %} 11 | {%- endif %} 12 | {% if categories|length > 0 -%} 13 | category: {{categories|join(', ')}} 14 | {%- endif %} 15 | {% if homepage|length > 0 -%} 16 | homepage: {{homepage}} 17 | {%- endif %} 18 | {% if license|length > 0 -%} 19 | license: {{license}} 20 | license-file: LICENSE.md 21 | {%- endif %} 22 | {% if sourceRepository|length > 0 -%} 23 | source-repository: {{sourceRepository}} 24 | {%- endif %} 25 | {% if testedWith|length > 0 -%} 26 | tested-with: {{testedWith|join(', ')}} 27 | {%- endif %} 28 | {% if description|length > 0 -%} 29 | description: {{description}} 30 | {%- endif %} 31 | {% if depend|length > 0 -%} 32 | depend: 33 | {%- for lib in depend %} 34 | - {{lib}} 35 | {%- endfor %} 36 | {% endif %} 37 | {%- if include|length > 0 -%} 38 | include: 39 | {%- for path in include %} 40 | - {{path}} 41 | {%- endfor %} 42 | {%- endif %} 43 | # End {{"\n"}} -------------------------------------------------------------------------------- /apkg/commands/uninstall.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | from pathlib import Path 14 | from pony.orm import db_session, commit 15 | 16 | from ..config import ( AGDA_DEFAULTS_PATH 17 | , AGDA_DIR_PATH 18 | , AGDA_LIBRARIES_PATH 19 | , AGDA_PKG_PATH 20 | , AGDA_VERSION 21 | , DATABASE_FILE_NAME 22 | , DATABASE_FILE_PATH 23 | , DATABASE_SEARCH_INDEXES_PATH 24 | , GITHUB_USER 25 | , INDEX_REPOSITORY_BRANCH 26 | , INDEX_REPOSITORY_NAME 27 | , INDEX_REPOSITORY_PATH 28 | , INDEX_REPOSITORY_URL 29 | , REPO 30 | , LIB_SUFFIX 31 | , PKG_SUFFIX 32 | ) 33 | 34 | from .write_defaults import write_defaults 35 | from ..service.readLibFile import readLibFile 36 | from ..service.database import db, pw 37 | from ..service.database import ( Library 38 | , LibraryVersion 39 | , Keyword 40 | , TestedWith 41 | , Dependency 42 | ) 43 | from ..service.logging import logger, clog 44 | 45 | 46 | # ---------------------------------------------------------------------------- 47 | 48 | # -- Command def. 49 | @click.group() 50 | def uninstall(): 51 | pass 52 | 53 | @db_session 54 | def uninstallLibrary(libname, database=False, remove_cache=False): 55 | library = Library.get(name = libname) 56 | if library is None \ 57 | or (not library.installed and not (remove_cache)): 58 | logger.info("This library is not installed.") 59 | return False 60 | 61 | try: 62 | if database: 63 | library.delete() 64 | else: 65 | if library.installed: 66 | library.uninstall(remove_cache) 67 | logger.info("Removed {} from the agda-pkg database.".format(libname)) 68 | commit() 69 | return True 70 | 71 | except Exception as e: 72 | logger.error(e) 73 | logger.error(" Unsuccessfully call.") 74 | return False 75 | 76 | @uninstall.command() 77 | @click.argument('libname') 78 | @click.option('--database' 79 | , type=bool 80 | , default=False 81 | , is_flag=True 82 | , help='Remove a package from the agda-pkg database') 83 | @click.option('--remove-cache' 84 | , type=bool 85 | , default=False 86 | , is_flag=True 87 | , help='Remove all package files.') 88 | @clog.simple_verbosity_option(logger) 89 | @click.option('--yes' 90 | , type=bool 91 | , is_flag=True 92 | , help='Yes for everything.') 93 | @click.pass_context 94 | @click.confirmation_option(prompt='Proceed?') 95 | @db_session 96 | def uninstall(ctx, libname, database, remove_cache,yes): 97 | """Uninstall a package.""" 98 | 99 | if libname == "." : 100 | pwd = Path().cwd() 101 | 102 | if not Path(pwd).exists(): 103 | logger.error(pwd + " doesn't exist!") 104 | return None 105 | 106 | agdaLibFiles = [ f for f in pwd.glob("*" + LIB_SUFFIX) if f.is_file() ] 107 | agdaPkgFiles = [ f for f in pwd.glob("*" + PKG_SUFFIX) if f.is_file() ] 108 | 109 | if len(agdaLibFiles) == 0 and len(agdaPkgFiles) == 0: 110 | logger.error("No libraries (" + LIB_SUFFIX + " or "\ 111 | + PKG_SUFFIX + ") files detected." ) 112 | return None 113 | 114 | libFile = Path("") 115 | 116 | # -- TODO: offer the posibility to create a file agda-pkg! 117 | if len(agdaPkgFiles) == 1: 118 | libFile = agdaPkgFiles[0] 119 | elif len(agdaLibFiles) == 1: 120 | # -- TODO: offer the posibility ssto create a file agda-pkg! 121 | libFile = agdaLibFiles[0] 122 | else: 123 | logger.error("None or many agda libraries files.") 124 | logger.info("[!] Use --name to specify the library name.") 125 | return None 126 | 127 | logger.info("Library file detected: " + libFile.name) 128 | info = readLibFile(libFile) 129 | 130 | libname = info.get("name", "") 131 | 132 | if libname == "." or libname == "": 133 | logger.error(" we could not know the name of the library to uninstall.") 134 | return 135 | 136 | if uninstallLibrary(libname, database, remove_cache): 137 | ctx.invoke(write_defaults, yes = yes) -------------------------------------------------------------------------------- /apkg/commands/update.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | 14 | from pony.orm import db_session, select 15 | 16 | from .install import install 17 | from .uninstall import uninstall 18 | from ..service.database import db 19 | from ..service.database import ( Library ) 20 | from ..service.logging import logger, clog 21 | 22 | # ---------------------------------------------------------------------------- 23 | 24 | @click.group() 25 | def update(): 26 | pass 27 | 28 | @update.command() 29 | @click.argument('libnames', nargs=-1) 30 | @clog.simple_verbosity_option(logger) 31 | @click.pass_context 32 | @db_session 33 | def update(ctx, libnames): 34 | """Update packages to latest indexed version.""" 35 | 36 | logger.info("Updating.") 37 | wasUpdated = False 38 | if len(libnames) == 0: 39 | libraries = select(l.name for l in Library if l.installed)[:] 40 | else: 41 | libraries = libnames 42 | 43 | for lname in libraries: 44 | library = Library.get(name = lname) 45 | installedVersion = library.getInstalledVersion() 46 | latestVersion = library.getLatestVersion() 47 | if installedVersion is not latestVersion: 48 | try: 49 | ctx.invoke(uninstall, libname=lname, yes=True) 50 | ctx.invoke(install, libnames=[lname], version=latestVersion.name, yes=True) 51 | wasUpdated = True 52 | except Exception as e: 53 | logger.error(e) 54 | 55 | if wasUpdated: 56 | logger.info("Libraries updated.") 57 | else: 58 | logger.info("Everything is up-to-date.") 59 | -------------------------------------------------------------------------------- /apkg/commands/upgrade.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | 13 | from .init import init 14 | from ..config import REPO 15 | from ..service.logging import logger, clog 16 | 17 | # ---------------------------------------------------------------------------- 18 | 19 | @click.group() 20 | def upgrade(): pass 21 | 22 | @upgrade.command() 23 | @click.pass_context 24 | def upgrade(ctx): 25 | """Update the list of available packages.""" 26 | try: 27 | origin = REPO.remotes["origin"] 28 | logger.info("Updating Agda-Pkg from " + [url for url in REPO.remote().urls][0]) 29 | for pull_info in origin.pull(): 30 | logger.info("%s to %s" % (pull_info.ref, pull_info.commit)) 31 | ctx.invoke(init, drop_tables=False) 32 | except Exception as e: 33 | logger.error(e) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apkg/commands/write_defaults.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import click 12 | from os import access, W_OK 13 | 14 | from pony.orm import db_session, select 15 | 16 | from ..__version__ import __version__ 17 | from .init import init 18 | from ..config import REPO, AGDA_DEFAULTS_PATH, AGDA_LIBRARIES_PATH 19 | from ..service.logging import logger, clog 20 | from ..service.database import Library , LibraryVersion 21 | 22 | # ---------------------------------------------------------------------------- 23 | 24 | @db_session 25 | def getLibraries(): 26 | return select(l for l in LibraryVersion if l.installed)[:] 27 | 28 | @db_session 29 | def getDefaultLibraries(): 30 | return select(l for l in Library if l.installed and l.default)[:] 31 | 32 | @click.group() 33 | def write_defaults(): pass 34 | 35 | @write_defaults.command() 36 | @click.option('--yes' 37 | , type=bool 38 | , is_flag=True 39 | , help='Yes for everything.') 40 | @clog.simple_verbosity_option(logger) 41 | @click.pass_context 42 | def write_defaults(ctx, yes): 43 | """Create/update 'defaults' and 'libraries' files for Agda.""" 44 | 45 | no_write_permission_msg = '[!] The current user has no permission to modify:' 46 | confirm_overwrite_msg = 'already exists\nDo you want to overwrite it?' 47 | header = "-- File generated by Agda-Pkg v{}\n".format(__version__) 48 | overwrite_confirm = lambda f : click.confirm("[!] {} {}".format(f.as_posix(),confirm_overwrite_msg)) 49 | 50 | write_lpath, write_dpath = True, True 51 | lpath = AGDA_LIBRARIES_PATH 52 | dpath = AGDA_DEFAULTS_PATH 53 | 54 | if not yes and (lpath.exists() and not overwrite_confirm(lpath)): 55 | write_lpath = False 56 | lpath.touch(exist_ok=True) 57 | 58 | if write_lpath and access(lpath, W_OK): 59 | try: 60 | lpath.write_text(header + \ 61 | '\n'.join([v.agdaLibFilePath.as_posix() for v in getLibraries()]) + '\n') 62 | click.echo("Updated ({})".format(lpath.as_posix())) 63 | except Exception as e: 64 | logger.error(e) 65 | elif write_lpath: 66 | click.echo(no_write_permission_msg) 67 | click.echo(" {}".format(lpath.as_posix())) 68 | 69 | if not yes and (dpath.exists() and not overwrite_confirm(dpath)): 70 | write_dpath = False 71 | dpath.touch(exist_ok=True) 72 | if write_dpath and access(dpath, W_OK): 73 | try: 74 | dpath.write_text(header + \ 75 | '\n'.join(lib.name for lib in getDefaultLibraries()) + '\n') 76 | click.echo("Updated ({})".format(dpath.as_posix())) 77 | except Exception as e: 78 | logger.error(e) 79 | elif write_dpath: 80 | click.echo(no_write_permission_msg) 81 | click.echo(" {}".format(dpath.as_posix())) -------------------------------------------------------------------------------- /apkg/config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import git 12 | import subprocess 13 | import os 14 | 15 | from pathlib import Path 16 | 17 | # ----------------------------------------------------------------------------- 18 | 19 | basedir = os.path.dirname(os.path.realpath(__file__)) 20 | 21 | # -- AGDA DIR: 22 | AGDA_DIR_PATH = Path().home().joinpath(".agda") 23 | if Path().cwd().joinpath(".agda").exists(): 24 | AGDA_DIR_PATH = Path().cwd().joinpath(".agda") 25 | if os.environ.get("AGDA_DIR", None) is not None: 26 | AGDA_DIR_PATH = Path(os.environ["AGDA_DIR"]) 27 | 28 | AGDA_DEFAULTS_PATH = AGDA_DIR_PATH.joinpath("defaults") 29 | AGDA_LIBRARIES_PATH = AGDA_DIR_PATH.joinpath("libraries") 30 | AGDA_VERSION = os.environ.get("AGDA_VERSION","") 31 | 32 | try: 33 | result = subprocess.run(["agda", "--version"], stdout=subprocess.PIPE) 34 | AGDA_VERSION = result.stdout.split()[2].decode() 35 | # # So far, Agda doesn't consider the commit, so we remove it. 36 | # AGDA_VERSION = AGDA_VERSION.split('-')[0] 37 | # AGDA_LIBRARIES_PATH = AGDA_DIR_PATH.joinpath("libraries-%s"%AGDA_VERSION) 38 | # # TODO : I should report that agda is having problems reading libraries 39 | # # from a file like "libraries-VERSION". Read the documentation. 40 | 41 | except Exception as e: 42 | print("[!] Agda may not be installed on this machine!") 43 | print(" Please consider to install Agda v2.6.1+") 44 | 45 | AGDA_PKG_PATH = Path().home().joinpath('.apkg' + \ 46 | ("@agda-" + AGDA_VERSION if len(AGDA_VERSION) > 0 else "")) 47 | 48 | GITHUB_USER = "agda" 49 | GITHUB_DOMAIN = "https://github.com/" 50 | GITHUB_API = "https://api.github.com/repos" 51 | 52 | # The github repository index of all agda packages 53 | INDEX_REPOSITORY_NAME = "package-index" 54 | INDEX_REPOSITORY_URL = \ 55 | GITHUB_DOMAIN + GITHUB_USER + "/" + INDEX_REPOSITORY_NAME + ".git" 56 | INDEX_REPOSITORY_BRANCH = "master" 57 | INDEX_REPOSITORY_PATH = AGDA_PKG_PATH.joinpath(INDEX_REPOSITORY_NAME) 58 | 59 | # this is folder where I keep all the source code for every library installed 60 | PACKAGE_SOURCES_NAME = "package-sources" 61 | PACKAGE_SOURCES_PATH = AGDA_PKG_PATH.joinpath(PACKAGE_SOURCES_NAME) 62 | 63 | # We want to search fast queries using a database 64 | DATABASE_FILE_NAME = INDEX_REPOSITORY_NAME + ".db" 65 | DATABASE_FILE_PATH = AGDA_PKG_PATH.joinpath(DATABASE_FILE_NAME) 66 | DATABASE_SEARCH_INDEXES_PATH = AGDA_PKG_PATH.joinpath("search-indexes") 67 | 68 | REPO = None 69 | 70 | PKG_SUFFIX = ".agda-pkg" 71 | LIB_SUFFIX = ".agda-lib" 72 | 73 | # ----------------------------------------------------------------------------- 74 | 75 | if not AGDA_PKG_PATH.exists(): 76 | AGDA_PKG_PATH.mkdir() 77 | 78 | if not INDEX_REPOSITORY_PATH.exists(): 79 | INDEX_REPOSITORY_PATH.mkdir() 80 | 81 | if not PACKAGE_SOURCES_PATH.exists(): 82 | PACKAGE_SOURCES_PATH.mkdir() 83 | 84 | try: 85 | REPO = git.Repo(INDEX_REPOSITORY_PATH, search_parent_directories=False) 86 | except: 87 | try: 88 | REPO = git.Repo.clone_from(INDEX_REPOSITORY_URL, INDEX_REPOSITORY_PATH) 89 | except Exception as e: 90 | print(e) 91 | 92 | if not DATABASE_FILE_PATH.exists(): 93 | DATABASE_FILE_PATH.touch() 94 | 95 | if not DATABASE_SEARCH_INDEXES_PATH.exists(): 96 | DATABASE_SEARCH_INDEXES_PATH.mkdir() 97 | 98 | if not AGDA_DIR_PATH.exists(): 99 | AGDA_DIR_PATH.mkdir() 100 | AGDA_DEFAULTS_PATH.touch() 101 | AGDA_LIBRARIES_PATH.touch() 102 | 103 | SUPPORT_FILES_PATH =(Path(basedir)/'support'/'nixos').resolve() 104 | -------------------------------------------------------------------------------- /apkg/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agda/agda-pkg/8d5cbcd76c1e4d150ad35ce39d1573c1f10f9fcd/apkg/service/__init__.py -------------------------------------------------------------------------------- /apkg/service/database.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import logging 12 | import shutil 13 | import yaml 14 | 15 | from pony.orm import * 16 | from ponywhoosh import PonyWhoosh 17 | from pprint import pprint 18 | from pathlib import Path 19 | 20 | from natsort import natsorted 21 | from operator import attrgetter, itemgetter 22 | 23 | from ..service.readLibFile import readLibFile 24 | from ..service.logging import logger, clog 25 | from ..config import ( AGDA_DEFAULTS_PATH 26 | , AGDA_DIR_PATH 27 | , AGDA_LIBRARIES_PATH 28 | , AGDA_PKG_PATH 29 | , AGDA_VERSION 30 | , DATABASE_FILE_NAME 31 | , DATABASE_FILE_PATH 32 | , DATABASE_SEARCH_INDEXES_PATH 33 | , GITHUB_USER 34 | , INDEX_REPOSITORY_BRANCH 35 | , INDEX_REPOSITORY_NAME 36 | , INDEX_REPOSITORY_PATH 37 | , INDEX_REPOSITORY_URL 38 | , PACKAGE_SOURCES_PATH 39 | , REPO 40 | , PKG_SUFFIX 41 | , LIB_SUFFIX 42 | ) 43 | 44 | # ---------------------------------------------------------------------------- 45 | 46 | # -- Search index 47 | pw = PonyWhoosh() 48 | 49 | pw.indexes_path = DATABASE_SEARCH_INDEXES_PATH 50 | pw.search_string_min_len = 1 51 | pw.writer_timeout = 3 52 | 53 | db = Database() 54 | 55 | # Library is the general object to store the information about 56 | # an Agda library. Each library is associated with its different 57 | # versions. These versions are instance of the object LibraryVersion. 58 | 59 | @pw.register_model('name', 'description', 'url') 60 | class Library(db.Entity): 61 | name = PrimaryKey(str) 62 | description = Optional(str, nullable=True) 63 | url = Optional(str, nullable=True) 64 | versions = Set('LibraryVersion') 65 | appearson = Set('Dependency') 66 | keywords = Set('Keyword') 67 | installed = Optional(bool, default=False) 68 | default = Optional(bool, default=True) 69 | 70 | def __str__(self): 71 | return self.name 72 | 73 | def __repr__(self): 74 | return self.name 75 | 76 | @property 77 | def info(self): 78 | return self.to_dict( with_collections=False 79 | , related_objects=False) 80 | 81 | @property 82 | def indexPath(self): 83 | return INDEX_REPOSITORY_PATH.joinpath("src").joinpath(self.name) 84 | 85 | def isIndexed(self): 86 | return self.indexPath.exist() 87 | 88 | def getSortedVersions(self): 89 | versions = [v for v in self.versions] 90 | return natsorted(versions, key=attrgetter('name')) 91 | 92 | def getInstalledVersion(self): 93 | versions = [v for v in self.versions if v.installed] 94 | if len(versions) == 1: return versions[0] 95 | return None 96 | 97 | def getLatestVersion(self): 98 | versions = self.getSortedVersions() 99 | if len(versions) > 0: return versions[-1] 100 | return None 101 | 102 | def getLatestCachedVersion(self): 103 | versions = [v for v in self.versions if v.cached] 104 | versions = natsorted(versions, key=attrgetter('name')) 105 | print(self.name) 106 | print(versions) 107 | if len(versions) > 0: return versions[-1] 108 | return None 109 | 110 | def freezeName(self): 111 | version = self.getInstalledVersion() 112 | if version is not None: 113 | return version.locationName() 114 | return self.name # --not sure about this 115 | 116 | def uninstall(self, remove_cache=False): 117 | 118 | self.installed = False 119 | self.default = False 120 | 121 | for v in self.versions: 122 | try: 123 | v.uninstall(remove_cache) 124 | except Exception as e: 125 | logger.error(e) 126 | logger.error(" Failed uninstallation ({}).".format(v.name)) 127 | 128 | 129 | def install(self, defaults=True): 130 | self.installed = True 131 | self.default = defaults 132 | 133 | # ---- 134 | 135 | @pw.register_model('name', 'description') 136 | class LibraryVersion(db.Entity): 137 | library = Required(Library) 138 | name = Optional(str, nullable=True, default="") 139 | sha = Optional(str) 140 | description = Optional(str, default="") 141 | license = Optional(str) 142 | include = Optional(str) 143 | depend = Set('Dependency') 144 | testedWith = Set('TestedWith') 145 | keywords = Set('Keyword') 146 | installed = Optional(bool, default=False) 147 | cached = Optional(bool, default=False) 148 | fromIndex = Optional(bool, default=False) 149 | fromUrl = Optional(bool, default=False) 150 | fromGit = Optional(bool, default=False) 151 | origin = Optional(str) # path, url, git 152 | editable = Optional(bool, default=False) 153 | 154 | composite_key(library, name) 155 | 156 | def __str__(self): 157 | return self.name 158 | 159 | def __repr__(self): 160 | return self.name 161 | 162 | @property 163 | def info(self): 164 | d = self.to_dict( with_collections=True 165 | , related_objects=True 166 | , exclude=["id"] 167 | ) 168 | 169 | fileInfo = self.readInfoFromLibFile() 170 | d.update(fileInfo) 171 | 172 | del d["name"] 173 | d["library"] = self.library.name 174 | d["version"] = self.name 175 | d["default"] = self.library.default 176 | d["description"] = self.library.description 177 | d["index_path"] = self.indexPath.as_posix() 178 | d["source_path"] = self.sourcePath.as_posix() 179 | 180 | return d 181 | 182 | def libraryVersionName(self, sep): 183 | return self.library.name.strip() + sep + self.name.strip() 184 | 185 | @property 186 | def locationName(self): 187 | return self.libraryVersionName("@") 188 | 189 | @property 190 | def freezeName(self): 191 | if self.name == "" : 192 | return self.library.name 193 | return self.libraryVersionName("==") 194 | 195 | def isCached(self): 196 | return self.cached 197 | 198 | def isIndexed(self): 199 | return self.fromIndex 200 | 201 | def isUserVersion(self): 202 | return (not self.isIndexed()) 203 | 204 | @property 205 | def indexPath(self): 206 | return (INDEX_REPOSITORY_PATH 207 | .joinpath("src") 208 | .joinpath(self.library.name) 209 | .joinpath("versions") 210 | .joinpath(self.name) 211 | ) 212 | 213 | @property 214 | def sourcePath(self): 215 | return (PACKAGE_SOURCES_PATH.joinpath(self.locationName) 216 | if not self.editable else Path(self.origin) 217 | ) 218 | 219 | @property 220 | def agdaPkgFilePath(self): 221 | return (self.indexPath.joinpath(self.library.name + PKG_SUFFIX) \ 222 | if (self.isIndexed() and not self.installed and not(self.editable)) 223 | else self.sourcePath.joinpath(self.library.name + PKG_SUFFIX)) 224 | 225 | @property 226 | def agdaLibFilePath(self): 227 | return (self.indexPath.joinpath(self.library.name + LIB_SUFFIX)\ 228 | if (self.isIndexed() and not self.installed and not(self.editable)) 229 | else self.sourcePath.joinpath(self.library.name + LIB_SUFFIX)) 230 | 231 | def getLibFilePath(self): 232 | if self.agdaPkgFilePath.exists(): 233 | return self.agdaPkgFilePath 234 | if self.agdaLibFilePath.exists(): 235 | return self.agdaLibFilePath 236 | raise ValueError(" No file descriptor for the version {} of {}." 237 | .format(self.name, self.library.name)) 238 | 239 | def isLatest(self): 240 | versions = self.library.getSortedVersions() 241 | return len(versions) > 0 and versions[-1].name == self.name 242 | 243 | def tolibFormat(self): 244 | msg = '\n'.join( 245 | [ "name: %s" % self.library.name 246 | , "include: %s" % ' '.join([inc for inc in self.include.split()]) 247 | , "depend: %s" % ' '.join([d.library.name for d in self.depend.split()]) 248 | ]) 249 | return '\n'.join(msg) 250 | 251 | def toPkgFormat(self): 252 | return yaml.dump(self.info, default_flow_style=False) 253 | 254 | def writeAgdaLibFile(self, path=None): 255 | if path is None: path = self.agdaLibFilePath() 256 | path = Path(path) 257 | if not path.exists(): path.touch() 258 | path.write_text(self.tolibFormat()) 259 | 260 | def writeAgdaPkgFile(self, path=None): 261 | if path is None: path = self.agdaPkgFilePath() 262 | path = Path(path) 263 | if path.exists(): path.touch() 264 | path.write_text(self.toPkgFormat()) 265 | 266 | def writeLibFile(self, path=None,format=PKG_SUFFIX): 267 | if format == PKG_SUFFIX: 268 | self.writeAgdaPkgFile(path) 269 | if format == LIB_SUFFIX: 270 | self.writeAgdaLibFile(path) 271 | raise ValueError(" " + format + " no supported") 272 | 273 | def readInfoFromLibFile(self): 274 | return readLibFile(self.getLibFilePath()) 275 | 276 | def removeSources(self): 277 | if not(self.editable): 278 | try: 279 | if self.sourcePath.exists(): 280 | shutil.rmtree(self.sourcePath.as_posix()) 281 | except Exception as e: 282 | logger.error(e) 283 | logger.error("Failed to remove " \ 284 | + self.sourcePath.as_posix()) 285 | 286 | def uninstall(self, remove_cache=True): 287 | 288 | self.installed = False 289 | self.library.installed = False 290 | self.library.default = False 291 | try: 292 | if remove_cache: 293 | self.cached = False 294 | self.removeSources() 295 | logger.info(" Version removed ({}).".format(self.name)) 296 | except Exception as e: 297 | logger.error(e) 298 | logger.error("Unsuccessfully to remove ({})" 299 | .format(self.name)) 300 | 301 | def install(self, defaults=True): 302 | 303 | for v in self.library.versions: 304 | v.installed = False 305 | 306 | self.installed = True 307 | self.cached = True 308 | 309 | self.library.install(defaults) 310 | 311 | @pw.register_model('word') 312 | class Keyword(db.Entity): 313 | word = PrimaryKey(str) 314 | libVersions = Set(LibraryVersion) 315 | libraries = Set(Library) 316 | 317 | def __str__(self): 318 | return self.word 319 | 320 | def __repr__(self): 321 | return self.word 322 | 323 | 324 | @pw.register_model('agdaVersion') 325 | class TestedWith(db.Entity): 326 | agdaVersion = PrimaryKey(str) 327 | libraries = Set(LibraryVersion) 328 | 329 | def __str__(self): 330 | return "agda-" + self.agdaVersion 331 | 332 | def __repr__(self): 333 | return "agda-" + self.agdaVersion 334 | 335 | 336 | class Dependency(db.Entity): 337 | id = PrimaryKey(int, auto=True) 338 | library = Required(Library) 339 | minVersion = Optional(str, default="") 340 | maxVersion = Optional(str, default="") 341 | supporting = Set(LibraryVersion) 342 | 343 | def __str__(self): 344 | text = self.minVersion \ 345 | + ("<=" if self.minVersion else "") \ 346 | + self.library.name \ 347 | + ("<=" if self.maxVersion else "") \ 348 | + self.maxVersion 349 | return text 350 | 351 | def __repr__(self): 352 | return str(self) 353 | 354 | try: 355 | db.bind('sqlite', DATABASE_FILE_PATH.as_posix(), create_db=True) 356 | db.generate_mapping(create_tables=True) 357 | except Exception as e: 358 | logger.error(e) 359 | -------------------------------------------------------------------------------- /apkg/service/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import click_log as clog 3 | 4 | # -- Logger def. 5 | logger = logging.getLogger(__name__) 6 | clog.basic_config(logger) 7 | -------------------------------------------------------------------------------- /apkg/service/readLibFile.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | import sys 12 | import yaml 13 | 14 | from pathlib import Path 15 | from pprint import pprint 16 | 17 | # ---------------------------------------------------------------------------- 18 | 19 | def readLibLegacyFile(fname): 20 | info = { "name": "", "version": "", "include": [], "depend":[]} 21 | libraryfile = Path(fname) 22 | 23 | assert libraryfile.exists() 24 | assert libraryfile.suffix == ".agda-lib" 25 | 26 | with libraryfile.open('r') as f: 27 | content = sum([ line.strip().split() for line in f.readlines()],[]) 28 | 29 | # 'name' field 30 | try: 31 | indexName = content.index("name:") 32 | name = content[indexName + 1] 33 | info["name"] = name.strip() 34 | except Exception as e: 35 | # print("[!] 'name' field not found ==> using filename instead.") 36 | info["name"] = libraryfile.name.split(libraryfile.suffix)[0] 37 | 38 | # 'version' field 39 | try: 40 | versionName = content.index("version:") 41 | version = content[versionName + 1] 42 | info["version"] = version.strip() 43 | except Exception as e: 44 | info["version"] = "" 45 | 46 | # check all the required fields exist in the file 47 | # otherwise send an error 48 | 49 | # 'include' field 50 | indexInclude = content.index("include:") 51 | i = indexInclude + 1 52 | while i < len(content) and \ 53 | (not "--" in content[i]) and \ 54 | (not ":" in content[i]) : 55 | info["include"].append(content[i].strip()) 56 | i += 1 57 | info["include"] = list(set(info["include"])) 58 | 59 | # 'depend' field 60 | try: 61 | indexDepend = content.index("depend:") 62 | i = indexDepend + 1 63 | try: 64 | while i < len(content) and \ 65 | (not "--" in content[i]) and \ 66 | (not ":" in content[i]) : 67 | info["depend"].append(content[i].strip()) 68 | i += 1 69 | info["depend"] = list(set(info["depend"])) 70 | except Exception as e: 71 | pass 72 | except Exception as e: 73 | pass 74 | return info 75 | 76 | def readPkgFile(fname): 77 | libraryfile = Path(fname) 78 | assert libraryfile.exists() 79 | assert libraryfile.suffix == ".agda-pkg" 80 | 81 | stream = libraryfile.open("r") 82 | docs = yaml.load(stream, Loader=yaml.FullLoader) 83 | assert "name" in docs.keys() and "include" in docs.keys() 84 | return docs 85 | 86 | def readLibFile(fname): 87 | libraryfile = Path(fname) 88 | if libraryfile.suffix == ".agda-lib": 89 | return readLibLegacyFile(fname) 90 | if libraryfile.suffix == ".agda-pkg": 91 | return readPkgFile(fname) 92 | return None 93 | -------------------------------------------------------------------------------- /apkg/service/utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | from pathlib import Path 12 | from pony.orm import * 13 | from urllib.parse import urlparse 14 | 15 | from ..service.database import db 16 | from ..service.database import ( Library 17 | , LibraryVersion 18 | ) 19 | 20 | # ---------------------------------------------------------------------------- 21 | 22 | # -- Some tests -- TODO: move these to some util module outside. 23 | def isURL(url): 24 | min_attr = ('scheme' , 'netloc') 25 | try: 26 | result = urlparse(url) 27 | # -- TODO : improve this test checking if it's available 28 | return all([result.scheme, result.netloc]) 29 | except: 30 | return False 31 | 32 | def isGit(url): 33 | min_attr = ('scheme' , 'netloc') 34 | try: 35 | result = urlparse(url) 36 | netloc = result.netloc 37 | # -- TODO : improve this test using gitpython 38 | return all([result.scheme, result.netloc]) and result.path.endswith(".git") 39 | except: 40 | return False 41 | 42 | @db_session 43 | def isIndexed(libname): 44 | return Library.get(name=libname) is not None 45 | 46 | def isLocal(path): 47 | return Path(path).exists() 48 | -------------------------------------------------------------------------------- /apkg/support/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agda/agda-pkg/8d5cbcd76c1e4d150ad35ce39d1573c1f10f9fcd/apkg/support/__init__.py -------------------------------------------------------------------------------- /apkg/support/nixos/agda_requirements.txt: -------------------------------------------------------------------------------- 1 | standard-library==v1.2 2 | -------------------------------------------------------------------------------- /apkg/support/nixos/deps.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | let 3 | thisAgda = pkgs.haskellPackages.ghcWithPackages ( p: [p.Agda p.ieee754] ); 4 | in 5 | agda.mkDerivation(self: { 6 | name = "commonAgdaDeps"; 7 | dontUnpack = true; 8 | myAgda = thisAgda; 9 | buildDepends = [ 10 | thisAgda 11 | # Using agda-pkg instead for deps for now: 12 | # pkgs.AgdaStdlib (pkgs.haskellPackages.ghcWithPackages ( p: [p.ieee]) ) 13 | # pkgs.agdaPrelude (pkgs.haskellPackages.ghcWithPackages ( p: [p.ieee]) ) 14 | ]; 15 | src = null; 16 | }) 17 | -------------------------------------------------------------------------------- /apkg/support/nixos/emacs.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | let 3 | myEmacs = pkgs.emacs26-nox; 4 | emacsWithPackages = (pkgs.emacsPackagesGen myEmacs).emacsWithPackages; 5 | deps = (import ./deps.nix); 6 | in 7 | emacsWithPackages (epkgs: (with epkgs.melpaStablePackages; [ 8 | magit # ; Integrate git 9 | zerodark-theme # ; Nicolas' theme 10 | ]) ++ (with epkgs.melpaPackages; [ 11 | # undo-tree # ; to show the undo tree 12 | # zoom-frm # ; increase/decrease font size for all buffers %lt;C-x C-+> 13 | ]) ++ (with epkgs.elpaPackages; [ 14 | auctex # ; LaTeX mode 15 | beacon # ; highlight my cursor when scrolling 16 | nameless # ; hide current package name everywhere in elisp code 17 | ]) ++ [ 18 | deps.myAgda # This is where agda-mode is located 19 | # pkgs.notmuch # From main packages set 20 | ]) 21 | -------------------------------------------------------------------------------- /apkg/support/nixos/hello-world.agda: -------------------------------------------------------------------------------- 1 | module hello-world where 2 | 3 | open import IO 4 | 5 | main = run (putStrLn "Hello, World!") 6 | -------------------------------------------------------------------------------- /apkg/support/nixos/shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | let 3 | deps = (import ./deps.nix); 4 | myEmacs = (import ./emacs.nix { inherit pkgs; }); 5 | 6 | in 7 | agda.mkDerivation(self: { 8 | name = "agdaShellEnv"; 9 | dontUnpack = true; 10 | buildDepends = deps.buildDepends; 11 | buildInputs = deps.buildInputs ++ [ 12 | myEmacs 13 | 14 | # these packages are required for virtualenv and pip to work: 15 | # 16 | mypy 17 | python38Full 18 | python38Packages.virtualenv 19 | ]; 20 | # libPath = deps.libPath; 21 | src = null; 22 | shellHook = '' 23 | 24 | # set SOURCE_DATE_EPOCH so that we can use python wheels 25 | SOURCE_DATE_EPOCH=$(date +%s) 26 | export LANG=en_US.UTF-8 27 | 28 | activate_python_env () { 29 | source venv/bin/activate 30 | export PATH=$PWD/venv/bin:$PATH 31 | export PYTHONPATH=$PWD:$PYTHONPATH 32 | } 33 | 34 | if [ ! -d "venv" ]; then 35 | # initialize python environment 36 | virtualenv venv 37 | activate_python_env 38 | pip install agda-pkg 39 | else 40 | activate_python_env 41 | fi 42 | 43 | export AGDA_PROJ_DIR=$PWD 44 | export AGDA_DIR=$AGDA_PROJ_DIR/.agda 45 | if [ ! -d "$AGDA_DIR" ]; then 46 | mkdir -p "$AGDA_DIR" 47 | apkg init 48 | apkg upgrade 49 | apkg install -r agda_requirements.txt 50 | fi 51 | 52 | 53 | if [ ! -f "$AGDA_PROJ_DIR/.emacs" ]; then 54 | export ORIG_HOME=$HOME 55 | export HOME=$AGDA_PROJ_DIR 56 | 57 | echo '(load (expand-file-name "~/.emacs") "" nil t)' > $AGDA_PROJ_DIR/.emacs 58 | agda-mode setup 59 | export EMACS_USER_FILE="$AGDA_PROJ_DIR/.emacs_user_config" 60 | if [ -f "$EMACS_USER_FILE" ]; then 61 | cat "$EMACS_USER_FILE" >> $AGDA_PROJ_DIR/.emacs 62 | fi 63 | export HOME=$ORIG_HOME 64 | unset ORIG_HOME 65 | rmdir .emacs.d 66 | unset EMACS_USER_FILE 67 | fi 68 | 69 | mymacs () { 70 | emacs -Q --load $AGDA_PROJ_DIR/.emacs $@ 71 | } 72 | ''; 73 | }) 74 | -------------------------------------------------------------------------------- /deploy.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | from pathlib import Path 12 | import click 13 | import subprocess 14 | 15 | from apkg.__version__ import __version__, __message__ 16 | 17 | # ---------------------------------------------------------------------------- 18 | 19 | print("Version: v" + __version__) 20 | print("Message: " + __message__) 21 | 22 | if click.confirm("Proceed"): 23 | subprocess.run(["git", "add" , "."], stdout=subprocess.PIPE) 24 | subprocess.run(["git", "commit" , "-am", "[ v{} ] {}".format(__version__, __message__)] 25 | , stdout=subprocess.PIPE) 26 | subprocess.run(["git", "tag" , "v{}".format(__version__)] 27 | , stdout=subprocess.PIPE) 28 | subprocess.run(["make", "push"], stdout=subprocess.PIPE) 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A package manager for Agda 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.1 2 | GitPython==3.1.1 3 | ponywhoosh==1.7.8 4 | natsort==7.0.1 5 | click-log==0.3.2 6 | humanize==0.5.1 7 | requests==2.23.0 8 | distlib==0.3.0 9 | PyYAML==5.3.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | apkg 3 | ~~~~ 4 | 5 | A package manager for Agda. 6 | 7 | ''' 8 | 9 | # ---------------------------------------------------------------------------- 10 | 11 | from __future__ import absolute_import, print_function 12 | 13 | 14 | import io 15 | import os 16 | import re 17 | 18 | from os.path import basename, dirname, join 19 | from setuptools import find_packages, setup 20 | 21 | from apkg.__version__ import __version__ 22 | 23 | # ---------------------------------------------------------------------------- 24 | 25 | def read(*names, **kwargs): 26 | return io.open( 27 | join(dirname(__file__), *names) 28 | , encoding=kwargs.get('encoding', 'utf8') 29 | ).read() 30 | 31 | 32 | setup( 33 | name='agda-pkg' 34 | , version=__version__ 35 | , python_requires='>=3.6.0' 36 | , url='https://github.com/agda/agda-pkg' 37 | , license='MIT' 38 | , author='Jonathan Prieto-Cubides and https://github.com/agda/agda-pkg/graphs/contributors' 39 | , author_email='jonathan.cubides@uib.no' 40 | , description='A package manager for Agda' 41 | , long_description='%s' % (read('README.md')) 42 | , long_description_content_type='text/markdown' 43 | , packages=find_packages() 44 | , zip_safe=False 45 | , include_package_data=True 46 | , package_dir={'apkg': 'apkg'} 47 | , package_data={'apkg': ['commands/templates/*', 'support/nixos/*']} 48 | , platforms='any' 49 | , keywords= 50 | [ 'agda' 51 | , 'package-manager' 52 | , 'agda-pkg' 53 | , 'apkg' 54 | ] 55 | , install_requires= 56 | [ 'click' 57 | , 'gitpython' 58 | , 'pony' 59 | , 'whoosh' 60 | , 'ponywhoosh' 61 | , 'natsort' 62 | , 'click-log' 63 | , 'requests' 64 | , 'humanize' 65 | , 'Jinja2' 66 | , 'distlib' 67 | , 'PyYAML>=5.1.1' 68 | ] 69 | , entry_points=''' 70 | [console_scripts] 71 | agda-pkg=apkg.apkg:cli 72 | apkg=apkg.apkg:cli 73 | ''' 74 | , classifiers= 75 | [ 'Intended Audience :: Developers' 76 | , 'License :: OSI Approved :: MIT License' 77 | , 'Operating System :: OS Independent' 78 | , 'Programming Language :: Python :: 3.6' 79 | ] 80 | ) 81 | -------------------------------------------------------------------------------- /tests/basic.py: -------------------------------------------------------------------------------- 1 | # no real tests now. 2 | 3 | import unittest 4 | 5 | class TestStringMethods(unittest.TestCase): 6 | 7 | def test_upper(self): 8 | self.assertEqual('foo'.upper(), 'FOO') 9 | 10 | def test_isupper(self): 11 | self.assertTrue('FOO'.isupper()) 12 | self.assertFalse('Foo'.isupper()) 13 | 14 | def test_split(self): 15 | s = 'hello world' 16 | self.assertEqual(s.split(), ['hello', 'world']) 17 | # check that s.split fails when the separator is not a string 18 | with self.assertRaises(TypeError): 19 | s.split(2) 20 | 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /tests/library.agda-lib: -------------------------------------------------------------------------------- 1 | name: LIBRARY-NAME -- Comment 2 | depend: LIB1 LIB2 3 | LIB3 4 | LIB4 5 | include: PATH1 6 | PATH2 7 | PATH3 8 | -------------------------------------------------------------------------------- /tests/library.agda-pkg: -------------------------------------------------------------------------------- 1 | name: mylibrary 2 | version: 0.0.1 3 | author: AuthorName 4 | category: [ classic, logic, theorems ] 5 | homepage: http://github.com/user/mylibrary 6 | license: MIT 7 | license-file: LICENSE.md 8 | source-repository: http://github.com/user/mylibrary.git 9 | tested-with: 2.5.6 10 | description: Put here a description. 11 | 12 | include: 13 | - PATH1 14 | - PATH2 15 | - PATH3 16 | depend: 17 | - LIB1 18 | - LIB2 19 | - LIB3 20 | - LIB4 --------------------------------------------------------------------------------